@bagelink/blox 1.12.5 → 1.12.11
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.
- package/dist/prerender-BwmtiKjz.js +287 -0
- package/dist/prerender-mMYLIIC8.cjs +308 -0
- package/dist/ssg/cli.cjs +124 -0
- package/dist/ssg/cli.d.ts +21 -0
- package/dist/ssg/cli.d.ts.map +1 -0
- package/dist/ssg/cli.mjs +123 -0
- package/dist/ssg/cms-routes.d.ts +8 -0
- package/dist/ssg/cms-routes.d.ts.map +1 -0
- package/dist/ssg/constants.d.ts +3 -0
- package/dist/ssg/constants.d.ts.map +1 -0
- package/dist/ssg/index.cjs +72 -0
- package/dist/ssg/index.d.ts +19 -0
- package/dist/ssg/index.d.ts.map +1 -0
- package/dist/ssg/index.mjs +72 -0
- package/dist/ssg/polyfill-node.d.ts +15 -0
- package/dist/ssg/polyfill-node.d.ts.map +1 -0
- package/dist/ssg/prerender.d.ts +44 -0
- package/dist/ssg/prerender.d.ts.map +1 -0
- package/dist/ssg/render-resolved-page.d.ts +27 -0
- package/dist/ssg/render-resolved-page.d.ts.map +1 -0
- package/dist/ssg/state-cache.d.ts +21 -0
- package/dist/ssg/state-cache.d.ts.map +1 -0
- package/package.json +13 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
async function fetchCmsPrerenderPaths(apiBase, websiteName) {
|
|
5
|
+
const sitesRes = await fetch(`${apiBase}/cms/websites`);
|
|
6
|
+
const sitesData = await sitesRes.json();
|
|
7
|
+
const sites = Array.isArray(sitesData) ? sitesData : sitesData.data ?? [];
|
|
8
|
+
const site = sites.find((s) => s.name === websiteName);
|
|
9
|
+
if ((site == null ? void 0 : site.id) == null || site.id === "") throw new Error(`Website "${websiteName}" not found at ${apiBase}`);
|
|
10
|
+
const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=en`);
|
|
11
|
+
const pagesData = await pagesRes.json();
|
|
12
|
+
const pages = Array.isArray(pagesData) ? pagesData : pagesData.data ?? [];
|
|
13
|
+
const routes = [];
|
|
14
|
+
for (const page of pages) {
|
|
15
|
+
const slug = page.slug ?? "/";
|
|
16
|
+
if (slug.includes(":") && page.data_bindings) {
|
|
17
|
+
for (const [, binding] of Object.entries(page.data_bindings)) {
|
|
18
|
+
const b = binding;
|
|
19
|
+
if (b.adapter === "datastore" && b.collection != null && b.collection !== "" && b.store != null && b.store !== "" && b.bind_by != null && b.bind_by !== "") {
|
|
20
|
+
try {
|
|
21
|
+
const itemsRes = await fetch(
|
|
22
|
+
`${apiBase}/datastore/${b.store}/collections/${b.collection}?limit=200`
|
|
23
|
+
);
|
|
24
|
+
const itemsData = await itemsRes.json();
|
|
25
|
+
const items = Array.isArray(itemsData) ? itemsData : itemsData.data ?? [];
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
const bindValue = item[b.bind_by];
|
|
28
|
+
if (bindValue != null && bindValue !== "") {
|
|
29
|
+
const concrete = slug.replace(/:([a-z_]+)/g, (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? ""));
|
|
30
|
+
routes.push(concrete);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.warn(` [blox-ssg] Could not expand ${slug}: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else if (!slug.includes(":")) {
|
|
39
|
+
routes.push(slug === "" ? "/" : slug);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...new Set(routes)];
|
|
43
|
+
}
|
|
44
|
+
async function polyfillBloxSsgGlobals(options) {
|
|
45
|
+
const { env } = await Promise.resolve().then(() => __viteBrowserExternal);
|
|
46
|
+
const pageUrl = (options == null ? void 0 : options.pageUrl) ?? env.BAGELINK_API_URL ?? "https://example.com/";
|
|
47
|
+
const { Window: HappyWindow } = await import("happy-dom");
|
|
48
|
+
const _win = new HappyWindow({ url: pageUrl });
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
if (g.window == null) g.window = g;
|
|
51
|
+
for (const key of Object.getOwnPropertyNames(_win)) {
|
|
52
|
+
if (key === "window" || key === "global" || key === "globalThis") continue;
|
|
53
|
+
try {
|
|
54
|
+
if (!(key in g)) g[key] = _win[key];
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const key of [
|
|
59
|
+
"document",
|
|
60
|
+
"location",
|
|
61
|
+
"localStorage",
|
|
62
|
+
"sessionStorage",
|
|
63
|
+
"navigator",
|
|
64
|
+
"Element",
|
|
65
|
+
"Document",
|
|
66
|
+
"HTMLElement",
|
|
67
|
+
"Event",
|
|
68
|
+
"CustomEvent",
|
|
69
|
+
"MouseEvent",
|
|
70
|
+
"KeyboardEvent",
|
|
71
|
+
"MutationObserver",
|
|
72
|
+
"IntersectionObserver",
|
|
73
|
+
"ResizeObserver",
|
|
74
|
+
"getComputedStyle",
|
|
75
|
+
"matchMedia",
|
|
76
|
+
"requestAnimationFrame",
|
|
77
|
+
"cancelAnimationFrame"
|
|
78
|
+
]) {
|
|
79
|
+
try {
|
|
80
|
+
g[key] = _win[key];
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
g.window = g;
|
|
85
|
+
g.window.location = _win.location;
|
|
86
|
+
g.window.document = _win.document;
|
|
87
|
+
}
|
|
88
|
+
const process = {};
|
|
89
|
+
const __viteBrowserExternal = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
90
|
+
__proto__: null,
|
|
91
|
+
default: process
|
|
92
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
93
|
+
async function prerender({
|
|
94
|
+
root = process.cwd(),
|
|
95
|
+
clientOutDir = "dist/client",
|
|
96
|
+
serverEntry = "dist/server/main.server.js",
|
|
97
|
+
paths = [],
|
|
98
|
+
crawl = true,
|
|
99
|
+
excludePaths = [],
|
|
100
|
+
failFast = false,
|
|
101
|
+
maxPages = 5e3,
|
|
102
|
+
mode = "dir"
|
|
103
|
+
} = {}) {
|
|
104
|
+
const absRoot = path.resolve(root);
|
|
105
|
+
const absClient = path.resolve(absRoot, clientOutDir);
|
|
106
|
+
const absServerEntry = path.resolve(absRoot, serverEntry);
|
|
107
|
+
const templatePath = path.join(absClient, "index.html");
|
|
108
|
+
const template = await fs.readFile(templatePath, "utf8");
|
|
109
|
+
const serverMod = await import(pathToFileURL(absServerEntry).href);
|
|
110
|
+
if (typeof serverMod.render !== "function") {
|
|
111
|
+
throw new TypeError(
|
|
112
|
+
`SSR entry must export async function render(url, ctx). Missing "render" in: ${absServerEntry}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
let manifest = null;
|
|
116
|
+
try {
|
|
117
|
+
const manifestPath = path.join(absClient, "ssr-manifest.json");
|
|
118
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
const isExcluded = makeExcludeChecker(excludePaths);
|
|
122
|
+
const queue = [];
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
enqueue("/", queue, seen, isExcluded);
|
|
125
|
+
for (const p of paths) enqueue(p, queue, seen, isExcluded);
|
|
126
|
+
const fontPreloads = await discoverFontPreloads(absClient);
|
|
127
|
+
const rendered = [];
|
|
128
|
+
const failures = [];
|
|
129
|
+
while (queue.length) {
|
|
130
|
+
if (rendered.length >= maxPages) break;
|
|
131
|
+
const urlPath = queue.shift();
|
|
132
|
+
if (urlPath == null || urlPath === "") continue;
|
|
133
|
+
if (isExcluded(urlPath)) continue;
|
|
134
|
+
try {
|
|
135
|
+
const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
|
|
136
|
+
manifest,
|
|
137
|
+
template
|
|
138
|
+
});
|
|
139
|
+
const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
|
|
140
|
+
const outfile = outFilePath(absClient, urlPath, mode);
|
|
141
|
+
await fs.mkdir(path.dirname(outfile), { recursive: true });
|
|
142
|
+
await fs.writeFile(outfile, outHtml, "utf8");
|
|
143
|
+
rendered.push(urlPath);
|
|
144
|
+
if (crawl) {
|
|
145
|
+
const discovered = discoverInternalLinks(outHtml);
|
|
146
|
+
for (const p of discovered) enqueue(p, queue, seen, isExcluded);
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
failures.push({ path: urlPath, error: err });
|
|
150
|
+
if (failFast) throw err;
|
|
151
|
+
console.warn(`[prerender] failed: ${urlPath}
|
|
152
|
+
`, err.stack ?? err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
rendered,
|
|
157
|
+
failures,
|
|
158
|
+
queuedRemaining: queue.length,
|
|
159
|
+
totalDiscovered: seen.size
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function normalizePath(p) {
|
|
163
|
+
if (!p) return "/";
|
|
164
|
+
try {
|
|
165
|
+
if (p.startsWith("http://") || p.startsWith("https://")) {
|
|
166
|
+
const u = new URL(p);
|
|
167
|
+
p = u.pathname + (u.search || "");
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
172
|
+
const hashIdx = p.indexOf("#");
|
|
173
|
+
if (hashIdx !== -1) p = p.slice(0, hashIdx);
|
|
174
|
+
p = p.replace(/\/{2,}/g, "/");
|
|
175
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
176
|
+
return p || "/";
|
|
177
|
+
}
|
|
178
|
+
function enqueue(p, queue, seen, isExcluded) {
|
|
179
|
+
const np = normalizePath(p);
|
|
180
|
+
if (seen.has(np)) return;
|
|
181
|
+
if (isExcluded(np)) return;
|
|
182
|
+
seen.add(np);
|
|
183
|
+
queue.push(np);
|
|
184
|
+
}
|
|
185
|
+
function makeExcludeChecker(excludePaths) {
|
|
186
|
+
const rules = Array.isArray(excludePaths) ? excludePaths : [excludePaths];
|
|
187
|
+
return (p) => {
|
|
188
|
+
const np = normalizePath(p);
|
|
189
|
+
for (const r of rules) {
|
|
190
|
+
if (typeof r === "string" && r === "") continue;
|
|
191
|
+
if (typeof r === "function") {
|
|
192
|
+
if (r(np)) return true;
|
|
193
|
+
} else if (r instanceof RegExp) {
|
|
194
|
+
if (r.test(np)) return true;
|
|
195
|
+
} else if (typeof r === "string") {
|
|
196
|
+
const prefix = normalizePath(r);
|
|
197
|
+
if (np === prefix || np.startsWith(`${prefix}/`)) return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function discoverFontPreloads(clientOutDir) {
|
|
204
|
+
const assetsDir = path.join(clientOutDir, "assets");
|
|
205
|
+
let cssFiles = [];
|
|
206
|
+
try {
|
|
207
|
+
const entries = await fs.readdir(assetsDir);
|
|
208
|
+
cssFiles = entries.filter((f) => f.endsWith(".css")).map((f) => path.join(assetsDir, f));
|
|
209
|
+
} catch {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
const seen = /* @__PURE__ */ new Set();
|
|
213
|
+
const woff2Re = /url\((["']?)(https?:\/\/[^"')]+\.woff2)\1\)/g;
|
|
214
|
+
for (const file of cssFiles) {
|
|
215
|
+
const css = await fs.readFile(file, "utf8");
|
|
216
|
+
for (const [, , url] of css.matchAll(woff2Re)) {
|
|
217
|
+
if (url !== "") seen.add(url);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [...seen].map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`).join("\n");
|
|
221
|
+
}
|
|
222
|
+
function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "") {
|
|
223
|
+
let out = template;
|
|
224
|
+
if (template.includes("<!--app-html-->")) {
|
|
225
|
+
out = template.replace("<!--app-html-->", appHtml);
|
|
226
|
+
} else if (template.includes('<div id="app"></div>')) {
|
|
227
|
+
out = template.replace('<div id="app"></div>', `<div id="app">${appHtml}</div>`);
|
|
228
|
+
}
|
|
229
|
+
if (htmlAttrs) {
|
|
230
|
+
const langMatch = htmlAttrs.match(/lang="([^"]*)"/);
|
|
231
|
+
if (langMatch) {
|
|
232
|
+
out = out.replace(/(<html[^>]*\s)lang="[^"]*"/, `$1lang="${langMatch[1]}"`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (fontPreloads) {
|
|
236
|
+
out = out.replace("</head>", `${fontPreloads}
|
|
237
|
+
</head>`);
|
|
238
|
+
}
|
|
239
|
+
if (head) {
|
|
240
|
+
const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
|
|
241
|
+
const descMatches = [...head.matchAll(descTagRe)];
|
|
242
|
+
if (descMatches.length > 1) {
|
|
243
|
+
for (let i = 0; i < descMatches.length - 1; i++) {
|
|
244
|
+
head = head.replace(descMatches[i][0], "");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (/<meta\s[^>]*name\s*=\s*["']description["']/i.test(head)) {
|
|
248
|
+
out = out.replace(/<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi, "");
|
|
249
|
+
}
|
|
250
|
+
out = out.replace("</head>", `${head}
|
|
251
|
+
</head>`);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
function outFilePath(absClientDir, urlPath, mode) {
|
|
256
|
+
const [pathname] = urlPath.split("?");
|
|
257
|
+
const safe = pathname === "/" ? "/" : pathname;
|
|
258
|
+
if (mode === "file") {
|
|
259
|
+
if (safe === "/") return path.join(absClientDir, "index.html");
|
|
260
|
+
return path.join(absClientDir, `${safe.slice(1)}.html`);
|
|
261
|
+
}
|
|
262
|
+
if (safe === "/") return path.join(absClientDir, "index.html");
|
|
263
|
+
return path.join(absClientDir, safe.slice(1), "index.html");
|
|
264
|
+
}
|
|
265
|
+
function discoverInternalLinks(html) {
|
|
266
|
+
const out = /* @__PURE__ */ new Set();
|
|
267
|
+
const hrefRe = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
268
|
+
let m;
|
|
269
|
+
while ((m = hrefRe.exec(html)) !== null) {
|
|
270
|
+
const raw = (m[1] ?? m[2] ?? "").trim();
|
|
271
|
+
if (!raw) continue;
|
|
272
|
+
if (raw.startsWith("mailto:") || raw.startsWith("tel:") || raw.startsWith("javascript:") || raw.startsWith("#")) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) continue;
|
|
276
|
+
if (/\.(?:pdf|png|jpe?g|gif|svg|webp|css|js|map|ico)(?:\?|$)/i.test(raw)) continue;
|
|
277
|
+
const candidate = raw.startsWith("/") ? raw : `/${raw}`;
|
|
278
|
+
out.add(candidate);
|
|
279
|
+
}
|
|
280
|
+
return [...out];
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
prerender as a,
|
|
284
|
+
process as b,
|
|
285
|
+
fetchCmsPrerenderPaths as f,
|
|
286
|
+
polyfillBloxSsgGlobals as p
|
|
287
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
const fs = require("node:fs/promises");
|
|
25
|
+
const path = require("node:path");
|
|
26
|
+
const node_url = require("node:url");
|
|
27
|
+
async function fetchCmsPrerenderPaths(apiBase, websiteName) {
|
|
28
|
+
const sitesRes = await fetch(`${apiBase}/cms/websites`);
|
|
29
|
+
const sitesData = await sitesRes.json();
|
|
30
|
+
const sites = Array.isArray(sitesData) ? sitesData : sitesData.data ?? [];
|
|
31
|
+
const site = sites.find((s) => s.name === websiteName);
|
|
32
|
+
if ((site == null ? void 0 : site.id) == null || site.id === "") throw new Error(`Website "${websiteName}" not found at ${apiBase}`);
|
|
33
|
+
const pagesRes = await fetch(`${apiBase}/cms/websites/${site.id}/pages?locale=en`);
|
|
34
|
+
const pagesData = await pagesRes.json();
|
|
35
|
+
const pages = Array.isArray(pagesData) ? pagesData : pagesData.data ?? [];
|
|
36
|
+
const routes = [];
|
|
37
|
+
for (const page of pages) {
|
|
38
|
+
const slug = page.slug ?? "/";
|
|
39
|
+
if (slug.includes(":") && page.data_bindings) {
|
|
40
|
+
for (const [, binding] of Object.entries(page.data_bindings)) {
|
|
41
|
+
const b = binding;
|
|
42
|
+
if (b.adapter === "datastore" && b.collection != null && b.collection !== "" && b.store != null && b.store !== "" && b.bind_by != null && b.bind_by !== "") {
|
|
43
|
+
try {
|
|
44
|
+
const itemsRes = await fetch(
|
|
45
|
+
`${apiBase}/datastore/${b.store}/collections/${b.collection}?limit=200`
|
|
46
|
+
);
|
|
47
|
+
const itemsData = await itemsRes.json();
|
|
48
|
+
const items = Array.isArray(itemsData) ? itemsData : itemsData.data ?? [];
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const bindValue = item[b.bind_by];
|
|
51
|
+
if (bindValue != null && bindValue !== "") {
|
|
52
|
+
const concrete = slug.replace(/:([a-z_]+)/g, (_m, p) => p === b.bind_by ? String(bindValue) : String(item[p] ?? ""));
|
|
53
|
+
routes.push(concrete);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.warn(` [blox-ssg] Could not expand ${slug}: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (!slug.includes(":")) {
|
|
62
|
+
routes.push(slug === "" ? "/" : slug);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(routes)];
|
|
66
|
+
}
|
|
67
|
+
async function polyfillBloxSsgGlobals(options) {
|
|
68
|
+
const { env } = await Promise.resolve().then(() => __viteBrowserExternal);
|
|
69
|
+
const pageUrl = (options == null ? void 0 : options.pageUrl) ?? env.BAGELINK_API_URL ?? "https://example.com/";
|
|
70
|
+
const { Window: HappyWindow } = await import("happy-dom");
|
|
71
|
+
const _win = new HappyWindow({ url: pageUrl });
|
|
72
|
+
const g = globalThis;
|
|
73
|
+
if (g.window == null) g.window = g;
|
|
74
|
+
for (const key of Object.getOwnPropertyNames(_win)) {
|
|
75
|
+
if (key === "window" || key === "global" || key === "globalThis") continue;
|
|
76
|
+
try {
|
|
77
|
+
if (!(key in g)) g[key] = _win[key];
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const key of [
|
|
82
|
+
"document",
|
|
83
|
+
"location",
|
|
84
|
+
"localStorage",
|
|
85
|
+
"sessionStorage",
|
|
86
|
+
"navigator",
|
|
87
|
+
"Element",
|
|
88
|
+
"Document",
|
|
89
|
+
"HTMLElement",
|
|
90
|
+
"Event",
|
|
91
|
+
"CustomEvent",
|
|
92
|
+
"MouseEvent",
|
|
93
|
+
"KeyboardEvent",
|
|
94
|
+
"MutationObserver",
|
|
95
|
+
"IntersectionObserver",
|
|
96
|
+
"ResizeObserver",
|
|
97
|
+
"getComputedStyle",
|
|
98
|
+
"matchMedia",
|
|
99
|
+
"requestAnimationFrame",
|
|
100
|
+
"cancelAnimationFrame"
|
|
101
|
+
]) {
|
|
102
|
+
try {
|
|
103
|
+
g[key] = _win[key];
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
g.window = g;
|
|
108
|
+
g.window.location = _win.location;
|
|
109
|
+
g.window.document = _win.document;
|
|
110
|
+
}
|
|
111
|
+
const process = {};
|
|
112
|
+
const __viteBrowserExternal = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
113
|
+
__proto__: null,
|
|
114
|
+
default: process
|
|
115
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
116
|
+
async function prerender({
|
|
117
|
+
root = process.cwd(),
|
|
118
|
+
clientOutDir = "dist/client",
|
|
119
|
+
serverEntry = "dist/server/main.server.js",
|
|
120
|
+
paths = [],
|
|
121
|
+
crawl = true,
|
|
122
|
+
excludePaths = [],
|
|
123
|
+
failFast = false,
|
|
124
|
+
maxPages = 5e3,
|
|
125
|
+
mode = "dir"
|
|
126
|
+
} = {}) {
|
|
127
|
+
const absRoot = path.resolve(root);
|
|
128
|
+
const absClient = path.resolve(absRoot, clientOutDir);
|
|
129
|
+
const absServerEntry = path.resolve(absRoot, serverEntry);
|
|
130
|
+
const templatePath = path.join(absClient, "index.html");
|
|
131
|
+
const template = await fs.readFile(templatePath, "utf8");
|
|
132
|
+
const serverMod = await import(node_url.pathToFileURL(absServerEntry).href);
|
|
133
|
+
if (typeof serverMod.render !== "function") {
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
`SSR entry must export async function render(url, ctx). Missing "render" in: ${absServerEntry}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
let manifest = null;
|
|
139
|
+
try {
|
|
140
|
+
const manifestPath = path.join(absClient, "ssr-manifest.json");
|
|
141
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
const isExcluded = makeExcludeChecker(excludePaths);
|
|
145
|
+
const queue = [];
|
|
146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
147
|
+
enqueue("/", queue, seen, isExcluded);
|
|
148
|
+
for (const p of paths) enqueue(p, queue, seen, isExcluded);
|
|
149
|
+
const fontPreloads = await discoverFontPreloads(absClient);
|
|
150
|
+
const rendered = [];
|
|
151
|
+
const failures = [];
|
|
152
|
+
while (queue.length) {
|
|
153
|
+
if (rendered.length >= maxPages) break;
|
|
154
|
+
const urlPath = queue.shift();
|
|
155
|
+
if (urlPath == null || urlPath === "") continue;
|
|
156
|
+
if (isExcluded(urlPath)) continue;
|
|
157
|
+
try {
|
|
158
|
+
const { html, head = "", htmlAttrs = "" } = await serverMod.render(urlPath, {
|
|
159
|
+
manifest,
|
|
160
|
+
template
|
|
161
|
+
});
|
|
162
|
+
const outHtml = injectIntoTemplate(template, head, html, fontPreloads, htmlAttrs);
|
|
163
|
+
const outfile = outFilePath(absClient, urlPath, mode);
|
|
164
|
+
await fs.mkdir(path.dirname(outfile), { recursive: true });
|
|
165
|
+
await fs.writeFile(outfile, outHtml, "utf8");
|
|
166
|
+
rendered.push(urlPath);
|
|
167
|
+
if (crawl) {
|
|
168
|
+
const discovered = discoverInternalLinks(outHtml);
|
|
169
|
+
for (const p of discovered) enqueue(p, queue, seen, isExcluded);
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
failures.push({ path: urlPath, error: err });
|
|
173
|
+
if (failFast) throw err;
|
|
174
|
+
console.warn(`[prerender] failed: ${urlPath}
|
|
175
|
+
`, err.stack ?? err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
rendered,
|
|
180
|
+
failures,
|
|
181
|
+
queuedRemaining: queue.length,
|
|
182
|
+
totalDiscovered: seen.size
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function normalizePath(p) {
|
|
186
|
+
if (!p) return "/";
|
|
187
|
+
try {
|
|
188
|
+
if (p.startsWith("http://") || p.startsWith("https://")) {
|
|
189
|
+
const u = new URL(p);
|
|
190
|
+
p = u.pathname + (u.search || "");
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
195
|
+
const hashIdx = p.indexOf("#");
|
|
196
|
+
if (hashIdx !== -1) p = p.slice(0, hashIdx);
|
|
197
|
+
p = p.replace(/\/{2,}/g, "/");
|
|
198
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
199
|
+
return p || "/";
|
|
200
|
+
}
|
|
201
|
+
function enqueue(p, queue, seen, isExcluded) {
|
|
202
|
+
const np = normalizePath(p);
|
|
203
|
+
if (seen.has(np)) return;
|
|
204
|
+
if (isExcluded(np)) return;
|
|
205
|
+
seen.add(np);
|
|
206
|
+
queue.push(np);
|
|
207
|
+
}
|
|
208
|
+
function makeExcludeChecker(excludePaths) {
|
|
209
|
+
const rules = Array.isArray(excludePaths) ? excludePaths : [excludePaths];
|
|
210
|
+
return (p) => {
|
|
211
|
+
const np = normalizePath(p);
|
|
212
|
+
for (const r of rules) {
|
|
213
|
+
if (typeof r === "string" && r === "") continue;
|
|
214
|
+
if (typeof r === "function") {
|
|
215
|
+
if (r(np)) return true;
|
|
216
|
+
} else if (r instanceof RegExp) {
|
|
217
|
+
if (r.test(np)) return true;
|
|
218
|
+
} else if (typeof r === "string") {
|
|
219
|
+
const prefix = normalizePath(r);
|
|
220
|
+
if (np === prefix || np.startsWith(`${prefix}/`)) return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function discoverFontPreloads(clientOutDir) {
|
|
227
|
+
const assetsDir = path.join(clientOutDir, "assets");
|
|
228
|
+
let cssFiles = [];
|
|
229
|
+
try {
|
|
230
|
+
const entries = await fs.readdir(assetsDir);
|
|
231
|
+
cssFiles = entries.filter((f) => f.endsWith(".css")).map((f) => path.join(assetsDir, f));
|
|
232
|
+
} catch {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
const seen = /* @__PURE__ */ new Set();
|
|
236
|
+
const woff2Re = /url\((["']?)(https?:\/\/[^"')]+\.woff2)\1\)/g;
|
|
237
|
+
for (const file of cssFiles) {
|
|
238
|
+
const css = await fs.readFile(file, "utf8");
|
|
239
|
+
for (const [, , url] of css.matchAll(woff2Re)) {
|
|
240
|
+
if (url !== "") seen.add(url);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return [...seen].map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`).join("\n");
|
|
244
|
+
}
|
|
245
|
+
function injectIntoTemplate(template, head, appHtml, fontPreloads, htmlAttrs = "") {
|
|
246
|
+
let out = template;
|
|
247
|
+
if (template.includes("<!--app-html-->")) {
|
|
248
|
+
out = template.replace("<!--app-html-->", appHtml);
|
|
249
|
+
} else if (template.includes('<div id="app"></div>')) {
|
|
250
|
+
out = template.replace('<div id="app"></div>', `<div id="app">${appHtml}</div>`);
|
|
251
|
+
}
|
|
252
|
+
if (htmlAttrs) {
|
|
253
|
+
const langMatch = htmlAttrs.match(/lang="([^"]*)"/);
|
|
254
|
+
if (langMatch) {
|
|
255
|
+
out = out.replace(/(<html[^>]*\s)lang="[^"]*"/, `$1lang="${langMatch[1]}"`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (fontPreloads) {
|
|
259
|
+
out = out.replace("</head>", `${fontPreloads}
|
|
260
|
+
</head>`);
|
|
261
|
+
}
|
|
262
|
+
if (head) {
|
|
263
|
+
const descTagRe = /<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi;
|
|
264
|
+
const descMatches = [...head.matchAll(descTagRe)];
|
|
265
|
+
if (descMatches.length > 1) {
|
|
266
|
+
for (let i = 0; i < descMatches.length - 1; i++) {
|
|
267
|
+
head = head.replace(descMatches[i][0], "");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (/<meta\s[^>]*name\s*=\s*["']description["']/i.test(head)) {
|
|
271
|
+
out = out.replace(/<meta\s[^>]*name\s*=\s*["']description["'][^>]*>/gi, "");
|
|
272
|
+
}
|
|
273
|
+
out = out.replace("</head>", `${head}
|
|
274
|
+
</head>`);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function outFilePath(absClientDir, urlPath, mode) {
|
|
279
|
+
const [pathname] = urlPath.split("?");
|
|
280
|
+
const safe = pathname === "/" ? "/" : pathname;
|
|
281
|
+
if (mode === "file") {
|
|
282
|
+
if (safe === "/") return path.join(absClientDir, "index.html");
|
|
283
|
+
return path.join(absClientDir, `${safe.slice(1)}.html`);
|
|
284
|
+
}
|
|
285
|
+
if (safe === "/") return path.join(absClientDir, "index.html");
|
|
286
|
+
return path.join(absClientDir, safe.slice(1), "index.html");
|
|
287
|
+
}
|
|
288
|
+
function discoverInternalLinks(html) {
|
|
289
|
+
const out = /* @__PURE__ */ new Set();
|
|
290
|
+
const hrefRe = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
291
|
+
let m;
|
|
292
|
+
while ((m = hrefRe.exec(html)) !== null) {
|
|
293
|
+
const raw = (m[1] ?? m[2] ?? "").trim();
|
|
294
|
+
if (!raw) continue;
|
|
295
|
+
if (raw.startsWith("mailto:") || raw.startsWith("tel:") || raw.startsWith("javascript:") || raw.startsWith("#")) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) continue;
|
|
299
|
+
if (/\.(?:pdf|png|jpe?g|gif|svg|webp|css|js|map|ico)(?:\?|$)/i.test(raw)) continue;
|
|
300
|
+
const candidate = raw.startsWith("/") ? raw : `/${raw}`;
|
|
301
|
+
out.add(candidate);
|
|
302
|
+
}
|
|
303
|
+
return [...out];
|
|
304
|
+
}
|
|
305
|
+
exports.fetchCmsPrerenderPaths = fetchCmsPrerenderPaths;
|
|
306
|
+
exports.polyfillBloxSsgGlobals = polyfillBloxSsgGlobals;
|
|
307
|
+
exports.prerender = prerender;
|
|
308
|
+
exports.process = process;
|
package/dist/ssg/cli.cjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const prerender = require("../prerender-mMYLIIC8.cjs");
|
|
4
|
+
async function resolveApiBase(mode) {
|
|
5
|
+
if (prerender.process.env.BAGELINK_API_URL != null && prerender.process.env.BAGELINK_API_URL !== "") {
|
|
6
|
+
return { apiBase: prerender.process.env.BAGELINK_API_URL };
|
|
7
|
+
}
|
|
8
|
+
const configPaths = [
|
|
9
|
+
`${prerender.process.cwd()}/bgl.config.ts`,
|
|
10
|
+
`${prerender.process.cwd()}/bgl.config.js`,
|
|
11
|
+
`${prerender.process.cwd()}/../bgl.config.ts`,
|
|
12
|
+
`${prerender.process.cwd()}/../bgl.config.js`
|
|
13
|
+
];
|
|
14
|
+
for (const configPath of configPaths) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await import(configPath);
|
|
17
|
+
const config = raw.default ?? raw;
|
|
18
|
+
const envConfig = typeof config === "function" ? config(mode) : config[mode];
|
|
19
|
+
if ((envConfig == null ? void 0 : envConfig.host) != null && envConfig.host !== "") {
|
|
20
|
+
return {
|
|
21
|
+
apiBase: envConfig.host,
|
|
22
|
+
websiteName: envConfig.websiteName
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
console.warn(" Could not load bgl.config, using BAGELINK_API_URL or default");
|
|
29
|
+
return { apiBase: "https://localhost:8000" };
|
|
30
|
+
}
|
|
31
|
+
async function main() {
|
|
32
|
+
const argv = prerender.process.argv.slice(2);
|
|
33
|
+
const extraPaths = [];
|
|
34
|
+
let crawl = false;
|
|
35
|
+
let mode = "production";
|
|
36
|
+
const excludePaths = ["/_blox_preview"];
|
|
37
|
+
for (const a of argv) {
|
|
38
|
+
if (a === "--crawl") {
|
|
39
|
+
crawl = true;
|
|
40
|
+
} else if (a === "--no-crawl") {
|
|
41
|
+
crawl = false;
|
|
42
|
+
} else if (a === "--help" || a === "-h") {
|
|
43
|
+
console.log(`
|
|
44
|
+
blox-ssg — Static Site Generator for @bagelink/blox
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
blox-ssg [options] [extra-paths...]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--mode=<env> Environment from bgl.config.ts (default: production)
|
|
51
|
+
--crawl Discover internal links from rendered HTML
|
|
52
|
+
--no-crawl Disable crawling (default)
|
|
53
|
+
--exclude=<path> Exclude path prefix from rendering
|
|
54
|
+
--help, -h Show this help
|
|
55
|
+
|
|
56
|
+
Environment:
|
|
57
|
+
BAGELINK_API_URL Override API base URL
|
|
58
|
+
WEBSITE_NAME Override CMS website name
|
|
59
|
+
`);
|
|
60
|
+
prerender.process.exit(0);
|
|
61
|
+
} else if (a.startsWith("--mode=")) {
|
|
62
|
+
mode = a.slice("--mode=".length);
|
|
63
|
+
} else if (a.startsWith("--exclude=")) {
|
|
64
|
+
excludePaths.push(a.slice("--exclude=".length));
|
|
65
|
+
} else {
|
|
66
|
+
extraPaths.push(a);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const { apiBase, websiteName: configWebsiteName } = await resolveApiBase(mode);
|
|
70
|
+
await prerender.polyfillBloxSsgGlobals({ pageUrl: apiBase });
|
|
71
|
+
const websiteName = prerender.process.env.WEBSITE_NAME ?? configWebsiteName ?? "default";
|
|
72
|
+
console.log(`Fetching routes from ${apiBase} (mode: ${mode}, site: ${websiteName})…`);
|
|
73
|
+
let paths = [];
|
|
74
|
+
try {
|
|
75
|
+
paths = await prerender.fetchCmsPrerenderPaths(apiBase, websiteName);
|
|
76
|
+
console.log(` Found ${paths.length} CMS routes`);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(" Failed to fetch CMS routes:", err.message);
|
|
79
|
+
paths = ["/"];
|
|
80
|
+
}
|
|
81
|
+
for (const p of extraPaths) {
|
|
82
|
+
if (!paths.includes(p)) paths.push(p);
|
|
83
|
+
}
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
try {
|
|
86
|
+
const result = await prerender.prerender({
|
|
87
|
+
root: prerender.process.cwd(),
|
|
88
|
+
clientOutDir: "dist",
|
|
89
|
+
serverEntry: "dist/server/main.server.js",
|
|
90
|
+
paths,
|
|
91
|
+
crawl,
|
|
92
|
+
excludePaths,
|
|
93
|
+
maxPages: 5e3,
|
|
94
|
+
mode: "file"
|
|
95
|
+
});
|
|
96
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
97
|
+
console.log(`
|
|
98
|
+
${"=".repeat(60)}`);
|
|
99
|
+
console.log("PRERENDER RESULTS");
|
|
100
|
+
console.log("=".repeat(60));
|
|
101
|
+
console.log(`Rendered: ${result.rendered.length} pages`);
|
|
102
|
+
console.log(`Failures: ${result.failures.length}`);
|
|
103
|
+
console.log(`Discovered: ${result.totalDiscovered}`);
|
|
104
|
+
console.log(`Time: ${elapsed}s`);
|
|
105
|
+
if (Number.parseFloat(elapsed) > 0) {
|
|
106
|
+
console.log(`Rate: ${(result.rendered.length / Number.parseFloat(elapsed)).toFixed(2)} pages/sec`);
|
|
107
|
+
}
|
|
108
|
+
console.log("=".repeat(60));
|
|
109
|
+
if (result.failures.length > 0) {
|
|
110
|
+
console.log("\nFAILURES:");
|
|
111
|
+
result.failures.slice(0, 10).forEach((f) => {
|
|
112
|
+
console.log(` - ${f.path}: ${f.error.message}`);
|
|
113
|
+
});
|
|
114
|
+
if (result.failures.length > 10) {
|
|
115
|
+
console.log(` ... and ${result.failures.length - 10} more`);
|
|
116
|
+
}
|
|
117
|
+
prerender.process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error("Fatal error:", err);
|
|
121
|
+
prerender.process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* blox-ssg CLI — generic static site generator for any @bagelink/blox site.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx blox-ssg # production mode
|
|
7
|
+
* bunx blox-ssg --mode=development
|
|
8
|
+
* bunx blox-ssg --mode=localhost
|
|
9
|
+
* bunx blox-ssg --crawl
|
|
10
|
+
* bunx blox-ssg --exclude=/admin
|
|
11
|
+
* bunx blox-ssg /about /pricing # extra paths
|
|
12
|
+
*
|
|
13
|
+
* Reads bgl.config.ts from cwd to resolve the API host.
|
|
14
|
+
* WEBSITE_NAME env var overrides the website name (defaults from config or 'default').
|
|
15
|
+
*
|
|
16
|
+
* Env overrides:
|
|
17
|
+
* BAGELINK_API_URL — skip config, use this host directly
|
|
18
|
+
* WEBSITE_NAME — CMS website name
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ssg/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
|
package/dist/ssg/cli.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { b as process, p as polyfillBloxSsgGlobals, f as fetchCmsPrerenderPaths, a as prerender } from "../prerender-BwmtiKjz.js";
|
|
3
|
+
async function resolveApiBase(mode) {
|
|
4
|
+
if (process.env.BAGELINK_API_URL != null && process.env.BAGELINK_API_URL !== "") {
|
|
5
|
+
return { apiBase: process.env.BAGELINK_API_URL };
|
|
6
|
+
}
|
|
7
|
+
const configPaths = [
|
|
8
|
+
`${process.cwd()}/bgl.config.ts`,
|
|
9
|
+
`${process.cwd()}/bgl.config.js`,
|
|
10
|
+
`${process.cwd()}/../bgl.config.ts`,
|
|
11
|
+
`${process.cwd()}/../bgl.config.js`
|
|
12
|
+
];
|
|
13
|
+
for (const configPath of configPaths) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await import(configPath);
|
|
16
|
+
const config = raw.default ?? raw;
|
|
17
|
+
const envConfig = typeof config === "function" ? config(mode) : config[mode];
|
|
18
|
+
if ((envConfig == null ? void 0 : envConfig.host) != null && envConfig.host !== "") {
|
|
19
|
+
return {
|
|
20
|
+
apiBase: envConfig.host,
|
|
21
|
+
websiteName: envConfig.websiteName
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.warn(" Could not load bgl.config, using BAGELINK_API_URL or default");
|
|
28
|
+
return { apiBase: "https://localhost:8000" };
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
const argv = process.argv.slice(2);
|
|
32
|
+
const extraPaths = [];
|
|
33
|
+
let crawl = false;
|
|
34
|
+
let mode = "production";
|
|
35
|
+
const excludePaths = ["/_blox_preview"];
|
|
36
|
+
for (const a of argv) {
|
|
37
|
+
if (a === "--crawl") {
|
|
38
|
+
crawl = true;
|
|
39
|
+
} else if (a === "--no-crawl") {
|
|
40
|
+
crawl = false;
|
|
41
|
+
} else if (a === "--help" || a === "-h") {
|
|
42
|
+
console.log(`
|
|
43
|
+
blox-ssg — Static Site Generator for @bagelink/blox
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
blox-ssg [options] [extra-paths...]
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--mode=<env> Environment from bgl.config.ts (default: production)
|
|
50
|
+
--crawl Discover internal links from rendered HTML
|
|
51
|
+
--no-crawl Disable crawling (default)
|
|
52
|
+
--exclude=<path> Exclude path prefix from rendering
|
|
53
|
+
--help, -h Show this help
|
|
54
|
+
|
|
55
|
+
Environment:
|
|
56
|
+
BAGELINK_API_URL Override API base URL
|
|
57
|
+
WEBSITE_NAME Override CMS website name
|
|
58
|
+
`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} else if (a.startsWith("--mode=")) {
|
|
61
|
+
mode = a.slice("--mode=".length);
|
|
62
|
+
} else if (a.startsWith("--exclude=")) {
|
|
63
|
+
excludePaths.push(a.slice("--exclude=".length));
|
|
64
|
+
} else {
|
|
65
|
+
extraPaths.push(a);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const { apiBase, websiteName: configWebsiteName } = await resolveApiBase(mode);
|
|
69
|
+
await polyfillBloxSsgGlobals({ pageUrl: apiBase });
|
|
70
|
+
const websiteName = process.env.WEBSITE_NAME ?? configWebsiteName ?? "default";
|
|
71
|
+
console.log(`Fetching routes from ${apiBase} (mode: ${mode}, site: ${websiteName})…`);
|
|
72
|
+
let paths = [];
|
|
73
|
+
try {
|
|
74
|
+
paths = await fetchCmsPrerenderPaths(apiBase, websiteName);
|
|
75
|
+
console.log(` Found ${paths.length} CMS routes`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(" Failed to fetch CMS routes:", err.message);
|
|
78
|
+
paths = ["/"];
|
|
79
|
+
}
|
|
80
|
+
for (const p of extraPaths) {
|
|
81
|
+
if (!paths.includes(p)) paths.push(p);
|
|
82
|
+
}
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
try {
|
|
85
|
+
const result = await prerender({
|
|
86
|
+
root: process.cwd(),
|
|
87
|
+
clientOutDir: "dist",
|
|
88
|
+
serverEntry: "dist/server/main.server.js",
|
|
89
|
+
paths,
|
|
90
|
+
crawl,
|
|
91
|
+
excludePaths,
|
|
92
|
+
maxPages: 5e3,
|
|
93
|
+
mode: "file"
|
|
94
|
+
});
|
|
95
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
96
|
+
console.log(`
|
|
97
|
+
${"=".repeat(60)}`);
|
|
98
|
+
console.log("PRERENDER RESULTS");
|
|
99
|
+
console.log("=".repeat(60));
|
|
100
|
+
console.log(`Rendered: ${result.rendered.length} pages`);
|
|
101
|
+
console.log(`Failures: ${result.failures.length}`);
|
|
102
|
+
console.log(`Discovered: ${result.totalDiscovered}`);
|
|
103
|
+
console.log(`Time: ${elapsed}s`);
|
|
104
|
+
if (Number.parseFloat(elapsed) > 0) {
|
|
105
|
+
console.log(`Rate: ${(result.rendered.length / Number.parseFloat(elapsed)).toFixed(2)} pages/sec`);
|
|
106
|
+
}
|
|
107
|
+
console.log("=".repeat(60));
|
|
108
|
+
if (result.failures.length > 0) {
|
|
109
|
+
console.log("\nFAILURES:");
|
|
110
|
+
result.failures.slice(0, 10).forEach((f) => {
|
|
111
|
+
console.log(` - ${f.path}: ${f.error.message}`);
|
|
112
|
+
});
|
|
113
|
+
if (result.failures.length > 10) {
|
|
114
|
+
console.log(` ... and ${result.failures.length - 10} more`);
|
|
115
|
+
}
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("Fatal error:", err);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
main();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchCmsPrerenderPaths(apiBase: string, websiteName: string): Promise<string[]>;
|
|
8
|
+
//# sourceMappingURL=cms-routes.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/ssg/constants.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,EAAG,gBAAyB,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const prerender = require("../prerender-mMYLIIC8.cjs");
|
|
4
|
+
const BLOX_STATE_WINDOW_KEY = "__BLOX_STATE__";
|
|
5
|
+
async function renderBloxSsgPage(options) {
|
|
6
|
+
const {
|
|
7
|
+
url,
|
|
8
|
+
resolvedData,
|
|
9
|
+
renderToString,
|
|
10
|
+
createAppForUrl,
|
|
11
|
+
stateWindowKey = BLOX_STATE_WINDOW_KEY
|
|
12
|
+
} = options;
|
|
13
|
+
const originalFetch = globalThis.fetch;
|
|
14
|
+
if (resolvedData != null) {
|
|
15
|
+
globalThis.fetch = async (input, init) => {
|
|
16
|
+
const u = String(input);
|
|
17
|
+
if (u.includes("resolve-path")) {
|
|
18
|
+
return new Response(JSON.stringify(resolvedData), {
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: { "Content-Type": "application/json" }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return originalFetch(input, init);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const { app: app1, router: router1 } = createAppForUrl(url);
|
|
28
|
+
await router1.push(url);
|
|
29
|
+
await router1.isReady();
|
|
30
|
+
await renderToString(app1);
|
|
31
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
32
|
+
const { app: app2, router: router2 } = createAppForUrl(url);
|
|
33
|
+
await router2.push(url);
|
|
34
|
+
await router2.isReady();
|
|
35
|
+
const html = await renderToString(app2);
|
|
36
|
+
const head = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
|
|
37
|
+
return { html, head };
|
|
38
|
+
} finally {
|
|
39
|
+
globalThis.fetch = originalFetch;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function installBloxStateCache(globalKey = BLOX_STATE_WINDOW_KEY) {
|
|
43
|
+
if (typeof window === "undefined") return;
|
|
44
|
+
const state = window[globalKey];
|
|
45
|
+
if (!state || typeof state !== "object" || Object.keys(state).length === 0) return;
|
|
46
|
+
const originalFetch = window.fetch.bind(window);
|
|
47
|
+
window.fetch = async function(input, init) {
|
|
48
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
49
|
+
if (typeof url === "string" && url.includes("resolve-path")) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(url, window.location.origin);
|
|
52
|
+
const path = parsed.searchParams.get("path") ?? window.location.pathname;
|
|
53
|
+
if (path !== "" && state[path] != null) {
|
|
54
|
+
const cached = state[path];
|
|
55
|
+
delete state[path];
|
|
56
|
+
return new Response(JSON.stringify(cached), {
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: { "Content-Type": "application/json" }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return originalFetch(input, init);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
exports.fetchCmsPrerenderPaths = prerender.fetchCmsPrerenderPaths;
|
|
68
|
+
exports.polyfillBloxSsgGlobals = prerender.polyfillBloxSsgGlobals;
|
|
69
|
+
exports.prerender = prerender.prerender;
|
|
70
|
+
exports.BLOX_STATE_WINDOW_KEY = BLOX_STATE_WINDOW_KEY;
|
|
71
|
+
exports.installBloxStateCache = installBloxStateCache;
|
|
72
|
+
exports.renderBloxSsgPage = renderBloxSsgPage;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { fetchCmsPrerenderPaths } from './cms-routes';
|
|
2
|
+
/**
|
|
3
|
+
* @bagelink/blox/ssg — Static Site Generation helpers for Blox CMS sites.
|
|
4
|
+
*
|
|
5
|
+
* Provides everything needed to prerender a Vue + Blox app at build time:
|
|
6
|
+
* - Browser global polyfills for Node.js
|
|
7
|
+
* - CMS route discovery (including template page expansion via data_bindings)
|
|
8
|
+
* - Double-render page renderer with state embedding
|
|
9
|
+
* - Client-side fetch interceptor for zero-API-call hydration
|
|
10
|
+
* - Full prerender orchestrator with crawl support
|
|
11
|
+
*/
|
|
12
|
+
export { BLOX_STATE_WINDOW_KEY } from './constants';
|
|
13
|
+
export { polyfillBloxSsgGlobals } from './polyfill-node';
|
|
14
|
+
export { prerender } from './prerender';
|
|
15
|
+
export type { PrerenderOptions, PrerenderResult, RenderContext, RenderResult } from './prerender';
|
|
16
|
+
export { renderBloxSsgPage } from './render-resolved-page';
|
|
17
|
+
export type { BloxSsgRouterLike } from './render-resolved-page';
|
|
18
|
+
export { installBloxStateCache } from './state-cache';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { f, p, a } from "../prerender-BwmtiKjz.js";
|
|
2
|
+
const BLOX_STATE_WINDOW_KEY = "__BLOX_STATE__";
|
|
3
|
+
async function renderBloxSsgPage(options) {
|
|
4
|
+
const {
|
|
5
|
+
url,
|
|
6
|
+
resolvedData,
|
|
7
|
+
renderToString,
|
|
8
|
+
createAppForUrl,
|
|
9
|
+
stateWindowKey = BLOX_STATE_WINDOW_KEY
|
|
10
|
+
} = options;
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
if (resolvedData != null) {
|
|
13
|
+
globalThis.fetch = async (input, init) => {
|
|
14
|
+
const u = String(input);
|
|
15
|
+
if (u.includes("resolve-path")) {
|
|
16
|
+
return new Response(JSON.stringify(resolvedData), {
|
|
17
|
+
status: 200,
|
|
18
|
+
headers: { "Content-Type": "application/json" }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return originalFetch(input, init);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const { app: app1, router: router1 } = createAppForUrl(url);
|
|
26
|
+
await router1.push(url);
|
|
27
|
+
await router1.isReady();
|
|
28
|
+
await renderToString(app1);
|
|
29
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
30
|
+
const { app: app2, router: router2 } = createAppForUrl(url);
|
|
31
|
+
await router2.push(url);
|
|
32
|
+
await router2.isReady();
|
|
33
|
+
const html = await renderToString(app2);
|
|
34
|
+
const head = resolvedData != null ? `<script>window[${JSON.stringify(stateWindowKey)}]=${JSON.stringify({ [url]: resolvedData })};${"<"}/script>` : "";
|
|
35
|
+
return { html, head };
|
|
36
|
+
} finally {
|
|
37
|
+
globalThis.fetch = originalFetch;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function installBloxStateCache(globalKey = BLOX_STATE_WINDOW_KEY) {
|
|
41
|
+
if (typeof window === "undefined") return;
|
|
42
|
+
const state = window[globalKey];
|
|
43
|
+
if (!state || typeof state !== "object" || Object.keys(state).length === 0) return;
|
|
44
|
+
const originalFetch = window.fetch.bind(window);
|
|
45
|
+
window.fetch = async function(input, init) {
|
|
46
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
47
|
+
if (typeof url === "string" && url.includes("resolve-path")) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url, window.location.origin);
|
|
50
|
+
const path = parsed.searchParams.get("path") ?? window.location.pathname;
|
|
51
|
+
if (path !== "" && state[path] != null) {
|
|
52
|
+
const cached = state[path];
|
|
53
|
+
delete state[path];
|
|
54
|
+
return new Response(JSON.stringify(cached), {
|
|
55
|
+
status: 200,
|
|
56
|
+
headers: { "Content-Type": "application/json" }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return originalFetch(input, init);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
BLOX_STATE_WINDOW_KEY,
|
|
67
|
+
f as fetchCmsPrerenderPaths,
|
|
68
|
+
installBloxStateCache,
|
|
69
|
+
p as polyfillBloxSsgGlobals,
|
|
70
|
+
a as prerender,
|
|
71
|
+
renderBloxSsgPage
|
|
72
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfill browser globals for Node SSG before importing Vue / @bagelink/blox.
|
|
3
|
+
*
|
|
4
|
+
* Call this as early as possible (before any Vue or Blox import) in your
|
|
5
|
+
* SSG script:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { polyfillBloxSsgGlobals } from '@bagelink/blox/ssg'
|
|
9
|
+
* await polyfillBloxSsgGlobals()
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function polyfillBloxSsgGlobals(options?: {
|
|
13
|
+
pageUrl?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=polyfill-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"polyfill-node.d.ts","sourceRoot":"","sources":["../../src/ssg/polyfill-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C1F"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type ExcludeRule = string | RegExp | ((path: string) => boolean);
|
|
2
|
+
export interface PrerenderOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
clientOutDir?: string;
|
|
5
|
+
serverEntry?: string;
|
|
6
|
+
paths?: string[];
|
|
7
|
+
crawl?: boolean;
|
|
8
|
+
excludePaths?: ExcludeRule | ExcludeRule[];
|
|
9
|
+
failFast?: boolean;
|
|
10
|
+
maxPages?: number;
|
|
11
|
+
mode?: 'dir' | 'file';
|
|
12
|
+
}
|
|
13
|
+
export interface RenderContext {
|
|
14
|
+
manifest: Record<string, string[]> | null;
|
|
15
|
+
template: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RenderResult {
|
|
18
|
+
html: string;
|
|
19
|
+
head?: string;
|
|
20
|
+
htmlAttrs?: string;
|
|
21
|
+
state?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface PrerenderResult {
|
|
24
|
+
rendered: string[];
|
|
25
|
+
failures: Array<{
|
|
26
|
+
path: string | undefined;
|
|
27
|
+
error: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
queuedRemaining: number;
|
|
30
|
+
totalDiscovered: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Prerender a Vue + Vite + Blox app to static HTML files.
|
|
34
|
+
*
|
|
35
|
+
* Features:
|
|
36
|
+
* - Starts from "/" (home).
|
|
37
|
+
* - Optionally accepts explicit paths (from CMS).
|
|
38
|
+
* - "crawl" (default true) discovers internal links in rendered HTML and queues them.
|
|
39
|
+
* - excludePaths can be strings, RegExp, or a predicate function.
|
|
40
|
+
* - Route-level failures are logged and skipped by default.
|
|
41
|
+
*/
|
|
42
|
+
export declare function prerender({ root, clientOutDir, serverEntry, paths, crawl, excludePaths, failFast, maxPages, mode, }?: PrerenderOptions): Promise<PrerenderResult>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=prerender.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface BloxSsgRouterLike {
|
|
2
|
+
push: (url: string) => Promise<void>;
|
|
3
|
+
isReady: () => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Render a Blox CMS page to static HTML with embedded state.
|
|
7
|
+
*
|
|
8
|
+
* Uses a double-render + `fetch` mock strategy so that the async
|
|
9
|
+
* `CmsPageView.load()` resolves before the final HTML is captured.
|
|
10
|
+
*
|
|
11
|
+
* Returns `{ html, head }` where `head` contains a `<script>` that sets
|
|
12
|
+
* `window.__BLOX_STATE__` for zero-API-call client hydration.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderBloxSsgPage<TApp>(options: {
|
|
15
|
+
url: string;
|
|
16
|
+
resolvedData: unknown | null;
|
|
17
|
+
renderToString: (app: TApp) => Promise<string>;
|
|
18
|
+
createAppForUrl: (url: string) => {
|
|
19
|
+
app: TApp;
|
|
20
|
+
router: BloxSsgRouterLike;
|
|
21
|
+
};
|
|
22
|
+
stateWindowKey?: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
html: string;
|
|
25
|
+
head?: string;
|
|
26
|
+
}>;
|
|
27
|
+
//# sourceMappingURL=render-resolved-page.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;GAQG;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,CA+C3C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side fetch interceptor for zero-cost hydration.
|
|
3
|
+
*
|
|
4
|
+
* During SSG, `entry-server` embeds resolved CMS data as
|
|
5
|
+
* `window.__BLOX_STATE__ = { [path]: resolvedData }`.
|
|
6
|
+
*
|
|
7
|
+
* This function patches `window.fetch` so the very first
|
|
8
|
+
* `GET …/resolve-path?path=…` call (made by CmsPageView on mount)
|
|
9
|
+
* returns the pre-fetched data instead of hitting the network.
|
|
10
|
+
*
|
|
11
|
+
* Subsequent navigations (SPA) go through normally.
|
|
12
|
+
*
|
|
13
|
+
* Call once, before app mount, in your client entry:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { installBloxStateCache } from '@bagelink/blox/ssg'
|
|
17
|
+
* installBloxStateCache()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function installBloxStateCache(globalKey?: string): void;
|
|
21
|
+
//# sourceMappingURL=state-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-cache.d.ts","sourceRoot":"","sources":["../../src/ssg/state-cache.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,GAAE,MAA8B,QAqC9E"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bagelink/blox",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.12.
|
|
4
|
+
"version": "1.12.11",
|
|
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",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"drag-and-drop",
|
|
28
28
|
"static-pages"
|
|
29
29
|
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"blox-ssg": "./dist/ssg/cli.mjs"
|
|
32
|
+
},
|
|
30
33
|
"sideEffects": false,
|
|
31
34
|
"exports": {
|
|
32
35
|
"./package.json": "./package.json",
|
|
@@ -39,6 +42,10 @@
|
|
|
39
42
|
"require": "./dist/index.cjs",
|
|
40
43
|
"import": "./dist/index.mjs"
|
|
41
44
|
},
|
|
45
|
+
"./ssg": {
|
|
46
|
+
"types": "./dist/ssg/index.d.ts",
|
|
47
|
+
"import": "./dist/ssg/index.mjs"
|
|
48
|
+
},
|
|
42
49
|
"./components": {
|
|
43
50
|
"types": "./dist/components/index.d.ts",
|
|
44
51
|
"import": "./dist/components/index.mjs"
|
|
@@ -69,6 +76,7 @@
|
|
|
69
76
|
"core",
|
|
70
77
|
"components",
|
|
71
78
|
"config",
|
|
79
|
+
"ssg",
|
|
72
80
|
"utils",
|
|
73
81
|
"views",
|
|
74
82
|
"setup.ts",
|
|
@@ -80,11 +88,15 @@
|
|
|
80
88
|
},
|
|
81
89
|
"peerDependencies": {
|
|
82
90
|
"@bagelink/vue": "*",
|
|
91
|
+
"happy-dom": ">=14.0.0",
|
|
83
92
|
"vite": ">=5.0.0",
|
|
84
93
|
"vue": "^3.3.0",
|
|
85
94
|
"vue-router": "^4.0.0"
|
|
86
95
|
},
|
|
87
96
|
"peerDependenciesMeta": {
|
|
97
|
+
"happy-dom": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
88
100
|
"vite": {
|
|
89
101
|
"optional": true
|
|
90
102
|
}
|