sitekit
A website as data — a closed set of section shapes, validated, with per-field provenance.
Why it exists
A generated site that cannot say where each sentence came from will eventually say something nobody wrote. The second generator was about to ship that.
How it works
Sections are a closed union, checked before render. One set of React renderers draws them. Every field can carry where it came from, so a page can show that a line was supplied rather than invented.
Where it fits
A generated site is data, and sitekit is that data: Camille, Substrata and Skif render through it. design-tokens is what it looks like. bip-kit is the long writing inside it.
API and examples
Documentation from source commit af403fd752ac · 2026-11-07. Source documentation can include work beyond the registry release.
- Why a closed union
- Install
- Use
- Application shell
- Provenance — the check behind "fabricate no facts"
- The styling contract
- Not in this package
- Verify
- Mobile tab bar
Read the package documentation
A website as data.
A site is pages; a page is sections; sections come from a closed union of shapes validated by Zod at runtime. One set of React renderers draws every site. A provenance sidecar records where every generated field came from — so "we fabricated nothing" is an assertion, not a promise.
Extracted from substrata's hosted-site model (2026-08). Decided in orangecat's ADR-0003 (the Site Factory).
Why a closed union
A generator emitting data against this schema is a task with a machine-checkable result; a generator emitting components is not. Per-site quality becomes a property of the renderers — fixing the cards renderer improves every site ever generated. And the schema is where review happens: validateSite() returns path: message errors a model can act on directly.
The current kinds: hero (with optional CTAs), prose, stats, meter, cards, definitions, feature (a figure beside a story), contact (locations, address, hours — the fields provenance exists to protect), faq, index, table. Most kinds take an optional anchor, and the schema rejects any index entry or hero action that points at a fragment nothing carries. Extending the union is a deliberate act in THIS repo — a new kind lands with its schema, its renderer, and its tests in one commit, and every consumer gets it on the next version bump.
Install
pnpm add @bitbaum/sitekit
ESM-only. react >= 18 is a peer dependency of @bitbaum/sitekit/react; the root export is framework-free (a Node generator or CI check can use it without React).
Use
import { validateSite, siteNavItems, sitePageAt, pageRendersOwnHeader } from '@bitbaum/sitekit';
const result = validateSite(generatorOutput);
if (!result.success) throw new Error(result.errors.join('\n'));
const site = result.data; // fully typed SiteSpec from here on
import Link from 'next/link';
import { SiteMasthead, SiteFooter, SiteSections } from '@bitbaum/sitekit/react';
<SiteMasthead chrome={site.chrome} navItems={siteNavItems(site.pages)} currentPath={path} Link={Link} />
<SiteSections sections={page.sections} />
<SiteFooter chrome={site.chrome} />
Link is the one framework seam: it defaults to a plain <a>, which is correct everywhere; a Next.js app passes next/link to get client-side navigation back.
Application shell
Product apps use the same masthead, navigation and footer with the application appearance. Import @bitbaum/sitekit/styles.css once. No Tailwind setup is required for this appearance; --sk-* variables map it to the product's shared design tokens. Existing document sites keep their utility-class appearance.
<SiteMasthead appearance="application" chrome={chrome} navItems={nav}
currentPath="analyze" mainId="main-content" Link={Link}
brand={<Brand />} actions={<AccountControl />} />
<SiteFooter appearance="application" chrome={chrome} Link={Link}
links={[{ href: '/roadmap', label: 'Roadmap' }]} extra="Beta" />
For a grouped application menu, pass navGroups alongside navItems. Each group has a label and items containing path, label and an optional description. Import SiteNavGroup from @bitbaum/sitekit/react for typed configuration. Nonempty groups replace the flat application navigation with one native disclosure and a responsive panel; omitted or empty groups retain the flat API. Group labels, destinations and descriptions belong to the host.
navGroups={[{ label: 'Create', items: [
{ path: 'write', label: 'Write', description: 'Start an article.' }
] }]}
Run pnpm exec playwright install --with-deps chromium before pnpm run verify. An existing Chromium can be selected with PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. The browser regression uses the public renderer and stylesheet at four widths.
The package owns the responsive menu, its native no-JavaScript fallback, Escape/outside/route dismissal, skip link, active states, touch targets and footer layout. Products supply labels, navigation data, identity and account slots. Fix shared chrome here, then release and update consumers through their normal dependency checks and deployment pipeline.
Provenance — the check behind "fabricate no facts"
A closed schema invites completion: a model handed a stats section wants numbers in it. So absence is first-class and typed. The generator emits a sidecar keyed by JSON path:
import { assertDeliverable, notFoundPaths } from '@bitbaum/sitekit';
const provenance = {
'chrome.name': { kind: 'scraped', url: 'https://cafe-beispiel.ch/' },
'pages.0.sections.1.paragraphs.0':{ kind: 'operator' }, // a human typed it
'pages.0.sections.4.cards.0.meta':{ kind: 'inferred' }, // a model invented it
'pages.0.sections.7.rows.0.1': { kind: 'not-found' }, // we looked; it isn't public
};
assertDeliverable(provenance); // throws: names the inferred path
notFoundPaths(provenance); // the honest gaps — in a pitch, these ARE the pitch
not-found is the difference between unknown and empty: it records that the looking happened, which is what makes "here is what we could not find out about you" an honest sentence instead of an excuse.
The styling contract
The renderers emit semantic utility classes and nothing else — no colours, faces, or radii ship in this package. Each consumer defines the tokens, which is how fifty sites share one renderer without looking like one company. (Uniform everywhere except aesthetics. Do NOT point consumers at @fleet/design-tokens values — those make things look like the fleet; inherit the shape, choose your own values.)
Classes the renderers use, to be defined by the consumer's Tailwind theme:
| Group | Classes |
|---|---|
| Text | `text-fg-primary` `text-fg-secondary` `text-fg-tertiary` `text-fg-muted` |
| Surfaces | `bg-surface-page` `bg-surface-raised` |
| Borders | `border-subtle` `border-strong` `divide-subtle` |
| Accent & status | `bg-accent` `decoration-accent` `bg-status-positive` `bg-status-warning` `bg-status-negative` |
| Type | `font-heading` `font-mono` `tracking-caps` `tracking-display` |
| Layout | `max-w-shell` `max-w-prose` `scrollbar-hide` |
With Tailwind 4, define them as @theme variables (--color-fg-primary, --font-heading, …) and add the package to content scanning:
@import 'tailwindcss';
@source '../node_modules/@bitbaum/sitekit/dist';
Substrata's app/globals.css is the reference implementation of the contract.
Not in this package
- No design tokens. See above — that is the point.
- No scraper.
siteFromUrl()(extraction from a live site, with
provenance) is the planned next module; the schema is its target.
- No HTTP, no framework. Same rule as every package in this fleet.
Verify
pnpm run verify # lint + typecheck + build + test — same command CI runs
Tests import the package by name (@bitbaum/sitekit, @bitbaum/sitekit/react), so a broken exports map fails in CI, not at the first consumer.
Part of bitbaum — AI-native products on open infrastructure, built in Zürich. Every package here lists the apps that use it: which apps use sitekit.
Application footers can pass groups: [{ label, links: [{ href, label }] }] for semantic navigation groups. Existing flat links remain supported. Consumers can configure --sk-header-height, --sk-mobile-header-height, --sk-display-weight, --sk-display-leading, --sk-display-tracking and --sk-footer-gap through their theme adapter, without replacing shared markup.
Mobile tab bar
SiteTabBar renders a fixed bottom navigation for small screens (hidden from 48rem up, where the masthead nav takes over). Pass 3–5 { path, label, icon } items and the current path; it renders its own spacer so content is not hidden.
Use SiteIcon for the icons — { icon: <SiteIcon name="write" /> } — not a unicode glyph: the set is drawn on currentColor, so icon and label take one colour and the current tab is one rule. Names: SITE_ICON_NAMES. Pass tabBar to SiteMasthead on the same page so the masthead drops its menu button on phones instead of repeating the tab bar's destinations:
<SiteMasthead appearance="application" tabBar navItems={nav} … />
<SiteTabBar items={nav.map((n) => ({ ...n, icon: <SiteIcon name={n.icon} /> }))} … />Changelog and releases
Read the repository changelog
This retrospective record was added on 2026-10-27 from the immutable sources linked below. Git commit dates are source dates, not inferred npm publication dates. A GitHub release does not itself establish npm publication. Application adoption and deployment evidence are tracked separately.
Unreleased
SiteIconandSITE_ICON_NAMES: one stroke icon set oncurrentColor(home, write, search, compass, star, user, chat, bell, settings, list, globe, book, heart, plus, menu, calendar, wallet, chart) for tab bars and account actions, so products stop passing unicode glyphs that render differently on every phone.SiteTabBarlook: equal cells in a grid, a 24px icon box (SVG or glyph), 11px/600 labels, the current tab marked by a short rule above its icon and full text colour instead of a fill, 56px cells with the safe-area inset outside them.SiteMastheadtabBarprop: when the page also renders aSiteTabBar, the masthead drops its menu button below 48rem — the tab bar is the navigation there — and keeps the account actions in the row.SiteTabBar: a mobile bottom tab bar (3–5 items, icon + label, nested-path current state, safe-area aware), hidden from 48rem up. First consumer: BiasLens.
0.4.2
- Tagged source
v0.4.2 - Source commit, dated 2026-10-24.
No authored release notes were found for this tag when this record was made. The links identify the verified source; missing feature history is not invented.
Release notes
What's Changed
* feat(shell): SiteIcon set, a tab bar that reads as one control, masthead tabBar prop by @catomean in https://github.com/bitbaum/sitekit/pull/45 * Keep grouped navigation within its shell and narrow account slots by @catomean in https://github.com/bitbaum/sitekit/pull/47 * Preserve host widget layout inside grouped actions by @catomean in https://github.com/bitbaum/sitekit/pull/48
Full Changelog: https://github.com/bitbaum/sitekit/compare/v0.6.1...v0.7.0
Release notes
What's Changed
* Prepare browser before npm publication verification by @catomean in https://github.com/bitbaum/sitekit/pull/46
Full Changelog: https://github.com/bitbaum/sitekit/compare/v0.6.0...v0.6.1
Release notes
What's Changed
* Add grouped descriptive application navigation by @catomean in https://github.com/bitbaum/sitekit/pull/43 * Use runner Chrome for required browser verification by @catomean in https://github.com/bitbaum/sitekit/pull/44
Full Changelog: https://github.com/bitbaum/sitekit/compare/v0.5.1...v0.6.0
Release notes
What's Changed
* fix(masthead): on the narrowest phones the account action moves into the menu by @catomean in https://github.com/bitbaum/sitekit/pull/42
Full Changelog: https://github.com/bitbaum/sitekit/compare/v0.5.0...v0.5.1
Release notes
What's Changed
* docs: record verified package release history by @catomean in https://github.com/bitbaum/sitekit/pull/34 * chore: add the fleet package template files this repo is missing (git-release.yml, release-notify.yml, release.yml, AGENTS.md, CLAUDE.md) by @catomean in https://github.com/bitbaum/sitekit/pull/33 * docs(agents): the org rules block, generated from bitbaum/fleet by @catomean in https://github.com/bitbaum/sitekit/pull/35 * chore(deps): dependabot leaves TypeScript 7 alone until typescript-eslint supports it by @catomean in https://github.com/bitbaum/sitekit/pull/36 * fix(release): respect the package build-output policy by @catomean in https://github.com/bitbaum/sitekit/pull/37 * docs(agents): the org rules block, generated from bitbaum/fleet by @catomean in https://github.com/bitbaum/sitekit/pull/38 * chore(deps): clear dev-toolchain audit findings inside existing ranges by @catomean in https://github.com/bitbaum/sitekit/pull/39 * chore(deps-dev): bump typescript-eslint from 8.72.0 to 8.72.1 in the minor-and-patch group by @dependabot[bot] in https://github.com/bitbaum/sitekit/pull/40 * feat(react): SiteTabBar mobile bottom navigation by @catomean in https://github.com/bitbaum/sitekit/pull/41
Full Changelog: https://github.com/bitbaum/sitekit/compare/v0.4.2...v0.5.0
Recent source changes
Repository activity can include unreleased work; it is not a list of deployed changes.
- chore(deps-dev): bump the minor-and-patch group with 3 updates (#50)
- docs(agents): the org rules block, generated from bitbaum/fleet (#49)
- release v0.7.0
- Preserve host widget layout inside grouped actions (#48)
- Keep grouped navigation within its shell and narrow account slots (#47)
- feat(shell): SiteIcon set, a tab bar that reads as one control, masthead tabBar prop (#45)
- release v0.6.1
- Prepare browser before npm publication verification (#46)
Where it is declared
Linked products declare this package in their manifests. A declaration alone does not verify every feature, version or deployed integration.
Sources and verification
Install commands and declared consumers come from Fleet’s generated registry. Documentation and release notes come from the package repository. The catalogue does not certify production journey coverage.
Documentation from source commit af403fd752ac · 2026-11-07. Source documentation can include work beyond the registry release.


