How sites work
How an mdlake site relates to the lake. Covers snapshot and live mode, lake.lock, the managed lake clone, the three content kinds and how images and files reach the built site.
On this page
An mdlake site is an Astro project that reads published content from the lake and turns it into pages. The site never stores the text. It stores bindings, a list of pages and your own code. This page explains the ideas behind that. Read it once before you plan or build a site, because the other pages in this section assume them.
If a term is new, What is mdlake introduces the lake, the workspace and the builder plugin.
The pieces
| Piece | What it is | Where it lives |
|---|---|---|
| The lake | A Git repository of Markdown files and asset sidecars, one per workspace. The CMS writes to it when an editor publishes. | The CMS mirrors it to GitHub as lake-<slug>. |
| The site | An Astro project that you own. It has its own Git repository. | Your repository, for example in Azure Repos or GitHub. |
| The kit | Two npm packages, @mdlake/lake-loader and @mdlake/site-kit. They hold the engine: modules, routes, layout, image handling and the mdlake commands. | vendor/*.tgz in the site, installed by npm install. |
| The plugin | mdlake-builder, a Claude Code plugin with the skills that plan, scaffold and change a site. | Installed in Claude Code. See Install the builder. |
A site stays small because the engine is in the kit. A new site holds its bindings (lake.config.ts), its page list (src/pages.config.ts), its design tokens (src/styles/global.css), generated copy, and whatever pages and components you add yourself.
Bind, never copy
Content reaches a page in one way only: the site binds to it and the build reads it from the lake.
- A document is bound by its ID, for example
about: '01JBXQ3F7M2N8P4R6S9T1V5W0A'. - A collection is bound by a query: a folder, optional tags and a sort order.
- An asset (a logo, a share image) is bound by its ID.
Text from the lake is never pasted into code. If an editor changes a document and the site takes the new content, the page changes. The code does not. The same rule applies to you when you write your own pages. See Custom pages.
The site also never asks the lake to change for its structure. If a slot needs a document and the lake has no tag that says so, you bind the document you already have. Missing content is a task for editors, not a reason to retag.
Snapshot and live
A site is a snapshot of the lake by default. It is built from one exact lake commit. Publishing in the CMS creates a new lake commit, but the site does not change until a developer moves the pin on purpose.
| Snapshot (default) | Live | |
|---|---|---|
| Built from | The commit in lake.lock | The newest commit of the lake |
| Publishing in the CMS | Changes nothing on the site | Shows up the next time the site is built |
| Moving to newer content | The refresh-content skill, after you review what changed | Not needed |
| Generated copy | Allowed | Not allowed (the build stops if a module binds it) |
| Good for | Releases, marketing sites, anything you review before it changes | Documentation that editors publish continuously |
The mode is chosen in the plan. It is recorded in lake.lock and in .mdlake/ruleset.json.
lake.lock
lake.lock sits in the site root and is committed with the site. A kit file, written by tools only.
{
"lake": {
"workspace": "mdlake-demo",
"remote": "https://github.com/acme/lake-mdlake-demo.git",
"commit": "9f2c1d7a4b6e8f0a1c3d5e7f9a1b3c5d7e9f0a1b",
"lockedAt": "2026-10-01T09:30:00.000Z"
},
"mode": "snapshot"
}| Field | Meaning |
|---|---|
workspace | The workspace slug. |
remote | The Git address of the lake (the CMS’s GitHub mirror). Optional. It is a plain HTTPS address and never contains a token. The loader refuses an address with a user name or password in it. |
commit | The lake commit the site is built from. |
lockedAt | When the lock was written. |
mode | snapshot or live. |
In snapshot mode a build stops if the lake clone is at another commit:
The lake at <full path of .mdlake/lake> is at 1a2b3c4d5e6f, but lake.lock locks 9f2c1d7a4b6e (snapshot mode).The message also says how to fix it. For a managed clone, run npx mdlake prepare to put it back at the locked commit. For a LAKE_PATH clone, check out the locked commit with git -C "<lake>" checkout <commit>. Never edit lake.lock by hand. The refresh-content skill moves it after you approve. See Refresh content.
Warning
LAKE_LOCK_IGNORE=1 skips the lock check and prints a warning. It exists for emergencies. Never deploy a build made with it.
The lake clone
A build needs the lake files on disk. The kit manages that clone for you.
lake.lockrecords the lake’sremoteand thecommit.npx mdlake prepareruns beforenpm run dev,npm run buildand the checks. It makes.mdlake/lakea Git repository at exactly that commit. It runsgit fetch --depth 1 origin <commit>and a detached checkout, so it downloads only that commit and not the history. When the commit is already there, it does nothing..mdlake/lakeis ignored by Git.
You need git on the computer. The fetch uses your own Git sign-in (a credential helper such as Git Credential Manager, or gh auth login). A CI runner uses MDLAKE_LAKE_TOKEN instead. See CI, the lake and caches.
One function decides which lake a command reads, in this order:
LAKE_PATH, if it is set in the environment or in the site’s.env. This is an optional override for a clone you keep by hand..mdlake/lake, whenlake.lockhas aremote.example-lake, the small demo lake that ships with the template.
LAKE_PATH still has to match the lock. The checks warn when the origin of that clone is not the remote in the lock.
A live site works the same way, except that mdlake prepare fetches the newest commit of the remote each time and the lock is not checked.
Note
The CMS on a developer’s computer (the Docker Compose setup) has no GitHub mirror. It publishes to data/git/<slug>.git in the repository. Clone that folder yourself and point LAKE_PATH at it. See Connect GitHub for the real mirror.
The clone builds, the MCP looks
Claude Code can reach the CMS through the plugin’s lake MCP server. It lets the skills search and read content as the CMS has it right now. That is quick, and it is what you want while planning.
It is not what a build uses. These always come from the clone:
- the pages of the build,
planCommitin the plan,- the hashes in generated-copy provenance,
lake.lock.
An ID that only the MCP shows was published after the clone’s commit. It is not bound until refresh-content brings it into the clone. This rule keeps a build reproducible: the same commit always gives the same site.
Content kinds
Every piece of lake text reaches a page in one of three ways. You pick the kind per module, not per site.
| Kind | What happens | Use it for | Example |
|---|---|---|---|
| Rendered | The whole body of a document is shown as written. | Pages that are a document | A legal page, a blog post, a docs page |
| Shaped | A module reads parts of a document through reading rules and lays them out. | The lake already has the sentence, quote or list the module needs | A hero headline from a lead sentence, a testimonial, a figures row, a FAQ |
| Generated | Copy is written from several documents with the generate-copy skill and stored in src/content/generated/<key>.md. | The right words do not exist in one place | A hero headline for the home page |
Shaped modules are tolerant. When the rules find nothing, the module renders nothing and records a warning. npm run check prints it under “Content fit”. A missing quote never fails the build.
Generated copy belongs to snapshot sites only, because it is tied to a lake commit. Documentation sites use rendered content only. See Generated copy.
How images and files are served
A built site serves every asset from its own domain. Pages link to /files/<id>/<name>.<ext>. The name is a slug of the title or alt text, and the extension comes from the file type. No storage address ends up in dist/.
How that works:
- Each asset has a sidecar in the lake,
assets/<id>.json. The CMS writes the public address of the file into it asurl. - During
astro build, the kit finds the/files/paths that the output links to, reads those files from the local lake or from theurl, and copies them intodist/files. - Raster images go through Astro’s image pipeline. They become AVIF and WebP in several widths with a JPEG or PNG fallback, shown as
<picture>withsrcset. - Downloads are cached in
node_modules/.cache/mdlake-assets. Converted images are cached innode_modules/.astro/assets.
If an asset cannot be fetched, or an asset:// ID has no sidecar, the build fails and names the asset and the pages that link to it. The machine that builds must therefore be able to reach the asset host.
PUBLIC_ASSET_BASE_URL overrides the sidecar address, for example for a CDN in front of storage. Most sites never set it. When it is set and differs from the sidecar addresses, npm run check warns so that a forgotten value in an old .env does not silently redirect a build.
astro dev has no build step. It uses the asset host’s addresses directly.
What a site contains
| Path | What |
|---|---|
lake.config.ts | Bindings, the site name and the slots of the site type. |
src/pages.config.ts | What each route shows: modules with a source and reading rules, and the navigation. |
src/content.config.ts | defineMdlakeCollections() from the kit: one Astro collection per binding. |
src/content/generated/ | Generated copy and its provenance. |
src/styles/global.css | Design tokens in an @theme block, mirrored from design.md. |
astro.config.mjs | defineMdlakeConfig(). Add your own Astro options here. |
site.config.ts | Optional. Overrides for kit components. |
redirects.json | { "/old/": "/new/" } for slugs that changed. |
design.md, plan.md | The design and the plan. |
lake.lock, site-snapshot.json, builder.json, .mdlake/*, vendor/*.tgz | Kit files. Tools write them. |
provenance.json | The ownership manifest. |
CLAUDE.md | The ruleset that Claude Code follows in this site. |
The home page, the header, the footer and the modules are not in this list, because they are not in your repository. They come from the kit in node_modules. Find what to change takes you from what you see on a page to the place that changes it.
Not every file is yours to edit. Each file belongs to one of three ownership zones. Ownership zones explains them.
What’s next
- Plan a site to read the lake and write the plan.
- Site types to see what each site type can show.
- Scaffold and build if you already have a plan.
