@lownoise-studio/rendershield 0.1.4

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.
Files changed (42) hide show
  1. package/DEPLOY.md +185 -0
  2. package/LICENSE +21 -0
  3. package/README.md +138 -0
  4. package/dist/cli.js +50 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/build.js +58 -0
  7. package/dist/commands/build.js.map +1 -0
  8. package/dist/commands/init.js +102 -0
  9. package/dist/commands/init.js.map +1 -0
  10. package/dist/commands/verify.js +100 -0
  11. package/dist/commands/verify.js.map +1 -0
  12. package/dist/core/generateRobots.js +14 -0
  13. package/dist/core/generateRobots.js.map +1 -0
  14. package/dist/core/generateSitemap.js +31 -0
  15. package/dist/core/generateSitemap.js.map +1 -0
  16. package/dist/core/generateWorker.js +77 -0
  17. package/dist/core/generateWorker.js.map +1 -0
  18. package/dist/core/loadConfig.js +60 -0
  19. package/dist/core/loadConfig.js.map +1 -0
  20. package/dist/core/loadMarkdown.js +63 -0
  21. package/dist/core/loadMarkdown.js.map +1 -0
  22. package/dist/core/renderHtml.js +69 -0
  23. package/dist/core/renderHtml.js.map +1 -0
  24. package/dist/core/validateOutput.js +121 -0
  25. package/dist/core/validateOutput.js.map +1 -0
  26. package/dist/types.js +2 -0
  27. package/dist/types.js.map +1 -0
  28. package/docs/deploy-cloudflare.md +204 -0
  29. package/package.json +43 -0
  30. package/src/cli.ts +54 -0
  31. package/src/commands/build.ts +72 -0
  32. package/src/commands/init.ts +112 -0
  33. package/src/commands/verify.ts +114 -0
  34. package/src/core/generateRobots.ts +18 -0
  35. package/src/core/generateSitemap.ts +38 -0
  36. package/src/core/generateWorker.ts +80 -0
  37. package/src/core/loadConfig.ts +74 -0
  38. package/src/core/loadMarkdown.ts +82 -0
  39. package/src/core/renderHtml.ts +76 -0
  40. package/src/core/validateOutput.ts +148 -0
  41. package/src/types/markdown-it.d.ts +20 -0
  42. package/src/types.ts +52 -0
