Every template that npm create @valbuild can make, in one repository, so they
can share code.
tanstack/full/ Full on TanStack Start: a themeable design system + sections
tanstack/minimal/ Minimal on TanStack Start: Val wired in, one black-and-white page
tanstack/blog/ Blog on TanStack Start: Full's theme and sections, a blog, authors, RSS
tanstack/docs/ Docs on TanStack Start: nested pages, a sidebar worked out from the pages
nextjs/full/ Full on Next.js (App Router), with the same components
nextjs/minimal/ Minimal on Next.js
shared/ code that more than one template is made of
templates.json which parts of shared/ each template uses
scripts/sync.mjs copies shared/ into the templates
catalog.json what `npm create @valbuild` and admin.val.build/new offer
mirrors.json the template repositories /new seeds from and copies, see below
scripts/mirror.sh publishes them, then has valbuild/home reseed
scripts/app-config.mjs turns on remote media in each mirror's val.config.ts
catalog/ the icons and screenshots the catalog points at
scripts/catalog.mjs checks catalog.json against the templates
Planned: blog and docs on Next.js.
TanStack Start is the primary one: a feature lands there first.
The Full, Blog and Docs templates are the same components, theme, sections and stories —
copied from shared/ — on different frameworks; what differs is routing,
layouts and src/framework.tsx.
The Minimal templates share nothing from shared/. Each has Val fully set up
on its framework — the Studio, the API and MCP — and one example page that its
README explains how to delete, and nothing a developer would have to undo: no
CSS framework, no components, no theme.
Every template serves Val's content tools over MCP at /api/mcp, which
npm create @valbuild can leave out. Which files, dependencies and doc sections
that removes is listed per template in catalog.json (below).
The old starters (valbuild/template-tanstack-starter and
valbuild/template-nextjs-starter) were brought in with their history and then
replaced, so git log still has them. The Next.js templates were built from
the old Next.js starter's wiring.
A template is downloaded with degit,
which copies one folder as it is (degit valbuild/templates/tanstack/full).
So a template cannot import from ../shared — whatever it uses from shared/
is copied into it and committed, and the template folder is an ordinary,
runnable project: cd tanstack/full && pnpm install && pnpm dev.
shared/ is the source of truth for those copies. CI fails if a template's
copy differs from it.
Either edit shared/ and copy it out:
node scripts/sync.mjs # shared/ -> every template that uses itor work inside a template, where you can run it, and pull the result back:
cd tanstack/full && pnpm dev # edit src/components/..., see it live
cd ../.. && node scripts/sync.mjs --pull tanstack/full && node scripts/sync.mjsnode scripts/sync.mjs --check is what CI runs.
CI is not switched on yet. The workflow is in
ci/check.ymlbecause the session that created this repository could not push to.github/workflows/. Move it there (git mv ci/check.yml .github/workflows/check.yml) to turn it on.
Shared code is the same file in every template. Everything a component needs
from the framework comes through one per-template file, src/framework.tsx:
RouterLink, ValImage, ValRichText, ValVideo and Val's types. A shared
component imports those from there and never from @tanstack/*, next/*,
@valbuild/tanstack or @valbuild/next directly. That is what lets the Full
templates on two frameworks be the same files.
Shared components are Server Components unless they need state: a shared
component with useState or an event handler starts with "use client",
which TanStack Start ignores and Next.js needs. That file, the routes, the
layout, the header and footer, the config files and package.json are the
template's own.
So are the media libraries in src/media/ (images.val.ts, videos.val.ts,
files.val.ts, …). Shared components import them by path — every media field
is s.image(imagesVal) and the like — but their entries are the template's
content, so each template keeps its own and the sync leaves them alone. A
template that uses the shared components must have all five.
catalog.json is the list of templates that can be created, read at run time
by npm create @valbuild and by admin.val.build/new. This repository decides
what is offered, in what order, with what name, icon and screenshots — a
template is added, renamed or retired here, without a release of the CLI.
Each entry also says what its optional features are MADE of, because only the
template knows: features.mcp lists the files and directories, the
dependencies and the docs (README.md, AGENTS.md, each with one
<!-- val:mcp:start --> … <!-- val:mcp:end --> region) that go when MCP is
declined, and features.imageUploads the file that is replaced and the
dependency that goes when image uploads are. regenerate names a script that
brings generated files up to date afterwards — TanStack's routeTree.gen.ts
imports every route, including the MCP ones.
node scripts/catalog.mjs --check checks every one of those claims, and CI runs
it: each path exists and stays inside its template, each dependency is
declared, each doc has its markers, and nothing left after a feature is removed
still imports a removed file or package.
The CLI reads the catalog from the same ref it downloads the template from. To
try a branch through npm create itself before it lands:
VAL_TEMPLATES_REF=my-branch npm create @valbuild@latestScreenshots are 1440×900, of the home page with nothing edited, in light and dark, from a production build.
admin.val.build/new makes two things from a template: a site, copied from a SEED the platform builds, and, when someone asks for it, their own GitHub repository with the code. GitHub can only make a repository from a WHOLE template repository, never from one folder of this one, so each template /new offers is also published as its own repository, a mirror:
| Folder | Mirror |
|---|---|
tanstack/full |
valbuild/template-tanstack-full |
tanstack/blog |
valbuild/template-tanstack-blog |
tanstack/docs |
valbuild/template-tanstack-docs |
tanstack/minimal |
valbuild/template-tanstack-minimal |
mirrors.json is that table. On every push to main that touches tanstack/,
.github/workflows/mirror.yml runs scripts/mirror.sh, which:
- Makes each mirror's
mainthe folder exactly as committed here, in one commit that names the commit it came from. Files gone from the folder go from the mirror. With one change:val.config.tsgetsfiles: { remote: true }(see below). - Only once every mirror has been pushed, starts
reseed-templates.ymlin valbuild/home, which rebuilds the seeds from the mirrors. A mirror that failed to push fails the run and nothing is reseeded. So the site a new project starts with and the code it is given on GitHub are always the same commit.
Never change a mirror directly. The next push here overwrites it.
A project made on /new runs in the Val app, which keeps every image, file and
video on Val's remote content host and refuses to start without
files: { remote: true } in initVal — there is no repository there for an
upload to go into. A project made with npm create @valbuild keeps its media
in its own repository, and with that setting every image in the template would
fail validation until uploaded, which needs a Val project it may not have yet.
So the templates here leave it out, and scripts/app-config.mjs adds it to
each mirror's val.config.ts as it is published. It edits text, so it refuses
a config it cannot read plainly — more than one initVal({, or a files key
already there — and mirror.sh runs node scripts/app-config.mjs --check
before pushing anything, so a template that stops matching fails the run rather
than leaving the mirrors at different commits. Run the same check after
changing a mirrored template's val.config.ts.
The template's own images are local paths in a mirror. The Studio does not
block on that (image:upload-remote is repaired, not shown), and
npx val validate --fix uploads them.
The workflow needs two repository secrets. Without either one it fails loudly, because a seed that quietly stops following this repository is the bug it exists to catch:
VAL_TEMPLATE_MIRROR_TOKEN: a fine-grained token for the mirrors only, with Contents and Workflows read and write. Workflows is needed because each mirror carries the template's.github/workflows.VAL_HOME_DISPATCH_TOKEN: a fine-grained token forvalbuild/homeonly, with Actions read and write.
To publish the mirrors again by hand, use Actions → Mirror templates and
reseed → Run workflow. It always publishes main.
- Make the folder a complete project (copying the closest template is
fastest), with its own
src/framework.tsx. - List the shared paths it uses in
templates.jsonand runnode scripts/sync.mjs. - Add a job for it to the CI workflow (
ci/check.ymluntil it is moved to.github/workflows/). - Add it to
catalog.json, with an icon and screenshots incatalog/, and runnode scripts/catalog.mjs --check. That is allnpm create @valbuildneeds to offer it. - For admin.val.build/new to offer it too: create an empty mirror repository,
tick Settings → General → Template repository on it, give
VAL_TEMPLATE_MIRROR_TOKENaccess to it, add it tomirrors.json, and add it toplatform/templates.jsonin valbuild/home.