@lownoise-studio/rendershield 0.3.1 → 1.1.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +73 -32
  2. package/CONTRIBUTING.md +41 -0
  3. package/README.md +227 -144
  4. package/SECURITY.md +25 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +39 -26
  8. package/dist/cli.js.map +1 -1
  9. package/dist/cliArgs.d.ts +8 -0
  10. package/dist/cliArgs.d.ts.map +1 -0
  11. package/dist/cliArgs.js +56 -0
  12. package/dist/cliArgs.js.map +1 -0
  13. package/dist/commands/build.d.ts +3 -0
  14. package/dist/commands/build.d.ts.map +1 -0
  15. package/dist/commands/build.js +12 -11
  16. package/dist/commands/build.js.map +1 -1
  17. package/dist/commands/init.d.ts +3 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +8 -7
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/verify.d.ts +33 -0
  22. package/dist/commands/verify.d.ts.map +1 -0
  23. package/dist/commands/verify.js +202 -117
  24. package/dist/commands/verify.js.map +1 -1
  25. package/dist/configPath.d.ts +7 -0
  26. package/dist/configPath.d.ts.map +1 -0
  27. package/dist/configPath.js +8 -0
  28. package/dist/configPath.js.map +1 -0
  29. package/dist/core/generateRobots.d.ts +3 -0
  30. package/dist/core/generateRobots.d.ts.map +1 -0
  31. package/dist/core/generateSitemap.d.ts +3 -0
  32. package/dist/core/generateSitemap.d.ts.map +1 -0
  33. package/dist/core/generateWorker.d.ts +3 -0
  34. package/dist/core/generateWorker.d.ts.map +1 -0
  35. package/dist/core/generateWorker.js +75 -75
  36. package/dist/core/generateWorker.js.map +1 -1
  37. package/dist/core/listOutputRoutes.d.ts +4 -0
  38. package/dist/core/listOutputRoutes.d.ts.map +1 -0
  39. package/dist/core/listOutputRoutes.js +40 -0
  40. package/dist/core/listOutputRoutes.js.map +1 -0
  41. package/dist/core/loadConfig.d.ts +5 -0
  42. package/dist/core/loadConfig.d.ts.map +1 -0
  43. package/dist/core/loadConfig.js +108 -58
  44. package/dist/core/loadConfig.js.map +1 -1
  45. package/dist/core/loadMarkdown.d.ts +3 -0
  46. package/dist/core/loadMarkdown.d.ts.map +1 -0
  47. package/dist/core/loadMarkdown.js +4 -3
  48. package/dist/core/loadMarkdown.js.map +1 -1
  49. package/dist/core/renderHtml.d.ts +3 -0
  50. package/dist/core/renderHtml.d.ts.map +1 -0
  51. package/dist/core/renderHtml.js +25 -10
  52. package/dist/core/renderHtml.js.map +1 -1
  53. package/dist/core/validateOutput.d.ts +28 -0
  54. package/dist/core/validateOutput.d.ts.map +1 -0
  55. package/dist/core/validateOutput.js +7 -1
  56. package/dist/core/validateOutput.js.map +1 -1
  57. package/dist/errors.d.ts +14 -0
  58. package/dist/errors.d.ts.map +1 -0
  59. package/dist/errors.js +24 -0
  60. package/dist/errors.js.map +1 -0
  61. package/dist/index.d.ts +22 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +20 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/types.d.ts +53 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/docs/CONFIG.md +70 -0
  70. package/docs/deploy-cloudflare.md +40 -14
  71. package/package.json +25 -2
  72. package/rendershield.config.schema.json +100 -0
  73. package/src/cli.ts +96 -75
  74. package/src/cliArgs.ts +71 -0
  75. package/src/commands/build.ts +200 -185
  76. package/src/commands/init.ts +8 -8
  77. package/src/commands/verify.ts +451 -236
  78. package/src/configPath.ts +17 -0
  79. package/src/core/generateWorker.ts +97 -97
  80. package/src/core/listOutputRoutes.ts +51 -0
  81. package/src/core/loadConfig.ts +282 -173
  82. package/src/core/loadMarkdown.ts +9 -3
  83. package/src/core/renderHtml.ts +36 -12
  84. package/src/core/validateOutput.ts +335 -328
  85. package/src/errors.ts +48 -0
  86. package/src/index.ts +43 -0
  87. package/src/types.ts +5 -2
