Skip to content
mdlake

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

PieceWhat it isWhere it lives
The lakeA 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 siteAn Astro project that you own. It has its own Git repository.Your repository, for example in Azure Repos or GitHub.
The kitTwo 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 pluginmdlake-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 fromThe commit in lake.lockThe newest commit of the lake
Publishing in the CMSChanges nothing on the siteShows up the next time the site is built
Moving to newer contentThe refresh-content skill, after you review what changedNot needed
Generated copyAllowedNot allowed (the build stops if a module binds it)
Good forReleases, marketing sites, anything you review before it changesDocumentation 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.

JSON
{
  "lake": {
    "workspace": "mdlake-demo",
    "remote": "https://github.com/acme/lake-mdlake-demo.git",
    "commit": "9f2c1d7a4b6e8f0a1c3d5e7f9a1b3c5d7e9f0a1b",
    "lockedAt": "2026-10-01T09:30:00.000Z"
  },
  "mode": "snapshot"
}
FieldMeaning
workspaceThe workspace slug.
remoteThe 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.
commitThe lake commit the site is built from.
lockedAtWhen the lock was written.
modesnapshot 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.

  1. lake.lock records the lake’s remote and the commit.
  2. npx mdlake prepare runs before npm run dev, npm run build and the checks. It makes .mdlake/lake a Git repository at exactly that commit. It runs git 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.
  3. .mdlake/lake is 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:

  1. 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.
  2. .mdlake/lake, when lake.lock has a remote.
  3. 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,
  • planCommit in 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.

KindWhat happensUse it forExample
RenderedThe whole body of a document is shown as written.Pages that are a documentA legal page, a blog post, a docs page
ShapedA module reads parts of a document through reading rules and lays them out.The lake already has the sentence, quote or list the module needsA hero headline from a lead sentence, a testimonial, a figures row, a FAQ
GeneratedCopy 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 placeA 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:

  1. Each asset has a sidecar in the lake, assets/<id>.json. The CMS writes the public address of the file into it as url.
  2. During astro build, the kit finds the /files/ paths that the output links to, reads those files from the local lake or from the url, and copies them into dist/files.
  3. 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> with srcset.
  4. Downloads are cached in node_modules/.cache/mdlake-assets. Converted images are cached in node_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

PathWhat
lake.config.tsBindings, the site name and the slots of the site type.
src/pages.config.tsWhat each route shows: modules with a source and reading rules, and the navigation.
src/content.config.tsdefineMdlakeCollections() from the kit: one Astro collection per binding.
src/content/generated/Generated copy and its provenance.
src/styles/global.cssDesign tokens in an @theme block, mirrored from design.md.
astro.config.mjsdefineMdlakeConfig(). Add your own Astro options here.
site.config.tsOptional. Overrides for kit components.
redirects.json{ "/old/": "/new/" } for slugs that changed.
design.md, plan.mdThe design and the plan.
lake.lock, site-snapshot.json, builder.json, .mdlake/*, vendor/*.tgzKit files. Tools write them.
provenance.jsonThe ownership manifest.
CLAUDE.mdThe 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