@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.
- package/CHANGELOG.md +73 -32
- package/CONTRIBUTING.md +41 -0
- package/README.md +227 -144
- package/SECURITY.md +25 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -26
- package/dist/cli.js.map +1 -1
- package/dist/cliArgs.d.ts +8 -0
- package/dist/cliArgs.d.ts.map +1 -0
- package/dist/cliArgs.js +56 -0
- package/dist/cliArgs.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +12 -11
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +8 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/verify.d.ts +33 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +202 -117
- package/dist/commands/verify.js.map +1 -1
- package/dist/configPath.d.ts +7 -0
- package/dist/configPath.d.ts.map +1 -0
- package/dist/configPath.js +8 -0
- package/dist/configPath.js.map +1 -0
- package/dist/core/generateRobots.d.ts +3 -0
- package/dist/core/generateRobots.d.ts.map +1 -0
- package/dist/core/generateSitemap.d.ts +3 -0
- package/dist/core/generateSitemap.d.ts.map +1 -0
- package/dist/core/generateWorker.d.ts +3 -0
- package/dist/core/generateWorker.d.ts.map +1 -0
- package/dist/core/generateWorker.js +75 -75
- package/dist/core/generateWorker.js.map +1 -1
- package/dist/core/listOutputRoutes.d.ts +4 -0
- package/dist/core/listOutputRoutes.d.ts.map +1 -0
- package/dist/core/listOutputRoutes.js +40 -0
- package/dist/core/listOutputRoutes.js.map +1 -0
- package/dist/core/loadConfig.d.ts +5 -0
- package/dist/core/loadConfig.d.ts.map +1 -0
- package/dist/core/loadConfig.js +108 -58
- package/dist/core/loadConfig.js.map +1 -1
- package/dist/core/loadMarkdown.d.ts +3 -0
- package/dist/core/loadMarkdown.d.ts.map +1 -0
- package/dist/core/loadMarkdown.js +4 -3
- package/dist/core/loadMarkdown.js.map +1 -1
- package/dist/core/renderHtml.d.ts +3 -0
- package/dist/core/renderHtml.d.ts.map +1 -0
- package/dist/core/renderHtml.js +25 -10
- package/dist/core/renderHtml.js.map +1 -1
- package/dist/core/validateOutput.d.ts +28 -0
- package/dist/core/validateOutput.d.ts.map +1 -0
- package/dist/core/validateOutput.js +7 -1
- package/dist/core/validateOutput.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/docs/CONFIG.md +70 -0
- package/docs/deploy-cloudflare.md +40 -14
- package/package.json +25 -2
- package/rendershield.config.schema.json +100 -0
- package/src/cli.ts +96 -75
- package/src/cliArgs.ts +71 -0
- package/src/commands/build.ts +200 -185
- package/src/commands/init.ts +8 -8
- package/src/commands/verify.ts +451 -236
- package/src/configPath.ts +17 -0
- package/src/core/generateWorker.ts +97 -97
- package/src/core/listOutputRoutes.ts +51 -0
- package/src/core/loadConfig.ts +282 -173
- package/src/core/loadMarkdown.ts +9 -3
- package/src/core/renderHtml.ts +36 -12
- package/src/core/validateOutput.ts +335 -328
- package/src/errors.ts +48 -0
- package/src/index.ts +43 -0
- package/src/types.ts +5 -2
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
-
- /
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
If it passes, crawlers are receiving the prerendered HTML.
|
|
140
157
|
|
|
141
|
-
|
|
158
|
+
### Manual checks (optional)
|
|
142
159
|
|
|
143
|
-
|
|
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: /
|
|
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
|
-
-
|
|
153
|
-
|
|
154
|
-
---
|
|
176
|
+
- The prerendered origin may be returning non-200 for that path
|
|
155
177
|
|
|
156
|
-
|
|
178
|
+
### Content comparison
|
|
157
179
|
|
|
158
180
|
Human request (SPA response):
|
|
159
181
|
|
|
160
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
rendershield
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
process.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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();
|