Skip to content
mdlake

Change the header, footer and home page

A worked example of giving a generated site its own header, footer and home page with two small files, while everything else stays the kit's.

On this page

For developers. This is how the site you may be reading now got its own header, footer and home page. It took two small files. Every reading page still comes from the kit, and no text from the lake was copied into code. The example is a docs site, and the same steps work for any site type.

The Astro you need

You can read these files with very little Astro.

You seeIt means
The part between the two --- linesCode that runs when the site is built. It imports things and fetches data.
Everything below itHTML. { } puts a value from the code into it.
A tag with a capital letter, such as <KitLayout>A component that was imported above.
<slot />The place where a component shows what you put between its tags.
slot="header" on an elementPut this element in the component’s place called header, in place of what is there by default.
Astro.propsWhat the component was given by whoever uses it.
client:loadRun this component in the browser too. Without it, no JavaScript reaches the browser.
class="..."Tailwind classes. Names such as text-ink and border-line are this site’s design tokens.

1. Find what draws it

Find what to change gives the answer:

  • The header and the footer are in the kit’s layout, dist/layouts/Layout.astro. It has slots named head, banner, header and footer, and the page goes in the unnamed slot.
  • The home page of a docs site is the docsPage module, dist/components/modules/DocsPage.astro. The same module also draws every reading page, so an override must leave those alone.

2. Write the layout

Do not copy the kit’s layout. Wrap it, and fill only the slots you want to change.

Astro
---
// src/layouts/DocsLayout.astro
import { ExternalLink } from '@lucide/astro'
import KitLayout from '@mdlake/site-kit/layouts/Layout.astro'
import { getLakeDocument, getSiteName, getSlotImage } from '@mdlake/site-kit/content'
import { assetUrl, routes } from '@mdlake/site-kit/lib/routes.js'
import type { ComponentProps } from 'astro/types'

type Props = ComponentProps<typeof KitLayout>
const siteName = await getSiteName()
const logo = await getSlotImage('logo')
const welcome = await getLakeDocument('home')
---

<KitLayout {...Astro.props}>
  <slot name="head" slot="head" />
  <header slot="header" class="border-b border-line">
    <div class="mdl-wide py-4">
      <a href={routes.home} class="inline-flex items-center gap-2 font-serif text-2xl text-ink no-underline hover:text-ink">
        {logo && <img src={assetUrl(logo.id)} width={logo.width} height={logo.height} alt="" class="h-9 w-auto" />}
        <span>{siteName}</span>
      </a>
    </div>
  </header>
  <slot />
  <footer slot="footer" class="border-t border-line bg-surface">
    <div class="mdl-wide flex flex-wrap items-start justify-between gap-x-section gap-y-block py-block">
      <div class="max-w-prose">
        <p class="font-serif text-xl">{siteName}</p>
        {welcome.shape.lead && <p class="mt-2 text-muted">{welcome.shape.lead}</p>}
      </div>
      <nav aria-label="Footer">
        <ul class="flex flex-col gap-2">
          {welcome.route && <li><a href={welcome.route} class="text-ink no-underline hover:text-accent hover:underline">Read the guide</a></li>}
          <li>
            <a href="https://app.mdlake.dev" class="inline-flex items-center gap-1 text-ink no-underline hover:text-accent hover:underline">
              Open the CMS
              <ExternalLink aria-hidden="true" class="size-4" />
            </a>
          </li>
        </ul>
      </nav>
    </div>
  </footer>
</KitLayout>

Four lines do the wrapping:

  • {...Astro.props} hands the page’s title and description on to the kit, which writes the SEO tags.
  • <slot name="head" slot="head" /> hands on what a page puts in the head. Leave it out and that is lost.
  • <slot /> is the page itself. Leave it out and the page is empty.
  • slot="header" and slot="footer" replace those two parts. The banner slot is not named here, so the kit’s preview notice stays.

The words come from the lake. The site name is in plan.md, and the footer’s text is the first paragraph of a document. The link labels are part of the site, like the labels on any button, so they may be in code.

If your site has main navigation

This docs site finds its way with the sidebar and search, so its header has no links. If you have links in the plan, keep them, and keep the menu for small screens.

Astro
---
// Add these to the code at the top of your layout.
import MobileNav from '@mdlake/site-kit/components/MobileNav.tsx'
import { nav } from '@mdlake/site-kit/lib/site.js'
---

