Skip to content
mdlake

Plan a site

Read the lake, answer the planning interview and approve plan.md and design.md before any code is written. For developers starting a new site with the builder plugin.

On this page

You plan a site with two skills. check-lake chooses the lake and says which content can fill the site type’s slots. plan-site interviews you and writes plan.md and design.md. Nothing is built yet. The plan is a document you read, change and approve first.

Planning first keeps the build predictable. The plan records what the site is for, which pages it has, which module shows which content, and which words must be written because the lake does not have them.

Before you start

  • The builder plugin is installed in Claude Code. See Install the builder.
  • git is installed and you can sign in to the lake’s GitHub mirror. git ls-remote <remote> should work from your terminal.
  • The lake has published content. A site only sees published documents. See Publishing for what editors do.
  • Optional but recommended: a workspace token in the site’s .env, so that the skills can ask the CMS where the lake is and search it. See Workspace tokens.

Make an empty folder for the site, put the two token lines in a .env in it, and start Claude Code there.

PowerShell
mkdir C:\Projects\acme-site
cd C:\Projects\acme-site

Create C:\Projects\acme-site\.env with these lines. The address is your workspace’s. The CMS shows both values in the workspace settings, under AI and API access.

MDLAKE_API_URL=https://cms.example.com/api/lake/acme
MDLAKE_TOKEN=mdl_...

Then start Claude Code in that folder.

PowerShell
claude

Warning

.env holds a secret. The template’s .gitignore already lists it. Never commit it.

Step 1: choose a site type

Tell Claude which kind of site you want. The plugin has four site types. Each one defines slots, the places a site fills with lake content, and a recommended set of pages.

Site typeFor
corporateA company site with about, services, cases, contact and privacy pages.
landing-pageOne page about one offer.
campaignA campaign message with supporting sections.
docsDocumentation or a handbook. Folders become sections.

Site types lists the slots and modules of each. If the site is documentation, say so: plan-site proposes the docs type whenever the purpose is to document something. The sidebar’s order comes from the lake: each page and each section (folder) has its own order, which editors set by dragging in the CMS (Site types).

Step 2: check the lake

Run the skill:

/mdlake-builder:check-lake

It does these things in order. You answer one question.

  1. Checks the ruleset. In an existing site, it tells you if the site is behind the plugin and offers upgrade-ruleset. See Upgrade and migrate.

  2. Chooses the lake. For a new site it asks the CMS for the lake’s Git address. It uses the MCP tool lake_info, or, without MCP, scripts/lake-info.mjs, which reads MDLAKE_API_URL and MDLAKE_TOKEN from your .env. You choose once:

    • use the CMS’s mirror (recommended),
    • use a clone you keep yourself (LAKE_PATH),
    • or clone from a Git address or path.

    If the CMS has no mirror yet, or the token is missing, the skill says why and offers the other two. It never assumes a path or an address from an earlier site.

  3. Fetches the newest commit. With the mirror, it makes .mdlake/lake in the site a clone at the lake’s newest commit. A site that already has a lake.lock only reports how far the lock is behind.

  4. Reads the lake. It loads _manifest.json and, with MCP connected, the CMS’s own manifest.

  5. Finds candidates for every slot from the site type, by title, folder, tag and summary. It lists image assets that look like a logo or a share image, and it checks that they have alt text.

  6. Reports and stops.

A report looks like this.

Site type: corporate
about     (document, required)    candidates:
  01JBXQ3F... "About Nordhaven"  Company/  [about, company]  title and folder match "About"
contact   (document, required)    none found
services  (collection, required)  folder Product/Features, 4 documents
cases     (collection, optional)  folder Customers + tag case-study, 3 documents
logo      (asset, optional)       01M5YQ88... logo-mark.png  tagged brand; alt "mdlake logo"
Site name: "mdlake" (About title and brand assets; never the workspace slug)
Unpublished: route-planning (draft)
Content missing: no document that looks like a privacy policy (an editor task)

Read it for three things.

  • A required slot with no candidate is blocking. The content is missing. An editor writes and publishes it, then you run check-lake again.
  • A candidate that is still a draft shows under “Unpublished”. It does not count until an editor publishes it.
  • An Alt text: line lists image assets whose alt text repeats that it is an image (“Photo of …”). It is a warning for editors, not a gap.

