Add a crawlable static mirror, generated at deploy time - #120
Conversation
The terminal renders all content via JavaScript on keystroke, so search engines and LLM crawlers previously saw ~200 characters of hidden text and a single indexable URL. scripts/build-pages.js now generates a plain-HTML mirror from the same config/*.js data at build time: /about/, /jobs/, /portfolio/(+63 companies), /team/(+7 people), robots.txt, sitemap.xml, llms.txt, and llms-full.txt. The terminal itself is visually unchanged. Also extracts firm-level facts (address, fund size, thesis) into config/firm.js so the terminal and static pages read one source of truth, fixes the canonical/og:url host mismatch (www vs apex) and relative og:image paths, and adds a `www` terminal command pointing at the mirror. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The mirror was committed to the repo, which made it possible for the generated HTML to disagree with config/*.js. Everything built to close that gap — the --check drift guard, its tests, the CI step, and the open question about a cron to re-sync — existed only because derived files were being tracked as sources. Make the mirror a build artifact instead: - npm run build now assembles dist/ from scratch and Netlify publishes that, so config/*.js reaches the static pages on deploy and there is nothing left that can go stale - index.html in the repo root is a template; the build injects the JSON-LD and <noscript> blocks into dist/index.html - the pure-data configs carry a guarded module.exports footer, so build-pages.js can require() them and the vm sandbox is gone. They still load as classic browser scripts in the bundle and welcome.htm - dropped findDrift/checkPages, build:pages:check, and the drift tests Also removes the previously committed build output (js/app.bundle.js, the vendor copies, the source maps) for the same reason. Publishing dist/ rather than "." means scripts/, tests/, package.json, and netlify.toml are no longer served from root.vc. config/*.js still ships: welcome.htm loads it with raw <script src> tags.
|
The generated pages themselves are good — the canonical/ The sync problem is self-inflicted. The mirror is derived from Rather than sync it better, I'd stop committing it. I put that up as #121, which targets this branch — merge it and this PR updates in place. One commit on top of
Two specifics on the blockers you flagged: You don't need the Netlify UI value. The Two things I'd flag as genuinely your call rather than settled:
Unrelated, but noticed while reading: the Vercel check on this PR is erroring, and the repo is clearly Netlify-deployed ( Verification on #121: 230/230 tests, every local Happy to be wrong about the trade in (1) — if you want the committed-output fallback, say so and I'll close #121. |
| // Regenerate in memory and compare against disk. Pure — no console output, no | ||
| // process.exit — so tests can call it directly and assert on the result. The | ||
| // CLI-facing checkPages() below is the thin wrapper that reports and exits. | ||
| function findDrift(files = buildPages()) { |
There was a problem hiding this comment.
findDrift is the clearest statement of the design's cost: it exists to detect a disagreement between a file and the thing that generated it. That disagreement is only possible because both are in git.
In #121 the mirror is written to dist/ and gitignored, so this function and checkPages are deleted outright. What replaces them is a determinism test — buildPages(loadConfig()) must equal itself across runs — which is the property you actually needed here (no key-order wobble, no timestamps), minus the on-disk comparison.
| }); | ||
| }); | ||
|
|
||
| describe("findDrift (the --check drift guard)", () => { |
There was a problem hiding this comment.
These four are well-written and I'd keep them if the design stayed. Worth noticing what they're testing though: three of them test the drift detector, and the fourth asserts the committed copy is currently in sync — which is true right up until someone pushes a config edit without rebuilding, which is the exact case the machinery is for.
#121 replaces this block with three tests that assert the structural property instead: the generator emits nothing into the repo root (except index.html, which it reads as a template and writes to dist/), the committed index.html sentinel regions are empty, and the build is deterministic.
| "scripts": { | ||
| "build": "node scripts/build-assets.js", | ||
| "build:pages": "node scripts/build-pages.js", | ||
| "build:pages:check": "node scripts/build-pages.js --check", |
There was a problem hiding this comment.
This is the script the missing CI step would have run. Since it can't be added without workflow scope, the drift guard is currently unenforced — the committed mirror can go stale on main today and nothing catches it.
That's not an argument for hurrying the CI step in; it's an argument that the guard is load-bearing in a way that's easy to leave half-connected. #121 deletes the script. .github/workflows/test.yml already runs npm run build, which covers the case that actually matters: a config edit that breaks the generator.
|
|
||
| Three things keep it in sync: | ||
| - `npm run build` generates it, so every Netlify deploy publishes current data | ||
| - the output is committed, so it survives a failed build and is reviewable in PRs |
There was a problem hiding this comment.
This is the one real property #121 gives up, so I want to make sure it's a deliberate trade rather than something I quietly dropped.
Publishing dist/ means a failed build fails the deploy instead of falling back to committed HTML. My read is that's the better failure mode — Netlify holds the last good deploy, so visitors see the previous correct site rather than pages that silently disagree with config/*.js — but you wrote this line on purpose and you may weigh it differently.
(The "reviewable in PRs" half is genuinely lost. 12k lines of generated HTML in a diff is arguably negative review signal, but that's a taste call.)
|
Correction to my comment above — do not disconnect the Vercel integration. I guessed it was vestigial. It isn't, and acting on that guess would break a working deploy. Details, because they matter for this PR:
So the red check isn't a stale integration. It's this repo sitting mid-migration:
That raises a question worth settling before this PR grows further, and it's not one I can answer from the code: Is
Neither of those is an argument against this PR's goal. I'd just rather find out now than after it lands on the branch that isn't the one being deployed. (FWIW the daily cycle's last run was 2026-06-02 and there have been no incarnation commits since, which is why the branch reads as parked from the outside — but that's inference, and you two know the actual intent.) |
The Vercel project is connected at the repo level, so it attempted a build on every branch. Only ai-incarnations carries a vercel.json, so builds on this line had nothing to read and failed — posting a red check on this PR, which has nothing to do with Vercel. root.vc is served by Netlify. Set git.deploymentEnabled: false so branches carrying this file don't trigger a Vercel deployment. Byte-identical to the copy on #122, which lands the same file on main along with the README rationale. The two merge cleanly in either order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records that Netlify serves root.vc from dist/, that the Vercel project belongs to the parked ai-incarnations branch, and why the two vercel.json files deliberately differ — so nobody reconciles them and breaks that branch's deploys. Folded in from #122 so this line carries the whole change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What this PR does, in shortroot.vc is a terminal that renders everything in JavaScript, so crawlers see a blank page. This generates a plain-HTML mirror of the same data — Three things make it work:
Also fixes two live bugs from the original: Net: −10,463 lines, 230 tests passing. Side effect worth knowing — |
Generate the static mirror at deploy time instead of committing it
The deploy preview failed after the switch to publish = "dist". The site has no .nvmrc, no engines field and no NODE_VERSION, so Netlify was using the legacy default it was created with. That was fine while build-assets.js only used fs.copyFileSync and fs.mkdirSync; it now uses fs.cpSync (Node >= 16.7) and fs.rmSync (>= 14.14). Pin to Node 20, the version .github/workflows/test.yml already uses, so a green CI run implies a buildable deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
The terminal renders everything via JavaScript on keystroke, so search engines and LLM crawlers previously saw ~200 characters of hidden text and a single indexable URL. This adds
scripts/build-pages.js, a build-time generator that produces a plain-HTML mirror from the sameconfig/*.jsdata — the terminal itself is visually unchanged./about/,/jobs/,/portfolio/(+ 63 company pages),/team/(+ 7 person pages)robots.txt(explicitly allows AI crawlers),sitemap.xml,llms.txt,llms-full.txtconfig/firm.jsextracted so the terminal and static pages share one source of truth for firm facts (address, fund size, thesis) instead of the terminal having its own copies buried incommands.jsog:urlpointed atwww.root.vc, which 301s to the apex, andog:imageused relative paths (Open Graph silently drops those)wwwterminal command pointing at the mirrorThe mirror is generated at deploy time, not committed
The first version of this PR committed the generated HTML. That made it possible for the output to disagree with
config/*.js, which is why it also needed a--checkdrift guard, drift tests, a CI step, and an open question about a cron to re-sync.None of that is here anymore, because the hazard isn't.
npm run buildassemblesdist/from scratch, Netlify publishesdist/, anddist/is gitignored. A config edit reaches the static pages on deploy because that is the only way the pages come into existence — nothing to check, nothing to re-sync, no cron.netlify.toml—command = "npm run build",publish = "dist"scripts/build-assets.js— wipes and rebuildsdist/, copies the static assets, then generates the mirrorindex.htmlin the repo root is now a template; the build injects the JSON-LD and<noscript>blocks intodist/index.htmlconfig/{firm,portfolio,team,jobs}.jscarry a guardedmodule.exportsfooter, sobuild-pages.jscanrequire()them and thevmsandbox is gone. They still load as classic browser scripts in the bundle and inwelcome.htmSide effect worth knowing: publishing
dist/rather than.meansroot.vc/package.json,/scripts/, and/tests/stop being served. They are public today.CI and hosting
The Vercel project is connected at the repo level, so it attempted a build on every branch. Only the parked
ai-incarnationsbranch (#110) carries avercel.json, so builds on this line had nothing to read and failed — posting a red check on unrelated PRs.vercel.jsonhere setsgit.deploymentEnabled: false, which turns off automatic Vercel deployments for branches carrying the file.root.vc deploys to Netlify, not Vercel.
ai-incarnationskeeps its ownvercel.jsonwithout that key and keeps deploying green — that exploratory branch is not part of this PR and nothing here touches it. The README records why the two files deliberately differ.The
--checkCI step this PR originally needed is gone: there is no committed copy to drift, and.github/workflows/test.ymlalready runsnpm run build, so a config edit that breaks the generator fails CI there. NoworkflowOAuth scope required.Test plan
npm test— 230/230 passingnpm run build— 79 mirror files, 212 files indist/src/hrefacross all 76 built HTML files resolves insidedist/dist/and loaded it in a browser: terminal boots with no console errors;firm/portfolio(63)/team(7) are live globals andmoduleisundefined, so the dual-mode guard is load-bearing in a real browserwelcome.htmstill renders its config-driven tables — that is the raw<script src>path the export footer could plausibly break/portfolio/zed/,/team/lee/,/robots.txt,/llms.txtserve correctly under directory-index routingconfig/portfolio.jsand rebuilt — it propagated to 12 files (bundle,index.html, both llms files, Zed's page, the portfolio index, 5 sibling sections). No commit involvedlocate,pine,upgrade) produce identical outputpublish = "dist"— that is the first time the toml settings are exercised by Netlify rather than locallyTrade-off to be aware of
Under
publish = "."a failed build still served the committed HTML. Publishingdist/means a failed build fails the deploy and Netlify holds the last good one. Stale pages are worse than a held deploy, but it is a real change from the original behaviour.🤖 Generated with Claude Code