@@ -0,0 +1,22 @@
1
+ /**
2
+ * RenderShield programmatic API.
3
+ *
4
+ * CLI usage: `npx rendershield <command>`
5
+ * Library usage: import commands and core helpers from `@lownoise-studio/rendershield`.
6
+ */
7
+ export { cmdInit } from "./commands/init.js";
8
+ export { cmdBuild } from "./commands/build.js";
9
+ export { cmdVerify, type VerifyOptions, type VerifyLocalResult, type VerifyProdResult, type VerifyResult, type VerifyPageResult, } from "./commands/verify.js";
10
+ export { loadConfig } from "./core/loadConfig.js";
11
+ export { loadAllMarkdownDocs } from "./core/loadMarkdown.js";
12
+ export { renderPageHtml } from "./core/renderHtml.js";
13
+ export { validatePrerenderHtml, checkPrerenderContract, type ValidateParams, type ContractCheckResult, } from "./core/validateOutput.js";
14
+ export { generateSitemapXml } from "./core/generateSitemap.js";
15
+ export { generateRobotsTxt } from "./core/generateRobots.js";
16
+ export { generateWorkerJs } from "./core/generateWorker.js";
17
+ export type { CommandOptions } from "./configPath.js";
18
+ export { DEFAULT_CONFIG_NAME, resolveConfigFile } from "./configPath.js";
19
+ export type { RenderShieldConfig, MarkdownDoc, SchemaType } from "./types.js";
20
+ export { SCHEMA_TYPES } from "./types.js";
21
+ export { RenderShieldError, isRenderShieldError, renderShieldError, formatCliError, type RenderShieldErrorCode, } from "./errors.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EACL,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * RenderShield programmatic API.
3
+ *
4
+ * CLI usage: `npx rendershield <command>`
5
+ * Library usage: import commands and core helpers from `@lownoise-studio/rendershield`.
6
+ */
7
+ export { cmdInit } from "./commands/init.js";
8
+ export { cmdBuild } from "./commands/build.js";
9
+ export { cmdVerify, } from "./commands/verify.js";
10
+ export { loadConfig } from "./core/loadConfig.js";
11
+ export { loadAllMarkdownDocs } from "./core/loadMarkdown.js";
12
+ export { renderPageHtml } from "./core/renderHtml.js";
13
+ export { validatePrerenderHtml, checkPrerenderContract, } from "./core/validateOutput.js";
14
+ export { generateSitemapXml } from "./core/generateSitemap.js";
15
+ export { generateRobotsTxt } from "./core/generateRobots.js";
16
+ export { generateWorkerJs } from "./core/generateWorker.js";
17
+ export { DEFAULT_CONFIG_NAME, resolveConfigFile } from "./configPath.js";
18
+ export { SCHEMA_TYPES } from "./types.js";
19
+ export { RenderShieldError, isRenderShieldError, renderShieldError, formatCliError, } from "./errors.js";
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EACL,SAAS,GAMV,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG5D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GAEf,MAAM,aAAa,CAAC"}
@@ -0,0 +1,53 @@
1
+ export declare const SCHEMA_TYPES: readonly ["Article", "BlogPosting", "WebPage"];
2
+ export type SchemaType = (typeof SCHEMA_TYPES)[number];
3
+ export type RenderShieldConfig = {
4
+ version: 1;
5
+ site: {
6
+ canonicalBase: string;
7
+ siteName: string;
8
+ defaultOgImage: string;
9
+ authorName: string;
10
+ };
11
+ content: {
12
+ markdown: {
13
+ baseDir: string;
14
+ collections: Array<{
15
+ name: string;
16
+ pattern: string;
17
+ routeBase: string;
18
+ schemaType: SchemaType;
19
+ }>;
20
+ };
21
+ };
22
+ output: {
23
+ outDir: string;
24
+ prettyHtml: boolean;
25
+ };
26
+ sitemap: {
27
+ enabled: boolean;
28
+ path: string;
29
+ };
30
+ robots: {
31
+ enabled: boolean;
32
+ path: string;
33
+ };
34
+ worker: {
35
+ enabled: boolean;
36
+ spaOrigin: string;
37
+ rewriteRouteBases: string[];
38
+ botUserAgentPatterns: string[];
39
+ debugHeaders: boolean;
40
+ };
41
+ };
42
+ export type MarkdownDoc = {
43
+ sourcePath: string;
44
+ collection: string;
45
+ routePath: string;
46
+ title: string;
47
+ excerpt: string;
48
+ datePublished: string;
49
+ coverImage: string;
50
+ slug: string;
51
+ htmlContent: string;
52
+ };
53
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,gDAAiD,CAAC;AAC3E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,OAAO,EAAE;QACP,QAAQ,EAAE;YACR,OAAO,EAAE,MAAM,CAAC;YAChB,WAAW,EAAE,KAAK,CAAC;gBACjB,IAAI,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM,CAAC;gBAChB,SAAS,EAAE,MAAM,CAAC;gBAClB,UAAU,EAAE,UAAU,CAAC;aACxB,CAAC,CAAC;SACJ,CAAC;KACH,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,EAAE,MAAM,EAAE,CAAC;QAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;QAC/B,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC"}
package/dist/types.js CHANGED
@@ -1,2 +1,2 @@
1
- export {};
1
+ export const SCHEMA_TYPES = ["Article", "BlogPosting", "WebPage"];
2
2
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,aAAa,EAAE,SAAS,CAAU,CAAC"}
package/docs/CONFIG.md ADDED
@@ -0,0 +1,70 @@
1
+ # Config reference
2
+
3
+ Default file: `rendershield.config.json` (override with `--config <path>`).
4
+
5
+ JSON Schema: [`rendershield.config.schema.json`](../rendershield.config.schema.json) (editor autocomplete).
6
+
7
+ ## Top level
8
+
9
+ | Field | Required | Description |
10
+ |-------|----------|-------------|
11
+ | `version` | yes | Must be `1`. |
12
+ | `site` | yes | Site metadata for URLs and JSON-LD. |
13
+ | `content` | yes | Markdown content sources. |
14
+ | `output` | yes | Build output directory. |
15
+ | `sitemap` | no | Defaults: `enabled: true`, `path: "/sitemap.xml"`. |
16
+ | `robots` | no | Defaults: `enabled: true`, `path: "/robots.txt"`. |
17
+ | `worker` | no | Defaults: `enabled: true` in `init` sample. |
18
+
19
+ ## `site`
20
+
21
+ | Field | Description |
22
+ |-------|-------------|
23
+ | `canonicalBase` | Public site origin, e.g. `https://example.com` (no trailing slash). |
24
+ | `siteName` | Appended to page titles. |
25
+ | `defaultOgImage` | Fallback OG image URL. |
26
+ | `authorName` | JSON-LD author for article types. |
27
+
28
+ ## `content.markdown.collections[]`
29
+
30
+ | Field | Description |
31
+ |-------|-------------|
32
+ | `name` | Collection id (used internally). |
33
+ | `pattern` | Glob under `baseDir`, e.g. `blog/**/*.md`. |
34
+ | `routeBase` | URL prefix, e.g. `/blog`. |
35
+ | `schemaType` | JSON-LD `@type`: `Article` (default), `BlogPosting`, or `WebPage`. |
36
+
37
+ ### Markdown frontmatter (per file)
38
+
39
+ Required: `title`, `excerpt`, `datePublished` (`YYYY-MM-DD`), `coverImage`, `slug`.
40
+
41
+ Route: `{routeBase}/{slug}` → `dist-prerender/.../index.html`.
42
+
43
+ ## `output`
44
+
45
+ | Field | Description |
46
+ |-------|-------------|
47
+ | `outDir` | Relative path inside project, e.g. `dist-prerender`. |
48
+ | `prettyHtml` | Pretty-print HTML (default `true`). |
49
+
50
+ ## `worker`
51
+
52
+ When `enabled: true`:
53
+
54
+ | Field | Description |
55
+ |-------|-------------|
56
+ | `spaOrigin` | Origin that serves prerendered + SPA assets, e.g. `https://app.example.com`. |
57
+ | `lovableOrigin` | **Deprecated** — same as `spaOrigin` (still accepted). |
58
+ | `rewriteRouteBases` | Bot rewrite prefixes, e.g. `["/blog/"]`. |
59
+ | `botUserAgentPatterns` | Case-insensitive substring match list. |
60
+ | `debugHeaders` | Extra `X-*` headers on Worker responses. |
61
+
62
+ ## CLI flags
63
+
64
+ ```bash
65
+ rendershield --config ./config/prerender.json build
66
+ rendershield verify --check # contract-check first built page
67
+ rendershield verify --all --check # contract-check every built page
68
+ rendershield verify --prod --all # prod-check every route from build output
69
+ rendershield verify --prod https://example.com/blog/post
70
+ ```
@@ -37,6 +37,12 @@ At a high level:
37
37
  - Known crawlers are routed to prerendered HTML files
