@bagelink/blox 1.12.22 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,18 +25,43 @@ const fs = require("node:fs/promises");
25
25
  const path = require("node:path");
26
26
  const process = require("node:process");
27
27
  const node_url = require("node:url");
28
- async function fetchCmsPrerenderPaths(apiBase, websiteName) {
28
+ async function fetchCmsSiteData(apiBase, websiteName) {
29
29
  const sitesRes = await fetch(`${apiBase}/cms/websites`);
30
30
  const sitesData = await sitesRes.json();
31
31
  const sites = Array.isArray(sitesData) ? sitesData : sitesData.data ?? [];
32
32
  const site = sites.find((s) => s.name === websiteName);
33
33
  if ((site == null ? void 0 : site.id) == null || site.id === "") throw new Error(`Website "${websiteName}" not found at ${apiBase}`);
34
- const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=en`);
34
+ let website = site;
35
+ try {
36
+ const wRes = await fetch(`${apiBase}/cms/websites/${site.id}`);
37
+ if (wRes.ok) website = await wRes.json();
38
+ } catch {
39
+ }
40
+ const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=${website.default_locale || "en"}`);
35
41
  const pagesData = await pagesRes.json();
36
42
  const pages = Array.isArray(pagesData) ? pagesData : pagesData.data ?? [];
43
+ const paths = await expandRoutes(apiBase, pages);
44
+ let redirects = [];
45
+ try {
46
+ const rRes = await fetch(`${apiBase}/cms/websites/${site.id}/redirects`);
47
+ if (rRes.ok) {
48
+ const rData = await rRes.json();
49
+ const items = Array.isArray(rData) ? rData : rData.data ?? [];
50
+ redirects = items.filter((r) => r.from_path && r.to_path);
51
+ }
52
+ } catch {
53
+ }
54
+ return { websiteId: site.id, website, paths, redirects };
55
+ }
56
+ async function fetchCmsPrerenderPaths(apiBase, websiteName) {
57
+ const { paths } = await fetchCmsSiteData(apiBase, websiteName);
58
+ return paths;
59
+ }
60
+ async function expandRoutes(apiBase, pages) {
37
61
  const routes = [];
38
62
  for (const page of pages) {
39
63
  const slug = page.slug ?? "/";
64
+ if (page.status && page.status !== "published") continue;
40
65
  if (slug.includes(":") && page.data_bindings) {
41
66
  for (const [, binding] of Object.entries(page.data_bindings)) {
42
67
  const b = binding;
@@ -50,7 +75,10 @@ async function fetchCmsPrerenderPaths(apiBase, websiteName) {
50
75
  for (const item of items) {
51
76
  const bindValue = item[b.bind_by];
52
77
  if (bindValue != null && bindValue !== "") {
53
- const concrete = slug.replace(/:([a-z_]+)/g, (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? ""));
78
+ const concrete = slug.replace(
79
+ /:([a-z_]+)/g,
80
+ (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? "")
81
+ );
54
82
  routes.push(concrete);
55
83
  }
56
84
  }
@@ -109,6 +137,150 @@ async function polyfillBloxSsgGlobals(options) {
109
137
  g.window.location = _win.location;
110
138
  g.window.document = _win.document;
111
139
  }
140
+ function buildPageHead(options) {
141
+ const { url, resolvedData, website, stateScript } = options;
142
+ const parts = [];
143
+ const meta = (website == null ? void 0 : website.meta) ?? {};
144
+ const page = resolvedData == null ? void 0 : resolvedData.page;
145
+ const ctx = extractPrimaryContext(resolvedData == null ? void 0 : resolvedData.contexts);
146
+ const title = (page == null ? void 0 : page.meta_title) || ctx.title || ((page == null ? void 0 : page.title) && meta.og_site_name ? `${page.title} · ${meta.og_site_name}` : null) || (page == null ? void 0 : page.title) || meta.default_meta_title;
147
+ if (title) {
148
+ const fullTitle = ctx.title && meta.og_site_name && !ctx.title.includes(meta.og_site_name) ? `${esc(ctx.title)} · ${esc(meta.og_site_name)}` : esc(title);
149
+ parts.push(`<title>${fullTitle}</title>`);
150
+ }
151
+ const description = (page == null ? void 0 : page.meta_description) || ctx.description || meta.default_meta_description;
152
+ if (description) {
153
+ parts.push(`<meta name="description" content="${esc(description)}">`);
154
+ }
155
+ const canonicalBase = (meta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
156
+ if (canonicalBase) {
157
+ parts.push(`<link rel="canonical" href="${esc(canonicalBase + url)}">`);
158
+ }
159
+ const ogTitle = (page == null ? void 0 : page.meta_title) || ctx.title || (page == null ? void 0 : page.title) || meta.og_site_name || meta.default_meta_title;
160
+ const ogDesc = (page == null ? void 0 : page.meta_description) || ctx.description || meta.og_description || meta.default_meta_description;
161
+ const ogImage = (page == null ? void 0 : page.og_image) || ctx.image || meta.default_og_image;
162
+ const ogType = meta.og_type || "website";
163
+ if (ogTitle) parts.push(`<meta property="og:title" content="${esc(ogTitle)}">`);
164
+ if (ogDesc) parts.push(`<meta property="og:description" content="${esc(ogDesc)}">`);
165
+ if (ogImage) parts.push(`<meta property="og:image" content="${esc(ogImage)}">`);
166
+ parts.push(`<meta property="og:type" content="${esc(ogType)}">`);
167
+ if (canonicalBase) parts.push(`<meta property="og:url" content="${esc(canonicalBase + url)}">`);
168
+ if (meta.og_site_name) parts.push(`<meta property="og:site_name" content="${esc(meta.og_site_name)}">`);
169
+ const twitterCard = meta.twitter_card || (ogImage ? "summary_large_image" : "summary");
170
+ parts.push(`<meta name="twitter:card" content="${esc(twitterCard)}">`);
171
+ if (ogTitle) parts.push(`<meta name="twitter:title" content="${esc(ogTitle)}">`);
172
+ if (ogDesc) parts.push(`<meta name="twitter:description" content="${esc(ogDesc)}">`);
173
+ if (ogImage) parts.push(`<meta name="twitter:image" content="${esc(ogImage)}">`);
174
+ if (meta.twitter_site) parts.push(`<meta name="twitter:site" content="${esc(meta.twitter_site)}">`);
175
+ const alternates = resolvedData == null ? void 0 : resolvedData.alternates;
176
+ if (alternates && canonicalBase) {
177
+ for (const [locale, slug] of Object.entries(alternates)) {
178
+ parts.push(`<link rel="alternate" hreflang="${esc(locale)}" href="${esc(canonicalBase + slug)}">`);
179
+ }
180
+ const defaultSlug = alternates[(website == null ? void 0 : website.default_locale) ?? "en"] ?? url;
181
+ parts.push(`<link rel="alternate" hreflang="x-default" href="${esc(canonicalBase + defaultSlug)}">`);
182
+ }
183
+ if (meta.noindex) {
184
+ parts.push('<meta name="robots" content="noindex, nofollow">');
185
+ }
186
+ if (stateScript) {
187
+ parts.push(stateScript);
188
+ }
189
+ return parts.join("\n");
190
+ }
191
+ function buildSiteHead(website) {
192
+ if (!website) return "";
193
+ const parts = [];
194
+ const meta = website.meta ?? {};
195
+ if (website.favicon_url) {
196
+ parts.push(`<link rel="icon" href="${esc(website.favicon_url)}">`);
197
+ }
198
+ if (meta.webclip) {
199
+ parts.push(`<link rel="apple-touch-icon" sizes="180x180" href="${esc(meta.webclip)}">`);
200
+ }
201
+ if (meta.verification_google) {
202
+ parts.push(`<meta name="google-site-verification" content="${esc(meta.verification_google)}">`);
203
+ }
204
+ if (meta.verification_bing) {
205
+ parts.push(`<meta name="msvalidate.01" content="${esc(meta.verification_bing)}">`);
206
+ }
207
+ if (meta.structured_data_org) {
208
+ try {
209
+ const json = typeof meta.structured_data_org === "string" ? meta.structured_data_org : JSON.stringify(meta.structured_data_org);
210
+ parts.push(`<script type="application/ld+json">${json}${"<"}/script>`);
211
+ } catch {
212
+ }
213
+ }
214
+ const plausibleId = website.plausible_site_id;
215
+ if (plausibleId) {
216
+ parts.push(`<script defer data-domain="${esc(plausibleId)}" src="https://plausible.io/js/script.js"><${"/"}script>`);
217
+ }
218
+ return parts.join("\n");
219
+ }
220
+ function generateSitemapXml(options) {
221
+ const { renderedPaths, canonicalBase, defaultChangefreq = "weekly", defaultPriority = "0.7" } = options;
222
+ const base = canonicalBase.replace(/\/$/, "");
223
+ const urls = renderedPaths.map((p) => {
224
+ const priority = p === "/" ? "1.0" : defaultPriority;
225
+ const changefreq = p === "/" ? "daily" : defaultChangefreq;
226
+ return ` <url>
227
+ <loc>${esc(base + p)}</loc>
228
+ <changefreq>${changefreq}</changefreq>
229
+ <priority>${priority}</priority>
230
+ </url>`;
231
+ });
232
+ return `<?xml version="1.0" encoding="UTF-8"?>
233
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
234
+ ${urls.join("\n")}
235
+ </urlset>
236
+ `;
237
+ }
238
+ function generateRobotsTxt(options) {
239
+ const { robotsTxt, canonicalBase, noindex } = options;
240
+ if (noindex) {
241
+ return `User-agent: *
242
+ Disallow: /
243
+ `;
244
+ }
245
+ const base = (canonicalBase || "").replace(/\/$/, "");
246
+ const sitemapLine = base ? `
247
+ Sitemap: ${base}/sitemap.xml
248
+ ` : "";
249
+ if (robotsTxt) {
250
+ if (sitemapLine && !robotsTxt.toLowerCase().includes("sitemap:")) {
251
+ return robotsTxt.trimEnd() + "\n" + sitemapLine;
252
+ }
253
+ return robotsTxt;
254
+ }
255
+ return `User-agent: *
256
+ Allow: /
257
+ ${sitemapLine}`;
258
+ }
259
+ function generateNetlifyRedirects(redirects) {
260
+ if (redirects.length === 0) return "";
261
+ return redirects.map((r) => `${r.from_path} ${r.to_path} ${r.status_code}`).join("\n") + "\n";
262
+ }
263
+ function stripTemplateSeoTags(template) {
264
+ return template.replace(/<title>[^<]*<\/title>/gi, "<!--ssg:title-->").replace(/<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi, "").replace(/<meta\s[^>]*property\s*=\s*["']og:[^"']*["'][^>]*>/gi, "").replace(/<meta\s[^>]*(?:property|name)\s*=\s*["']twitter:[^"']*["'][^>]*>/gi, "").replace(/<link\s[^>]*rel\s*=\s*["']apple-touch-icon["'][^>]*>/gi, "").replace(/<link\s[^>]*rel\s*=\s*["']icon["'][^>]*>/gi, "").replace(/\n\s*\n\s*\n/g, "\n\n");
265
+ }
266
+ function extractPrimaryContext(contexts) {
267
+ const empty = { title: null, description: null, image: null };
268
+ if (!contexts || typeof contexts !== "object") return empty;
269
+ const ctx = Object.values(contexts).find((c) => c != null);
270
+ if (!ctx) return empty;
271
+ const str = (key) => {
272
+ const v = ctx[key];
273
+ return typeof v === "string" && v.trim() ? v.trim() : null;
274
+ };
275
+ return {
276
+ title: str("meta_title") || str("title"),
277
+ description: str("meta_description") || str("excerpt") || str("description") || str("blurb") || str("summary"),
278
+ image: str("og_image") || str("cover_image_url") || str("image_url") || str("image")
279
+ };
280
+ }
281
+ function esc(s) {
282
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
283
+ }
112
284
  async function prerender({
113
285
  root = process.cwd(),
114
286
  clientOutDir = "dist/client",
@@ -118,13 +290,21 @@ async function prerender({
118
290
  excludePaths = [],
119
291
  failFast = false,
120
292
  maxPages = 5e3,
121
- mode = "dir"
293
+ mode = "dir",
294
+ website = null,
295
+ redirects = []
122
296
  } = {}) {
123
297
  const absRoot = path.resolve(root);
124
298
  const absClient = path.resolve(absRoot, clientOutDir);
125
299
  const absServerEntry = path.resolve(absRoot, serverEntry);
126
300
  const templatePath = path.join(absClient, "index.html");
127
- const template = await fs.readFile(templatePath, "utf8");
301
+ const rawTemplate = await fs.readFile(templatePath, "utf8");
302
+ let template = stripTemplateSeoTags(rawTemplate);
303
+ const siteHead = buildSiteHead(website);
304
+ if (siteHead) {
305
+ template = template.replace("</head>", `${siteHead}
306
+ </head>`);
307
+ }
128
308
  const serverMod = await import(node_url.pathToFileURL(absServerEntry).href);
129
309
  if (typeof serverMod.render !== "function") {
130
310
  throw new TypeError(
@@ -153,7 +333,8 @@ async function prerender({
153
333
  try {
154
334
  const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
155
335
  manifest,
156
- template
336
+ template,
337
+ website
157
338
  });
158
339
  const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
159
340
  const outfile = outFilePath(absClient, urlPath, mode);
@@ -171,6 +352,31 @@ async function prerender({
171
352
  `, err.stack ?? err);
172
353
  }
173
354
  }
355
+ const websiteMeta = (website == null ? void 0 : website.meta) ?? {};
356
+ const canonicalBase = (websiteMeta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
357
+ if (canonicalBase && rendered.length > 0) {
358
+ const sitemap = generateSitemapXml({ renderedPaths: rendered, canonicalBase });
359
+ await fs.writeFile(path.join(absClient, "sitemap.xml"), sitemap, "utf8");
360
+ console.log(` Generated sitemap.xml (${rendered.length} URLs)`);
361
+ }
362
+ const robotsTxt = generateRobotsTxt({
363
+ robotsTxt: website == null ? void 0 : website.robots_txt,
364
+ canonicalBase,
365
+ noindex: websiteMeta.noindex
366
+ });
367
+ await fs.writeFile(path.join(absClient, "robots.txt"), robotsTxt, "utf8");
368
+ console.log(" Generated robots.txt");
369
+ if (redirects.length > 0) {
370
+ const redirectsContent = generateNetlifyRedirects(redirects);
371
+ const redirectsPath = path.join(absClient, "_redirects");
372
+ let existing = "";
373
+ try {
374
+ existing = await fs.readFile(redirectsPath, "utf8");
375
+ } catch {
376
+ }
377
+ await fs.writeFile(redirectsPath, redirectsContent + existing, "utf8");
378
+ console.log(` Generated _redirects (${redirects.length} rules)`);
379
+ }
174
380
  return {
175
381
  rendered,
176
382
  failures,
@@ -256,6 +462,9 @@ function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "
256
462
  </head>`);
257
463
  }
258
464
  if (head) {
465
+ if (/<title>/.test(head)) {
466
+ out = out.replace("<!--ssg:title-->", "");
467
+ }
259
468
  const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
260
469
  const descMatches = [...head.matchAll(descTagRe)];
261
470
  if (descMatches.length > 1) {
@@ -298,6 +507,12 @@ function discoverInternalLinks(html) {
298
507
  }
299
508
  return [...out];
300
509
  }
510
+ exports.buildPageHead = buildPageHead;
511
+ exports.buildSiteHead = buildSiteHead;
301
512
  exports.fetchCmsPrerenderPaths = fetchCmsPrerenderPaths;
513
+ exports.fetchCmsSiteData = fetchCmsSiteData;
514
+ exports.generateNetlifyRedirects = generateNetlifyRedirects;
515
+ exports.generateRobotsTxt = generateRobotsTxt;
516
+ exports.generateSitemapXml = generateSitemapXml;
302
517
  exports.polyfillBloxSsgGlobals = polyfillBloxSsgGlobals;
303
518
  exports.prerender = prerender;
package/dist/ssg/cli.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  "use strict";
3
3
  const process = require("node:process");
4
- const prerender = require("../prerender-6jE_obPj.cjs");
4
+ const prerender = require("../prerender-DN7Ugbtc.cjs");
5
5
  async function resolveApiBase(mode) {
6
6
  if (process.env.BAGELINK_API_URL != null && process.env.BAGELINK_API_URL !== "") {
7
7
  return { apiBase: process.env.BAGELINK_API_URL };
@@ -72,11 +72,17 @@ Environment:
72
72
  const websiteName = process.env.WEBSITE_NAME ?? configWebsiteName ?? "default";
73
73
  console.log(`Fetching routes from ${apiBase} (mode: ${mode}, site: ${websiteName})…`);
74
74
  let paths = [];
75
+ let website = null;
76
+ let redirects = [];
75
77
  try {
76
- paths = await prerender.fetchCmsPrerenderPaths(apiBase, websiteName);
78
+ const siteData = await prerender.fetchCmsSiteData(apiBase, websiteName);
79
+ paths = siteData.paths;
80
+ website = siteData.website;
81
+ redirects = siteData.redirects;
77
82
  console.log(` Found ${paths.length} CMS routes`);
83
+ if (redirects.length > 0) console.log(` Found ${redirects.length} redirects`);
78
84
  } catch (err) {
79
- console.error(" Failed to fetch CMS routes:", err.message);
85
+ console.error(" Failed to fetch CMS data:", err.message);
80
86
  paths = ["/"];
81
87
  }
82
88
  for (const p of extraPaths) {
@@ -92,7 +98,9 @@ Environment:
92
98
  crawl,
93
99
  excludePaths,
94
100
  maxPages: 5e3,
95
- mode: "file"
101
+ mode: "file",
102
+ website,
103
+ redirects
96
104
  });
97
105
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(2);
98
106
  console.log(`
package/dist/ssg/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import process from "node:process";
3
- import { p as polyfillBloxSsgGlobals, f as fetchCmsPrerenderPaths, a as prerender } from "../prerender-DYmDaqcz.js";
3
+ import { p as polyfillBloxSsgGlobals, a as fetchCmsSiteData, c as prerender } from "../prerender-BXf1VF7r.js";
4
4
  async function resolveApiBase(mode) {
5
5
  if (process.env.BAGELINK_API_URL != null && process.env.BAGELINK_API_URL !== "") {
6
6
  return { apiBase: process.env.BAGELINK_API_URL };
@@ -71,11 +71,17 @@ Environment:
71
71
  const websiteName = process.env.WEBSITE_NAME ?? configWebsiteName ?? "default";
72
72
  console.log(`Fetching routes from ${apiBase} (mode: ${mode}, site: ${websiteName})…`);
73
73
  let paths = [];
74
+ let website = null;
75
+ let redirects = [];
74
76
  try {
75
- paths = await fetchCmsPrerenderPaths(apiBase, websiteName);
77
+ const siteData = await fetchCmsSiteData(apiBase, websiteName);
78
+ paths = siteData.paths;
79
+ website = siteData.website;
80
+ redirects = siteData.redirects;
76
81
  console.log(` Found ${paths.length} CMS routes`);
82
+ if (redirects.length > 0) console.log(` Found ${redirects.length} redirects`);
77
83
  } catch (err) {
78
- console.error(" Failed to fetch CMS routes:", err.message);
84
+ console.error(" Failed to fetch CMS data:", err.message);
79
85
  paths = ["/"];
80
86
  }
81
87
  for (const p of extraPaths) {
@@ -91,7 +97,9 @@ Environment:
91
97
  crawl,
92
98
  excludePaths,
93
99
  maxPages: 5e3,
94
- mode: "file"
100
+ mode: "file",
101
+ website,
102
+ redirects
95
103
  });
96
104
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(2);
97
105
  console.log(`
@@ -1,8 +1,18 @@
1
+ import { WebsiteRead } from '../api/types';
2
+ import { RedirectEntry } from './seo';
3
+ export interface CmsSiteData {
4
+ websiteId: string;
5
+ website: WebsiteRead;
6
+ paths: string[];
7
+ redirects: RedirectEntry[];
8
+ }
1
9
  /**
2
- * Collect prerender paths from the Bagelink CMS.
3
- *
4
- * Static slugs are returned as-is. Template pages (e.g. `/blog/:slug`) are
5
- * expanded into concrete paths using their `data_bindings` + the datastore.
10
+ * Fetch everything the SSG needs from the CMS in one go:
11
+ * website settings, page routes, and redirects.
12
+ */
13
+ export declare function fetchCmsSiteData(apiBase: string, websiteName: string): Promise<CmsSiteData>;
14
+ /**
15
+ * Legacy compat: fetch only prerender paths (no website/redirect data).
6
16
  */
7
17
  export declare function fetchCmsPrerenderPaths(apiBase: string, websiteName: string): Promise<string[]>;
8
18
  //# sourceMappingURL=cms-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cms-routes.d.ts","sourceRoot":"","sources":["../../src/ssg/cms-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAqDnB"}
1
+ {"version":3,"file":"cms-routes.d.ts","sourceRoot":"","sources":["../../src/ssg/cms-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAY1C,MAAM,WAAW,WAAW;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,WAAW,CAAA;IACpB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,SAAS,EAAE,aAAa,EAAE,CAAA;CAC1B;AAMD;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAqCtB;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const prerender = require("../prerender-6jE_obPj.cjs");
3
+ const prerender = require("../prerender-DN7Ugbtc.cjs");
4
4
  const constants = require("../constants-fZvybj0k.cjs");
5
5
  const ssg_client = require("./client.cjs");
6
6
  async function renderBloxSsgPage(options) {
@@ -9,7 +9,8 @@ async function renderBloxSsgPage(options) {
9
9
  resolvedData,
10
10
  renderToString,
11
11
  createAppForUrl,
12
- stateWindowKey = constants.BLOX_STATE_WINDOW_KEY
12
+ stateWindowKey = constants.BLOX_STATE_WINDOW_KEY,
13
+ website = null
13
14
  } = options;
14
15
  const g = globalThis;
15
16
  const prevState = g[stateWindowKey];
@@ -21,8 +22,16 @@ async function renderBloxSsgPage(options) {
21
22
  await router.push(url);
22
23
  await router.isReady();
23
24
  const html = await renderToString(app);
24
- const head = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
25
- return { html, head };
25
+ const stateScript = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
26
+ const head = prerender.buildPageHead({
27
+ url,
28
+ resolvedData,
29
+ website,
30
+ stateScript
31
+ });
32
+ const lang = (website == null ? void 0 : website.default_locale) || "en";
33
+ const htmlAttrs = `lang="${lang}"`;
34
+ return { html, head, htmlAttrs };
26
35
  } finally {
27
36
  if (prevState !== void 0) {
28
37
  g[stateWindowKey] = prevState;
@@ -31,7 +40,13 @@ async function renderBloxSsgPage(options) {
31
40
  }
32
41
  }
33
42
  }
43
+ exports.buildPageHead = prerender.buildPageHead;
44
+ exports.buildSiteHead = prerender.buildSiteHead;
34
45
  exports.fetchCmsPrerenderPaths = prerender.fetchCmsPrerenderPaths;
46
+ exports.fetchCmsSiteData = prerender.fetchCmsSiteData;
47
+ exports.generateNetlifyRedirects = prerender.generateNetlifyRedirects;
48
+ exports.generateRobotsTxt = prerender.generateRobotsTxt;
49
+ exports.generateSitemapXml = prerender.generateSitemapXml;
35
50
  exports.polyfillBloxSsgGlobals = prerender.polyfillBloxSsgGlobals;
36
51
  exports.prerender = prerender.prerender;
37
52
  exports.BLOX_STATE_WINDOW_KEY = constants.BLOX_STATE_WINDOW_KEY;
@@ -1,13 +1,15 @@
1
- export { fetchCmsPrerenderPaths } from './cms-routes';
1
+ export { fetchCmsPrerenderPaths, fetchCmsSiteData } from './cms-routes';
2
+ export type { CmsSiteData } from './cms-routes';
2
3
  /**
3
4
  * @bagelink/blox/ssg — Static Site Generation helpers for Blox CMS sites.
4
5
  *
5
6
  * Provides everything needed to prerender a Vue + Blox app at build time:
6
7
  * - Browser global polyfills for Node.js
7
8
  * - CMS route discovery (including template page expansion via data_bindings)
8
- * - Double-render page renderer with state embedding
9
+ * - Page renderer with state embedding and full SEO head
9
10
  * - Client-side fetch interceptor for zero-API-call hydration
10
11
  * - Full prerender orchestrator with crawl support
12
+ * - SEO asset generation (sitemap.xml, robots.txt, _redirects)
11
13
  */
12
14
  export { BLOX_STATE_WINDOW_KEY } from './constants';
13
15
  export { polyfillBloxSsgGlobals } from './polyfill-node';
@@ -15,5 +17,7 @@ export { prerender } from './prerender';
15
17
  export type { PrerenderOptions, PrerenderResult, RenderContext, RenderResult } from './prerender';
16
18
  export { renderBloxSsgPage } from './render-resolved-page';
17
19
  export type { BloxSsgRouterLike } from './render-resolved-page';
20
+ export { buildPageHead, buildSiteHead, generateNetlifyRedirects, generateRobotsTxt, generateSitemapXml } from './seo';
21
+ export type { RedirectEntry, SeoPageData, WebsiteMeta } from './seo';
18
22
  export { installBloxStateCache } from './state-cache';
19
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssg/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACrD;;;;;;;;;GASG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssg/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C;;;;;;;;;;GAUG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAA;AACrH,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA"}
@@ -1,4 +1,5 @@
1
- import { f, p, a } from "../prerender-DYmDaqcz.js";
1
+ import { b as buildPageHead } from "../prerender-BXf1VF7r.js";
2
+ import { d, f, a, g, e, h, p, c } from "../prerender-BXf1VF7r.js";
2
3
  import { B as BLOX_STATE_WINDOW_KEY } from "../constants-BIbQhd3z.js";
3
4
  import { installBloxStateCache } from "./client.mjs";
4
5
  async function renderBloxSsgPage(options) {
@@ -7,33 +8,48 @@ async function renderBloxSsgPage(options) {
7
8
  resolvedData,
8
9
  renderToString,
9
10
  createAppForUrl,
10
- stateWindowKey = BLOX_STATE_WINDOW_KEY
11
+ stateWindowKey = BLOX_STATE_WINDOW_KEY,
12
+ website = null
11
13
  } = options;
12
- const g = globalThis;
13
- const prevState = g[stateWindowKey];
14
+ const g2 = globalThis;
15
+ const prevState = g2[stateWindowKey];
14
16
  if (resolvedData != null) {
15
- g[stateWindowKey] = { [url]: resolvedData };
17
+ g2[stateWindowKey] = { [url]: resolvedData };
16
18
  }
17
19
  try {
18
20
  const { app, router } = createAppForUrl(url);
19
21
  await router.push(url);
20
22
  await router.isReady();
21
23
  const html = await renderToString(app);
22
- const head = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
23
- return { html, head };
24
+ const stateScript = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
25
+ const head = buildPageHead({
26
+ url,
27
+ resolvedData,
28
+ website,
29
+ stateScript
30
+ });
31
+ const lang = (website == null ? void 0 : website.default_locale) || "en";
32
+ const htmlAttrs = `lang="${lang}"`;
33
+ return { html, head, htmlAttrs };
24
34
  } finally {
25
35
  if (prevState !== void 0) {
26
- g[stateWindowKey] = prevState;
36
+ g2[stateWindowKey] = prevState;
27
37
  } else {
28
- delete g[stateWindowKey];
38
+ delete g2[stateWindowKey];
29
39
  }
30
40
  }
31
41
  }
32
42
  export {
33
43
  BLOX_STATE_WINDOW_KEY,
44
+ buildPageHead,
45
+ d as buildSiteHead,
34
46
  f as fetchCmsPrerenderPaths,
47
+ a as fetchCmsSiteData,
48
+ g as generateNetlifyRedirects,
49
+ e as generateRobotsTxt,
50
+ h as generateSitemapXml,
35
51
  installBloxStateCache,
36
52
  p as polyfillBloxSsgGlobals,
37
- a as prerender,
53
+ c as prerender,
38
54
  renderBloxSsgPage
39
55
  };
@@ -1,3 +1,5 @@
1
+ import { WebsiteRead } from '../api/types';
2
+ import { RedirectEntry } from './seo';
1
3
  type ExcludeRule = string | RegExp | ((path: string) => boolean);
2
4
  export interface PrerenderOptions {
3
5
  root?: string;
@@ -9,10 +11,16 @@ export interface PrerenderOptions {
9
11
  failFast?: boolean;
10
12
  maxPages?: number;
11
13
  mode?: 'dir' | 'file';
14
+ /** Website settings for SEO injection. */
15
+ website?: WebsiteRead | null;
16
+ /** Redirects to write as _redirects (Netlify). */
17
+ redirects?: RedirectEntry[];
12
18
  }
13
19
  export interface RenderContext {
14
20
  manifest: Record<string, string[]> | null;
15
21
  template: string;
22
+ /** Website settings passed through to the render function. */
23
+ website?: WebsiteRead | null;
16
24
  }
17
25
  export interface RenderResult {
18
26
  html: string;
@@ -38,7 +46,9 @@ export interface PrerenderResult {
38
46
  * - "crawl" (default true) discovers internal links in rendered HTML and queues them.
39
47
  * - excludePaths can be strings, RegExp, or a predicate function.
40
48
  * - Route-level failures are logged and skipped by default.
49
+ * - Generates sitemap.xml, robots.txt, and _redirects post-render.
50
+ * - Strips hardcoded SEO tags from template and injects per-page equivalents.
41
51
  */
42
- export declare function prerender({ root, clientOutDir, serverEntry, paths, crawl, excludePaths, failFast, maxPages, mode, }?: PrerenderOptions): Promise<PrerenderResult>;
52
+ export declare function prerender({ root, clientOutDir, serverEntry, paths, crawl, excludePaths, failFast, maxPages, mode, website, redirects, }?: PrerenderOptions): Promise<PrerenderResult>;
43
53
  export {};
44
54
  //# sourceMappingURL=prerender.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prerender.d.ts","sourceRoot":"","sources":["../../src/ssg/prerender.ts"],"names":[],"mappings":"AAKA,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;AAEhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,YAAY,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAA;IACzC,QAAQ,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAMD,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC7D,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,EAC/B,IAAoB,EACpB,YAA4B,EAC5B,WAA0C,EAC1C,KAAU,EACV,KAAY,EACZ,YAAiB,EACjB,QAAgB,EAChB,QAAe,EACf,IAAY,GACZ,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6ElD"}
1
+ {"version":3,"file":"prerender.d.ts","sourceRoot":"","sources":["../../src/ssg/prerender.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAS1C,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;AAEhE,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,YAAY,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,kDAAkD;IAClD,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAA;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAMD,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC7D,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,EAC/B,IAAoB,EACpB,YAA4B,EAC5B,WAA0C,EAC1C,KAAU,EACV,KAAY,EACZ,YAAiB,EACjB,QAAgB,EAChB,QAAe,EACf,IAAY,EACZ,OAAc,EACd,SAAc,GACd,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CA0HlD"}
@@ -1,16 +1,19 @@
1
+ import { WebsiteRead } from '../api/types';
1
2
  export interface BloxSsgRouterLike {
2
3
  push: (url: string) => Promise<void>;
3
4
  isReady: () => Promise<void>;
4
5
  }
5
6
  /**
6
- * Render a Blox CMS page to static HTML with embedded state.
7
+ * Render a Blox CMS page to static HTML with embedded state and full SEO head.
7
8
  *
8
9
  * Sets `globalThis.__BLOX_STATE__` *before* rendering so that
9
10
  * `CmsPageView` can hydrate synchronously from the embedded data
10
11
  * instead of waiting for an async fetch.
11
12
  *
12
- * Returns `{ html, head }` where `head` contains a `<script>` that sets
13
- * `window.__BLOX_STATE__` for zero-API-call client hydration.
13
+ * Returns `{ html, head, htmlAttrs }` where:
14
+ * - `head` contains per-page `<title>`, meta tags, OG, canonical, hreflang,
15
+ * plus the `<script>` that sets `window.__BLOX_STATE__`
16
+ * - `htmlAttrs` contains the `lang` attribute
14
17
  */
15
18
  export declare function renderBloxSsgPage<TApp>(options: {
16
19
  url: string;
@@ -21,8 +24,11 @@ export declare function renderBloxSsgPage<TApp>(options: {
21
24
  router: BloxSsgRouterLike;
22
25
  };
23
26
  stateWindowKey?: string;
27
+ /** Website settings for SEO tags. If omitted, only state script is emitted. */
28
+ website?: WebsiteRead | null;
24
29
  }): Promise<{
25
30
  html: string;
26
31
  head?: string;
32
+ htmlAttrs?: string;
27
33
  }>;
28
34
  //# sourceMappingURL=render-resolved-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render-resolved-page.d.ts","sourceRoot":"","sources":["../../src/ssg/render-resolved-page.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE;IACtD,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,cAAc,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,iBAAiB,CAAA;KAAE,CAAA;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqC3C"}
1
+ {"version":3,"file":"render-resolved-page.d.ts","sourceRoot":"","sources":["../../src/ssg/render-resolved-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAK/C,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE;IACtD,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,cAAc,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,iBAAiB,CAAA;KAAE,CAAA;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CAC5B,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkD/D"}