Skip to content

Repository files navigation

rainbowindex.dev

The website for Rainbow Index. It serves two things from one origin:

  • A landing page, for people. React on Vite, styled with Rainbow Index itself.
  • The documentation, as markdown, for agents. Every page of the package's docs is published verbatim at a stable URL, alongside llms.txt and llms-full.txt, and an agent that asks for text/markdown on the HTML URL gets the markdown back.

None of the documentation is written here. It is generated out of the package repository on every dev start and every build, and none of it is checked in.

Setup

pnpm install
pnpm dev

That is all of it. This repository builds on its own: pnpm dev and pnpm build download the documentation from the package repository first, and nothing else has to be checked out for either to work. The compiler comes from npm, the prose comes from GitHub, and neither is a directory someone has to remember to clone.

Scripts

Script What it does
pnpm dev Regenerates the docs, then starts the Vite dev server.
pnpm build Regenerates the docs, typechecks all three TypeScript projects, and builds to dist/.
pnpm run docs Regenerates the docs only. Spell out run — pnpm docs is pnpm's own command.
pnpm test Vitest over the edge negotiation rule.
pnpm lint Oxlint.
pnpm preview Serves the built dist/.

pnpm preview is a static server: the middleware does not run under it, so Accept-based negotiation is only live once deployed. The .md URLs and llms.txt are static files and work everywhere.

How the docs get here

pnpm run docs downloads docs/*.md and the README from the package repository, and writes 27 files into public/:

public/llms.txt        The index: one entry per page, with a summary.
public/llms-full.txt   The whole corpus concatenated, ~230 KB.
public/docs/*.md       A twin of every documentation page, 24 in all.
public/README.md       A twin of the package README.

All four are in .gitignore. The package repository is the source of truth, and a copy checked in here would go stale and be served as if it were current. Because generation is wired into dev and build rather than run by hand, that cannot happen quietly.

The split between the two scripts is the point. scripts/docs.mjs downloads a release tarball from codeload.github.com and unpacks it into node_modules/.cache/, where it is reused, because a release tag does not move. scripts/generate-llms.mjs turns that prose into llms.txt, llms-full.txt and the twins — it takes the corpus directory as an argument and reaches for no path of its own.

So the site depends on the package's documentation, and on none of its tooling. That is what makes the build work anywhere: the markdown is committed and public, and everything done to it happens in this repository.

The ref is this site's own version rather than a number kept in step by hand: the site is versioned as the release it documents, so package.json already holds the answer and a version bump carries the docs with it. That puts an order on releasing — tag the package, then bump the site — and RAINBOWINDEX_DOCS_REF covers the window in between (main publishes docs ahead of a release).

Content negotiation

Three conventions reach the same corpus.

curl https://rainbowindex.dev/llms.txt
curl https://rainbowindex.dev/docs/theming.md
curl -H "Accept: text/markdown" https://rainbowindex.dev/docs/theming

The first two are static files. The third is middleware.ts, running on Vercel Edge across /docs, /docs/:path* and /readme: it rewrites to the twin in place, so the URL the client sees does not change. Both branches send Vary: Accept, without which a CDN would cache one representation and hand it to everyone.

The rule itself is in edge/accept.ts, deliberately apart from the middleware so it can be read and tested without a runtime around it. It is conservative on purpose: markdown is served only to a client that names a markdown type and ranks it above text/html. Wildcards are dropped rather than expanded, so the */* that curl and most HTTP clients send by default is not read as a request for markdown — otherwise the default representation would stop being the default for nearly everyone. A browser is unaffected: it sends text/html and never mentions markdown.

Layout

index.html                 The shell. One slot, filled by React.
middleware.ts              Vercel Edge entry point.
edge/accept.ts             The negotiation rule: Accept header in, path out.
edge/accept.test.ts        Its tests — the cases that break silently.
scripts/docs.mjs           Downloads the package's docs, writes public/.
scripts/generate-llms.mjs  Corpus in, llms.txt and the twins out.
src/main.tsx               The page: logo, two links.
src/components/icons.tsx   The logo and social marks, as inline SVG.
src/css/index.css          The theme, and every rule on the page.
public/fonts/              Self-hosted Circular, preloaded by the @font block.
public/favicon.svg

Styling

src/css/index.css is the whole stylesheet: one @import "rainbowindex", the theme, and the rules. There is no second styling system to fall back on — this site is a consumer of the package it documents, so a regression in the compiler shows up on the homepage.

The theme declares fonts through @font (with preload: true, which is why the .woff2 files are local), colors through @color with the light and dark value of each token in a single OKLCH pair, and @rounded squircle. Dark mode needs no separate block: it falls out of those pairs.

Everything else is @a against that theme. Logo strokes are theme colors (stroke-ri-blue, stroke-ri-green), not baked-in fills, so the mark follows the palette rather than needing a second asset.

TypeScript

Three projects, because the code runs in three places:

Project Covers Environment
tsconfig.app.json src Browser: DOM plus vite/client.
tsconfig.node.json vite.config.ts Build tooling: Node types.
tsconfig.edge.json middleware.ts, edge/ Vercel Edge: web APIs only.

The edge project is the reason they are split. Its runtime is web APIs, not Node, so types is empty and Request, Response and URL reach TypeScript through lib: DOM — edge code cannot then typecheck against globals it will not have at runtime.

pnpm build runs tsc -b, which builds all three.

Imports in middleware.ts and edge/ end in .js although the files are .ts, and that is load-bearing. Vercel type-checks the middleware entry point itself, under nodenext resolution and without allowImportingTsExtensions — settings this repository neither uses nor controls. Exactly one specifier satisfies both it and tsc -b here:

Written as Vercel says
./edge/accept.ts TS5097 — needs allowImportingTsExtensions.
./edge/accept TS2835 — nodenext requires an explicit extension.
./edge/accept.js Resolves to accept.ts, under nodenext, bundler and Vite alike.

Both wrong forms typecheck cleanly under tsc -b here and fail only on deploy, which is as late as a build error can be found.

Deploying

Vercel. pnpm build produces a static dist/, and middleware.ts at the root is picked up as the edge function — the only part of the site that is not static, because content negotiation branches on a request header and so cannot be.

The build reaches the network before it reaches Vite: the documentation is downloaded from the package repository's release tag on the way past. Nothing else is needed on the machine, which is the whole reason it is done that way.

Not here yet

There are no HTML documentation pages. The docs are published in markdown only, so /docs/theming in a browser has nothing to render — the route exists for agents, which get the twin. Adding an HTML view means rendering the corpus at build time and giving those routes something to fall through to; the negotiation layer already sends the right clients elsewhere and would not need to change.

About

Rainbow Index Website

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages