Search and SEO
Markbook ships search and a complete SEO meta block by default. The only thing you configure is your site's URL.
Full-text search (Pagefind)#
Pagefind is wired in automatically. Both markbook build and markbook dev invoke it after writing your pages:
dist/
├── pagefind/
│ ├── pagefind-entry.json ← manifest
│ ├── pagefind-ui.js + .css ← the search widget
│ ├── index/<hash>.pf_index ← the searchable index
│ ├── fragment/<hash>.pf_fragment ← content snippets returned in results
│ └── wasm.en.pagefind ← per-language WASMWhere the search input appears#
The built-in shell drops it into the header automatically. In a custom HTML layout, place the {{ search }} placeholder where you want it:
<nav class="topnav">
<a href="/">Brand</a>
{{ search }}
</nav>{{ search }} expands to <div id="markbook-search-ui"> which Pagefind UI mounts into.
Keyboard navigation#
The search box is fully keyboard-driven — no mouse required:
| Key | Action |
|---|---|
Cmd/Ctrl-K or / |
Focus the search input from anywhere on the page |
ArrowDown / ArrowUp |
Move through the results (wraps at the ends; ArrowUp off the first result returns to the input) |
Enter |
Open the focused result |
Escape |
Return focus to the input |
Focus moves to the actual result links, so each shows a :focus-visible ring and screen readers announce it as you go. This is layered on top of Pagefind's default UI automatically whenever search is enabled.
What gets indexed#
Pagefind walks every .html file in the output directory and indexes the text inside elements marked data-pagefind-body. The built-in shell puts that attribute on the <article> wrapper automatically. In a custom layout, you write it yourself:
<article data-pagefind-body>
{{ content }}
</article>Keeping noise out of results#
Elements marked data-pagefind-ignore="all" are excluded from both the index AND the displayed excerpts. Markbook applies it automatically to:
- The
#permalink anchors appended to every H2/H3 (so search snippets don't readContainers#). - The "View / Copy as Markdown" button row.
If your own content has elements you don't want indexed (nav widgets, ads, footer links), add data-pagefind-ignore="all" to them. The literal string "all" is required — an empty value is silently treated as unset.
Styling#
Pagefind UI reads --pagefind-ui-* CSS variables. Markbook's BASE_CSS sets them to brand tokens; if you've disabled base CSS, set them yourself:
:root {
--pagefind-ui-primary: var(--my-accent);
--pagefind-ui-text: var(--my-fg);
--pagefind-ui-background: var(--my-bg);
--pagefind-ui-border-radius: 12px;
}For deeper changes, override .pagefind-ui__* classes directly.
Keyboard shortcuts#
Cmd/Ctrl+K and / both focus the search input (when not already typing into another input). Wired up via a small boot script Markbook always inlines.
Disabling search#
Set search: false in markbook.config.ts to skip Pagefind entirely:
export default defineConfig({
search: false,
});With it off, neither markbook build nor markbook dev invokes Pagefind, so no pagefind/ directory is written, {{ search }} renders empty, and the Pagefind UI script is omitted from {{ bodyEnd }} (the story entry script still loads). Reach for this on single-page or marketing sites that don't need search — or on platforms where Pagefind's native binary can't run, notably ARM64 Linux with a 16K memory page size (e.g. Raspberry Pi 5), where it aborts with Unsupported system page size.
SEO meta block#
For every page, Markbook injects a complete SEO block into <head> — both via the built-in shell AND via the {{ head }} placeholder in custom layouts. Per-page values cascade: frontmatter > config defaults.
| Tag | Source | When emitted |
|---|---|---|
<meta name="description"> |
frontmatter description → config.description |
When non-empty (skipped to avoid a Lighthouse warning, or when your layout already provides its own <meta name="description">) |
<meta name="theme-color"> |
config.themeColor (default #0a1228) |
Always |
<meta name="color-scheme" content="light dark"> |
constant | Always |
<link rel="canonical"> |
${siteUrl}/${page.htmlRelPath} — index.html collapses to its directory URL (/, /guides/) |
Only when siteUrl is set |
| `<meta property="og | title | description |
| `<meta name="twitter | title | description |
Title & description, without the duplicates. The browser/
og/<page title> — <site title>, but when the page title already equalsconfig.title(typical on the homepage) Markbook uses it once — noMy Site — My Site. And if your custom layout hand-writes its own<meta name="description" content="{{ description }}">, Markbook skips its built-in one so the tag isn't emitted twice (theog:/twitter:descriptionvariants are still injected).
Configure once#
export default defineConfig({
title: 'My Site',
description: 'A short blurb. Used as default for og:description.',
siteUrl: 'https://my-site.com', // strict validation; no trailing slash
themeColor: '#7c3aed',
ogImage: 'https://my-site.com/og.png', // absolute URL (not prepended with siteUrl)
});Per-page overrides#
---
title: Pricing
description: Plans that scale with your team.
ogImage: https://my-site.com/og/pricing.png
---sitemap.xml + robots.txt#
Auto-generated whenever siteUrl is set. Emitted by both markbook build (to dist/) and markbook dev (to the in-memory build dir, served from /).
sitemap.xml — every page with <lastmod>. index.html collapses to its directory URL for cleaner canonical form:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://my-site.com/</loc>
<lastmod>2026-06-04</lastmod>
</url>
<url>
<loc>https://my-site.com/pricing.html</loc>
<lastmod>2026-06-04</lastmod>
</url>
</urlset>robots.txt — references the sitemap:
User-agent: *
Allow: /
Sitemap: https://my-site.com/sitemap.xmlWhen siteUrl is unset, neither file is emitted (the sitemap spec requires absolute URLs — no point shipping a stub).
llms.txt for AI consumption#
Markbook also emits an llms.txt index following the llmstxt.org spec, plus per-page plain-markdown mirrors at /llms/<page>.txt. AI assistants can fetch these to get clean text without HTML noise.
Both the built-in shell and the marketing demo's footer link to /llms.txt. Per-page "View as Markdown" / "Copy as Markdown" buttons appear above each article by default; set llmsButtons: false in config to suppress them.
Each .txt file:
- Starts with a UTF-8 BOM (
EF BB BF) so browsers detect the encoding regardless of hostContent-Typeheaders. - Is served with
Content-Type: text/plain; charset=utf-8bymarkbook devandmarkbook preview.
Next steps#
- Config reference → —
siteUrl,themeColor,ogImage,llmsButtons. - Customization → — restyle search to fit your brand.
- CLI reference → — when to use
devvspreview.