package/DEPLOY.md ADDED
@@ -0,0 +1,185 @@
1
+ Deploying RenderShield
2
+
3
+ This document describes how to deploy prerendered output generated by RenderShield so that crawlers receive complete static HTML while users continue to receive your application.
4
+
5
+ RenderShield does not run your app.
6
+ It only produces files and optional routing logic.
7
+
8
+ If RenderShield builds successfully, the output is safe to deploy.
9
+
10
+ 1. Build
11
+
12
+ From the RenderShield project root:
13
+
14
+ npm install
15
+ npm run build
16
+
17
+ This generates prerendered output in:
18
+
19
+ dist-prerender/
20
+
21
+ Typical contents:
22
+
23
+ dist-prerender/
24
+ ├─ content/
25
+ │ └─ example/
26
+ │ └─ index.html
27
+ ├─ sitemap.xml
28
+ ├─ robots.txt
29
+ └─ worker.js
30
+
31
+ The exact structure depends on your configured routes.
32
+
33
+ 2. Hosting the prerendered output
34
+
35
+ You must host the contents of dist-prerender/ on a static origin that can serve files directly.
36
+
37
+ The static origin must be able to serve:
38
+
39
+ /content/example/index.html
40
+ /sitemap.xml
41
+ /robots.txt
42
+
43
+ Common options include:
44
+
45
+ a static hosting bucket
46
+
47
+ a CDN-backed static site
48
+
49
+ the same host as your SPA, under a different path or subdomain
50
+
51
+ RenderShield does not require a specific host.
52
+
53
+ 3. Routing crawlers to prerendered HTML (optional but recommended)
54
+
55
+ To ensure crawlers receive prerendered HTML while humans receive your app, you must route requests based on user-agent.
56
+
57
+ RenderShield provides an optional Cloudflare Worker (worker.js) for this purpose.
58
+
59
+ The worker:
60
+
61
+ detects crawler user agents
62
+
63
+ rewrites matching routes to prerendered index.html
64
+
65
+ forwards human traffic unchanged
66
+
67
+ Humans keep the SPA.
68
+ Crawlers get static HTML.
69
+
70
+ 4. Deploying the Cloudflare Worker
71
+
72
+ Create a Cloudflare account (free plan is sufficient).
73
+
74
+ Deploy worker.js as a Worker.
75
+
76
+ Attach the Worker to your domain with a route:
77
+
78
+ yourdomain.com/*
79
+ www.yourdomain.com/
80
+ *
81
+
82
+ Without a route, the Worker will never run.
83
+
84
+ Ensure the DNS record for your domain is proxied (orange cloud ON).
85
+
86
+ 5. SSL configuration
87
+
88
+ In Cloudflare:
89
+
90
+ Set SSL/TLS mode to:
91
+
92
+ Full
93
+ or
94
+ Full (Strict)
95
+
96
+ Do not use “Flexible”.
97
+
98
+ Incorrect SSL mode can cause redirect loops that prevent crawlers from reaching prerendered content.
99
+
100
+ 6. Sitemap and robots
101
+
102
+ Upload these files from dist-prerender/ to your static origin:
103
+
104
+ sitemap.xml
105
+ robots.txt
106
+
107
+ Then submit your sitemap URL to your search engine consoles as needed.
108
+
109
+ Example robots.txt:
110
+
111
+ User-agent: *
112
+ Allow: /
113
+ Sitemap: https://yourdomain.com/sitemap.xml
114
+
115
+ 7. Verification
116
+
117
+ RenderShield includes a verification command that prints smoke tests.
118
+
119
+ Run:
120
+
121
+ node dist/cli.js verify
122
+
123
+ This prints three checks:
124
+
125
+ Human request (should return your app shell)
126
+
127
+ Crawler request (should return prerendered HTML)
128
+
129
+ Debug headers (confirms worker routing)
130
+
131
+ If verification passes, deployment is correct.
132
+
133
+ 8. What RenderShield guarantees
134
+
135
+ RenderShield guarantees only this:
136
+
137
+ If a page builds successfully, the generated HTML:
138
+
139
+ contains real content
140
+
141
+ includes required metadata
142
+
143
+ is readable by crawlers
144
+
145
+ RenderShield does not guarantee:
146
+
147
+ rankings
148
+
149
+ traffic
150
+
151
+ indexing speed
152
+
153
+ AI citations
154
+
155
+ It guarantees correctness of output.
156
+
157
+ 9. Common failure modes
158
+
159
+ If crawlers do not see prerendered content:
160
+
161
+ Worker route is missing or incorrect
162
+
163
+ DNS proxy is OFF
164
+
165
+ Static origin is unreachable
166
+
167
+ SSL mode is misconfigured
168
+
169
+ Prerendered files are not deployed
170
+
171
+ RenderShield verification output will point to these issues.
172
+
173
+ Summary
174
+
175
+ RenderShield separates responsibilities cleanly:
176
+
177
+ Your app serves users
178
+
179
+ RenderShield serves crawlers
180
+
181
+ The edge decides who gets what
182
+
183
+ If it builds, it ships.
184
+
185
+ That is the contract.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lownoise Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # RenderShield
2
+
3
+ RenderShield ensures crawlers receive complete, static HTML instead of an empty
4
+ JavaScript shell — without changing how humans experience your app.
5
+
6
+ It prerenders content ahead of time and can route crawler requests to that output
7
+ at the edge, while normal users continue to receive the SPA.
8
+
9
+ No frameworks required.
10
+ No vendor lock-in.
11
+ No guessing what bots see.
12
+
13
+ If a page builds, the HTML is complete. If it isn’t, the build fails.
14
+
15
+ ---
16
+
17
+ ## What problem this solves
18
+
19
+ Many modern apps technically work but are invisible to:
20
+
21
+ - search engines
22
+ - social preview scrapers
23
+ - AI crawlers
24
+
25
+ The content exists only after JavaScript executes.
26
+ Crawlers do not wait.
27
+
28
+ RenderShield fixes this by generating static, bot-readable HTML and refusing
29
+ to ship incomplete output.
30
+
31
+ ---
32
+
33
+ ## What RenderShield does
34
+
35
+ - Converts structured content into full static HTML pages
36
+ - Injects:
37
+ - title
38
+ - meta description
39
+ - canonical link
40
+ - Open Graph tags
41
+ - Twitter tags
42
+ - JSON-LD (Article)
43
+ - Generates:
44
+ - index.html per route
45
+ - sitemap.xml
46
+ - robots.txt
47
+ - optional Cloudflare Worker
48
+ - Validates output
49
+ - missing title, metadata, or content causes the build to fail
50
+
51
+ If it builds, crawlers will see real content.
52
+
53
+ ---
54
+
55
+ ## What it does not do
56
+
57
+ - It does not run your application
58
+ - It does not execute JavaScript for bots
59
+ - It does not guess content
60
+ - It does not promise rankings, traffic, or citations
61
+ - It does not replace your SPA
62
+
63
+ It guarantees one thing only:
64
+ that crawlers receive complete HTML.
65
+
66
+ ---
67
+
68
+ ## Quickstart
69
+
70
+ 1) Install dependencies
71
+
72
+ Run:
73
+
74
+ npm install
75
+
76
+ 2) Initialize RenderShield
77
+
78
+ Run:
79
+
80
+ npm run build
81
+
82
+ This creates:
83
+ - rendershield.config.json
84
+ - sample content
85
+ - a prerender output directory
86
+
87
+ 3) Add content
88
+
89
+ Place structured content under:
90
+
91
+ content/
92
+
93
+ 4) Build prerendered output
94
+
95
+ Run:
96
+
97
+ node dist/cli.js build
98
+
99
+ Output is written to:
100
+
101
+ dist-prerender/
102
+
103
+ 5) Verify output
104
+
105
+ Run:
106
+
107
+ node dist/cli.js verify
108
+
109
+ This prints curl commands you can use to confirm crawler behavior.
110
+
111
+ ---
112
+
113
+ ## Deployment
114
+
115
+ RenderShield is designed to run behind Cloudflare.
116
+
117
+ A Worker routes crawler requests to prerendered HTML while passing all other
118
+ traffic through unchanged.
119
+
120
+ See:
121
+ docs/deploy-cloudflare.md
122
+
123
+ ---
124
+
125
+ ## Philosophy
126
+
127
+ RenderShield is intentionally boring.
128
+
129
+ It does not attempt to outsmart crawlers or simulate browsers.
130
+ It produces correct HTML and refuses to ship broken output.
131
+
132
+ If crawlers cannot read it, the build fails.
133
+
134
+ ---
135
+
136
+ ## License
137
+
138
+ MIT License
package/dist/cli.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { cmdInit } from "./commands/init.js";
3
+ import { cmdBuild } from "./commands/build.js";
4
+ import { cmdVerify } from "./commands/verify.js";
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
+ Notes:
15
+ - Config file: rendershield.config.json
16
+ - Content: content/blog/*.md (frontmatter required)
17
+ - Output: dist-prerender/
18
+ `);
19
+ }
20
+ async function main() {
21
+ const cmd = process.argv[2]?.trim();
22
+ if (!cmd || cmd === "-h" || cmd === "--help") {
23
+ printHelp();
24
+ process.exit(0);
25
+ }
26
+ try {
27
+ if (cmd === "init") {
28
+ await cmdInit();
29
+ return;
30
+ }
31
+ if (cmd === "build") {
32
+ await cmdBuild();
33
+ return;
34
+ }
35
+ if (cmd === "verify") {
36
+ await cmdVerify();
37
+ return;
38
+ }
39
+ console.error(`Unknown command: ${cmd}\n`);
40
+ printHelp();
41
+ process.exit(1);
42
+ }
43
+ catch (err) {
44
+ const msg = err?.message ? String(err.message) : String(err);
45
+ console.error(`\nRenderShield error: ${msg}\n`);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ main();
50
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +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;;;;;;;;;;;;CAYb,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,SAAS,EAAE,CAAC;YAClB,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"}
@@ -0,0 +1,58 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { loadConfig } from "../core/loadConfig.js";
4
+ import { loadAllMarkdownDocs } from "../core/loadMarkdown.js";
5
+ import { renderPageHtml } from "../core/renderHtml.js";
6
+ import { generateSitemapXml } from "../core/generateSitemap.js";
7
+ import { generateRobotsTxt } from "../core/generateRobots.js";
8
+ import { generateWorkerJs } from "../core/generateWorker.js";
9
+ import { validatePrerenderHtml } from "../core/validateOutput.js";
10
+ function routeToOutDir(outDirAbs, routePath) {
11
+ // /blog/slug -> outDir/blog/slug/index.html
12
+ const clean = routePath.replace(/^\//, "");
13
+ return path.join(outDirAbs, clean);
14
+ }
15
+ export async function cmdBuild(cwd = process.cwd()) {
16
+ const cfg = await loadConfig(cwd);
17
+ const outDirAbs = path.join(cwd, cfg.output.outDir);
18
+ // Clean output (boring + deterministic)
19
+ await fs.remove(outDirAbs);
20
+ await fs.ensureDir(outDirAbs);
21
+ const docs = await loadAllMarkdownDocs(cfg, cwd);
22
+ if (docs.length === 0) {
23
+ throw new Error("No markdown documents found. Check content paths/patterns.");
24
+ }
25
+ // Generate pages (validate BEFORE writing)
26
+ for (const doc of docs) {
27
+ const pageDir = routeToOutDir(outDirAbs, doc.routePath);
28
+ await fs.ensureDir(pageDir);
29
+ const outFile = path.join(pageDir, "index.html");
30
+ const html = renderPageHtml(cfg, doc);
31
+ validatePrerenderHtml({
32
+ html,
33
+ outFile,
34
+ routePath: doc.routePath,
35
+ });
36
+ await fs.writeFile(outFile, html, "utf8");
37
+ }
38
+ // sitemap.xml
39
+ if (cfg.sitemap.enabled) {
40
+ const sitemapXml = generateSitemapXml(cfg, docs);
41
+ const sitemapPath = path.join(outDirAbs, cfg.sitemap.path.replace(/^\//, ""));
42
+ await fs.writeFile(sitemapPath, sitemapXml, "utf8");
43
+ }
44
+ // robots.txt
45
+ if (cfg.robots.enabled) {
46
+ const robotsTxt = generateRobotsTxt(cfg);
47
+ const robotsPath = path.join(outDirAbs, cfg.robots.path.replace(/^\//, ""));
48
+ await fs.writeFile(robotsPath, robotsTxt, "utf8");
49
+ }
50
+ // worker.js
51
+ if (cfg.worker.enabled) {
52
+ const workerJs = generateWorkerJs(cfg);
53
+ await fs.writeFile(path.join(outDirAbs, "worker.js"), workerJs, "utf8");
54
+ }
55
+ console.log(`Built ${docs.length} pages into ${cfg.output.outDir}/`);
56
+ console.log(`Output includes: ${cfg.sitemap.enabled ? "sitemap.xml " : ""}${cfg.robots.enabled ? "robots.txt " : ""}${cfg.worker.enabled ? "worker.js" : ""}`);
57
+ }
58
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ const CONFIG_NAME = "rendershield.config.json";
4
+ const DEFAULT_CONFIG = {
5
+ version: 1,
6
+ site: {
7
+ canonicalBase: "https://example.com",
8
+ siteName: "Example Site",
9
+ defaultOgImage: "https://example.com/og/default.jpg",
10
+ authorName: "Your Name"
11
+ },
12
+ content: {
13
+ markdown: {
14
+ baseDir: "content",
15
+ collections: [
16
+ {
17
+ name: "pages",
18
+ pattern: "pages/**/*.md",
19
+ routeBase: "/pages",
20
+ schemaType: "Article"
21
+ }
22
+ ]
23
+ }
24
+ },
25
+ output: {
26
+ outDir: "dist-prerender",
27
+ prettyHtml: true
28
+ },
29
+ sitemap: {
30
+ enabled: true,
31
+ path: "/sitemap.xml"
32
+ },
33
+ robots: {
34
+ enabled: true,
35
+ path: "/robots.txt"
36
+ },
37
+ worker: {
38
+ enabled: true,
39
+ lovableOrigin: "https://YOUR_SITE.lovable.app",
40
+ rewriteRouteBases: ["/pages/"],
41
+ botUserAgentPatterns: [
42
+ "googlebot",
43
+ "bingbot",
44
+ "gptbot",
45
+ "claudebot",
46
+ "perplexitybot",
47
+ "twitterbot",
48
+ "facebookexternalhit",
49
+ "linkedinbot",
50
+ "slackbot",
51
+ "discordbot",
52
+ "whatsapp",
53
+ "telegram"
54
+ ],
55
+ debugHeaders: true
56
+ }
57
+ };
58
+ const today = new Date().toISOString().slice(0, 10);
59
+ const SAMPLE_POST = `---
60
+ title: Hello World
61
+ excerpt: This is a sample page generated by RenderShield.
62
+ datePublished: ${today}
63
+ coverImage: /images/hello.jpg
64
+ slug: hello-world
65
+ ---
66
+
67
+ This is **RenderShield**.
68
+
69
+ - Crawlers should see real HTML.
70
+ - Humans keep the SPA.
71
+
72
+ That's the whole trick.
73
+ `;
74
+ export async function cmdInit(cwd = process.cwd()) {
75
+ const configPath = path.join(cwd, CONFIG_NAME);
76
+ const contentDir = path.join(cwd, "content", "pages");
77
+ const samplePath = path.join(contentDir, "hello-world.md");
78
+ const configExists = await fs.pathExists(configPath);
79
+ if (!configExists) {
80
+ await fs.writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf8");
81
+ console.log(`Created ${CONFIG_NAME}`);
82
+ }
83
+ else {
84
+ console.log(`${CONFIG_NAME} already exists (leaving it alone)`);
85
+ }
86
+ await fs.ensureDir(contentDir);
87
+ const sampleExists = await fs.pathExists(samplePath);
88
+ if (!sampleExists) {
89
+ await fs.writeFile(samplePath, SAMPLE_POST, "utf8");
90
+ console.log(`Created sample content: content/pages/hello-world.md`);
91
+ }
92
+ else {
93
+ console.log(`Sample content already exists (leaving it alone)`);
94
+ }
95
+ console.log(`
96
+ Next:
97
+ 1) Edit rendershield.config.json
98
+ 2) Add content under content/pages/
99
+ 3) Run: rendershield build
100
+ `);
101
+ }
102
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { loadConfig } from "../core/loadConfig.js";
4
+ function joinUrl(base, routePath) {
5
+ const b = base.endsWith("/") ? base.slice(0, -1) : base;
6
+ const p = routePath.startsWith("/") ? routePath : `/${routePath}`;
7
+ return b + p;
8
+ }
9
+ async function findFirstIndexHtml(outDirAbs) {
10
+ const stack = [outDirAbs];
11
+ while (stack.length > 0) {
12
+ const current = stack.pop();
13
+ let entries;
14
+ try {
15
+ entries = await fs.readdir(current, { withFileTypes: true });
16
+ }
17
+ catch {
18
+ continue;
19
+ }
20
+ entries.sort((a, b) => a.name.localeCompare(b.name));
21
+ for (const entry of entries) {
22
+ const full = path.join(current, entry.name);
23
+ if (entry.isDirectory()) {
24
+ stack.push(full);
25
+ continue;
26
+ }
27
+ if (entry.isFile() && entry.name.toLowerCase() === "index.html") {
28
+ // Ignore index.html at the output root; prefer a routed page like
29
+ // <section>/<slug>/index.html (or deeper).
30
+ const rel = path.relative(outDirAbs, full);
31
+ const parts = rel.split(path.sep).filter(Boolean);
32
+ if (parts.length >= 2)
33
+ return full;
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function indexHtmlPathToRoute(outDirAbs, indexPathAbs) {
40
+ const rel = path.relative(outDirAbs, indexPathAbs);
41
+ // rel: <section>/<slug>/index.html
42
+ const noFile = rel.replace(/index\.html$/i, "");
43
+ const normalized = noFile.split(path.sep).join("/").replace(/\/+$/, "");
44
+ return "/" + normalized.replace(/^\/+/, "");
45
+ }
46
+ export async function cmdVerify(cwd = process.cwd()) {
47
+ const cfg = await loadConfig(cwd);
48
+ const outDirAbs = path.join(cwd, cfg.output.outDir);
49
+ const exists = await fs.pathExists(outDirAbs);
50
+ 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
58
+ `);
59
+ return;
60
+ }
61
+ const firstIndex = await findFirstIndexHtml(outDirAbs);
62
+ 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
70
+ `);
71
+ return;
72
+ }
73
+ const routePath = indexHtmlPathToRoute(outDirAbs, firstIndex);
74
+ 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 headers (if debugHeaders enabled):
95
+ X-Bot-Detected: true
96
+ X-Prerender: true
97
+ X-Final-Path: ${routePath.replace(/\/?$/, "/index.html")}
98
+ `);
99
+ }
100
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export function generateRobotsTxt(cfg) {
2
+ const sitemapUrl = cfg.sitemap.enabled
3
+ ? `${cfg.site.canonicalBase.replace(/\/$/, "")}${cfg.sitemap.path}`
4
+ : "";
5
+ const lines = [
6
+ "User-agent: *",
7
+ "Allow: /",
8
+ ];
9
+ if (sitemapUrl) {
10
+ lines.push(`Sitemap: ${sitemapUrl}`);
11
+ }
12
+ return lines.join("\n") + "\n";
13
+ }
14
+ //# sourceMappingURL=generateRobots.js.map