The skill never asks you to retag, move or rename lake content to fit the site, and it never invents content to fill a gap.

Step 3: the interview

Run the next skill:

/mdlake-builder:plan-site

First it looks at the lake: folders and tags, the shape of documents (lead, sections, quotes, lists, figures), brand assets and folders that look like audiences or legal or recruiting content. It notes the lake commit it read. That commit becomes planCommit.

Then it interviews you. There are about eight questions, in one or two batches. Every question has a recommended answer and a free-text option. Answer “defaults” at any point and the skill stops asking and uses its proposals. Whatever the lake answers with confidence is pre-filled and only confirmed.

QuestionWhat it decides
Purpose (leads, inform, recruit, campaign, document, or something else)The primary call to action and what the home page leads with. document proposes the docs site type.
AudienceWho the copy speaks to. Proposed from the lake, for example the documents in a Solutions folder.
Primary call to actionThe hero button and the call-to-action panels. The skill checks the lake has a page for it (a contact or demo document). If not, it records a content gap.
ScopeWhich folders and collections may be used. A recruiting folder is off unless the purpose is recruiting. Legal pages in scope are named as single documents.
Must-have pagesPages the content does not suggest, such as pricing. The lake cannot fill them? They become content gaps.
Brand, tone and design.mdTone words and whether you already have a design.md.
Modesnapshot (recommended) or live.
AvoidContent, claims or pages that must not appear.

In live mode the plan has no generated copy: the script refuses a live plan with generated modules or jobs. See How sites work.

Step 4: read plan.md

The skill writes plan.md in the site folder. It is Markdown for you. The skill’s script reads only the fenced YAML blocks. Everything else is prose.

PartWhat it holds
## BriefThe answers in prose, then one ```yaml brief block.
SiteA summary, then one ```yaml site block. It becomes lake.config.ts.
PagesA table of routes with module, kind and reason, then one ```yaml page block per route. They become src/pages.config.ts.
Generated copyOne ```yaml generated block with the copy jobs.
Content gapsProse, plus an optional ```yaml gaps block.
Open questionsProse.

The brief

FieldMeaning
workspaceThe workspace slug.
lakePath.mdlake/lake for a managed lake, otherwise the path you gave.
planCommitThe lake commit that was read. scaffold-site locks exactly this commit.
purposeleads, inform, recruit, campaign, document, or other: <words>.
audienceA list of audiences.
primaryCtalabel and href (a page of the plan), and optionally gap, the key of a content gap when the lake has no target.
scopeLake folders or document paths the site may use.
mustHaveA list of { name, page } or { name, gap }. Give exactly one of page and gap.
toneTone words.
modesnapshot or live. It must equal the site’s mode.
avoidThings that must not appear.
inspirationSites or notes to take cues from.

The brief is the plan’s contract. The hero and the call to action serve primaryCta. Documents outside scope are not used. avoid is a hard limit. Every mustHave is a page of the plan or a content gap.

An example

This is a complete plan for a small documentation site.

Markdown
## Brief

Purpose: document a product. Readers are developers who install and use the Tarn command line tool.

```yaml brief
workspace: tarn-docs
lakePath: .mdlake/lake
planCommit: 9f2c1d7a4b6e8f0a1c3d5e7f9a1b3c5d7e9f0a1b
purpose: document
audience: [Developers who install and use the Tarn command line tool]
primaryCta: { label: Read the docs, href: / }
scope: [Docs]
mustHave: []
tone: [Plain, exact]
mode: snapshot
avoid: []
inspiration: []
```

```yaml site
name: Tarn Docs
type: docs
mode: snapshot
workspace: tarn-docs
slots:
  docs: { folder: Docs, sort: order }
```

```yaml page
key: home
path: /
nav: Docs
modules:
  - { module: docsPage, kind: rendered, from: { collection: docs }, heading: Tarn documentation }
```

```yaml page
key: docs
path: /[...slug]/
each: docs
modules:
  - { module: docsPage, kind: rendered, from: { current: true }, collection: docs }
```

