Skip to content
mdlake

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.md and an approved design.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.json sets that as engines.node.
  • git on the path, and Git sign-in for the lake’s mirror.
  • A browser for the accessibility check. Install it once with npx playwright install chromium in the site, after npm install.

Step 1: run the skill

In Claude Code, in the site folder:

/mdlake-builder:scaffold-site

The skill works through these steps. You are asked to decide at two of them, noted below.

  1. Gate. It runs check-design.mjs <site> --gate. design.md must exist with status: approved, or have no status field at all (a file you supplied yourself). If not, the skill stops and sends you back to plan-site. It reads the mode from the plan’s brief and does not ask again.
  2. Copy the template. It copies the plugin’s template/ into the site and leaves out example-lake/, node_modules/, dist/, .astro/, public/example-assets/, the example src/content/generated/ and the template’s example design.md.
  3. Install the kit. vendor-packages.mjs puts the packed @mdlake/lake-loader and @mdlake/site-kit tarballs in vendor/, points package.json at them, and then it runs npm install. It reports the vendored version. Commit vendor/.
  4. Add the ruleset. scaffold-ruleset.mjs writes the site’s CLAUDE.md (the rules Claude Code follows in this site), .mdlake/ruleset.json with the versions and the mode, and the ruleset version in builder.json. A CLAUDE.md that already exists stays first and the ruleset is appended under its own heading.
  5. Write .env.example. It holds SITE_URL and a commented LAKE_PATH. .env itself is ignored by Git.
  6. Put the lake at the plan’s commit. lake-status.mjs --ensure <site> --remote <remote> --commit <planCommit> makes .mdlake/lake a clone at exactly the commit the plan was made from.
  7. Turn the plan into code. plan-to-config.mjs <site> --require-plan-commit validates plan.md and writes lake.config.ts, src/pages.config.ts and .mdlake/generate-jobs.json. If it reports problems, fix plan.md with the skill before going on.
  8. Lock the lake. write-lake-lock.mjs writes lake.lock from the plan’s commit, with the mirror as remote.
  9. Generate copy. It runs each job of .mdlake/generate-jobs.json through generate-copy. You decide: review each job in npm 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 marked promote only write a draft for editors. A live site has no jobs.
  10. Apply the design. apply-design.mjs writes the colour, font, radius and container tokens of design.md into the @theme block of src/styles/global.css and checks contrast. It must pass. The skill then applies the rest by hand from design.md: type scale, spacing and component choices, with tokens and Tailwind classes and no hard-coded colours.
  11. Record ownership. The scripts have already recorded every kit and generated file in provenance.json. See Ownership zones.
  12. Run the checks. npm run check, and in a snapshot site then npx mdlake snapshot to write site-snapshot.json.
  13. Commit. You decide: “Initialise a git repository and make the first commit?” commit-site.mjs <site> --init creates the repository when there is none, makes sure .gitignore lists node_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.
  14. 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:

PowerShell
Copy-Item .env.example .env

A 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:

PowerShell
npm run dev

The 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.

CommandWhat it does
npm run devThe dev server on port 4321.
npm run buildprebuild 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 previewServes dist/ after a build (astro preview). Docs search works here, not in astro dev.
npm run checkAll checks. See below.
npm run preview:cmsShows 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

PowerShell
npm run check

npm 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.

StepScriptWhat it tests
Preparemdlake prepareFetches the locked lake commit into .mdlake/lake when needed, and writes .mdlake/types/lake.d.ts and .mdlake/hosting.json.
Rulesetcheck:rulesetWarns, but does not fail, when the site’s ruleset is behind the installed plugin.
Ownershipcheck:ownershipA 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).
Hostingcheck:hostingThe hosting tier, the adapter, the pipelines and the Bicep agree. A static site without pipelines passes.
Typescheck:typesmdlake prepare, then astro check: TypeScript and Astro diagnostics.
Buildbuildastro build. It fails when the lake has invalid files or a binding is broken.
Bindingscheck:bindingsmdlake prepare, then mdlake check-bindings. See below.
Accessibilitycheck:a11ymdlake 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 that lake.config.ts does not have.
  • An image field 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:

HeadingWhat it means
Content fitReading 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.
LinksA document with a page links to a document that has no page. The link is shown as text.
AssetsAn asset:// ID has no sidecar. The image is shown without dimensions.
Generated contentA generated file is stale (a source changed or was unpublished) or hand-edited. See Generated copy.
Asset overridePUBLIC_ASSET_BASE_URL differs from the addresses in the sidecars.
Lake originLAKE_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

SymptomCause and fix
The skill stops at the gatedesign.md is a draft or missing. Approve it in plan-site.
mdlake prepare cannot fetch the lakegit 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 1The plan has problems. It lists them. Fix plan.md.
plan-to-config prints “NOT WRITTEN” and exits 3A 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 IDThe asset has no sidecar, or its url cannot be reached from this machine.
Colours fail contrastapply-design.mjs exits 1. Darken the accent or lighten the canvas in design.md.
check:a11y cannot launch a browserRun npx playwright install chromium.

What’s next