@bagelink/blox 1.12.22 → 1.13.0

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.
@@ -2,18 +2,43 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { pathToFileURL } from "node:url";
5
- async function fetchCmsPrerenderPaths(apiBase, websiteName) {
5
+ async function fetchCmsSiteData(apiBase, websiteName) {
6
6
  const sitesRes = await fetch(`${apiBase}/cms/websites`);
7
7
  const sitesData = await sitesRes.json();
8
8
  const sites = Array.isArray(sitesData) ? sitesData : sitesData.data ?? [];
9
9
  const site = sites.find((s) => s.name === websiteName);
10
10
  if ((site == null ? void 0 : site.id) == null || site.id === "") throw new Error(`Website "${websiteName}" not found at ${apiBase}`);
11
- const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=en`);
11
+ let website = site;
12
+ try {
13
+ const wRes = await fetch(`${apiBase}/cms/websites/${site.id}`);
14
+ if (wRes.ok) website = await wRes.json();
15
+ } catch {
16
+ }
17
+ const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=${website.default_locale || "en"}`);
12
18
  const pagesData = await pagesRes.json();
13
19
  const pages = Array.isArray(pagesData) ? pagesData : pagesData.data ?? [];
20
+ const paths = await expandRoutes(apiBase, pages);
21
+ let redirects = [];
22
+ try {
23
+ const rRes = await fetch(`${apiBase}/cms/websites/${site.id}/redirects`);
24
+ if (rRes.ok) {
25
+ const rData = await rRes.json();
26
+ const items = Array.isArray(rData) ? rData : rData.data ?? [];
27
+ redirects = items.filter((r) => r.from_path && r.to_path);
28
+ }
29
+ } catch {
30
+ }
31
+ return { websiteId: site.id, website, paths, redirects };
32
+ }
33
+ async function fetchCmsPrerenderPaths(apiBase, websiteName) {
34
+ const { paths } = await fetchCmsSiteData(apiBase, websiteName);
35
+ return paths;
36
+ }
37
+ async function expandRoutes(apiBase, pages) {
14
38
  const routes = [];
15
39
  for (const page of pages) {
16
40
  const slug = page.slug ?? "/";
41
+ if (page.status && page.status !== "published") continue;
17
42
  if (slug.includes(":") && page.data_bindings) {
18
43
  for (const [, binding] of Object.entries(page.data_bindings)) {
19
44
  const b = binding;
@@ -27,7 +52,10 @@ async function fetchCmsPrerenderPaths(apiBase, websiteName) {
27
52
  for (const item of items) {
28
53
  const bindValue = item[b.bind_by];
29
54
  if (bindValue != null && bindValue !== "") {
30
- const concrete = slug.replace(/:([a-z_]+)/g, (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? ""));
55
+ const concrete = slug.replace(
56
+ /:([a-z_]+)/g,
57
+ (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? "")
58
+ );
31
59
  routes.push(concrete);
32
60
  }
33
61
  }
@@ -86,6 +114,133 @@ async function polyfillBloxSsgGlobals(options) {
86
114
  g.window.location = _win.location;
87
115
  g.window.document = _win.document;
88
116
  }
117
+ function buildPageHead(options) {
118
+ const { url, resolvedData, website, stateScript } = options;
119
+ const parts = [];
120
+ const meta = (website == null ? void 0 : website.meta) ?? {};
121
+ const page = resolvedData == null ? void 0 : resolvedData.page;
122
+ const title = (page == null ? void 0 : page.meta_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;
123
+ if (title) {
124
+ parts.push(`<title>${esc(title)}</title>`);
125
+ }
126
+ const description = (page == null ? void 0 : page.meta_description) || meta.default_meta_description;
127
+ if (description) {
128
+ parts.push(`<meta name="description" content="${esc(description)}">`);
129
+ }
130
+ const canonicalBase = (meta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
131
+ if (canonicalBase) {
132
+ parts.push(`<link rel="canonical" href="${esc(canonicalBase + url)}">`);
133
+ }
134
+ const ogTitle = (page == null ? void 0 : page.meta_title) || (page == null ? void 0 : page.title) || meta.og_site_name || meta.default_meta_title;
135
+ const ogDesc = (page == null ? void 0 : page.meta_description) || meta.og_description || meta.default_meta_description;
136
+ const ogImage = (page == null ? void 0 : page.og_image) || meta.default_og_image;
137
+ const ogType = meta.og_type || "website";
138
+ if (ogTitle) parts.push(`<meta property="og:title" content="${esc(ogTitle)}">`);
139
+ if (ogDesc) parts.push(`<meta property="og:description" content="${esc(ogDesc)}">`);
140
+ if (ogImage) parts.push(`<meta property="og:image" content="${esc(ogImage)}">`);
141
+ parts.push(`<meta property="og:type" content="${esc(ogType)}">`);
142
+ if (canonicalBase) parts.push(`<meta property="og:url" content="${esc(canonicalBase + url)}">`);
143
+ if (meta.og_site_name) parts.push(`<meta property="og:site_name" content="${esc(meta.og_site_name)}">`);
144
+ const twitterCard = meta.twitter_card || "summary_large_image";
145
+ parts.push(`<meta name="twitter:card" content="${esc(twitterCard)}">`);
146
+ if (ogTitle) parts.push(`<meta name="twitter:title" content="${esc(ogTitle)}">`);
147
+ if (ogDesc) parts.push(`<meta name="twitter:description" content="${esc(ogDesc)}">`);
148
+ if (ogImage) parts.push(`<meta name="twitter:image" content="${esc(ogImage)}">`);
149
+ if (meta.twitter_site) parts.push(`<meta name="twitter:site" content="${esc(meta.twitter_site)}">`);
150
+ const alternates = resolvedData == null ? void 0 : resolvedData.alternates;
151
+ if (alternates && canonicalBase) {
152
+ for (const [locale, slug] of Object.entries(alternates)) {
153
+ parts.push(`<link rel="alternate" hreflang="${esc(locale)}" href="${esc(canonicalBase + slug)}">`);
154
+ }
155
+ const defaultSlug = alternates[(website == null ? void 0 : website.default_locale) ?? "en"] ?? url;
156
+ parts.push(`<link rel="alternate" hreflang="x-default" href="${esc(canonicalBase + defaultSlug)}">`);
157
+ }
158
+ if (meta.noindex) {
159
+ parts.push('<meta name="robots" content="noindex, nofollow">');
160
+ }
161
+ if (stateScript) {
162
+ parts.push(stateScript);
163
+ }
164
+ return parts.join("\n");
165
+ }
166
+ function buildSiteHead(website) {
167
+ if (!website) return "";
168
+ const parts = [];
169
+ const meta = website.meta ?? {};
170
+ if (website.favicon_url) {
171
+ parts.push(`<link rel="icon" href="${esc(website.favicon_url)}">`);
172
+ }
173
+ if (meta.webclip) {
174
+ parts.push(`<link rel="apple-touch-icon" sizes="180x180" href="${esc(meta.webclip)}">`);
175
+ }
176
+ if (meta.verification_google) {
177
+ parts.push(`<meta name="google-site-verification" content="${esc(meta.verification_google)}">`);
178
+ }
179
+ if (meta.verification_bing) {
180
+ parts.push(`<meta name="msvalidate.01" content="${esc(meta.verification_bing)}">`);
181
+ }
182
+ if (meta.structured_data_org) {
183
+ try {
184
+ const json = typeof meta.structured_data_org === "string" ? meta.structured_data_org : JSON.stringify(meta.structured_data_org);
185
+ parts.push(`<script type="application/ld+json">${json}${"<"}/script>`);
186
+ } catch {
187
+ }
188
+ }
189
+ const plausibleId = website.plausible_site_id;
190
+ if (plausibleId) {
191
+ parts.push(`<script defer data-domain="${esc(plausibleId)}" src="https://plausible.io/js/script.js"><${"/"}script>`);
192
+ }
193
+ return parts.join("\n");
194
+ }
195
+ function generateSitemapXml(options) {
196
+ const { renderedPaths, canonicalBase, defaultChangefreq = "weekly", defaultPriority = "0.7" } = options;
197
+ const base = canonicalBase.replace(/\/$/, "");
198
+ const urls = renderedPaths.map((p) => {
199
+ const priority = p === "/" ? "1.0" : defaultPriority;
200
+ const changefreq = p === "/" ? "daily" : defaultChangefreq;
201
+ return ` <url>
202
+ <loc>${esc(base + p)}</loc>
203
+ <changefreq>${changefreq}</changefreq>
204
+ <priority>${priority}</priority>
205
+ </url>`;
206
+ });
207
+ return `<?xml version="1.0" encoding="UTF-8"?>
208
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
209
+ ${urls.join("\n")}
210
+ </urlset>
211
+ `;
212
+ }
213
+ function generateRobotsTxt(options) {
214
+ const { robotsTxt, canonicalBase, noindex } = options;
215
+ if (noindex) {
216
+ return `User-agent: *
217
+ Disallow: /
218
+ `;
219
+ }
220
+ const base = (canonicalBase || "").replace(/\/$/, "");
221
+ const sitemapLine = base ? `
222
+ Sitemap: ${base}/sitemap.xml
223
+ ` : "";
224
+ if (robotsTxt) {
225
+ if (sitemapLine && !robotsTxt.toLowerCase().includes("sitemap:")) {
226
+ return robotsTxt.trimEnd() + "\n" + sitemapLine;
227
+ }
228
+ return robotsTxt;
229
+ }
230
+ return `User-agent: *
231
+ Allow: /
232
+ ${sitemapLine}`;
233
+ }
234
+ function generateNetlifyRedirects(redirects) {
235
+ if (redirects.length === 0) return "";
236
+ return redirects.map((r) => `${r.from_path} ${r.to_path} ${r.status_code}`).join("\n") + "\n";
237
+ }
238
+ function stripTemplateSeoTags(template) {
239
+ 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");
240
+ }
241
+ function esc(s) {
242
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
243
+ }
89
244
  async function prerender({
90
245
  root = process.cwd(),
91
246
  clientOutDir = "dist/client",
@@ -95,13 +250,21 @@ async function prerender({
95
250
  excludePaths = [],
96
251
  failFast = false,
97
252
  maxPages = 5e3,
98
- mode = "dir"
253
+ mode = "dir",
254
+ website = null,
255
+ redirects = []
99
256
  } = {}) {
100
257
  const absRoot = path.resolve(root);
101
258
  const absClient = path.resolve(absRoot, clientOutDir);
102
259
  const absServerEntry = path.resolve(absRoot, serverEntry);
103
260
  const templatePath = path.join(absClient, "index.html");
104
- const template = await fs.readFile(templatePath, "utf8");
261
+ const rawTemplate = await fs.readFile(templatePath, "utf8");
262
+ let template = stripTemplateSeoTags(rawTemplate);
263
+ const siteHead = buildSiteHead(website);
264
+ if (siteHead) {
265
+ template = template.replace("</head>", `${siteHead}
266
+ </head>`);
267
+ }
105
268
  const serverMod = await import(pathToFileURL(absServerEntry).href);
106
269
  if (typeof serverMod.render !== "function") {
107
270
  throw new TypeError(
@@ -130,7 +293,8 @@ async function prerender({
130
293
  try {
131
294
  const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
132
295
  manifest,
133
- template
296
+ template,
297
+ website
134
298
  });
135
299
  const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
136
300
  const outfile = outFilePath(absClient, urlPath, mode);
@@ -148,6 +312,31 @@ async function prerender({
148
312
  `, err.stack ?? err);
149
313
  }
150
314
  }
315
+ const websiteMeta = (website == null ? void 0 : website.meta) ?? {};
316
+ const canonicalBase = (websiteMeta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
317
+ if (canonicalBase && rendered.length > 0) {
318
+ const sitemap = generateSitemapXml({ renderedPaths: rendered, canonicalBase });
319
+ await fs.writeFile(path.join(absClient, "sitemap.xml"), sitemap, "utf8");
320
+ console.log(` Generated sitemap.xml (${rendered.length} URLs)`);
321
+ }
322
+ const robotsTxt = generateRobotsTxt({
323
+ robotsTxt: website == null ? void 0 : website.robots_txt,
324
+ canonicalBase,
325
+ noindex: websiteMeta.noindex
326
+ });
327
+ await fs.writeFile(path.join(absClient, "robots.txt"), robotsTxt, "utf8");
328
+ console.log(" Generated robots.txt");
329
+ if (redirects.length > 0) {
330
+ const redirectsContent = generateNetlifyRedirects(redirects);
331
+ const redirectsPath = path.join(absClient, "_redirects");
332
+ let existing = "";
333
+ try {
334
+ existing = await fs.readFile(redirectsPath, "utf8");
335
+ } catch {
336
+ }
337
+ await fs.writeFile(redirectsPath, redirectsContent + existing, "utf8");
338
+ console.log(` Generated _redirects (${redirects.length} rules)`);
339
+ }
151
340
  return {
152
341
  rendered,
153
342
  failures,
@@ -233,6 +422,9 @@ function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "
233
422
  </head>`);
234
423
  }
235
424
  if (head) {
425
+ if (/<title>/.test(head)) {
426
+ out = out.replace("<!--ssg:title-->", "");
427
+ }
236
428
  const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
237
429
  const descMatches = [...head.matchAll(descTagRe)];
238
430
  if (descMatches.length > 1) {
@@ -276,7 +468,13 @@ function discoverInternalLinks(html) {
276
468
  return [...out];
277
469
  }
278
470
  export {
279
- prerender as a,
471
+ fetchCmsSiteData as a,
472
+ buildPageHead as b,
473
+ prerender as c,
474
+ buildSiteHead as d,
475
+ generateRobotsTxt as e,
280
476
  fetchCmsPrerenderPaths as f,
477
+ generateNetlifyRedirects as g,
478
+ generateSitemapXml as h,
281
479
  polyfillBloxSsgGlobals as p
282
480
  };
@@ -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,133 @@ 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 title = (page == null ? void 0 : page.meta_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;
146
+ if (title) {
147
+ parts.push(`<title>${esc(title)}</title>`);
148
+ }
149
+ const description = (page == null ? void 0 : page.meta_description) || meta.default_meta_description;
150
+ if (description) {
151
+ parts.push(`<meta name="description" content="${esc(description)}">`);
152
+ }
153
+ const canonicalBase = (meta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
154
+ if (canonicalBase) {
155
+ parts.push(`<link rel="canonical" href="${esc(canonicalBase + url)}">`);
156
+ }
157
+ const ogTitle = (page == null ? void 0 : page.meta_title) || (page == null ? void 0 : page.title) || meta.og_site_name || meta.default_meta_title;
158
+ const ogDesc = (page == null ? void 0 : page.meta_description) || meta.og_description || meta.default_meta_description;
159
+ const ogImage = (page == null ? void 0 : page.og_image) || meta.default_og_image;
160
+ const ogType = meta.og_type || "website";
161
+ if (ogTitle) parts.push(`<meta property="og:title" content="${esc(ogTitle)}">`);
162
+ if (ogDesc) parts.push(`<meta property="og:description" content="${esc(ogDesc)}">`);
163
+ if (ogImage) parts.push(`<meta property="og:image" content="${esc(ogImage)}">`);
164
+ parts.push(`<meta property="og:type" content="${esc(ogType)}">`);
165
+ if (canonicalBase) parts.push(`<meta property="og:url" content="${esc(canonicalBase + url)}">`);
166
+ if (meta.og_site_name) parts.push(`<meta property="og:site_name" content="${esc(meta.og_site_name)}">`);
167
+ const twitterCard = meta.twitter_card || "summary_large_image";
168
+ parts.push(`<meta name="twitter:card" content="${esc(twitterCard)}">`);
169
+ if (ogTitle) parts.push(`<meta name="twitter:title" content="${esc(ogTitle)}">`);
170
+ if (ogDesc) parts.push(`<meta name="twitter:description" content="${esc(ogDesc)}">`);
171
+ if (ogImage) parts.push(`<meta name="twitter:image" content="${esc(ogImage)}">`);
172
+ if (meta.twitter_site) parts.push(`<meta name="twitter:site" content="${esc(meta.twitter_site)}">`);
173
+ const alternates = resolvedData == null ? void 0 : resolvedData.alternates;
174
+ if (alternates && canonicalBase) {
175
+ for (const [locale, slug] of Object.entries(alternates)) {
176
+ parts.push(`<link rel="alternate" hreflang="${esc(locale)}" href="${esc(canonicalBase + slug)}">`);
177
+ }
178
+ const defaultSlug = alternates[(website == null ? void 0 : website.default_locale) ?? "en"] ?? url;
179
+ parts.push(`<link rel="alternate" hreflang="x-default" href="${esc(canonicalBase + defaultSlug)}">`);
180
+ }
181
+ if (meta.noindex) {
182
+ parts.push('<meta name="robots" content="noindex, nofollow">');
183
+ }
184
+ if (stateScript) {
185
+ parts.push(stateScript);
186
+ }
187
+ return parts.join("\n");
188
+ }
189
+ function buildSiteHead(website) {
190
+ if (!website) return "";
191
+ const parts = [];
192
+ const meta = website.meta ?? {};
193
+ if (website.favicon_url) {
194
+ parts.push(`<link rel="icon" href="${esc(website.favicon_url)}">`);
195
+ }
196
+ if (meta.webclip) {
197
+ parts.push(`<link rel="apple-touch-icon" sizes="180x180" href="${esc(meta.webclip)}">`);
198
+ }
199
+ if (meta.verification_google) {
200
+ parts.push(`<meta name="google-site-verification" content="${esc(meta.verification_google)}">`);
201
+ }
202
+ if (meta.verification_bing) {
203
+ parts.push(`<meta name="msvalidate.01" content="${esc(meta.verification_bing)}">`);
204
+ }
205
+ if (meta.structured_data_org) {
206
+ try {
207
+ const json = typeof meta.structured_data_org === "string" ? meta.structured_data_org : JSON.stringify(meta.structured_data_org);
208
+ parts.push(`<script type="application/ld+json">${json}${"<"}/script>`);
209
+ } catch {
210
+ }
211
+ }
212
+ const plausibleId = website.plausible_site_id;
213
+ if (plausibleId) {
214
+ parts.push(`<script defer data-domain="${esc(plausibleId)}" src="https://plausible.io/js/script.js"><${"/"}script>`);
215
+ }
216
+ return parts.join("\n");
217
+ }
218
+ function generateSitemapXml(options) {
219
+ const { renderedPaths, canonicalBase, defaultChangefreq = "weekly", defaultPriority = "0.7" } = options;
220
+ const base = canonicalBase.replace(/\/$/, "");
221
+ const urls = renderedPaths.map((p) => {
222
+ const priority = p === "/" ? "1.0" : defaultPriority;
223
+ const changefreq = p === "/" ? "daily" : defaultChangefreq;
224
+ return ` <url>
225
+ <loc>${esc(base + p)}</loc>
226
+ <changefreq>${changefreq}</changefreq>
227
+ <priority>${priority}</priority>
228
+ </url>`;
229
+ });
230
+ return `<?xml version="1.0" encoding="UTF-8"?>
231
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
232
+ ${urls.join("\n")}
233
+ </urlset>
234
+ `;
235
+ }
236
+ function generateRobotsTxt(options) {
237
+ const { robotsTxt, canonicalBase, noindex } = options;
238
+ if (noindex) {
239
+ return `User-agent: *
240
+ Disallow: /
241
+ `;
242
+ }
243
+ const base = (canonicalBase || "").replace(/\/$/, "");
244
+ const sitemapLine = base ? `
245
+ Sitemap: ${base}/sitemap.xml
246
+ ` : "";
247
+ if (robotsTxt) {
248
+ if (sitemapLine && !robotsTxt.toLowerCase().includes("sitemap:")) {
249
+ return robotsTxt.trimEnd() + "\n" + sitemapLine;
250
+ }
251
+ return robotsTxt;
252
+ }
253
+ return `User-agent: *
254
+ Allow: /
255
+ ${sitemapLine}`;
256
+ }
257
+ function generateNetlifyRedirects(redirects) {
258
+ if (redirects.length === 0) return "";
259
+ return redirects.map((r) => `${r.from_path} ${r.to_path} ${r.status_code}`).join("\n") + "\n";
260
+ }
261
+ function stripTemplateSeoTags(template) {
262
+ 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");
263
+ }
264
+ function esc(s) {
265
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
266
+ }
112
267
  async function prerender({
113
268
  root = process.cwd(),
114
269
  clientOutDir = "dist/client",
@@ -118,13 +273,21 @@ async function prerender({
118
273
  excludePaths = [],
119
274
  failFast = false,
120
275
  maxPages = 5e3,
121
- mode = "dir"
276
+ mode = "dir",
277
+ website = null,
278
+ redirects = []
122
279
  } = {}) {
123
280
  const absRoot = path.resolve(root);
124
281
  const absClient = path.resolve(absRoot, clientOutDir);
125
282
  const absServerEntry = path.resolve(absRoot, serverEntry);
126
283
  const templatePath = path.join(absClient, "index.html");
127
- const template = await fs.readFile(templatePath, "utf8");
284
+ const rawTemplate = await fs.readFile(templatePath, "utf8");
285
+ let template = stripTemplateSeoTags(rawTemplate);
286
+ const siteHead = buildSiteHead(website);
287
+ if (siteHead) {
288
+ template = template.replace("</head>", `${siteHead}
289
+ </head>`);
290
+ }
128
291
  const serverMod = await import(node_url.pathToFileURL(absServerEntry).href);
129
292
  if (typeof serverMod.render !== "function") {
130
293
  throw new TypeError(
@@ -153,7 +316,8 @@ async function prerender({
153
316
  try {
154
317
  const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
155
318
  manifest,
156
- template
319
+ template,
320
+ website
157
321
  });
158
322
  const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
159
323
  const outfile = outFilePath(absClient, urlPath, mode);
@@ -171,6 +335,31 @@ async function prerender({
171
335
  `, err.stack ?? err);
172
336
  }
173
337
  }
338
+ const websiteMeta = (website == null ? void 0 : website.meta) ?? {};
339
+ const canonicalBase = (websiteMeta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
340
+ if (canonicalBase && rendered.length > 0) {
341
+ const sitemap = generateSitemapXml({ renderedPaths: rendered, canonicalBase });
342
+ await fs.writeFile(path.join(absClient, "sitemap.xml"), sitemap, "utf8");
343
+ console.log(` Generated sitemap.xml (${rendered.length} URLs)`);
344
+ }
345
+ const robotsTxt = generateRobotsTxt({
346
+ robotsTxt: website == null ? void 0 : website.robots_txt,
347
+ canonicalBase,
348
+ noindex: websiteMeta.noindex
349
+ });
350
+ await fs.writeFile(path.join(absClient, "robots.txt"), robotsTxt, "utf8");
351
+ console.log(" Generated robots.txt");
352
+ if (redirects.length > 0) {
353
+ const redirectsContent = generateNetlifyRedirects(redirects);
354
+ const redirectsPath = path.join(absClient, "_redirects");
355
+ let existing = "";
356
+ try {
357
+ existing = await fs.readFile(redirectsPath, "utf8");
358
+ } catch {
359
+ }
360
+ await fs.writeFile(redirectsPath, redirectsContent + existing, "utf8");
361
+ console.log(` Generated _redirects (${redirects.length} rules)`);
362
+ }
174
363
  return {
175
364
  rendered,
176
365
  failures,
@@ -256,6 +445,9 @@ function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "
256
445
  </head>`);
257
446
  }
258
447
  if (head) {
448
+ if (/<title>/.test(head)) {
449
+ out = out.replace("<!--ssg:title-->", "");
450
+ }
259
451
  const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
260
452
  const descMatches = [...head.matchAll(descTagRe)];
261
453
  if (descMatches.length > 1) {
@@ -298,6 +490,12 @@ function discoverInternalLinks(html) {
298
490
  }
299
491
  return [...out];
300
492
  }
493
+ exports.buildPageHead = buildPageHead;
494
+ exports.buildSiteHead = buildSiteHead;
301
495
  exports.fetchCmsPrerenderPaths = fetchCmsPrerenderPaths;
496
+ exports.fetchCmsSiteData = fetchCmsSiteData;
497
+ exports.generateNetlifyRedirects = generateNetlifyRedirects;
498
+ exports.generateRobotsTxt = generateRobotsTxt;
499
+ exports.generateSitemapXml = generateSitemapXml;
302
500
  exports.polyfillBloxSsgGlobals = polyfillBloxSsgGlobals;
303
501
  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-Br-vA3A0.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-1I_qD2kp.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-Br-vA3A0.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-1I_qD2kp.js";
2
+ import { d, f, a, g, e, h, p, c } from "../prerender-1I_qD2kp.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"}
@@ -0,0 +1,66 @@
1
+ import { PageRead, WebsiteRead } from '../api/types';
2
+ /** Resolved page data shape (subset of PageResolveRead). */
3
+ export interface SeoPageData {
4
+ page: PageRead;
5
+ alternates?: Record<string, string>;
6
+ contexts?: Record<string, Record<string, unknown> | null>;
7
+ }
8
+ /** Website meta bag (freeform JSON stored on WebsiteRead.meta). */
9
+ export interface WebsiteMeta {
10
+ default_meta_title?: string;
11
+ default_meta_description?: string;
12
+ default_og_image?: string;
13
+ webclip?: string;
14
+ og_site_name?: string;
15
+ og_description?: string;
16
+ og_type?: string;
17
+ twitter_card?: string;
18
+ twitter_site?: string;
19
+ canonical_base_url?: string;
20
+ noindex?: boolean;
21
+ verification_google?: string;
22
+ verification_bing?: string;
23
+ structured_data_org?: unknown;
24
+ [key: string]: unknown;
25
+ }
26
+ export interface RedirectEntry {
27
+ from_path: string;
28
+ to_path: string;
29
+ status_code: number;
30
+ }
31
+ /**
32
+ * Build a complete `<head>` fragment for a single page.
33
+ *
34
+ * Includes: title, meta description, OG tags, Twitter Card, canonical,
35
+ * hreflang alternates, noindex, and the __BLOX_STATE__ script.
36
+ */
37
+ export declare function buildPageHead(options: {
38
+ url: string;
39
+ resolvedData: SeoPageData | null;
40
+ website: WebsiteRead | null;
41
+ stateScript?: string;
42
+ }): string;
43
+ /**
44
+ * Build site-wide `<head>` tags: verification, structured data, analytics, favicon.
45
+ * These replace hardcoded equivalents in the HTML template.
46
+ */
47
+ export declare function buildSiteHead(website: WebsiteRead | null): string;
48
+ export declare function generateSitemapXml(options: {
49
+ renderedPaths: string[];
50
+ canonicalBase: string;
51
+ defaultChangefreq?: string;
52
+ defaultPriority?: string;
53
+ }): string;
54
+ export declare function generateRobotsTxt(options: {
55
+ robotsTxt?: string | null;
56
+ canonicalBase?: string;
57
+ noindex?: boolean;
58
+ }): string;
59
+ export declare function generateNetlifyRedirects(redirects: RedirectEntry[]): string;
60
+ /**
61
+ * Strip hardcoded SEO tags from the HTML template so SSG can inject
62
+ * per-page equivalents. Removes: title, meta description, og:*, twitter:*,
63
+ * apple-touch-icon, and favicon.
64
+ */
65
+ export declare function stripTemplateSeoTags(template: string): string;
66
+ //# sourceMappingURL=seo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/ssg/seo.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAMzD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,QAAQ,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;CACzD;AAED,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;CACnB;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,WAAW,GAAG,IAAI,CAAA;IAChC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG,MAAM,CAsET;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,CA4CjE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC3C,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,GAAG,MAAM,CAmBT;AAMD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAC1C,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG,MAAM,CAmBT;AAMD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,CAK3E;AAMD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgB7D"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/blox",
3
3
  "type": "module",
4
- "version": "1.12.22",
4
+ "version": "1.13.0",
5
5
  "description": "Blox page builder library for drag-and-drop page building and static data management",
6
6
  "author": {
7
7
  "name": "Bagel Studio",