Skip to main content

Nubbin

A page builder that lives inside your codebase. You decide what can go on a page by writing blocks; someone else arranges them without touching the code. What they arrange is data, what you wrote is the contract, and publishing turns one into an immutable artifact the site serves.

A terminal publishes a route while the browser beside it changes: two sections swap places,
revert, and a card is rewritten — all without a rebuild

Install

npm install @nubbin/core @nubbin/react @nubbin/next @nubbin/store-fs
npm install -D @nubbin/cli

@nubbin/core is the contract and depends on nothing. The other packages supply replaceable adapters and complete editor surfaces. Your host injects every external effect through the repository's public boundaries: @nubbin/react, @nubbin/next, @nubbin/store-fs, and @nubbin/cli for the terminal.

More about the project at nubbin.io.

Define a block

A block is a schema beside a component. Props are inferred from the schema rather than declared again next to it, so the two cannot disagree.

// Hero.schema.ts — any Standard Schema; zod here
export const heroSchema = z.object({
headline: z.string(),
tone: z.enum(["light", "dark"]),
});

// Hero.block.ts
export const heroBlock = defineBlock({
name: "Hero",
description: "The opening statement of a page.",
icon: "🖼",
category: "Heroes & Banners",
docs: { figma: "https://example.com/figma/hero" },
schema: heroSchema,
component: Hero,
version: 1,
slots: {},
});

Register it, twice

Publishing and rendering need different things, so there are two registries and neither substitutes for the other.

// compile side — schemas and versions, what a document is validated against
export const registry = createRegistry([heroBlock]);

// render side — a loader per block, which the bundler splits into its own chunk
export const blockRegistry = defineRegistry({
Hero: () => import("./blocks/Hero").then((module) => module.Hero),
});

Publish a page

nubbin.config.ts tells the command line where everything lives. document is a function rather than a table, because where documents live is yours.

export default defineConfig({
catalog,
registry,
store: createFsArtifactStore("./.nubbin"),
document: (route) => documents[route] ?? null,
});
npx nubbin publish /pricing

Compiling validates the document against every block's schema and serialises it. Nothing is built and nothing is deployed — the artifact is written, then the route is pointed at it.

Render it

One catch-all route resolves the artifact for a path and hands it to the renderer.

const artifact = await resolveArtifact(store, routeFromSlug(slug));
if (artifact === null) notFound();

return <Renderer artifact={artifact} registry={blockRegistry} />;

Let someone else edit it

Everything above is yours. The point of the blocks you registered is that the next change to the page does not come back to you.

pnpm --filter studio dev

The studio reads your catalog and builds a palette from it. A block is dragged in, its props edit against the schema you wrote, and Publish runs the same compile the command line does — so a page nobody could publish from a terminal is a page nobody can publish from the editor either. Running the studio covers what it saves and when.

Where to go next

If you areRead
Trying to understand the modelHow it works
Writing blocksBlocks and the catalog
Publishing from CI or a terminalCompiling, artifacts, the command line
Rendering in an appThe renderer and the Next.js binding
Letting someone else edit pagesRunning the studio
Looking for a signatureThe API reference, written from the packages' own sources on every docs build

Every document

ReadForStatus
How it works
concepts/architecture.mdHow the contract/content/output split and the compile-at-publish pipeline fit together. Start here.stable
concepts/domain-model.mdEvery entity, what owns it, and where it lives across the three layersdraft
concepts/authoring-flows.mdWhat an author does step by step, and the failure modes each flow carriesdraft
concepts/studio.mdHow the self-hosted canvas, cross-iframe drag, and preview are architectedstable
concepts/api.mdThe shape of defineBlock through compile and render, and where UI hints livedraft
concepts/architecture-plan-contract.mdThe twelve answers describing who runs what, the code that carries them, and everything derived from themdraft
Reference — authoring
reference/authoring/blocks.mddefineBlock and createRegistry as shipped — what registration rejects, and what an artifact records about a blockreference
reference/authoring/catalog.mddefineCatalog as shipped — entries, field hints, defaults, and schema introspectionreference
Reference — publishing
reference/publishing/compile.mdcompile as shipped — the document shape, the two validation passes, and every issue codereference
reference/publishing/artifacts.mdThe Artifact and ArtifactStore contracts as shipped, with the compatibility and rollback checksreference
reference/publishing/cli.md@nubbin/cli as shipped — the config file it resolves, the commands, and what each exit code meansreference
Reference — rendering
reference/rendering/renderer.md@nubbin/react as shipped — the Renderer server component, the registry types, and the hole resolverreference
reference/rendering/next.md@nubbin/next as shipped — route resolution, static params, hole fetch options, and the two publish callsreference
Reference — editing
reference/editing/studio.mdThe editor as shipped — what it reads from a catalog, how a draft is saved, and what a consumer replacesreference
Contributing
contributing/documents.mdWhat belongs in a document rather than an issue, and the gates that hold this corpus to itstable
contributing/gates.mdEvery gate, what it enforces, where its verdict comes from, and the three that stay localstable
contributing/releasing.mdHow a version reaches npm, what decides it, and the two behaviours that surprise peoplestable
contributing/environment.mdThe plugins, skills and toolchain this repository is worked on with, and how to reproduce themstable
contributing/public-repository.mdWhat may not appear here, and how to publish a finding without its provenancestable
Decisions
decisions/Settled choices and the reasoning behind them, one file per decisionstable

The generated API reference carries no row: docs/reference/generated/ is written by the docs build from the packages' own sources, so listing it here would be a hand-maintained copy of a directory listing.

draft means the shape is expected to move. stable means changing it is a design change, not an edit. reference means the page describes the shipped surface — it changes when the code does.