Rules for the YAML:

  • Quote any string that contains a colon. Inside { ... }, quote any string that contains a comma.
  • A page path starts and ends with /. A detail page has [slug] and each: <collection>. Only a docsPage may use [...slug].
  • kind is rendered, shaped or generated, and the script checks that it matches the module.
  • Sources are slot keys or document keys of the plan, never raw text.
  • A module may carry a note that says why. It becomes a comment in pages.config.ts.

Generated copy and gaps

Some copy has to be written from several documents. Each one is a job in the generated block.

YAML
- key: home-hero
  brief: "A short hero for the home page: what we do and for whom. Headline at most 8 words."
  sources: [01JBXQ3F7M2N8P4R6S9T1V5W0A, 01JBXQ4G8N3P9Q5R7T2V6W1X0B]
  fields: [headline, subline]
  feeds: Hero on /

Every generated: key a module uses needs a job. A job needs at least one source and at least one field. Sources must be published documents, and they should stay inside the scope. The writing script refuses an unpublished source.

A content gap is copy that the design wants and the lake lacks. Editors own it. Give each gap an action: omit leaves it out, promote writes a draft document for editors from named sources. A draft without sources is not allowed. The plan never fills a gap with invented text.

YAML
- { key: pricing, need: A pricing page with real prices, action: omit }

Step 5: design.md

design.md holds the site’s colours, fonts, spacing and tone. It is settled here, together with the plan, and never in scaffold-site.

  • You have a design.md. Give the skill the path or paste the text. It is used as given.
  • You do not. The skill drafts one from ruleset/design.template.md. It sets the tone from the brief, picks colours from the logo (it reads the logo’s dominant colours and takes the first non-neutral one as the accent candidate) and uses your inspiration notes. It adjusts colours until every pair passes contrast, then shows it. A draft carries status: draft in its frontmatter.

Colours in the ## Colours table must be six-digit hex values (#a8442a). The tokens of the ## Theme table (font-*, radius-*, container-*, spacing-*) go into the site’s theme.

The file is found case-insensitively, so design.md and DESIGN.md both work. Having both is an error.

Contrast must reach WCAG AA, 4.5:1, for the colour pairs the template uses. The skill runs check-contrast.mjs before showing the draft.

Step 6: validate and approve

Before the stop, the skill runs two checks you can also run yourself. <plugin> is the folder shown as “Read from” by claude plugin list.

PowerShell
node "<plugin>\scripts\plan-to-config.mjs" . --check
node "<plugin>\scripts\lake-links.mjs" .

plan-to-config.mjs --check must print “Plan OK”. It checks the brief against the plan (mode, scope, the home call to action, must-haves) and against the lake (scope entries exist, bound documents lie inside the scope). lake-links.mjs lists links in the planned pages that point to documents without a page. They would show as plain text. You can add the page, add the document to the scope, or accept the text.

Then the skill stops. It shows the route table, the generated-copy jobs, the gaps, the open questions, the brief and the design. Read it, edit plan.md and design.md as you like, and approve them together. On approval of a drafted design, the skill runs check-design.mjs <site> --approve, which sets status: approved.

A design that you did not explicitly approve stays draft, and scaffold-site refuses to start.

What can go wrong

Message or symptomCause and fix
A required slot has no candidateThe content is missing. Ask an editor to write and publish it, then run check-lake again.
plan-to-config reports a YAML errorA string with a colon or a comma needs quotes.
brief scope: "X" matches no documentA scope entry must be a lake folder or a document path relative to the lake root.
X is outside the brief's scopeA bound document lives outside scope. Add it to the scope or bind another document.
collection X: without a folder it reaches the whole lakeEvery collection needs a folder that lies inside the scope.
the hero or a CTA must serve the primary call to actionThe home page needs a hero action to primaryCta.href, or a cta module with the same label. A docsPage home serves it by itself.
A new document is missing from the planIt was published after planCommit. Plan again against the newer commit, or leave it for refresh-content.
No mirror address from the CMSThe workspace has no GitHub mirror yet, or the token is missing. See Connect GitHub.

What’s next