@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.
@@ -0,0 +1,497 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { pathToFileURL } from "node:url";
5
+ async function fetchCmsSiteData(apiBase, websiteName) {
6
+ const sitesRes = await fetch(`${apiBase}/cms/websites`);
7
+ const sitesData = await sitesRes.json();
8
+ const sites = Array.isArray(sitesData) ? sitesData : sitesData.data ?? [];
9
+ const site = sites.find((s) => s.name === websiteName);
10
+ if ((site == null ? void 0 : site.id) == null || site.id === "") throw new Error(`Website "${websiteName}" not found at ${apiBase}`);
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"}`);
18
+ const pagesData = await pagesRes.json();
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) {
38
+ const routes = [];
39
+ for (const page of pages) {
40
+ const slug = page.slug ?? "/";
41
+ if (page.status && page.status !== "published") continue;
42
+ if (slug.includes(":") && page.data_bindings) {
43
+ for (const [, binding] of Object.entries(page.data_bindings)) {
44
+ const b = binding;
45
+ if (b.adapter === "datastore" && b.collection != null && b.collection !== "" && b.store != null && b.store !== "" && b.bind_by != null && b.bind_by !== "") {
46
+ try {
47
+ const itemsRes = await fetch(
48
+ `${apiBase}/datastore/${b.store}/collections/${b.collection}?limit=200`
49
+ );
50
+ const itemsData = await itemsRes.json();
51
+ const items = Array.isArray(itemsData) ? itemsData : itemsData.data ?? [];
52
+ for (const item of items) {
53
+ const bindValue = item[b.bind_by];
54
+ if (bindValue != null && bindValue !== "") {
55
+ const concrete = slug.replace(
56
+ /:([a-z_]+)/g,
57
+ (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? "")
58
+ );
59
+ routes.push(concrete);
60
+ }
61
+ }
62
+ } catch (e) {
63
+ console.warn(` [blox-ssg] Could not expand ${slug}: ${e.message}`);
64
+ }
65
+ }
66
+ }
67
+ } else if (!slug.includes(":")) {
68
+ routes.push(slug === "" ? "/" : slug);
69
+ }
70
+ }
71
+ return [...new Set(routes)];
72
+ }
73
+ async function polyfillBloxSsgGlobals(options) {
74
+ const { env } = await import("node:process");
75
+ const pageUrl = (options == null ? void 0 : options.pageUrl) ?? env.BAGELINK_API_URL ?? "https://example.com/";
76
+ const { Window: HappyWindow } = await import("happy-dom");
77
+ const _win = new HappyWindow({ url: pageUrl });
78
+ const g = globalThis;
79
+ if (g.window == null) g.window = g;
80
+ for (const key of Object.getOwnPropertyNames(_win)) {
81
+ if (key === "window" || key === "global" || key === "globalThis") continue;
82
+ try {
83
+ if (!(key in g)) g[key] = _win[key];
84
+ } catch {
85
+ }
86
+ }
87
+ for (const key of [
88
+ "document",
89
+ "location",
90
+ "localStorage",
91
+ "sessionStorage",
92
+ "navigator",
93
+ "Element",
94
+ "Document",
95
+ "HTMLElement",
96
+ "Event",
97
+ "CustomEvent",
98
+ "MouseEvent",
99
+ "KeyboardEvent",
100
+ "MutationObserver",
101
+ "IntersectionObserver",
102
+ "ResizeObserver",
103
+ "getComputedStyle",
104
+ "matchMedia",
105
+ "requestAnimationFrame",
106
+ "cancelAnimationFrame"
107
+ ]) {
108
+ try {
109
+ g[key] = _win[key];
110
+ } catch {
111
+ }
112
+ }
113
+ g.window = g;
114
+ g.window.location = _win.location;
115
+ g.window.document = _win.document;
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 ctx = extractPrimaryContext(resolvedData == null ? void 0 : resolvedData.contexts);
123
+ 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;
124
+ if (title) {
125
+ 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);
126
+ parts.push(`<title>${fullTitle}</title>`);
127
+ }
128
+ const description = (page == null ? void 0 : page.meta_description) || ctx.description || meta.default_meta_description;
129
+ if (description) {
130
+ parts.push(`<meta name="description" content="${esc(description)}">`);
131
+ }
132
+ const canonicalBase = (meta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
133
+ if (canonicalBase) {
134
+ parts.push(`<link rel="canonical" href="${esc(canonicalBase + url)}">`);
135
+ }
136
+ 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;
137
+ const ogDesc = (page == null ? void 0 : page.meta_description) || ctx.description || meta.og_description || meta.default_meta_description;
138
+ const ogImage = (page == null ? void 0 : page.og_image) || ctx.image || meta.default_og_image;
139
+ const ogType = meta.og_type || "website";
140
+ if (ogTitle) parts.push(`<meta property="og:title" content="${esc(ogTitle)}">`);
141
+ if (ogDesc) parts.push(`<meta property="og:description" content="${esc(ogDesc)}">`);
142
+ if (ogImage) parts.push(`<meta property="og:image" content="${esc(ogImage)}">`);
143
+ parts.push(`<meta property="og:type" content="${esc(ogType)}">`);
144
+ if (canonicalBase) parts.push(`<meta property="og:url" content="${esc(canonicalBase + url)}">`);
145
+ if (meta.og_site_name) parts.push(`<meta property="og:site_name" content="${esc(meta.og_site_name)}">`);
146
+ const twitterCard = meta.twitter_card || (ogImage ? "summary_large_image" : "summary");
147
+ parts.push(`<meta name="twitter:card" content="${esc(twitterCard)}">`);
148
+ if (ogTitle) parts.push(`<meta name="twitter:title" content="${esc(ogTitle)}">`);
149
+ if (ogDesc) parts.push(`<meta name="twitter:description" content="${esc(ogDesc)}">`);
150
+ if (ogImage) parts.push(`<meta name="twitter:image" content="${esc(ogImage)}">`);
151
+ if (meta.twitter_site) parts.push(`<meta name="twitter:site" content="${esc(meta.twitter_site)}">`);
152
+ const alternates = resolvedData == null ? void 0 : resolvedData.alternates;
153
+ if (alternates && canonicalBase) {
154
+ for (const [locale, slug] of Object.entries(alternates)) {
155
+ parts.push(`<link rel="alternate" hreflang="${esc(locale)}" href="${esc(canonicalBase + slug)}">`);
156
+ }
157
+ const defaultSlug = alternates[(website == null ? void 0 : website.default_locale) ?? "en"] ?? url;
158
+ parts.push(`<link rel="alternate" hreflang="x-default" href="${esc(canonicalBase + defaultSlug)}">`);
159
+ }
160
+ if (meta.noindex) {
161
+ parts.push('<meta name="robots" content="noindex, nofollow">');
162
+ }
163
+ if (stateScript) {
164
+ parts.push(stateScript);
165
+ }
166
+ return parts.join("\n");
167
+ }
168
+ function buildSiteHead(website) {
169
+ if (!website) return "";
170
+ const parts = [];
171
+ const meta = website.meta ?? {};
172
+ if (website.favicon_url) {
173
+ parts.push(`<link rel="icon" href="${esc(website.favicon_url)}">`);
174
+ }
175
+ if (meta.webclip) {
176
+ parts.push(`<link rel="apple-touch-icon" sizes="180x180" href="${esc(meta.webclip)}">`);
177
+ }
178
+ if (meta.verification_google) {
179
+ parts.push(`<meta name="google-site-verification" content="${esc(meta.verification_google)}">`);
180
+ }
181
+ if (meta.verification_bing) {
182
+ parts.push(`<meta name="msvalidate.01" content="${esc(meta.verification_bing)}">`);
183
+ }
184
+ if (meta.structured_data_org) {
185
+ try {
186
+ const json = typeof meta.structured_data_org === "string" ? meta.structured_data_org : JSON.stringify(meta.structured_data_org);
187
+ parts.push(`<script type="application/ld+json">${json}${"<"}/script>`);
188
+ } catch {
189
+ }
190
+ }
191
+ const plausibleId = website.plausible_site_id;
192
+ if (plausibleId) {
193
+ parts.push(`<script defer data-domain="${esc(plausibleId)}" src="https://plausible.io/js/script.js"><${"/"}script>`);
194
+ }
195
+ return parts.join("\n");
196
+ }
197
+ function generateSitemapXml(options) {
198
+ const { renderedPaths, canonicalBase, defaultChangefreq = "weekly", defaultPriority = "0.7" } = options;
199
+ const base = canonicalBase.replace(/\/$/, "");
200
+ const urls = renderedPaths.map((p) => {
201
+ const priority = p === "/" ? "1.0" : defaultPriority;
202
+ const changefreq = p === "/" ? "daily" : defaultChangefreq;
203
+ return ` <url>
204
+ <loc>${esc(base + p)}</loc>
205
+ <changefreq>${changefreq}</changefreq>
206
+ <priority>${priority}</priority>
207
+ </url>`;
208
+ });
209
+ return `<?xml version="1.0" encoding="UTF-8"?>
210
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
211
+ ${urls.join("\n")}
212
+ </urlset>
213
+ `;
214
+ }
215
+ function generateRobotsTxt(options) {
216
+ const { robotsTxt, canonicalBase, noindex } = options;
217
+ if (noindex) {
218
+ return `User-agent: *
219
+ Disallow: /
220
+ `;
221
+ }
222
+ const base = (canonicalBase || "").replace(/\/$/, "");
223
+ const sitemapLine = base ? `
224
+ Sitemap: ${base}/sitemap.xml
225
+ ` : "";
226
+ if (robotsTxt) {
227
+ if (sitemapLine && !robotsTxt.toLowerCase().includes("sitemap:")) {
228
+ return robotsTxt.trimEnd() + "\n" + sitemapLine;
229
+ }
230
+ return robotsTxt;
231
+ }
232
+ return `User-agent: *
233
+ Allow: /
234
+ ${sitemapLine}`;
235
+ }
236
+ function generateNetlifyRedirects(redirects) {
237
+ if (redirects.length === 0) return "";
238
+ return redirects.map((r) => `${r.from_path} ${r.to_path} ${r.status_code}`).join("\n") + "\n";
239
+ }
240
+ function stripTemplateSeoTags(template) {
241
+ 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");
242
+ }
243
+ function extractPrimaryContext(contexts) {
244
+ const empty = { title: null, description: null, image: null };
245
+ if (!contexts || typeof contexts !== "object") return empty;
246
+ const ctx = Object.values(contexts).find((c) => c != null);
247
+ if (!ctx) return empty;
248
+ const str = (key) => {
249
+ const v = ctx[key];
250
+ return typeof v === "string" && v.trim() ? v.trim() : null;
251
+ };
252
+ return {
253
+ title: str("meta_title") || str("title"),
254
+ description: str("meta_description") || str("excerpt") || str("description") || str("blurb") || str("summary"),
255
+ image: str("og_image") || str("cover_image_url") || str("image_url") || str("image")
256
+ };
257
+ }
258
+ function esc(s) {
259
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
260
+ }
261
+ async function prerender({
262
+ root = process.cwd(),
263
+ clientOutDir = "dist/client",
264
+ serverEntry = "dist/server/main.server.js",
265
+ paths = [],
266
+ crawl = true,
267
+ excludePaths = [],
268
+ failFast = false,
269
+ maxPages = 5e3,
270
+ mode = "dir",
271
+ website = null,
272
+ redirects = []
273
+ } = {}) {
274
+ const absRoot = path.resolve(root);
275
+ const absClient = path.resolve(absRoot, clientOutDir);
276
+ const absServerEntry = path.resolve(absRoot, serverEntry);
277
+ const templatePath = path.join(absClient, "index.html");
278
+ const rawTemplate = await fs.readFile(templatePath, "utf8");
279
+ let template = stripTemplateSeoTags(rawTemplate);
280
+ const siteHead = buildSiteHead(website);
281
+ if (siteHead) {
282
+ template = template.replace("</head>", `${siteHead}
283
+ </head>`);
284
+ }
285
+ const serverMod = await import(pathToFileURL(absServerEntry).href);
286
+ if (typeof serverMod.render !== "function") {
287
+ throw new TypeError(
288
+ `SSR entry must export async function render(url, ctx). Missing "render" in: ${absServerEntry}`
289
+ );
290
+ }
291
+ let manifest = null;
292
+ try {
293
+ const manifestPath = path.join(absClient, "ssr-manifest.json");
294
+ manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
295
+ } catch {
296
+ }
297
+ const isExcluded = makeExcludeChecker(excludePaths);
298
+ const queue = [];
299
+ const seen = /* @__PURE__ */ new Set();
300
+ enqueue("/", queue, seen, isExcluded);
301
+ for (const p of paths) enqueue(p, queue, seen, isExcluded);
302
+ const fontPreloads = await discoverFontPreloads(absClient);
303
+ const rendered = [];
304
+ const failures = [];
305
+ while (queue.length) {
306
+ if (rendered.length >= maxPages) break;
307
+ const urlPath = queue.shift();
308
+ if (urlPath == null || urlPath === "") continue;
309
+ if (isExcluded(urlPath)) continue;
310
+ try {
311
+ const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
312
+ manifest,
313
+ template,
314
+ website
315
+ });
316
+ const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
317
+ const outfile = outFilePath(absClient, urlPath, mode);
318
+ await fs.mkdir(path.dirname(outfile), { recursive: true });
319
+ await fs.writeFile(outfile, outHtml, "utf8");
320
+ rendered.push(urlPath);
321
+ if (crawl) {
322
+ const discovered = discoverInternalLinks(outHtml);
323
+ for (const p of discovered) enqueue(p, queue, seen, isExcluded);
324
+ }
325
+ } catch (err) {
326
+ failures.push({ path: urlPath, error: err });
327
+ if (failFast) throw err;
328
+ console.warn(`[prerender] failed: ${urlPath}
329
+ `, err.stack ?? err);
330
+ }
331
+ }
332
+ const websiteMeta = (website == null ? void 0 : website.meta) ?? {};
333
+ const canonicalBase = (websiteMeta.canonical_base_url || (website == null ? void 0 : website.domain) || "").replace(/\/$/, "");
334
+ if (canonicalBase && rendered.length > 0) {
335
+ const sitemap = generateSitemapXml({ renderedPaths: rendered, canonicalBase });
336
+ await fs.writeFile(path.join(absClient, "sitemap.xml"), sitemap, "utf8");
337
+ console.log(` Generated sitemap.xml (${rendered.length} URLs)`);
338
+ }
339
+ const robotsTxt = generateRobotsTxt({
340
+ robotsTxt: website == null ? void 0 : website.robots_txt,
341
+ canonicalBase,
342
+ noindex: websiteMeta.noindex
343
+ });
344
+ await fs.writeFile(path.join(absClient, "robots.txt"), robotsTxt, "utf8");
345
+ console.log(" Generated robots.txt");
346
+ if (redirects.length > 0) {
347
+ const redirectsContent = generateNetlifyRedirects(redirects);
348
+ const redirectsPath = path.join(absClient, "_redirects");
349
+ let existing = "";
350
+ try {
351
+ existing = await fs.readFile(redirectsPath, "utf8");
352
+ } catch {
353
+ }
354
+ await fs.writeFile(redirectsPath, redirectsContent + existing, "utf8");
355
+ console.log(` Generated _redirects (${redirects.length} rules)`);
356
+ }
357
+ return {
358
+ rendered,
359
+ failures,
360
+ queuedRemaining: queue.length,
361
+ totalDiscovered: seen.size
362
+ };
363
+ }
364
+ function normalizePath(p) {
365
+ if (!p) return "/";
366
+ try {
367
+ if (p.startsWith("http://") || p.startsWith("https://")) {
368
+ const u = new URL(p);
369
+ p = u.pathname + (u.search || "");
370
+ }
371
+ } catch {
372
+ }
373
+ if (!p.startsWith("/")) p = `/${p}`;
374
+ const hashIdx = p.indexOf("#");
375
+ if (hashIdx !== -1) p = p.slice(0, hashIdx);
376
+ p = p.replace(/\/{2,}/g, "/");
377
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
378
+ return p || "/";
379
+ }
380
+ function enqueue(p, queue, seen, isExcluded) {
381
+ const np = normalizePath(p);
382
+ if (seen.has(np)) return;
383
+ if (isExcluded(np)) return;
384
+ seen.add(np);
385
+ queue.push(np);
386
+ }
387
+ function makeExcludeChecker(excludePaths) {
388
+ const rules = Array.isArray(excludePaths) ? excludePaths : [excludePaths];
389
+ return (p) => {
390
+ const np = normalizePath(p);
391
+ for (const r of rules) {
392
+ if (typeof r === "string" && r === "") continue;
393
+ if (typeof r === "function") {
394
+ if (r(np)) return true;
395
+ } else if (r instanceof RegExp) {
396
+ if (r.test(np)) return true;
397
+ } else if (typeof r === "string") {
398
+ const prefix = normalizePath(r);
399
+ if (np === prefix || np.startsWith(`${prefix}/`)) return true;
400
+ }
401
+ }
402
+ return false;
403
+ };
404
+ }
405
+ async function discoverFontPreloads(clientOutDir) {
406
+ const assetsDir = path.join(clientOutDir, "assets");
407
+ let cssFiles = [];
408
+ try {
409
+ const entries = await fs.readdir(assetsDir);
410
+ cssFiles = entries.filter((f) => f.endsWith(".css")).map((f) => path.join(assetsDir, f));
411
+ } catch {
412
+ return "";
413
+ }
414
+ const seen = /* @__PURE__ */ new Set();
415
+ const woff2Re = /url\((["']?)(https?:\/\/[^"')]+\.woff2)\1\)/g;
416
+ for (const file of cssFiles) {
417
+ const css = await fs.readFile(file, "utf8");
418
+ for (const [, , url] of css.matchAll(woff2Re)) {
419
+ if (url !== "") seen.add(url);
420
+ }
421
+ }
422
+ return [...seen].map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`).join("\n");
423
+ }
424
+ function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "") {
425
+ let out = template;
426
+ if (template.includes("<!--app-html-->")) {
427
+ out = template.replace("<!--app-html-->", appHtml);
428
+ } else if (template.includes('<div id="app"></div>')) {
429
+ out = template.replace('<div id="app"></div>', `<div id="app">${appHtml}</div>`);
430
+ }
431
+ if (htmlAttrs) {
432
+ const langMatch = htmlAttrs.match(/lang="([^"]*)"/);
433
+ if (langMatch) {
434
+ out = out.replace(/(<html[^>]*\s)lang="[^"]*"/, `$1lang="${langMatch[1]}"`);
435
+ }
436
+ }
437
+ if (fontPreloads) {
438
+ out = out.replace("</head>", `${fontPreloads}
439
+ </head>`);
440
+ }
441
+ if (head) {
442
+ if (/<title>/.test(head)) {
443
+ out = out.replace("<!--ssg:title-->", "");
444
+ }
445
+ const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
446
+ const descMatches = [...head.matchAll(descTagRe)];
447
+ if (descMatches.length > 1) {
448
+ for (let i = 0; i < descMatches.length - 1; i++) {
449
+ head = head.replace(descMatches[i][0], "");
450
+ }
451
+ }
452
+ if (/<meta\s[^>]*name\s*=\s*["']description["']/i.test(head)) {
453
+ out = out.replace(/<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi, "");
454
+ }
455
+ out = out.replace("</head>", `${head}
456
+ </head>`);
457
+ }
458
+ return out;
459
+ }
460
+ function outFilePath(absClientDir, urlPath, mode) {
461
+ const [pathname] = urlPath.split("?");
462
+ const safe = pathname === "/" ? "/" : pathname;
463
+ if (mode === "file") {
464
+ if (safe === "/") return path.join(absClientDir, "index.html");
465
+ return path.join(absClientDir, `${safe.slice(1)}.html`);
466
+ }
467
+ if (safe === "/") return path.join(absClientDir, "index.html");
468
+ return path.join(absClientDir, safe.slice(1), "index.html");
469
+ }
470
+ function discoverInternalLinks(html) {
471
+ const out = /* @__PURE__ */ new Set();
472
+ const hrefRe = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
473
+ let m;
474
+ while ((m = hrefRe.exec(html)) !== null) {
475
+ const raw = (m[1] ?? m[2] ?? "").trim();
476
+ if (!raw) continue;
477
+ if (raw.startsWith("mailto:") || raw.startsWith("tel:") || raw.startsWith("javascript:") || raw.startsWith("#")) {
478
+ continue;
479
+ }
480
+ if (raw.startsWith("http://") || raw.startsWith("https://")) continue;
481
+ if (/\.(?:pdf|png|jpe?g|gif|svg|webp|css|js|map|ico)(?:\?|$)/i.test(raw)) continue;
482
+ const candidate = raw.startsWith("/") ? raw : `/${raw}`;
483
+ out.add(candidate);
484
+ }
485
+ return [...out];
486
+ }
487
+ export {
488
+ fetchCmsSiteData as a,
489
+ buildPageHead as b,
490
+ prerender as c,
491
+ buildSiteHead as d,
492
+ generateRobotsTxt as e,
493
+ fetchCmsPrerenderPaths as f,
494
+ generateNetlifyRedirects as g,
495
+ generateSitemapXml as h,
496
+ polyfillBloxSsgGlobals as p
497
+ };