Skip to content

Add a crawlable static mirror, generated at deploy time - #120

Merged
ledwards merged 6 commits into
mainfrom
claude/root-vc-seo-llm-discovery-fb2218
Aug 4, 2026
Merged

Add a crawlable static mirror, generated at deploy time#120
ledwards merged 6 commits into
mainfrom
claude/root-vc-seo-llm-discovery-fb2218

Conversation

@kanetronv2

@kanetronv2 kanetronv2 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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 same config/*.js data — the terminal itself is visually unchanged.

  • New pages: /about/, /jobs/, /portfolio/ (+ 63 company pages), /team/ (+ 7 person pages)
  • New files: robots.txt (explicitly allows AI crawlers), sitemap.xml, llms.txt, llms-full.txt
  • config/firm.js extracted 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 in commands.js
  • Fixes two live bugs: canonical/og:url pointed at www.root.vc, which 301s to the apex, and og:image used relative paths (Open Graph silently drops those)
  • New www terminal command pointing at the mirror

The 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 --check drift 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 build assembles dist/ from scratch, Netlify publishes dist/, and dist/ 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.tomlcommand = "npm run build", publish = "dist"
  • scripts/build-assets.js — wipes and rebuilds dist/, copies the static assets, then generates the mirror
  • index.html in the repo root is now a template; the build injects the JSON-LD and <noscript> blocks into dist/index.html
  • config/{firm,portfolio,team,jobs}.js 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 in welcome.htm

Side effect worth knowing: publishing dist/ rather than . means root.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-incarnations branch (#110) carries a vercel.json, so builds on this line had nothing to read and failed — posting a red check on unrelated PRs. vercel.json here sets git.deploymentEnabled: false, which turns off automatic Vercel deployments for branches carrying the file.

root.vc deploys to Netlify, not Vercel. ai-incarnations keeps its own vercel.json without 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 --check CI step this PR originally needed is gone: there is no committed copy to drift, and .github/workflows/test.yml already runs npm run build, so a config edit that breaks the generator fails CI there. No workflow OAuth scope required.

Test plan

  • npm test — 230/230 passing
  • npm run build — 79 mirror files, 212 files in dist/
  • Every local src/href across all 76 built HTML files resolves inside dist/
  • Served dist/ and loaded it in a browser: terminal boots with no console errors; firm/portfolio (63)/team (7) are live globals and module is undefined, so the dual-mode guard is load-bearing in a real browser
  • welcome.htm still 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.txt serve correctly under directory-index routing
  • End-to-end: edited one line in config/portfolio.js and rebuilt — it propagated to 12 files (bundle, index.html, both llms files, Zed's page, the portfolio index, 5 sibling sections). No commit involved
  • Terminal's refactored commands (locate, pine, upgrade) produce identical output
  • Confirm the Netlify deploy preview on this PR builds with the new publish = "dist" — that is the first time the toml settings are exercised by Netlify rather than locally

Trade-off to be aware of

Under publish = "." a failed build still served the committed HTML. Publishing dist/ 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

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>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-website Error Error Aug 3, 2026 7:21pm

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.
@ledwards

ledwards commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The generated pages themselves are good — the canonical/og:url fix and pulling firm facts out of commands.js into config/firm.js are both real wins. My pushback is on where the output lives, because I think it's the thing creating the two open questions at the bottom of this description.

The sync problem is self-inflicted. The mirror is derived from config/*.js and then committed. That's the only reason it can disagree with its source. Everything built to close that gap — findDrift, checkPages, build:pages:check, the four drift tests, the CI step blocked on workflow scope, and the cron/auto-sync question — exists to manage a hazard that committing the output introduced.

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 faf71b5, nothing force-pushed.

npm run build assembles dist/, Netlify publishes dist/, dist/ is gitignored. A config edit reaches the static pages on deploy because that's the only way the pages exist at all. Nothing to check, nothing to re-sync, no cron. 98 files, +202 / −10,463.

Two specifics on the blockers you flagged:

You don't need the Netlify UI value. .gitignore ignores css/xterm.css, js/xterm*.js*, and js/aalib.js*, and index.html references all three. The site works today, so a build command already runs, and npm run build is the only script that produces them. Setting it in the toml just captures what's already configured — safe to land.

The workflow-scope CI step is moot under this design. There's no committed copy to drift. .github/workflows/test.yml already runs npm run build, so a config edit that breaks the generator fails CI there.

Two things I'd flag as genuinely your call rather than settled:

  1. Failed builds behave differently. Under publish = "." a broken build still served the committed HTML. Publishing dist/ means a broken build fails the deploy and Netlify holds the last good one. I think that's better — stale pages are worse than a held deploy — but it's a real change from the "survives a failed build" property you called out, so I don't want to slip it past you.

  2. publish = "." is serving the whole repo right now. root.vc/package.json, /scripts/build-pages.js, and /tests/ are all live today. The dist/ switch 404s them as a side effect. Worth knowing that's a current condition either way, independent of which design wins. (config/*.js still ships — welcome.htm loads it with raw <script src> tags.)

Unrelated, but noticed while reading: the Vercel check on this PR is erroring, and the repo is clearly Netlify-deployed (netlify.toml, netlify/functions/, netlify-cli). Is that integration vestigial? If something is actually building on Vercel, neither of our netlify.toml changes reaches it and we should sort that out separately.

Verification on #121: 230/230 tests, every local src/href across all 76 built HTML files resolves inside dist/, terminal boots clean in a browser with firm/portfolio(63)/team(7) as live globals, welcome.htm still renders its config-driven tables, and a one-line edit to config/portfolio.js propagated to 12 built files with no commit involved.

Happy to be wrong about the trade in (1) — if you want the committed-output fallback, say so and I'll close #121.

@ledwards ledwards left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anchoring the point from my top-level comment to the specific code. All four of these disappear in #121 — not because the code is wrong, but because the problem it solves stops existing once the output isn't committed.

Comment thread scripts/build-pages.js Outdated
// 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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/build-pages.test.js Outdated
});
});

describe("findDrift (the --check drift guard)", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread package.json Outdated
"scripts": {
"build": "node scripts/build-assets.js",
"build:pages": "node scripts/build-pages.js",
"build:pages:check": "node scripts/build-pages.js --check",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread README.md Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

@ledwards

ledwards commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:

origin/ai-incarnations is strictly ahead of main — it contains every commit on main plus 18 more, including #110 (feat: weekly AI-reinvented root.vc + Vercel migration), a vercel.json, /archive/, and .github/workflows/daily-cycle.yml. Vercel builds it successfully on every commit. It fails on main-line branches only because main has no vercel.json for it to read.

So the red check isn't a stale integration. It's this repo sitting mid-migration:

main ai-incarnations
Tip e988c7b, 2026-04-16 b980538, 2026-06-02
Host config netlify.toml vercel.json
Vercel check ❌ fail ✅ success
Serving root.vc today ✅ (server: Netlify)

That raises a question worth settling before this PR grows further, and it's not one I can answer from the code:

Is ai-incarnations the intended future of main, or a parked experiment? Both this PR and my #121 are built on main, the older line. If that branch is meant to land, two things here need rethinking rather than just rebasing:

  1. Its vercel.json sets "outputDirectory": ".", which is the same publish-the-repo-root shape Generate the static mirror at deploy time instead of committing it #121 argues against — and it would override the publish = "dist" change on the host that actually serves the site.
  2. The daily incarnation cycle regenerates the site on a theme each day. A crawlable mirror generated from config/*.js may compose with that fine — config is the stable data layer underneath the themes — but "generated pages at fixed URLs" and "the whole site is re-themed daily" is a real design interaction, not a merge conflict.

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.)

ledwards and others added 2 commits August 3, 2026 16:46
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>
@ledwards

ledwards commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What this PR does, in short

root.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 — /about/, /jobs/, /portfolio/ + 63 company pages, /team/ + 7 people, plus robots.txt, sitemap.xml, and llms.txt for AI crawlers. The terminal looks and behaves exactly the same.

Three things make it work:

  1. config/*.js is the only source of truth. Edit a portfolio description and it regenerates that page, both indexes, the sitemap, the llms files, and the homepage's <noscript> block.

  2. The pages are never committed. npm run build produces dist/, Netlify publishes dist/, dist/ is gitignored. They can't go stale because they only exist as build output — no cron, no drift check, no rebuild-and-commit step.

  3. Vercel stops failing. It was building every branch and failing on this one; now it's off for the main line. Netlify keeps serving the site. The ai-incarnations experiment is untouched and not part of this PR.

Also fixes two live bugs from the original: canonical/og:url pointed at www.root.vc (which 301s to the apex), and og:image used relative paths that Open Graph silently drops.

Net: −10,463 lines, 230 tests passing. Side effect worth knowing — root.vc/package.json and /scripts/ stop being publicly served. They are public today.

Generate the static mirror at deploy time instead of committing it
@ledwards ledwards changed the title Add crawlable static mirror for SEO and LLM discovery Add a crawlable static mirror, generated at deploy time Aug 4, 2026
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>
@ledwards
ledwards merged commit b2bf3fd into main Aug 4, 2026
6 checks passed
@ledwards
ledwards deleted the claude/root-vc-seo-llm-discovery-fb2218 branch August 4, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants