Skip to content
mdlake

Custom components

Add your own components and override kit modules, parts and the layout from site.config.ts. Covers design tokens, which overrides exist, and how to keep an override working across upgrades.

On this page

The kit draws every module of a generated site: the hero, the feature grid, the FAQ and the rest. You change how they look in three steps, from least to most work: change a design token, fill a layout slot, or override a component. This page covers all three, and your own components.

Kit files are locked, so you never edit them. You register a replacement instead. The replacement is a file of yours, in the site zone, that no tool rewrites. See Ownership zones.

Choose the smallest change

You want toUse
Other colours, fonts, radius or spacingA design token
Something extra in the header, footer or banner, or in the headA layout slot
A different look for one module, such as the heroA module override
A different card grid, image, navigation, search or SEOA part override
A component that no module offers, used by your own pagesA site component

An override is yours to maintain. Every kit upgrade can change the props or the markup of the component you replaced, so prefer a token or a slot when one is enough.

Design tokens

The look of a site comes from the @theme block in src/styles/global.css. The kit’s components use these tokens and never fixed values. design.md is the source: apply-design.mjs writes its tokens into the block when the site is scaffolded, and the block says that the tokens mirror design.md one to one. To change the design, edit design.md, run the regenerate skill (it runs apply-design.mjs, and the contrast must pass), and then adjust the components that follow from it. A design.md that is still a draft is not applied. Applying the design replaces the tokens that design.md lists and adds the ones that are missing. A token that you added to the block by hand and that design.md does not list is left as it is. A token that design.md does list is overwritten with the value from design.md.

GroupTokens
Colour--color-canvas, --color-surface, --color-line, --color-ink, --color-muted, --color-accent, --color-accent-strong, --color-on-accent
Fonts--font-serif, --font-sans
Widths--container-site (72rem), --container-column (42rem), --container-prose (72ch)
Shape--radius-card, --radius-button
Spacing--spacing-section, --spacing-block, --spacing-page. The kit also uses --spacing-tight and --spacing-gutter.
Image caps--spacing-image-body (20rem), --spacing-image-square (16rem), --spacing-image-logo (5rem), --spacing-image-hero (28rem)
Buttons--color-action, --color-action-ink, --color-action-hover, --font-action

Tailwind turns them into classes such as bg-canvas, text-ink, rounded-card, mt-section and gap-block.

Button tokens

Buttons use the action tokens, and links keep --color-accent. When the action tokens are not set, they fall back to the accent values. That lets a site have near-black buttons and rust-coloured links without any override. Do not override the hero or the CTA only to change a button colour or font. If you already did, npx mdlake migrate --buttons can replace the override with tokens. See Upgrade and migrate.

Contrast

scaffold-site checks the contrast of the colour pairs of design.md against WCAG AA (4.5:1). If you change a colour, check the pair yourself, and run npm run check, which includes the accessibility check at desktop and phone widths.

The layout and its slots

Every page is drawn by the kit Layout (@mdlake/site-kit/layouts/Layout.astro). It has four named slots. Fill one from a page of yours, or from a layout that wraps it.

SlotDefault content
headNothing extra. Use it for a <link> or a <meta> tag.
bannerThe preview banner when a candidate lake or a CMS preview is on, otherwise nothing.
headerThe logo and the navigation.
footerThe footer columns.
defaultThe page content.
Astro
---
// src/pages/offer.astro
import Layout from '@mdlake/site-kit/layouts/Layout.astro'
import { getLakeDocument } from '@mdlake/site-kit/content'

const offer = await getLakeDocument('offer')
---

<Layout title={offer.title} description={offer.summary}>
  <link slot="head" rel="preload" href="/fonts/display.woff2" as="font" type="font/woff2" crossorigin />
  <div class="mdl-column" set:html={await offer.html()} />
</Layout>

Important

A banner slot that you fill replaces the preview banner. Keep the banner when the page can be previewed, or editors will not see that the page shows a preview.

To change the layout of the whole site, register your own layout with layout in site.config.ts. It may wrap the kit layout and fill its slots. Change the header, footer and home page shows this from start to end.

site.config.ts

site.config.ts is optional and site-owned. Create it when you need your first override.

TypeScript
// site.config.ts
import { defineSiteConfig } from '@mdlake/site-kit'

export default defineSiteConfig({
  modules: { hero: './src/components/MyHero.astro' },
  layout: './src/layouts/SiteLayout.astro',
  parts: { LakeImage: './src/components/MyLakeImage.astro' },
})
KeyWhat it replacesNames it accepts
modulesA page modulehero, featureGrid, steps, testimonial, stats, faq, cta, richText, caseHeader, docsPage, form, endOfPage
layoutThe page layoutA path
partsA shared partCardGrid, CodeCopy, DocsSearch, DocsSidebar, FormFields, LakeImage, MobileNav, Prose, SEO

Rules:

  • Each value is a path from the site root, such as ./src/components/MyHero.astro.
  • Everything you do not name comes from the kit.
  • Restart astro dev after you change the file. The dev server reads it at start.
  • Write a module or part override as an .astro file, like the kit’s own. The kit places a module on the page without a client: directive, so a .tsx file registered as a module would not become interactive. For an interactive piece, write a .tsx island and use it inside your .astro override (or a site component) with a client: directive such as client:load.

