Scaffold and build
Turn an approved plan into a working site with scaffold-site, then run it, build it and read what npm run check tests. For developers.
On this page
scaffold-site creates the site from the plan. It copies the template, installs the kit, turns plan.md into configuration, locks the lake commit, writes the generated copy and applies the design. Then you run the site in your own terminal, build it and run the checks.
Before you start
- An approved
plan.mdand an approveddesign.md, both from Plan a site. The skill checks this first and refuses to start otherwise. - Node.js 22.23.4 or newer. The template’s
package.jsonsets that asengines.node. giton the path, and Git sign-in for the lake’s mirror.- A browser for the accessibility check. Install it once with
npx playwright install chromiumin the site, afternpm install.
Step 1: run the skill
In Claude Code, in the site folder:
/mdlake-builder:scaffold-siteThe skill works through these steps. You are asked to decide at two of them, noted below.
- Gate. It runs
check-design.mjs <site> --gate.design.mdmust exist withstatus: approved, or have nostatusfield at all (a file you supplied yourself). If not, the skill stops and sends you back toplan-site. It reads the mode from the plan’s brief and does not ask again. - Copy the template. It copies the plugin’s
template/into the site and leaves outexample-lake/,node_modules/,dist/,.astro/,public/example-assets/, the examplesrc/content/generated/and the template’s exampledesign.md. - Install the kit.
vendor-packages.mjsputs the packed@mdlake/lake-loaderand@mdlake/site-kittarballs invendor/, pointspackage.jsonat them, and then it runsnpm install. It reports the vendored version. Commitvendor/. - Add the ruleset.
scaffold-ruleset.mjswrites the site’sCLAUDE.md(the rules Claude Code follows in this site),.mdlake/ruleset.jsonwith the versions and the mode, and the ruleset version inbuilder.json. ACLAUDE.mdthat already exists stays first and the ruleset is appended under its own heading. - Write
.env.example. It holdsSITE_URLand a commentedLAKE_PATH..envitself is ignored by Git. - Put the lake at the plan’s commit.
lake-status.mjs --ensure <site> --remote <remote> --commit <planCommit>makes.mdlake/lakea clone at exactly the commit the plan was made from. - Turn the plan into code.
plan-to-config.mjs <site> --require-plan-commitvalidatesplan.mdand writeslake.config.ts,src/pages.config.tsand.mdlake/generate-jobs.json. If it reports problems, fixplan.mdwith the skill before going on. - Lock the lake.
write-lake-lock.mjswriteslake.lockfrom the plan’s commit, with the mirror asremote. - Generate copy. It runs each job of
.mdlake/generate-jobs.jsonthroughgenerate-copy. You decide: review each job innpm run dev, or answer “approve all” for the first scaffold and look at every written copy and its sources in one list at the end. Jobs markedpromoteonly write a draft for editors. A live site has no jobs. - Apply the design.
apply-design.mjswrites the colour, font, radius and container tokens ofdesign.mdinto the@themeblock ofsrc/styles/global.cssand checks contrast. It must pass. The skill then applies the rest by hand fromdesign.md: type scale, spacing and component choices, with tokens and Tailwind classes and no hard-coded colours. - Record ownership. The scripts have already recorded every kit and generated file in
provenance.json. See Ownership zones. - Run the checks.
npm run check, and in a snapshot site thennpx mdlake snapshotto writesite-snapshot.json. - Commit. You decide: “Initialise a git repository and make the first commit?”
commit-site.mjs <site> --initcreates the repository when there is none, makes sure.gitignorelistsnode_modules,dist,.astro,.env,.mdlake/lake-diff.json,.mdlake/usage-*and.mdlake/types, and commits with a message that names the plan and the lake commit. It asks for a Git identity when none is set. - Summary. It lists what was generated, the content gaps and open questions that still need an editor or you, and anything from the plan it could not do.
Important
Never run scaffold-site again to change the design. Edit design.md and use the regenerate skill. See Upgrade and migrate for version changes and Working with AI for the other skills.
Step 2: run the site
Use your own terminal for the dev server, not the one inside Claude Code. Astro 7 detects AI agents and then prints JSON log lines instead of readable output.
Create your .env from the example:
Copy-Item .env.example .envA site whose lake.lock has a remote needs no LAKE_PATH. The .env of a site that also uses the MCP bridge or the draft preview holds MDLAKE_API_URL and MDLAKE_TOKEN. See Preview drafts.
Start the dev server:
npm run devThe site is at http://localhost:4321. Before Astro starts, predev runs mdlake prepare --dev. It fetches the locked lake commit if it is not there, writes the key types for your code and records the hosting tier. --dev keeps a candidate commit that Refresh content left checked out, so you can look at newer content.
| Command | What it does |
|---|---|
npm run dev | The dev server on port 4321. |
npm run build | prebuild runs mdlake prepare, then astro build writes dist/. A build also copies the assets that pages link to into dist/files. A docs site gets a Pagefind search index in dist/pagefind. |
npm run preview | Serves dist/ after a build (astro preview). Docs search works here, not in astro dev. |
npm run check | All checks. See below. |
npm run preview:cms | Shows the CMS drafts. See Preview drafts. |
Note
In a snapshot site npm run build and the checks always use the locked lake commit. A managed clone is put back at the lock by mdlake prepare. If you keep your own clone with LAKE_PATH and it has moved, the build stops with the message described in How sites work.
Step 3: run the checks
npm run checknpm run check stops at the first failing step. It runs these steps in order. The first one is mdlake prepare, so a fresh checkout and a CI run have the locked lake and the typed lake keys before the other steps look for them.
| Step | Script | What it tests |
|---|---|---|
| Prepare | mdlake prepare | Fetches the locked lake commit into .mdlake/lake when needed, and writes .mdlake/types/lake.d.ts and .mdlake/hosting.json. |
| Ruleset | check:ruleset | Warns, but does not fail, when the site’s ruleset is behind the installed plugin. |
| Ownership | check:ownership | A hand-edited kit file is an error. A hand-edited generated file is a warning. It also warns when site-snapshot.json is missing or older than lake.lock, and when .mdlake/types/lake.d.ts is missing (a standalone check-ownership still warns about that; inside npm run check the file exists because mdlake prepare ran first). |
| Hosting | check:hosting | The hosting tier, the adapter, the pipelines and the Bicep agree. A static site without pipelines passes. |
| Types | check:types | mdlake prepare, then astro check: TypeScript and Astro diagnostics. |
| Build | build | astro build. It fails when the lake has invalid files or a binding is broken. |
| Bindings | check:bindings | mdlake prepare, then mdlake check-bindings. See below. |
| Accessibility | check:a11y | mdlake check-a11y. See below. |
What check-bindings tests
check-bindings loads the lake at the locked commit and compares it with lake.config.ts and src/pages.config.ts. It prints errors and warnings and ends with a summary line such as Bindings: 4 documents, 2 collections, 0 errors, 3 warnings (2 content fit, 1 generated).
Errors fail the check:
- An invalid lake file, reported as
path: message. - A bound document or collection that is missing at the locked commit, or a required slot with no published content. The message names the key, and for a key that your own code reads it names the files that read it.
- A code call such as
getLakeDocument('x')with a key thatlake.config.tsdoes not have. - An
imagefield that points to something that is not an image asset. - In a documentation site, two documents at one address.
- Generated copy that is invalid, or bound by a live site.
Warnings do not fail:
| Heading | What it means |
|---|---|
| Content fit | Reading rules found nothing, or a document is thin. For example “wanted attributedQuote, fell back to: summary”, a document with almost no body text (under 40 words), or alt text that repeats that it is an image. Fix these in the lake or in the composition. Never hard-code the missing text. |
| Links | A document with a page links to a document that has no page. The link is shown as text. |
| Assets | An asset:// ID has no sidecar. The image is shown without dimensions. |
| Generated content | A generated file is stale (a source changed or was unpublished) or hand-edited. See Generated copy. |
| Asset override | PUBLIC_ASSET_BASE_URL differs from the addresses in the sidecars. |
| Lake origin | LAKE_PATH is set and its origin is not the lock’s remote. |
What check-a11y tests
check-a11y serves dist/ locally and runs axe-core (WCAG 2.x A and AA) with Playwright against every page, at 1280 px and 390 px width. It also audits the open mobile menu and tests the skip link on the home page. Serious and critical violations fail the check. Smaller ones are printed as warn. Alt text that repeats that it is an image is listed at the end as a warning.
It needs the browser from the prerequisites. Without it, the step fails to start.
What can go wrong
| Symptom | Cause and fix |
|---|---|
| The skill stops at the gate | design.md is a draft or missing. Approve it in plan-site. |
mdlake prepare cannot fetch the lake | git is missing, your sign-in is refused, or the commit is not on the remote. The message says which. Test with git ls-remote <remote>. In CI, set MDLAKE_LAKE_TOKEN. |
plan-to-config refuses and exits 1 | The plan has problems. It lists them. Fix plan.md. |
plan-to-config prints “NOT WRITTEN” and exits 3 | A target file was adopted or edited by hand. Keep it, or run again with --overwrite for an edited (not adopted) file. See Ownership zones. |
| The build fails naming an asset ID | The asset has no sidecar, or its url cannot be reached from this machine. |
| Colours fail contrast | apply-design.mjs exits 1. Darken the accent or lighten the canvas in design.md. |
check:a11y cannot launch a browser | Run npx playwright install chromium. |
What’s next
- Site types to see which modules you can add or change.
- Refresh content when editors publish new content.
- Custom pages to add pages of your own.
- Choose a hosting model and Deploy overview when the site is ready to go live.