38
38
  - All other traffic passes through to your SPA unchanged
39
39
 
40
+ Every response from the Worker includes an **x-rendershield** header so routing is observable:
41
+
42
+ - **pass-through** — request was not rewritten (human or path not in rewrite bases)
43
+ - **bot-hit** — bot request was rewritten and prerendered HTML was served
44
+ - **bot-fallback** — bot request was rewritten but the origin returned non-200; Worker fell back to the SPA
45
+
40
46
  If prerendered output is missing or incomplete, RenderShield fails the build.
41
47
 
42
48
  ---
@@ -104,7 +110,7 @@ The Worker must be able to fetch prerendered files.
104
110
 
105
111
  You need a static origin that serves paths like:
106
112
 
107
- - /content/example/index.html
113
+ - /blog/example/index.html (or your configured route base)
108
114
  - /sitemap.xml
109
115
  - /robots.txt
110
116
 
@@ -132,36 +138,56 @@ Submit the sitemap URL in Google Search Console.
132
138
 
133
139
  ## Verification
134
140
 
135
- RenderShield includes a verify command to guide testing.
141
+ ### Production verification (recommended)
142
+
143
+ After deploying the Worker and hosting prerendered output, run:
144
+
145
+ ```bash
146
+ rendershield verify --prod https://yourdomain.com
147
+ ```
148
+
149
+ This command:
136
150
 