Override a kit module

  1. Look at what the module receives. The component gets the same props as the kit’s. Every module gets data (what the module resolved from the lake and the reading rules). featureGrid, steps, stats and faq also get index, the position of the module on the page. richText and caseHeader also get back, the link to the page that lists the collection (href, label and an optional heading), or nothing. Read the kit’s component for the exact shape, for example node_modules/@mdlake/site-kit/dist/components/modules/Hero.astro, but do not edit it there. Find what to change lists the file of each module.
  2. Write your component in src/components/. Keep the props of the kit component.
  3. Register it in site.config.ts.
  4. Restart astro dev and run npm run check.
Astro
---
// src/components/MyHero.astro
import LakeImage from '@mdlake/site-kit/components/LakeImage.astro'
import type { Resolved } from '@mdlake/site-kit/lib/modules.js'

interface Props { data: Extract<Resolved, { module: 'hero' }> }
const { data } = Astro.props
const { image } = data
---

<section class="mdl-wide grid items-center gap-block pt-page md:grid-cols-2 md:pt-section">
  <div>
    <h1 class="font-serif text-ink">{data.headline}</h1>
    {data.subline && <p class="mt-6 text-muted">{data.subline}</p>}
    {data.actions.map((action) => (
      <a href={action.href} class="rounded-button border border-action bg-action px-5 py-3 font-action text-action-ink no-underline">
        {action.label}
      </a>
    ))}
  </div>
  {image && <LakeImage asset={image} alt={image.alt} aspect="4 / 3" loading="eager" class="rounded-card" />}
</section>

The Resolved type comes from @mdlake/site-kit/lib/modules.js, which the package exports through ./lib/*. The kit’s own components import it by relative path.

The hero’s data has headline, subline, actions (each with label, href and a style) and image. Other modules have their own shapes. Read the kit’s component for the module you override, because the shapes can change between kit versions.

Content still comes from the lake. Your override receives the resolved text as props, so the plan and the reading rules keep working. Do not import lake text into the component.

If npm run check fails with an astro check error in an override after an upgrade, the kit changed the props. Compare your file with the kit’s new one and port the change.

Override a kit part

Parts are the pieces that several modules share. The card grid, the image, the prose renderer and the SEO tags are examples. A part override replaces it everywhere. Register it under parts.

Warning

A LakeImage override opts out of the kit’s image pipeline, which gives responsive AVIF and WebP, srcset and lazy loading. Copy the <Picture> branch of the kit’s component if you still want those.

Leave the kit’s imports alone

The kit’s own components import each other through the aliases @mdlake/site/module/*, @mdlake/site/part/* and @mdlake/site/layout. These aliases resolve to your override when you registered one, and to the kit’s file when you did not. That is how a part override reaches the modules that use it. In your own code, import from the package paths shown on this page (@mdlake/site-kit/...). The aliases are made for the kit’s own files. Nothing in the kit or the template documents them for site code, so do not depend on them there.

Add a site component

For something that is not a module, such as a pricing table that three of your pages use, write a component of your own. It lives in src/components/, is site-owned, and is never touched by refresh-content, regenerate or upgrade-ruleset.

Rules for a component:

  • Style it with Tailwind classes on the tokens and with the tokens of global.css. Write no hard-coded colours, font sizes or pixel spacing.
  • Interactive elements use react-aria-components. Write an island as .tsx.
  • Icons come from Lucide only: @lucide/astro in Astro files, lucide-react in React islands. Do not draw SVGs or use Unicode glyphs as icons.
  • Content arrives as props. The page reads it with getLakeDocument or getLakeCollection and passes it down. The component never imports lake text. See Custom pages.
  • No server code. A server route is API routes and a form is Forms.
  • Accessibility: semantic HTML first, WCAG 2.1 AA, a visible focus state. npm run check runs the axe check.
Astro
---
// src/components/PlanCard.astro
import { ArrowRight } from '@lucide/astro'

interface Props {
  title: string
  summary: string
  href?: string | undefined
}
const { title, summary, href } = Astro.props
---

<div class="rounded-card border border-line bg-surface p-block">
  <h3 class="font-serif text-ink">{title}</h3>
  <p class="text-muted">{summary}</p>
  {href && (
    <a href={href} class="text-accent">
      Read more <ArrowRight aria-hidden="true" />
    </a>
  )}
</div>

Use it from a page:

Astro
---
import PlanCard from '../components/PlanCard.astro'
import { getLakeCollection } from '@mdlake/site-kit/content'

const plans = await getLakeCollection('plans')
---

{plans.map((plan) => <PlanCard title={plan.title} summary={plan.summary} href={plan.route} />)}

Check and commit

PowerShell
npm run check

The check builds the site, tests the bindings and runs the accessibility check. Look at the result in npm run dev and compare it with design.md. Commit only your own files. The commit-site.mjs script the skill uses takes a list of paths and refuses nothing that is yours.

What can go wrong

SymptomCause and fix
The override is ignoredThe path in site.config.ts is wrong, or the dev server was not restarted.
astro check fails in an override after an upgradeThe kit’s props changed. Port your component to the new props.
Edit hook blocks a fileIt is a kit file or a locked file. Register an override instead.
A button has the wrong colour in one module onlyA leftover override. Replace it with tokens (migrate --buttons).
Contrast fails in the accessibility checkA colour pair in design.md is below 4.5:1. Change the tokens through design.md.
Icons look wrong or missingYou used a glyph or an icon from another set. Use Lucide.

What’s next