Skip to content
mdlake

Generated copy

Write copy such as a hero headline from several lake documents, with provenance, and keep it honest. Covers the generate-copy skill, stale and hand-edited files, and promoting copy to the lake.

On this page

Most text on a site is read from one lake document. Some copy is not. A home page hero needs a sentence about what the company does and for whom, and no single document says that. Generated copy is text written from several lake documents with the generate-copy skill and stored in the site, together with a record of where it came from.

Generated copy is a last resort. First try rendered and shaped content, which use the lake’s own words. See How sites work.

When to use it

SituationUse
The document is the page (a legal page, a post)Rendered
The lake already contains the sentence, quote or list the module needsShaped
The right copy must be written from many documents (a hero, a “why us” line, an intro, a call to action)Generated
Editors should own the copyGenerate it, then promote it to a lake document

Generated copy exists on snapshot sites only. It is tied to a lake commit. A live site refuses to build when a module binds generated copy, and the plan refuses a live plan with generated modules. Documentation sites use rendered content only.

What a generated file looks like

Each copy is a Markdown file in src/content/generated/. The file name is the key. The frontmatter is the provenance.

YAML
---
key: home-hero
brief: 'A short, calm hero for the home page: what the company does and for whom.'
sources:
  - id: 01JBXQ3F7M2N8P4R6S9T1V5W0A
    title: Calm technology for growing companies
    contentHash: 30a5a87b2b4e141f390d7400d90aa558e6732c6b3e1c945e63b15bb353d5d3b1
  - id: 01JBXQ4G8N3P9Q5R7T2V6W1X0B
    title: Process automation
    contentHash: 784eabd30438ab572d80418de40fa67e1a4c8715463c5d769368fa4bb6221029
lakeCommit: 9f2c1d7a4b6e8f0a1c3d5e7f9a1b3c5d7e9f0a1b
generatedAt: '2026-10-27T16:19:52.219Z'
generator: claude-code (claude-sonnet-5-5)
fields:
  headline: Software your team likes using
  subline: >-
    We replace fragile spreadsheets and tangled systems, and automate the
    repetitive work, one system at a time.
outputHash: f525b50969321ee3ed977a99bb0a26ea89a00630130f80ac8da51911ce6da6ce
---
FieldMeaning
keyLowercase letters, digits and hyphens. It must match the file name.
briefThe instruction that produced the copy, in plain words. Regeneration reuses it.
sourcesThe documents the copy was written from: ID, title and contentHash. At least one.
lakeCommitThe commit of lake.lock when the copy was written.
generatedAt, generatorWhen, and by which tool and model.
fieldsThe named outputs, such as headline and subline.
outputHashA hash of the fields and the body. A mismatch means someone edited the file.

The text after the frontmatter is an optional Markdown body. A module can read it as the field body.

contentHash covers what a writer can read from a document: its title, summary, sorted tags, lead image ID and body, with line endings and surrounding whitespace normalised. Status, dates, slug and path are not part of it. A source that is unpublished or deleted is reported separately.

Rules

  • No invention. Write only from the chosen sources. No facts, numbers, names or claims that they do not contain. Every sentence must be traceable. Follow the voice in design.md.
  • Never edit a generated file by hand, and never write provenance by hand. Change the brief or the sources and run the skill again. The script computes the hashes.
  • A number the sources do not contain is refused. write-generated.mjs checks every number in the copy against the source text and stops. --allow-unsourced overrides the check. Use it only when you can justify the number.
  • Sources must be published in the lake clone at lake.lock. A document that the clone lacks, or has with older text, is not a source until refresh-content.

Step 1: run the skill

Scaffolding runs your plan’s jobs for you. To write or rewrite one by hand:

/mdlake-builder:generate-copy

The skill asks for three things, unless the plan already has them:

  • a key, lowercase with hyphens, such as home-hero,
  • a brief in plain words, such as “A hero for the home page: what we do and for whom. Headline at most 8 words”,
  • which module fields it feeds, such as Hero headline and subline.

When you regenerate a stale file, the key and the brief are taken from src/content/generated/<key>.md.

Step 2: retrieve and choose sources

With the lake MCP tools connected, the skill finds sources with search_documents and reads them with get_document. Without MCP it searches the clone:

PowerShell
node "<plugin>\scripts\lake-search.mjs" . hero company services --limit 8

lake-search.mjs ranks titles, tags, summaries, headings and bodies, and prints snippets. It writes nothing. It finds the lake itself.

The skill then lists 2 to 5 documents with their ID, title and why. It stops and waits for you to confirm or change them. It writes no copy before you answer.

Step 3: write the file