137
- ### Worker execution test
151
+ - Fetches the URL as Googlebot
152
+ - Asserts the response has **x-rendershield: bot-hit** (proving the Worker served prerendered HTML)
153
+ - Validates metadata, JSON-LD, and article content
154
+ - Exits with code 1 if the header is missing, is `bot-fallback`, or the contract fails
138
155
 
139
- Run:
156
+ If it passes, crawlers are receiving the prerendered HTML.
140
157
 
141
- curl -I -H "User-Agent: GPTBot" https://yourdomain.com/content/example
158
+ ### Manual checks (optional)
142
159
 
143
- If debug headers are enabled, you should see headers similar to:
160
+ Check the response header:
161
+
162
+ ```bash
163
+ curl -I -H "User-Agent: Googlebot" https://yourdomain.com/blog/example
164
+ ```
165
+
166
+ You should see **x-rendershield: bot-hit**. If debug headers are enabled in config, you may also see:
144
167
 
145
168
  - X-Bot-Detected: true
146
169
  - X-Prerender: true
147
- - X-Final-Path: /content/example/index.html
170
+ - X-Final-Path: /blog/example/index.html
171
+
172
+ If **x-rendershield** is missing or **bot-fallback**:
148
173
 
149
- If these headers are missing:
150
174
  - The Worker route may not be attached
