@bigin-io/site-contract
v1.0.1
Published
Single source of truth for site.config, content schemas, and the design-token spec
Readme
@bigin-io/site-contract
Single source of truth for site.config, the content/frontmatter schemas, and
the design-token spec. Both renderers (ADR-1), the site Worker (ADR-5), and
every pipeline skill read their shapes from here and nowhere else.
Framework-agnostic by construction: no renderer import, no Worker import, no
host global. zod is the only runtime dependency.
Install
pnpm add @bigin-io/site-contractConsumption modes (ADR-8)
| Consumer | How it resolves |
| --- | --- |
| A package inside this monorepo | "@bigin-io/site-contract": "workspace:^" |
| A client site repo | a pinned semver range from npm (public since ADR-18; GitHub Packages before it) |
A site repo needs the scope mapped to the registry and a token:
# .npmrc
# Nothing. The @bigin-io scope publishes publicly to npm (ADR-18), so a
# site repo needs no registry mapping and no token at all. This block used
# to read:
#
# # Not needed: the scope is public on npm (ADR-18).
# //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Verified against a real install of 0.2.0 from the registry, from a repo outside
this workspace (story 6.5, T6). Those two lines are the whole of it — nothing
else is needed to resolve the package or any of its subpaths.
The token needs read:packages, and where it comes from differs.
| Where | The token |
| --- | --- |
| GitHub Actions, same org | the built-in GITHUB_TOKEN, with permissions: packages: read on the job |
| A developer's machine | a PAT with read:packages, exported as GITHUB_TOKEN |
There is no ambient GITHUB_TOKEN outside Actions, so a local npm install
fails without one. The failure is specific and worth recognising: a token
carrying repo but not read:packages returns
403 Forbidden - Permission permission_denied: The token provided does not match expected scopes.not a 404 or an auth prompt. gh auth refresh -h github.com -s read:packages
adds the scope to an existing gh login.
Your tsconfig needs a URL global. zod's declaration files reference
URL, so a consumer typechecking with skipLibCheck: false and a bare
"lib": ["ES2022"], "types": [] fails in node_modules/zod, not in its own
code. "types": ["node"] (or a DOM lib) resolves it. With that, the published
declarations typecheck clean under strict, noUncheckedIndexedAccess and
exactOptionalPropertyTypes.
Usage
import { defineSiteConfig, parseSiteConfig, SCHEMA_VERSION } from '@bigin-io/site-contract'
export default defineSiteConfig({
schemaVersion: SCHEMA_VERSION,
tier: 'content',
renderer: 'nuxt',
// ...
})import { createContentSchemas } from '@bigin-io/site-contract/content'
import { toCssVars } from '@bigin-io/site-contract/tokens'
import { contentSiteConfig } from '@bigin-io/site-contract/fixtures'
const { page, post, section, collections } = createContentSchemas(config)
const cssVars = toCssVars(config.design.tokens)Content schemas are a factory, not constants: tier and media zone decide what
content is legal (ADR-11, ADR-12), so a simple-tier site rejects a
media.<domain> image reference at parse time.
Two stages
| Schema | Gate | Allows |
| --- | --- | --- |
| siteConfigDraftSchema | G0 (sf-intake) | design.tokens, pages, collections, and cloudflare still absent |
| siteConfigSchema | G4 (build) | everything present |
Anything that parses as complete also parses as draft — the cross-field rules are presence-guarded and shared between the two.
Versioning policy
schemaVersion is the contract's major line; CONTRACT_VERSION is the package
semver, recorded in build manifests for NFR-6.
CONTRACT_VERSION always equals this package's package.json version.
scripts/dist/bin/sync-versions.js rewrites it during changeset version, so
the two move in one commit, and test/public-api.test.ts fails if they ever
part company. Do not edit the literal by hand.
| Change | Release |
| --- | --- |
| Additive optional field | minor, no schemaVersion bump |
| New required field | major + bump + migration |
| Removed field or changed semantics | major + bump + migration |
| A --sf-* CSS variable rename | major + bump + migration |
Migrations shipped
| To | Change | What a site repo does |
| --- | --- | --- |
| 2 | site.config.budgets removed (story 6.9) | Delete the budgets block and set schemaVersion: 2, or run migrateSiteConfig() |
schemaVersion: 2 is a breaking change from 0.4.0. A config that keeps
the block is refused by name, not quietly stripped — so an upgrade cannot
silently discard a budget a site meant to keep. NFR-1's budgets and the
public/ image warning are repository constants in @bigin-io/site-tools now,
and a site states neither. Note what that costs in both directions: a site
can no longer ask to be faster than the platform floor either. That is a
platform change, not a config field.
The --sf-* map and the leaf-key-name set are snapshot-locked in the test
suite. A snapshot diff means a contract change, so it needs a PR — not a
snapshot update.
After story 1.1 the contract is frozen: changes go through PR + review only,
and every lane rebases on the merged change before continuing
(CLAUDE.md § Workflow).
What this package does not do
It validates parsed objects. Loading site.config.ts (a TypeScript module)
and reading markdown off disk are the renderer's and pipeline's job — a
file-reading API here would be a contract-scope change.