The skill writes the copy only from those sources and calls the script:

PowerShell
node "<plugin>\scripts\write-generated.mjs" . --key home-hero `
  --brief "A hero for the home page: what we do and for whom." `
  --source 01JBXQ3F7M2N8P4R6S9T1V5W0A --source 01JBXQ4G8N3P9Q5R7T2V6W1X0B `
  --field headline="Software your team likes using" `
  --field subline="We replace fragile spreadsheets and automate the repetitive work." `
  --model claude-sonnet-5-5
OptionMeaning
--keyThe key. It names the file.
--briefThe brief, in plain words.
--sourceA source document ID. Repeat it, or separate IDs with commas.
--field name=valueAn output. Repeat it.
--body-file <file>A Markdown body.
--modelThe model name for generator.
--lake <dir>A lake folder other than the site’s own.
--commit <hash>The lake commit to record when there is no lake.lock.
--allow-unsourcedAllow numbers that no source contains.
--overwriteRegenerate a file that was edited by hand. Never an adopted file.
--promote <key>Write a lake draft instead. See below.

The script needs npm install to have run in the site. It takes the commit from lake.lock, computes the source hashes and the output hash, and writes through the ownership guard. A file you adopted, or edited by hand, is not overwritten. The script prints what it would have changed and exits with code 3. See Ownership zones.

Step 4: bind it

A module binds copy with { generated: '<key>', field: '<name>' } in src/pages.config.ts:

TypeScript
{
  module: 'hero',
  from: { document: ['home', 'about'] },
  headline: { generated: 'home-hero', field: 'headline' },
  subline: { generated: 'home-hero', field: 'subline' },
}

The subline may also stay a reading rule such as ['summary']. A cta module takes text: { generated: '<key>', field: 'text' }.

When the generated file is missing, the module falls back to its reading rules and records a “Content fit” warning, so a site never breaks for a missing file. Then run:

PowerShell
npm run check:bindings

The file must show as fresh.

Step 5: review

The skill stops again. It shows the copy with its sources and asks you to look at it in npm run dev. To change anything, adjust the brief or the sources and repeat from the retrieve step. Never patch the file. Commit the generated file together with the pages.config.ts change only after approval.

Stale and hand-edited

npm run check:bindings and refresh-content report one of three states for each file.

StateMeaningWhat to do
freshThe sources are unchanged and the copy is untouched.Nothing.
staleA source changed, was unpublished or was deleted.Regenerate it with the same brief through the skill.
hand-editedThe copy no longer matches outputHash.Regenerate it, or revert it, or adopt it.

A file can be both. It is then reported as stale.

The report names the cause: source changed: "Process automation", source unpublished: "...", or “edited by hand: adopt it (npx mdlake adopt) or regenerate it or change the brief”. Stale and hand-edited files are warnings. They do not fail the check. Invalid provenance, and a live site that binds generated copy, do fail it.

If you want to keep a hand-edited copy, run npx mdlake adopt src/content/generated/<key>.md. It becomes a site file. No tool writes it again, refresh-content still tells you when its sources change, and you update the text yourself.

mdlake check-bindings --strict-generated turns stale and hand-edited files into errors. The template’s own CI uses it. Scaffolded sites keep the warnings.

Promote copy to the lake

Sometimes editors should own the copy. --promote turns a generated file into a draft document they can publish.

PowerShell
node "<plugin>\scripts\write-generated.mjs" . --promote home-hero

It writes .mdlake/promote/home-hero.md: a Markdown file with frontmatter and no id. The title is the headline or title field, the summary is the subline or summary field, any other fields become ## sections, tags is [generated-draft] and status is draft.

  1. Give the file to the editors. They import it in the CMS and publish it. See Importing content.
  2. After the next refresh-content, bind the module to that document in lake.config.ts and src/pages.config.ts.
  3. Delete src/content/generated/<key>.md.

A content gap with action: promote in plan.md becomes a job that scaffold-site runs this way. Such a job only writes the draft, and the module stays unbound until editors publish it.

What can go wrong

SymptomCause and fix
Refusing to write: 12 does not appear in any sourceThe copy contains a number that no source has. Remove it, or add the source that has it.
Source ... is not publishedPick a published document.
Source ... is not a document of the lakeThe ID is wrong, or the clone is older than the document. Run refresh-content first.
pages.config.ts binds generated content ... which needs a snapshot siteThe site is live. Bind the modules to lake documents, or switch to snapshot.
Invalid generated contentA file’s provenance is broken, or the key does not match the file name. Regenerate it.
The script exits 3The file is adopted or edited by hand. Keep it, or use --overwrite for an edited one.

What’s next