151
175
  - The Cloudflare proxy may be disabled
152
- - DNS may still point to a different origin
153
-
154
- ---
176
+ - The prerendered origin may be returning non-200 for that path
155
177
 
156
- ## Content comparison
178
+ ### Content comparison
157
179
 
158
180
  Human request (SPA response):
159
181
 
160
- curl -s https://yourdomain.com/content/example | grep -i "<title>"
182
+ ```bash
183
+ curl -s https://yourdomain.com/blog/example | grep -i "<title>"
184
+ ```
161
185
 
162
186
  Crawler request (prerendered HTML):
163
187
 
164
- curl -s -H "User-Agent: Googlebot" https://yourdomain.com/content/example | grep -i "<title>"
188
+ ```bash
189
+ curl -s -H "User-Agent: Googlebot" https://yourdomain.com/blog/example | grep -i "<title>"
190
+ ```
165
191
 
166
192
  The crawler response should contain the prerendered, route-specific title.
167
193
 
package/package.json CHANGED
@@ -1,12 +1,33 @@
1
1
  {
2
2
  "name": "@lownoise-studio/rendershield",
3
- "version": "0.3.1",
3
+ "version": "1.1.0",
4
4
  "description": "Boring bot-aware prerendering: real HTML for bots, SPA for humans.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "keywords": [
11
+ "prerender",
12
+ "seo",
13
+ "bots",
14
+ "spa",
15
+ "static-site",
16
+ "cloudflare-workers",
17
+ "markdown"
18
+ ],
7
19
  "bin": {
8
20
  "rendershield": "dist/cli.js"
9
21
  },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
10
31
  "scripts": {
11
32
  "build": "tsc -p tsconfig.json",
12
33
  "dev": "node --enable-source-maps dist/cli.js",
@@ -19,10 +40,12 @@
19
40
  "dist/**",
20
41
  "src/**",
21
42
  "docs/**",
22
- "content/**",
43
+ "rendershield.config.schema.json",
23
44
  "DEPLOY.md",
24
45
  "README.md",
25
46
  "CHANGELOG.md",
47
+ "CONTRIBUTING.md",
48
+ "SECURITY.md",
26
49
  "LICENSE"
27
50
  ],
