@lownoise-studio/rendershield 0.1.4 → 0.3.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.
- package/dist/cli.js +19 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.js +96 -0
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/verify.js +129 -38
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/generateWorker.js +75 -68
- package/dist/core/generateWorker.js.map +1 -1
- package/dist/core/loadConfig.js +53 -1
- package/dist/core/loadConfig.js.map +1 -1
- package/dist/core/validateOutput.js +154 -29
- package/dist/core/validateOutput.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +60 -54
- package/src/commands/build.ts +185 -72
- package/src/commands/verify.ts +233 -114
- package/src/core/generateWorker.ts +87 -80
- package/src/core/loadConfig.ts +142 -74
- package/src/core/validateOutput.ts +214 -37
package/dist/cli.js
CHANGED
|
@@ -3,18 +3,21 @@ import { cmdInit } from "./commands/init.js";
|
|
|
3
3
|
import { cmdBuild } from "./commands/build.js";
|
|
4
4
|
import { cmdVerify } from "./commands/verify.js";
|
|
5
5
|
function printHelp() {
|
|
6
|
-
console.log(`
|
|
7
|
-
RenderShield (v0) — boring bot-aware prerendering.
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
rendershield init
|
|
11
|
-
rendershield build
|
|
12
|
-
rendershield verify
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
console.log(`
|
|
7
|
+
RenderShield (v0) — boring bot-aware prerendering.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
rendershield init
|
|
11
|
+
rendershield build
|
|
12
|
+
rendershield verify [--prod <url>]
|
|
13
|
+
|
|
14
|
+
verify Print curl commands for local/build output.
|
|
15
|
+
verify --prod Fetch URL as bot and human; verify bot sees full HTML and contract fields.
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
- Config file: rendershield.config.json
|
|
19
|
+
- Content: content/<collection>/**/*.md (frontmatter required)
|
|
20
|
+
- Output: dist-prerender/
|
|
18
21
|
`);
|
|
19
22
|
}
|
|
20
23
|
async function main() {
|
|
@@ -33,7 +36,10 @@ async function main() {
|
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
if (cmd === "verify") {
|
|
36
|
-
|
|
39
|
+
const args = process.argv.slice(3);
|
|
40
|
+
const prodIdx = args.indexOf("--prod");
|
|
41
|
+
const prodUrl = prodIdx >= 0 && args[prodIdx + 1] ? args[prodIdx + 1].trim() : undefined;
|
|
42
|
+
await cmdVerify(undefined, prodUrl ? { prodUrl } : undefined);
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
45
|
console.error(`Unknown command: ${cmd}\n`);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;CAeb,CAAC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAEpC,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC7C,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACnB,MAAM,OAAO,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,MAAM,QAAQ,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACnC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YACzF,MAAM,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YAC9D,OAAO;QACT,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;QAC3C,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,MAAM,GAAG,GAAG,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
|
package/dist/commands/build.js
CHANGED
|
@@ -12,8 +12,103 @@ function routeToOutDir(outDirAbs, routePath) {
|
|
|
12
12
|
const clean = routePath.replace(/^\//, "");
|
|
13
13
|
return path.join(outDirAbs, clean);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates output path before any destructive operation (fs.remove).
|
|
17
|
+
* Pass: outDir is a subdirectory inside project root; no symlink escape.
|
|
18
|
+
* Fail: "/", "C:\", "..", "../", or outDir (or any of its existing parents) resolving outside project.
|
|
19
|
+
* Policy: strict — build hard-fails before any delete attempt.
|
|
20
|
+
* Segment-safe: uses path.relative(root, out) only; no prefix startsWith.
|
|
21
|
+
*/
|
|
22
|
+
async function validateOutputPath(outDir, cwd) {
|
|
23
|
+
const cwdAbs = path.resolve(cwd);
|
|
24
|
+
let cwdReal;
|
|
25
|
+
try {
|
|
26
|
+
cwdReal = await fs.realpath(cwdAbs);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
throw new Error(`Cannot resolve project root: ${cwdAbs}. ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
const outDirAbs = path.resolve(cwdAbs, outDir);
|
|
33
|
+
// Order: realpath → normalize → then case-fold for comparisons
|
|
34
|
+
const normalizedCwd = path.normalize(cwdReal);
|
|
35
|
+
const normalizedOut = path.normalize(outDirAbs);
|
|
36
|
+
// Segment-safe: use path.relative only (no prefix startsWith on full path). Cross-platform: relative()
|
|
37
|
+
// rarely returns absolute on Windows; startsWith("..") and path.isAbsolute(rel) cover escapes.
|
|
38
|
+
const relative = path.relative(normalizedCwd, normalizedOut);
|
|
39
|
+
if (relative.startsWith("..") || relative === ".." || path.isAbsolute(relative)) {
|
|
40
|
+
throw new Error(`Output directory "${outDir}" resolves outside project root. Use a relative path within the project.`);
|
|
41
|
+
}
|
|
42
|
+
// Reject: root filesystem (/, C:\, C:/) — early reject; real safety is "inside root" above
|
|
43
|
+
const rootPaths = ["/", "c:\\", "c:/"];
|
|
44
|
+
const outLower = normalizedOut.toLowerCase();
|
|
45
|
+
if (rootPaths.includes(outLower)) {
|
|
46
|
+
throw new Error(`Output directory "${outDir}" resolves to root filesystem. This is not allowed for safety.`);
|
|
47
|
+
}
|
|
48
|
+
// Reject: output dir equals project root (would delete entire project). Case-fold after normalize.
|
|
49
|
+
const cwdLower = normalizedCwd.toLowerCase();
|
|
50
|
+
if (outLower === cwdLower) {
|
|
51
|
+
throw new Error(`Output directory "${outDir}" cannot be the project root. Use a subdirectory (e.g. dist-prerender).`);
|
|
52
|
+
}
|
|
53
|
+
if (await fs.pathExists(outDirAbs)) {
|
|
54
|
+
// Path exists: resolve symlinks and ensure real path is inside root
|
|
55
|
+
let outDirReal;
|
|
56
|
+
try {
|
|
57
|
+
outDirReal = await fs.realpath(outDirAbs);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
outDirReal = outDirAbs;
|
|
61
|
+
}
|
|
62
|
+
const outDirRealNorm = path.normalize(outDirReal);
|
|
63
|
+
const relativeReal = path.relative(normalizedCwd, outDirRealNorm);
|
|
64
|
+
if (relativeReal.startsWith("..") || relativeReal === ".." || path.isAbsolute(relativeReal)) {
|
|
65
|
+
throw new Error(`Output directory "${outDir}" resolves (via symlink) outside project root. Use a path that does not escape the project.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Path does not exist: walk up to nearest existing parent, realpath it, ensure it stays inside root.
|
|
70
|
+
// Guards e.g. outDir "dist-link/prerender-new" where dist-link is a symlink to /.
|
|
71
|
+
// If we never find an existing parent (or the only one is root), we must reject — do not treat as "fine."
|
|
72
|
+
let current = normalizedOut;
|
|
73
|
+
const rootDir = path.normalize(path.parse(normalizedCwd).root);
|
|
74
|
+
let foundParentInsideRoot = false;
|
|
75
|
+
while (current) {
|
|
76
|
+
if (await fs.pathExists(current)) {
|
|
77
|
+
let parentReal;
|
|
78
|
+
try {
|
|
79
|
+
parentReal = await fs.realpath(current);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
parentReal = current;
|
|
83
|
+
}
|
|
84
|
+
const parentRealNorm = path.normalize(parentReal);
|
|
85
|
+
// Nearest existing parent is filesystem root → outside project, reject
|
|
86
|
+
const parentLower = parentRealNorm.toLowerCase();
|
|
87
|
+
if (rootPaths.includes(parentLower)) {
|
|
88
|
+
throw new Error(`Output directory "${outDir}" has a parent that resolves to filesystem root. Use a path inside the project.`);
|
|
89
|
+
}
|
|
90
|
+
const relParent = path.relative(normalizedCwd, parentRealNorm);
|
|
91
|
+
if (relParent.startsWith("..") || relParent === ".." || path.isAbsolute(relParent)) {
|
|
92
|
+
throw new Error(`Output directory "${outDir}" has a parent that resolves (via symlink) outside project root. Use a path that does not escape the project.`);
|
|
93
|
+
}
|
|
94
|
+
foundParentInsideRoot = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const parent = path.dirname(current);
|
|
98
|
+
if (parent === current)
|
|
99
|
+
break;
|
|
100
|
+
current = parent;
|
|
101
|
+
}
|
|
102
|
+
if (!foundParentInsideRoot) {
|
|
103
|
+
// No existing parent found before hitting dirname loop stop (shouldn't happen on a normal FS)
|
|
104
|
+
throw new Error(`Output directory "${outDir}" could not be validated: no existing parent path found. Use a path inside the project.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
15
108
|
export async function cmdBuild(cwd = process.cwd()) {
|
|
16
109
|
const cfg = await loadConfig(cwd);
|
|
110
|
+
// Validate output path before any destructive operations
|
|
111
|
+
await validateOutputPath(cfg.output.outDir, cwd);
|
|
17
112
|
const outDirAbs = path.join(cwd, cfg.output.outDir);
|
|
18
113
|
// Clean output (boring + deterministic)
|
|
19
114
|
await fs.remove(outDirAbs);
|
|
@@ -32,6 +127,7 @@ export async function cmdBuild(cwd = process.cwd()) {
|
|
|
32
127
|
html,
|
|
33
128
|
outFile,
|
|
34
129
|
routePath: doc.routePath,
|
|
130
|
+
sourcePath: doc.sourcePath,
|
|
35
131
|
});
|
|
36
132
|
await fs.writeFile(outFile, html, "utf8");
|
|
37
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,SAAS,aAAa,CAAC,SAAiB,EAAE,SAAiB;IACzD,4CAA4C;IAC5C,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAChD,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/verify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadConfig } from "../core/loadConfig.js";
|
|
4
|
+
import { checkPrerenderContract } from "../core/validateOutput.js";
|
|
4
5
|
function joinUrl(base, routePath) {
|
|
5
6
|
const b = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
6
7
|
const p = routePath.startsWith("/") ? routePath : `/${routePath}`;
|
|
@@ -43,58 +44,148 @@ function indexHtmlPathToRoute(outDirAbs, indexPathAbs) {
|
|
|
43
44
|
const normalized = noFile.split(path.sep).join("/").replace(/\/+$/, "");
|
|
44
45
|
return "/" + normalized.replace(/^\/+/, "");
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
const BOT_UA = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)";
|
|
48
|
+
const HUMAN_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
49
|
+
/** Heuristic: likely SPA shell if body has almost no visible content and no article. */
|
|
50
|
+
function looksLikeSpaShell(html) {
|
|
51
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
52
|
+
const bodyHtml = bodyMatch ? bodyMatch[1] : html;
|
|
53
|
+
const noScript = bodyHtml
|
|
54
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
55
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
56
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ");
|
|
57
|
+
const text = noScript.replace(/<\/?[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
58
|
+
if (text.length < 150) {
|
|
59
|
+
return { likely: true, reason: `Body text very short (${text.length} chars); likely app shell.` };
|
|
60
|
+
}
|
|
61
|
+
if (!/<article\b/i.test(html)) {
|
|
62
|
+
return { likely: true, reason: "No <article> present; may be SPA shell." };
|
|
63
|
+
}
|
|
64
|
+
const rootOnly = /<body[^>]*>\s*<div[^>]*id=["'](?:root|app|__next)["'][^>]*>\s*<\/div>\s*<\/body>/i.test(html.replace(/\s+/g, " "));
|
|
65
|
+
if (rootOnly) {
|
|
66
|
+
return { likely: true, reason: "Single root div (e.g. #root, #app) with no content." };
|
|
67
|
+
}
|
|
68
|
+
return { likely: false };
|
|
69
|
+
}
|
|
70
|
+
export async function cmdVerify(cwd = process.cwd(), options) {
|
|
71
|
+
if (options?.prodUrl) {
|
|
72
|
+
await runVerifyProd(options.prodUrl);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
47
75
|
const cfg = await loadConfig(cwd);
|
|
48
76
|
const outDirAbs = path.join(cwd, cfg.output.outDir);
|
|
49
77
|
const exists = await fs.pathExists(outDirAbs);
|
|
50
78
|
if (!exists) {
|
|
51
|
-
console.log(`
|
|
52
|
-
RenderShield verify
|
|
53
|
-
|
|
54
|
-
No prerender output directory found: ${cfg.output.outDir}/
|
|
55
|
-
|
|
56
|
-
Run:
|
|
57
|
-
node dist/cli.js build
|
|
79
|
+
console.log(`
|
|
80
|
+
RenderShield verify
|
|
81
|
+
|
|
82
|
+
No prerender output directory found: ${cfg.output.outDir}/
|
|
83
|
+
|
|
84
|
+
Run:
|
|
85
|
+
node dist/cli.js build
|
|
58
86
|
`);
|
|
59
87
|
return;
|
|
60
88
|
}
|
|
61
89
|
const firstIndex = await findFirstIndexHtml(outDirAbs);
|
|
62
90
|
if (!firstIndex) {
|
|
63
|
-
console.log(`
|
|
64
|
-
RenderShield verify
|
|
65
|
-
|
|
66
|
-
No prerendered pages found inside: ${cfg.output.outDir}/
|
|
67
|
-
|
|
68
|
-
Run:
|
|
69
|
-
node dist/cli.js build
|
|
91
|
+
console.log(`
|
|
92
|
+
RenderShield verify
|
|
93
|
+
|
|
94
|
+
No prerendered pages found inside: ${cfg.output.outDir}/
|
|
95
|
+
|
|
96
|
+
Run:
|
|
97
|
+
node dist/cli.js build
|
|
70
98
|
`);
|
|
71
99
|
return;
|
|
72
100
|
}
|
|
73
101
|
const routePath = indexHtmlPathToRoute(outDirAbs, firstIndex);
|
|
74
102
|
const url = joinUrl(cfg.site.canonicalBase, routePath);
|
|
75
|
-
console.log(`
|
|
76
|
-
RenderShield verify
|
|
77
|
-
|
|
78
|
-
Using:
|
|
79
|
-
canonicalBase: ${cfg.site.canonicalBase}
|
|
80
|
-
routePath: ${routePath}
|
|
81
|
-
output file: ${path.relative(cwd, firstIndex)}
|
|
82
|
-
|
|
83
|
-
Smoke tests:
|
|
84
|
-
|
|
85
|
-
1) Human (usually SPA shell):
|
|
86
|
-
curl -s ${url} | grep -i "<title>"
|
|
87
|
-
|
|
88
|
-
2) Bot (should see prerendered, route-specific title):
|
|
89
|
-
curl -s -H "User-Agent: Googlebot" ${url} | grep -i "<title>"
|
|
90
|
-
|
|
91
|
-
3) Debug headers (Worker must be routed + proxy ON):
|
|
92
|
-
curl -I -H "User-Agent: GPTBot" ${url}
|
|
93
|
-
|
|
94
|
-
Expected
|
|
95
|
-
|
|
96
|
-
X-Prerender: true
|
|
97
|
-
X-Final-Path: ${routePath.replace(/\/?$/, "/index.html")}
|
|
103
|
+
console.log(`
|
|
104
|
+
RenderShield verify
|
|
105
|
+
|
|
106
|
+
Using:
|
|
107
|
+
canonicalBase: ${cfg.site.canonicalBase}
|
|
108
|
+
routePath: ${routePath}
|
|
109
|
+
output file: ${path.relative(cwd, firstIndex)}
|
|
110
|
+
|
|
111
|
+
Smoke tests:
|
|
112
|
+
|
|
113
|
+
1) Human (usually SPA shell):
|
|
114
|
+
curl -s ${url} | grep -i "<title>"
|
|
115
|
+
|
|
116
|
+
2) Bot (should see prerendered, route-specific title):
|
|
117
|
+
curl -s -H "User-Agent: Googlebot" ${url} | grep -i "<title>"
|
|
118
|
+
|
|
119
|
+
3) Debug headers (Worker must be routed + proxy ON):
|
|
120
|
+
curl -I -H "User-Agent: GPTBot" ${url}
|
|
121
|
+
|
|
122
|
+
Expected: x-rendershield: bot-hit (proves Worker served prerender to bot).
|
|
123
|
+
If debugHeaders enabled: X-Bot-Detected, X-Prerender, X-Final-Path.
|
|
98
124
|
`);
|
|
99
125
|
}
|
|
126
|
+
async function runVerifyProd(url) {
|
|
127
|
+
const normalizedUrl = url.startsWith("http") ? url : `https://${url}`;
|
|
128
|
+
let botHtml;
|
|
129
|
+
let humanHtml;
|
|
130
|
+
let botStatus;
|
|
131
|
+
let humanStatus;
|
|
132
|
+
let xRenderShield;
|
|
133
|
+
try {
|
|
134
|
+
const [botRes, humanRes] = await Promise.all([
|
|
135
|
+
fetch(normalizedUrl, {
|
|
136
|
+
headers: { "User-Agent": BOT_UA },
|
|
137
|
+
redirect: "follow",
|
|
138
|
+
}),
|
|
139
|
+
fetch(normalizedUrl, {
|
|
140
|
+
headers: { "User-Agent": HUMAN_UA },
|
|
141
|
+
redirect: "follow",
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
botStatus = botRes.status;
|
|
145
|
+
humanStatus = humanRes.status;
|
|
146
|
+
xRenderShield = botRes.headers.get("x-rendershield");
|
|
147
|
+
botHtml = await botRes.text();
|
|
148
|
+
humanHtml = await humanRes.text();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
throw new Error(`RenderShield verify --prod: failed to fetch ${normalizedUrl}. ${msg}`);
|
|
153
|
+
}
|
|
154
|
+
// Prove routing: Worker must set x-rendershield: bot-hit for bot requests. No inference.
|
|
155
|
+
const routingOk = xRenderShield === "bot-hit";
|
|
156
|
+
if (xRenderShield === "bot-fallback") {
|
|
157
|
+
throw new Error(`RenderShield verify --prod: bot request received x-rendershield: bot-fallback. ` +
|
|
158
|
+
`Prerender origin returned non-200; Worker fell back to SPA. Fix deployment or origin so bots get prerendered HTML.`);
|
|
159
|
+
}
|
|
160
|
+
if (!routingOk) {
|
|
161
|
+
throw new Error(`RenderShield verify --prod: expected x-rendershield: bot-hit (proving Worker routed bot to prerender). ` +
|
|
162
|
+
`Got: ${xRenderShield ?? "(missing)"}. Ensure the Worker is deployed and bound to this route.`);
|
|
163
|
+
}
|
|
164
|
+
const contract = checkPrerenderContract(botHtml, {
|
|
165
|
+
routePath: normalizedUrl,
|
|
166
|
+
outFile: normalizedUrl,
|
|
167
|
+
});
|
|
168
|
+
const humanSpa = looksLikeSpaShell(humanHtml);
|
|
169
|
+
// Report
|
|
170
|
+
console.log(`
|
|
171
|
+
RenderShield verify --prod
|
|
172
|
+
URL: ${normalizedUrl}
|
|
173
|
+
|
|
174
|
+
Fetch:
|
|
175
|
+
Bot (Googlebot): ${botStatus} (${botHtml.length} bytes) x-rendershield: ${xRenderShield ?? "(none)"}
|
|
176
|
+
Human (Chrome): ${humanStatus} (${humanHtml.length} bytes)
|
|
177
|
+
|
|
178
|
+
Routing: x-rendershield: bot-hit (Worker served prerendered HTML to bot)
|
|
179
|
+
|
|
180
|
+
Bot contract (title, meta, canonical, OG, JSON-LD, article):
|
|
181
|
+
${contract.ok ? "PASS — all required fields present." : "FAIL — missing or invalid:"}
|
|
182
|
+
${contract.missing.length > 0 ? contract.missing.map((m) => ` - ${m}`).join("\n") : ""}
|
|
183
|
+
|
|
184
|
+
Human response:
|
|
185
|
+
${humanSpa.likely ? `Likely SPA shell: ${humanSpa.reason ?? "unknown"}` : "Has substantial content (not a minimal SPA shell)."}
|
|
186
|
+
`);
|
|
187
|
+
if (!contract.ok) {
|
|
188
|
+
throw new Error(`Production URL did not satisfy bot contract. Missing: ${contract.missing.join("; ")}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
100
191
|
//# sourceMappingURL=verify.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,74 +4,81 @@ function jsStringArray(arr) {
|
|
|
4
4
|
export function generateWorkerJs(cfg) {
|
|
5
5
|
const patterns = cfg.worker.botUserAgentPatterns.map((p) => p.toLowerCase());
|
|
6
6
|
const rewriteBases = cfg.worker.rewriteRouteBases;
|
|
7
|
-
return `/**
|
|
8
|
-
* RenderShield Worker (generated)
|
|
9
|
-
* Serves prerendered /index.html to bots on selected route bases.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const BOT_SUBSTRINGS = ${jsStringArray(patterns)};
|
|
13
|
-
const REWRITE_BASES = ${jsStringArray(rewriteBases)};
|
|
14
|
-
|
|
15
|
-
function isBot(ua) {
|
|
16
|
-
const s = (ua || "").toLowerCase();
|
|
17
|
-
return BOT_SUBSTRINGS.some((sub) => s.includes(sub));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function shouldRewrite(pathname) {
|
|
21
|
-
return REWRITE_BASES.some((base) => pathname.startsWith(base));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function toIndexHtml(pathname) {
|
|
25
|
-
let p = pathname;
|
|
26
|
-
if (!p.endsWith("/")) p += "/";
|
|
27
|
-
return p + "index.html";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default {
|
|
31
|
-
async fetch(request, env, ctx) {
|
|
32
|
-
const url = new URL(request.url);
|
|
33
|
-
const ua = request.headers.get("User-Agent") || "";
|
|
34
|
-
const bot = isBot(ua);
|
|
35
|
-
|
|
36
|
-
const isGetLike = request.method === "GET" || request.method === "HEAD";
|
|
37
|
-
const rewrite = bot && isGetLike && shouldRewrite(url.pathname);
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
if (!rewrite)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
7
|
+
return `/**
|
|
8
|
+
* RenderShield Worker (generated)
|
|
9
|
+
* Serves prerendered /index.html to bots on selected route bases.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const BOT_SUBSTRINGS = ${jsStringArray(patterns)};
|
|
13
|
+
const REWRITE_BASES = ${jsStringArray(rewriteBases)};
|
|
14
|
+
|
|
15
|
+
function isBot(ua) {
|
|
16
|
+
const s = (ua || "").toLowerCase();
|
|
17
|
+
return BOT_SUBSTRINGS.some((sub) => s.includes(sub));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldRewrite(pathname) {
|
|
21
|
+
return REWRITE_BASES.some((base) => pathname.startsWith(base));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toIndexHtml(pathname) {
|
|
25
|
+
let p = pathname;
|
|
26
|
+
if (!p.endsWith("/")) p += "/";
|
|
27
|
+
return p + "index.html";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
async fetch(request, env, ctx) {
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
const ua = request.headers.get("User-Agent") || "";
|
|
34
|
+
const bot = isBot(ua);
|
|
35
|
+
|
|
36
|
+
const isGetLike = request.method === "GET" || request.method === "HEAD";
|
|
37
|
+
const rewrite = bot && isGetLike && shouldRewrite(url.pathname);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (!rewrite) {
|
|
41
|
+
const passResp = await fetch(request);
|
|
42
|
+
const out = new Response(passResp.body, passResp);
|
|
43
|
+
out.headers.set("x-rendershield", "pass-through");
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const origin = ${JSON.stringify(cfg.worker.lovableOrigin)};
|
|
48
|
+
const finalPath = toIndexHtml(url.pathname);
|
|
49
|
+
const originUrl = origin + finalPath + url.search;
|
|
50
|
+
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
headers.set("Accept", "text/html");
|
|
53
|
+
headers.set("User-Agent", ua);
|
|
54
|
+
|
|
55
|
+
const resp = await fetch(originUrl, { method: "GET", headers });
|
|
56
|
+
|
|
57
|
+
if (!resp.ok) {
|
|
58
|
+
const fallbackUrl = origin + url.pathname + url.search;
|
|
59
|
+
const fb = await fetch(fallbackUrl, { method: "GET", headers });
|
|
60
|
+
const out = new Response(fb.body, fb);
|
|
61
|
+
out.headers.set("x-rendershield", "bot-fallback");
|
|
62
|
+
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
63
|
+
out.headers.set("X-Prerender-Fallback", "true");
|
|
64
|
+
out.headers.set("X-Requested-Path", url.pathname);` : ""}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const out = new Response(resp.body, resp);
|
|
69
|
+
out.headers.set("x-rendershield", "bot-hit");
|
|
70
|
+
${cfg.worker.debugHeaders ? `out.headers.set("X-Bot-Detected", "true");
|
|
71
|
+
out.headers.set("X-Prerender", "true");
|
|
72
|
+
out.headers.set("X-Final-Path", finalPath);` : ""}
|
|
73
|
+
return out;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return new Response("Worker error: " + (err && err.message ? err.message : String(err)), {
|
|
76
|
+
status: 500,
|
|
77
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
75
82
|
`;
|
|
76
83
|
}
|
|
77
84
|
//# sourceMappingURL=generateWorker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateWorker.js","sourceRoot":"","sources":["../../src/core/generateWorker.ts"],"names":[],"mappings":"AAEA,SAAS,aAAa,CAAC,GAAa;IAClC,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAuB;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7E,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAElD,OAAO;;;;;yBAKgB,aAAa,CAAC,QAAQ,CAAC;wBACxB,aAAa,CAAC,YAAY,CAAC
|
|
1
|
+
{"version":3,"file":"generateWorker.js","sourceRoot":"","sources":["../../src/core/generateWorker.ts"],"names":[],"mappings":"AAEA,SAAS,aAAa,CAAC,GAAa;IAClC,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAuB;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7E,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAElD,OAAO;;;;;yBAKgB,aAAa,CAAC,QAAQ,CAAC;wBACxB,aAAa,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAkC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;UAerD,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;;2DAEuB,CAAC,CAAC,CAAC,EAAE;;;;;;QAMxD,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;;kDAEgB,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;CAUtD,CAAC;AACF,CAAC"}
|
package/dist/core/loadConfig.js
CHANGED
|
@@ -18,6 +18,45 @@ function coerceBoolFlag(parsed, key, defaultEnabled) {
|
|
|
18
18
|
}
|
|
19
19
|
return { enabled: v.enabled };
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Worker config policy: strict mode.
|
|
23
|
+
* When worker.enabled is true, all of lovableOrigin, rewriteRouteBases, and
|
|
24
|
+
* botUserAgentPatterns are required and must be non-empty. No defaults are
|
|
25
|
+
* supplied — build fails with a crisp error.
|
|
26
|
+
* lovableOrigin: must be http: or https: only (no file:, javascript:, protocol-relative).
|
|
27
|
+
* botUserAgentPatterns: substring match only (not regex); empty strings rejected to avoid overmatching.
|
|
28
|
+
*/
|
|
29
|
+
function validateWorkerWhenEnabled(parsed) {
|
|
30
|
+
if (!parsed.worker?.lovableOrigin || typeof parsed.worker.lovableOrigin !== "string") {
|
|
31
|
+
throw new Error(`worker.lovableOrigin is required when worker.enabled is true. Example: "https://your-site.lovable.app"`);
|
|
32
|
+
}
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(parsed.worker.lovableOrigin);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`worker.lovableOrigin must be a valid URL. Got: "${parsed.worker.lovableOrigin}"`);
|
|
39
|
+
}
|
|
40
|
+
const scheme = url.protocol.toLowerCase();
|
|
41
|
+
if (scheme !== "http:" && scheme !== "https:") {
|
|
42
|
+
throw new Error(`worker.lovableOrigin must use http or https. Got: "${url.protocol}"`);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(parsed.worker?.rewriteRouteBases) ||
|
|
45
|
+
parsed.worker.rewriteRouteBases.length === 0) {
|
|
46
|
+
throw new Error(`worker.rewriteRouteBases must be a non-empty array when worker.enabled is true. Example: ["/blog/"]`);
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(parsed.worker?.botUserAgentPatterns) ||
|
|
49
|
+
parsed.worker.botUserAgentPatterns.length === 0) {
|
|
50
|
+
throw new Error(`worker.botUserAgentPatterns must be a non-empty array when worker.enabled is true. Example: ["googlebot", "bingbot"]`);
|
|
51
|
+
}
|
|
52
|
+
// Substring match only; empty string would match every User-Agent
|
|
53
|
+
for (let i = 0; i < parsed.worker.botUserAgentPatterns.length; i++) {
|
|
54
|
+
const p = parsed.worker.botUserAgentPatterns[i];
|
|
55
|
+
if (typeof p !== "string" || p.trim() === "") {
|
|
56
|
+
throw new Error(`worker.botUserAgentPatterns[${i}] must be a non-empty string (substring match). Empty or invalid entry would overmatch.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
21
60
|
export async function loadConfig(cwd = process.cwd()) {
|
|
22
61
|
const p = path.join(cwd, CONFIG_NAME);
|
|
23
62
|
const exists = await fs.pathExists(p);
|
|
@@ -54,7 +93,20 @@ export async function loadConfig(cwd = process.cwd()) {
|
|
|
54
93
|
// Validate + default these optional sections to prevent TypeErrors later
|
|
55
94
|
parsed.sitemap = coerceBoolFlag(parsed, "sitemap", true);
|
|
56
95
|
parsed.robots = coerceBoolFlag(parsed, "robots", true);
|
|
57
|
-
|
|
96
|
+
const workerFlag = coerceBoolFlag(parsed, "worker", true);
|
|
97
|
+
parsed.worker = workerFlag;
|
|
98
|
+
if (parsed.worker.enabled) {
|
|
99
|
+
validateWorkerWhenEnabled(parsed);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
parsed.worker = {
|
|
103
|
+
enabled: false,
|
|
104
|
+
lovableOrigin: "",
|
|
105
|
+
rewriteRouteBases: [],
|
|
106
|
+
botUserAgentPatterns: [],
|
|
107
|
+
debugHeaders: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
58
110
|
return parsed;
|
|
59
111
|
}
|
|
60
112
|
//# sourceMappingURL=loadConfig.js.map
|