@koehler8/cms 1.0.0-beta.37 → 1.0.0-beta.39
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/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Detect a page whose root container never mounted any real markup — just
|
|
5
|
+
// Vue's SSR placeholder comment for "nothing rendered here". This is the
|
|
6
|
+
// signature of a rare, silent failure in the onServerPrefetch data-fetch
|
|
7
|
+
// path: @vue/server-renderer's renderComponentVNode gates a component's
|
|
8
|
+
// render on `Promise.all(prefetches).catch(NOOP)` (see server-renderer.cjs.js
|
|
9
|
+
// in any consuming site's node_modules) — if that promise ever rejects, the
|
|
10
|
+
// renderer swallows the error entirely and renders anyway, using whatever
|
|
11
|
+
// default/empty state the component held. No warning, no non-zero exit —
|
|
12
|
+
// just well-formed, blank HTML on disk. A real page from this framework is
|
|
13
|
+
// never legitimately empty (Home.vue always renders at least the skip-link +
|
|
14
|
+
// <main>, DraftGate renders a dialog, 404 renders NotFound), so any match
|
|
15
|
+
// here is unconditionally a bug. Kept in its own module (no side effects on
|
|
16
|
+
// import) so it's unit-testable without tripping index.mjs's top-level
|
|
17
|
+
// main() invocation — see bin/cms-ssg-build.js.
|
|
18
|
+
const EMPTY_ROOT_RE = /<div id="[^"]+"[^>]*>\s*<!---->\s*<\/div>/;
|
|
19
|
+
|
|
20
|
+
export async function findEmptyRenders(dir) {
|
|
21
|
+
const empties = [];
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await fsp.readdir(dir, { recursive: true });
|
|
25
|
+
} catch {
|
|
26
|
+
return empties;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.endsWith('index.html')) continue;
|
|
30
|
+
const full = path.join(dir, entry);
|
|
31
|
+
let html;
|
|
32
|
+
try {
|
|
33
|
+
html = await fsp.readFile(full, 'utf8');
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (EMPTY_ROOT_RE.test(html)) {
|
|
38
|
+
empties.push(full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return empties;
|
|
42
|
+
}
|
|
@@ -26,6 +26,7 @@ import fs from 'node:fs';
|
|
|
26
26
|
import fsp from 'node:fs/promises';
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import os from 'node:os';
|
|
29
|
+
import { findEmptyRenders } from './findEmptyRenders.mjs';
|
|
29
30
|
|
|
30
31
|
const CWD = process.cwd();
|
|
31
32
|
const TAG = '[cms-ssg-build]';
|
|
@@ -157,6 +158,14 @@ async function main() {
|
|
|
157
158
|
console.log(`${TAG} ~${estRoutes} routes ≤ shard size ${opts.shardSize}: running a single vite-ssg build (no sharding needed).`);
|
|
158
159
|
const r = await runViteSsg({ heap: opts.heap, passthrough: opts.passthrough });
|
|
159
160
|
process.stdout.write(r.out);
|
|
161
|
+
if (r.code === 0) {
|
|
162
|
+
const empties = await findEmptyRenders(path.join(CWD, 'dist'));
|
|
163
|
+
if (empties.length) {
|
|
164
|
+
console.error(`${TAG} build succeeded but ${empties.length} page(s) rendered with an empty body — a known intermittent SSR race (see findEmptyRenders in this file). Re-run the build:`);
|
|
165
|
+
for (const f of empties) console.error(`${TAG} - ${f}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
160
169
|
process.exit(r.code);
|
|
161
170
|
}
|
|
162
171
|
|
|
@@ -175,7 +184,20 @@ async function main() {
|
|
|
175
184
|
const started = Date.now();
|
|
176
185
|
process.stdout.write(`${TAG} shard ${s.k + 1}/${N} rendering…\n`);
|
|
177
186
|
const r = await runViteSsg({ shard: `${s.k}/${N}`, outDir: s.outDir, heap: opts.heap, passthrough: opts.passthrough });
|
|
178
|
-
|
|
187
|
+
let empties = [];
|
|
188
|
+
if (r.code === 0) {
|
|
189
|
+
empties = await findEmptyRenders(path.join(CWD, s.outDir));
|
|
190
|
+
}
|
|
191
|
+
const ok = r.code === 0 && empties.length === 0;
|
|
192
|
+
process.stdout.write(`${TAG} shard ${s.k + 1}/${N} ${ok ? 'ok' : 'FAILED'} (${((Date.now() - started) / 1000).toFixed(1)}s)\n`);
|
|
193
|
+
if (empties.length) {
|
|
194
|
+
return {
|
|
195
|
+
...r,
|
|
196
|
+
code: 1,
|
|
197
|
+
out: `${r.out}\n${TAG} ${empties.length} page(s) rendered with an empty body — a known intermittent SSR race (see findEmptyRenders in this file):\n${empties.map((f) => ` - ${f}`).join('\n')}\n`,
|
|
198
|
+
k: s.k,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
179
201
|
return { ...r, k: s.k };
|
|
180
202
|
});
|
|
181
203
|
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* - "Disallow: <prefix>" for each entry in site.draftPaths
|
|
16
16
|
* - "Disallow: <path>" for each page with draft === true
|
|
17
17
|
*
|
|
18
|
+
* site.robots.allowAiCrawlers (opt-in, default off): appends a dedicated block
|
|
19
|
+
* per known AI/LLM crawler (GPTBot, ClaudeBot, PerplexityBot, CCBot, etc.),
|
|
20
|
+
* each carrying the exact same Disallow list as the wildcard block — i.e.
|
|
21
|
+
* identical effective access, just declared explicitly per bot. Several sites
|
|
22
|
+
* were hand-editing this list directly into public/robots.txt; since that file
|
|
23
|
+
* is regenerated on every build, the hand edits were silently reverted on the
|
|
24
|
+
* next `build:ssg`/`generate:public-assets` run. This option makes the
|
|
25
|
+
* customization durable. Skipped entirely when site.draft === true — a
|
|
26
|
+
* pre-launch site shouldn't explicitly invite crawlers.
|
|
27
|
+
*
|
|
18
28
|
* If sitemapUrl is provided, appends a "Sitemap: <url>" line.
|
|
19
29
|
*/
|
|
20
30
|
|
|
@@ -22,18 +32,27 @@ import { normalizeDraftPath } from './draftMode.js';
|
|
|
22
32
|
|
|
23
33
|
const FRAMEWORK_DEFAULTS = ['/admin'];
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
35
|
+
// Order/grouping mirrors the hand-rolled lists sites had converged on before
|
|
36
|
+
// this became a config option.
|
|
37
|
+
const AI_CRAWLER_GROUPS = [
|
|
38
|
+
{ comment: 'OpenAI / ChatGPT', agents: ['GPTBot', 'OAI-SearchBot', 'ChatGPT-User'] },
|
|
39
|
+
{ comment: 'Anthropic / Claude', agents: ['anthropic-ai', 'ClaudeBot'] },
|
|
40
|
+
{ comment: 'Perplexity', agents: ['PerplexityBot'] },
|
|
41
|
+
{ comment: 'Common Crawl (used by many LLM training datasets)', agents: ['CCBot'] },
|
|
42
|
+
{ comment: 'Google', agents: ['Googlebot', 'Googlebot-Extended'] },
|
|
43
|
+
{ comment: 'Microsoft / Bing', agents: ['Bingbot'] },
|
|
44
|
+
{ comment: 'Meta', agents: ['FacebookBot'] },
|
|
45
|
+
{ comment: 'Diffbot', agents: ['Diffbot'] },
|
|
46
|
+
];
|
|
31
47
|
|
|
48
|
+
export function buildRobotsTxt(siteConfig, sitemapUrl = '') {
|
|
32
49
|
const site = siteConfig?.site || {};
|
|
33
50
|
const pages = siteConfig?.pages || {};
|
|
34
51
|
|
|
52
|
+
const disallows = [...FRAMEWORK_DEFAULTS];
|
|
53
|
+
|
|
35
54
|
if (site.draft === true) {
|
|
36
|
-
|
|
55
|
+
disallows.push('/');
|
|
37
56
|
} else {
|
|
38
57
|
const additional = new Set();
|
|
39
58
|
|
|
@@ -51,7 +70,25 @@ export function buildRobotsTxt(siteConfig, sitemapUrl = '') {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
for (const p of Array.from(additional).sort()) {
|
|
54
|
-
|
|
73
|
+
disallows.push(p);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines = ['User-agent: *'];
|
|
78
|
+
for (const d of disallows) {
|
|
79
|
+
lines.push(`Disallow: ${d}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (site.robots?.allowAiCrawlers === true && site.draft !== true) {
|
|
83
|
+
for (const group of AI_CRAWLER_GROUPS) {
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(`# ${group.comment}`);
|
|
86
|
+
for (const agent of group.agents) {
|
|
87
|
+
lines.push(`User-agent: ${agent}`);
|
|
88
|
+
for (const d of disallows) {
|
|
89
|
+
lines.push(`Disallow: ${d}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
55
92
|
}
|
|
56
93
|
}
|
|
57
94
|
|