28
51
  "repository": {
@@ -0,0 +1,100 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/Lownoise-Studio/rendershield/rendershield.config.schema.json",
4
+ "title": "RenderShield config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["version", "site", "content", "output"],
8
+ "properties": {
9
+ "version": { "const": 1 },
10
+ "site": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["canonicalBase", "siteName", "defaultOgImage", "authorName"],
14
+ "properties": {
15
+ "canonicalBase": { "type": "string", "format": "uri" },
16
+ "siteName": { "type": "string", "minLength": 1 },
17
+ "defaultOgImage": { "type": "string", "minLength": 1 },
18
+ "authorName": { "type": "string", "minLength": 1 }
19
+ }
20
+ },
21
+ "content": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["markdown"],
25
+ "properties": {
26
+ "markdown": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["baseDir", "collections"],
30
+ "properties": {
31
+ "baseDir": { "type": "string", "minLength": 1 },
32
+ "collections": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "items": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["name", "pattern", "routeBase"],
39
+ "properties": {
40
+ "name": { "type": "string", "minLength": 1 },
41
+ "pattern": { "type": "string", "minLength": 1 },
42
+ "routeBase": { "type": "string", "minLength": 1 },
43
+ "schemaType": {
44
+ "type": "string",
45
+ "enum": ["Article", "BlogPosting", "WebPage"],
46
+ "default": "Article"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "output": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["outDir"],
59
+ "properties": {
60
+ "outDir": { "type": "string", "minLength": 1 },
61
+ "prettyHtml": { "type": "boolean", "default": true }
62
+ }
63
+ },
64
+ "sitemap": {
65
+ "type": "object",
66
+ "properties": {
67
+ "enabled": { "type": "boolean" },
68
+ "path": { "type": "string", "pattern": "^/" }
69
+ }
70
+ },
71
+ "robots": {
72
+ "type": "object",
73
+ "properties": {
74
+ "enabled": { "type": "boolean" },
75
+ "path": { "type": "string", "pattern": "^/" }
76
+ }
77
+ },
78
+ "worker": {
79
+ "type": "object",
80
+ "properties": {
81
+ "enabled": { "type": "boolean" },
82
+ "spaOrigin": { "type": "string", "format": "uri" },
83
+ "lovableOrigin": {
84
+ "type": "string",
85
+ "format": "uri",
86
+ "description": "Deprecated alias for spaOrigin."
87
+ },
88
+ "rewriteRouteBases": {
89
+ "type": "array",
90
+ "items": { "type": "string", "minLength": 1 }
91
+ },
92
+ "botUserAgentPatterns": {
93
+ "type": "array",
94
+ "items": { "type": "string", "minLength": 1 }
95
+ },
96
+ "debugHeaders": { "type": "boolean" }
97
+ }
98
+ }
99
+ }
100
+ }
package/src/cli.ts CHANGED
@@ -1,75 +1,96 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- import { cmdInit } from "./commands/init.js";
4
- import { cmdBuild } from "./commands/build.js";
5
- import { cmdVerify } from "./commands/verify.js";
6
-
7
- const require = createRequire(import.meta.url);
8
- const pkg = require("../package.json") as { version?: string };
9
- const VERSION = pkg.version ?? "0.0.0";
10
-
11
- function printHelp() {
12
- console.log(`
13
- RenderShield v${VERSION} — boring bot-aware prerendering.
14
-
15
- Usage:
16
- rendershield init
17
- rendershield build
18
- rendershield verify [--prod <url>]
19
-
20
- verify Print curl commands for local/build output.
21
- verify --prod Fetch URL as bot and human; verify bot sees full HTML and contract fields.
22
- Requires x-rendershield: bot-hit from the Worker.
23
-
24
- Notes:
25
- - Config file: rendershield.config.json
26
- - Content: content/<collection>/**/*.md (frontmatter required)
27
- - Output: dist-prerender/
28
- `);
29
- }
30
-
31
- async function main() {
32
- const args = process.argv.slice(2);
33
- const cmd = args[0]?.trim();
34
-
35
- if (!cmd || cmd === "-h" || cmd === "--help") {
36
- printHelp();
37
- process.exit(0);
38
- }
39
- if (cmd === "-V" || cmd === "--version") {
40
- console.log(VERSION);
41
- process.exit(0);
42
- }
43
-
44
- try {
45
- if (cmd === "init") {
46
- await cmdInit();
47
- return;
48
- }
49
- if (cmd === "build") {
50
- await cmdBuild();
51
- return;
52
- }
53
- if (cmd === "verify") {
54
- const verifyArgs = args.slice(1);
55
- if (verifyArgs.includes("-h") || verifyArgs.includes("--help")) {
56
- printHelp();
57
- process.exit(0);
58
- }
59
- const prodIdx = verifyArgs.indexOf("--prod");
60
- const prodUrl = prodIdx >= 0 && verifyArgs[prodIdx + 1] ? verifyArgs[prodIdx + 1].trim() : undefined;
61
- await cmdVerify(undefined, prodUrl ? { prodUrl } : undefined);
62
- return;
63
- }
64
-
65
- console.error(`Unknown command: ${cmd}\n`);
66
- printHelp();
67
- process.exit(1);
68
- } catch (err: unknown) {
69
- const msg = err instanceof Error ? err.message : String(err);
70
- console.error(`\nRenderShield error: ${msg}\n`);
71
- process.exit(1);
72
- }
73
- }
74
-
75
- main();
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { cmdInit } from "./commands/init.js";
4
+ import { cmdBuild } from "./commands/build.js";
5
+ import { cmdVerify } from "./commands/verify.js";
6
+ import { formatCliError, renderShieldError } from "./errors.js";
7
+ import { extractGlobalOptions, parseVerifyArgs } from "./cliArgs.js";
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require("../package.json") as { version?: string };
11
+ const VERSION = pkg.version ?? "0.0.0";
12
+
13
+ function printHelp() {
14
+ console.log(`
15
+ RenderShield v${VERSION} — boring bot-aware prerendering.
16
+
17
+ Usage:
18
+ rendershield [--config <path>] init
19
+ rendershield [--config <path>] build
20
+ rendershield [--config <path>] verify [options]
21
+
22
+ Global:
23
+ --config <path> Config file (default: rendershield.config.json)
24
+
25
+ Verify:
26
+ verify Print curl smoke-test commands for first built page
27
+ verify --check Validate built HTML contract (first page)
28
+ verify --all --check Validate contract for every built page
29
+ verify --prod <url> Fetch URL as bot; require x-rendershield: bot-hit + contract
30
+ verify --prod --all Check every route from build output in production
31
+
32
+ Notes:
33
+ - Content: content/<collection>/**/*.md (frontmatter required)
34
+ - Output: dist-prerender/ (see config)
35
+ - Config reference: docs/CONFIG.md
36
+ `);
37
+ }
38
+
39
+ async function main() {
40
+ const { options: globalOptions, rest } = extractGlobalOptions(
41
+ process.argv.slice(2)
42
+ );
43
+ const cmd = rest[0]?.trim();
44
+ const cmdArgs = rest.slice(1);
45
+
46
+ if (!cmd || cmd === "-h" || cmd === "--help") {
47
+ printHelp();
48
+ process.exit(0);
49
+ }
50
+ if (cmd === "-V" || cmd === "--version") {
51
+ console.log(VERSION);
52
+ process.exit(0);
53
+ }
54
+
55
+ try {
56
+ if (cmd === "init") {
57
+ await cmdInit(undefined, globalOptions);
58
+ return;
59
+ }
60
+ if (cmd === "build") {
61
+ await cmdBuild(undefined, globalOptions);
62
+ return;
63
+ }
64
+ if (cmd === "verify") {
65
+ if (cmdArgs.includes("-h") || cmdArgs.includes("--help")) {
66
+ printHelp();
67
+ process.exit(0);
68
+ }
69
+ const verifyOptions = parseVerifyArgs(cmdArgs, globalOptions);
70
+ if (verifyOptions.prod && !verifyOptions.prodUrl && !verifyOptions.all) {
71
+ throw renderShieldError(
72
+ "CLI_INVALID_ARGS",
73
+ "verify --prod requires a URL, or use --prod --all. Example: rendershield verify --prod https://example.com/blog/hello-world"
74
+ );
75
+ }
76
+ if (verifyOptions.all && !verifyOptions.check && !verifyOptions.prod) {
77
+ throw renderShieldError(
78
+ "CLI_INVALID_ARGS",
79
+ "verify --all requires --check (local) or --prod (production). Example: rendershield verify --all --check"
80
+ );
81
+ }
82
+ await cmdVerify(undefined, verifyOptions);
83
+ return;
84
+ }
85
+
86
+ console.error(`Unknown command: ${cmd}\n`);
87
+ printHelp();
88
+ process.exit(1);
89
+ } catch (err: unknown) {
90
+ const msg = formatCliError(err);
91
+ console.error(`\nRenderShield error: ${msg}\n`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ main();