eb88138d15
Five parallel lanes plus an integration pass. The header, gallery, router and sitemap are all generated from the demo registry, so adding src/demos/<slug>/ puts a demo everywhere with zero edits to shared files — which is the whole reason demo nine cannot break demo one. check-demos enforces the twelve contract rules: 142 checks over one live demo. Two worth naming. The shell may not mention a specific slug, because an 'if (slug === wordle)' in src/components/demo/ is a contract bug wearing a patch. And a spec-status demo must ship a real specification — task, actions, grader, counterweight, eval command — since a coming-soon card reads worse than an honest empty gallery. Bundle budget holds: entry 108.79 kB gzipped against a 160 kB ceiling, the demo chunk 21.15 kB against 90 kB. recharts is 108 kB gzipped and lives behind a lazy import so it never touches the entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* `dist/sitemap.xml` and `robots.txt`.
|
|
*
|
|
* PIG's own site is `noindex` — it is a private tool. This one is the exact
|
|
* inverse: it exists to be found, shared and quoted, so the robots file opens
|
|
* everything and points at a sitemap listing every route the router actually
|
|
* produces.
|
|
*
|
|
* The route list is the same one `prerender.mjs` bakes, from the same reader,
|
|
* so a sitemap entry cannot point at a URL that has no prerendered document.
|
|
* Those two drifting apart is how you end up submitting 14 URLs and having 13
|
|
* of them indexed as the homepage.
|
|
*
|
|
* `robots.txt` is written into `public/` (source, committed) AND into `dist/`
|
|
* (because this runs after the build that would have copied it). The sitemap is
|
|
* a build artifact only — it lists what this build contains.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { SITE_ORIGIN, abs, die, exists, expandRoutes, green, loadAllMetas, loadVerticals, rel } from './_lib.mjs';
|
|
|
|
const DIST = abs('dist');
|
|
const PUBLIC = abs('public');
|
|
|
|
const { metas } = loadAllMetas();
|
|
const verticals = loadVerticals();
|
|
if (verticals.error) die(`could not read the verticals: ${verticals.error}`);
|
|
|
|
const { routes, errors } = expandRoutes({ metas, verticals: verticals.list });
|
|
if (errors.length) die(`route enumeration failed:\n - ${errors.join('\n - ')}`);
|
|
|
|
/* ------------------------------------------------------------- robots.txt */
|
|
|
|
// Kept byte-identical to the committed public/robots.txt so re-running this
|
|
// script is a no-op in the diff. If you change the wording, change it here —
|
|
// this is the generator, and the committed file is its output.
|
|
const robots = [
|
|
'# The inverse of primeintellectgrowth.com, deliberately: being found is the',
|
|
'# entire point of this site.',
|
|
'User-agent: *',
|
|
'Allow: /',
|
|
'',
|
|
`Sitemap: ${SITE_ORIGIN}/sitemap.xml`,
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.mkdirSync(PUBLIC, { recursive: true });
|
|
fs.writeFileSync(path.join(PUBLIC, 'robots.txt'), robots, 'utf8');
|
|
|
|
/* ------------------------------------------------------------ sitemap.xml */
|
|
|
|
if (!exists(path.join(DIST, 'index.html'))) {
|
|
die(
|
|
`${rel(DIST)}/index.html does not exist, so there is no build to describe. ` +
|
|
'Run `pnpm build` first. (public/robots.txt has been written.)',
|
|
);
|
|
}
|
|
|
|
// One date for the whole build. Per-file mtimes would claim the vertical pages
|
|
// changed whenever anything in the bundle did, which is true of a hash-named
|
|
// asset and useless to a crawler.
|
|
const lastmod = new Date().toISOString().slice(0, 10);
|
|
|
|
const xml = [
|
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
...routes.map((route) =>
|
|
[
|
|
' <url>',
|
|
` <loc>${escapeXml(`${SITE_ORIGIN}${route.path === '/' ? '/' : route.path}`)}</loc>`,
|
|
` <lastmod>${lastmod}</lastmod>`,
|
|
' </url>',
|
|
].join('\n'),
|
|
),
|
|
'</urlset>',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(DIST, 'sitemap.xml'), xml, 'utf8');
|
|
fs.writeFileSync(path.join(DIST, 'robots.txt'), robots, 'utf8');
|
|
|
|
console.log(green(`sitemap: ${routes.length} URLs -> ${rel(path.join(DIST, 'sitemap.xml'))}`));
|
|
console.log(green(`robots: ${rel(path.join(PUBLIC, 'robots.txt'))} and ${rel(path.join(DIST, 'robots.txt'))}`));
|
|
|
|
/** Sitemap URLs are XML text; a bare `&` in a query string invalidates the file. */
|
|
function escapeXml(value) {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|