<header slot="header" class="border-b border-line">
  <div class="mdl-wide flex items-center justify-between gap-6 py-4">
    <a href={routes.home} class="font-serif text-2xl text-ink no-underline">{siteName}</a>
    <nav aria-label="Main" class="hidden md:block">
      <ul class="flex gap-1">
        {nav.map((link) => <li><a href={link.href}>{link.label}</a></li>)}
      </ul>
    </nav>
    <MobileNav client:load links={nav} current={Astro.url.pathname} />
  </div>
</header>

3. Bind the document

The home page needs a document to read from, so bind it in plan.md. The docs site type has an optional home slot for this.

YAML
slots:
  docs: { folder: Docs, sort: order }
  home: <the document's id>
  logo: { asset: <the asset's id> }
PowerShell
node "<plugin>\scripts\plan-to-config.mjs" .

Only now does getLakeDocument('home') exist for the site. Before this, astro check reports it as an error.

4. Write the home page

The kit’s module gets data, and data.mode says which kind of page this is. Hand every reading page straight back to the kit, and draw only the home page yourself.

Astro
---
// src/components/DocsPage.astro
import DocsSearch from '@mdlake/site-kit/components/DocsSearch.tsx'
import DocsSidebar from '@mdlake/site-kit/components/DocsSidebar.tsx'
import KitDocsPage from '@mdlake/site-kit/components/modules/DocsPage.astro'
import { getLakeDocument } from '@mdlake/site-kit/content'
import type { Resolved } from '@mdlake/site-kit/lib/modules.js'

interface Props { data: Extract<Resolved, { module: 'docsPage' }> }
const { data } = Astro.props
const isHome = data.mode === 'home'
const welcome = isHome ? await getLakeDocument('home') : undefined
---

{
  !isHome || !welcome ? (
    <KitDocsPage data={data} />
  ) : (
    <div class="mdl-wide pt-page">
      <h1 class="text-4xl md:text-5xl">{welcome.title}</h1>
      {welcome.shape.lead && <p class="mt-4 text-xl text-muted">{welcome.shape.lead}</p>}
      <div class="lg:hidden"><DocsSidebar client:load tree={data.tree} current={Astro.url.pathname} label="Menu" /></div>
      {data.search && <DocsSearch client:load />}
      {data.sections.map((section) => (
        <section>
          {section.label && <h2 class="text-2xl">{section.label}</h2>}
          <ul>
            {section.pages.map((entry) => <li><a href={entry.href}>{entry.title}</a></li>)}
          </ul>
        </section>
      ))}
    </div>
  )
}

This is the shape of the file, shortened. The real one also lays the sections out as cards, and links to the subheadings of the welcome document with extractToc from @mdlake/site-kit/lib/docs.js.

On the document you get from getLakeDocument, title, summary and route are plain values, shape.lead is its first paragraph, and await html() is the whole text.

Interactive parts need client:load, or they do nothing in the browser. Import them from @mdlake/site-kit/components/.

5. Register both files

Create site.config.ts in the site root.

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

export default defineSiteConfig({
  modules: { docsPage: './src/components/DocsPage.astro' },
  layout: './src/layouts/DocsLayout.astro',
})

Restart astro dev. It reads this file at start, and the log then names each override.

6. Check and record it

PowerShell
npm run check

Look at the site in npm run dev at phone width too. If you changed what the plan or the design says, keep them true. This site’s design.md now says the docs home is its own, and plan.md says which file draws it. Without that, the next person who regenerates from them is misled.

The two new files are yours. No skill, refresh or upgrade writes to them. After a kit upgrade, astro check tells you if the kit’s props changed.

What can go wrong

SymptomCause and fix
The accessibility check times out looking for a Menu buttonAt phone width it opens the docs menu on / and audits it. Keep DocsSidebar on the home page with label="Menu".
Cannot find module '@mdlake/site/part/...'That path is not typed in site code. Import from @mdlake/site-kit/components/....
getLakeDocument('home') is a type errorThe slot is not bound yet. Add it to plan.md and run plan-to-config.
plan-to-config says lake.config.ts was edited by handYour change to the file differs from the plan. Put it in plan.md and run with --overwrite. See Ownership zones.
The page is emptyThe layout has no <slot />.
An empty <nav> or an empty menu button in the headerThe kit’s header with no links in the plan. Replace the header slot, as above.
A button in your page does nothingThe component has no client:load.
The override is ignoredThe path in site.config.ts is wrong, or astro dev was not restarted.

What’s next