# Customization Markbook layers customization from "smallest change you might want" to "I want to write the HTML myself." Pick the smallest one that solves your problem. | Layer | What it does | Use when | | --- | --- | --- | | 1. `css` | Inline your CSS after Markbook's built-in styles | You just want to recolor, change fonts, or restyle a few elements | | 2. `disableBaseCss` | Opt out of the built-in chrome stylesheet entirely | You want to design every element from scratch (often paired with layer 3) | | 3. `layoutsDir` + `layout` | Replace Markbook's `` shell with your own HTML files | You want a marketing landing, a blog template, anything that isn't a docs chrome | | 4. `transformHtml` | Post-process every page's final HTML string | Narrow tweaks — analytics injection, attribute rewrites, structured-data tags | Each layer is opt-in; you can use one, two, all four, or none. :::callout{type=tip} **Pick the smallest layer that solves your problem.** Token overrides via `css` cover ~80% of branding needs. Reach for `disableBaseCss` + `layoutsDir` only when the docs chrome itself is wrong for your site. ::: ## Layer 1 — Token overrides (`css`) The cheapest fix. Markbook's chrome is driven by CSS variables (`--mb-bg`, `--mb-fg`, `--mb-accent`, `--mb-link`, `--mb-content-width`, ...). Override them in a CSS file: ```css /* markbook.css */ :root { --mb-accent: #ff6b35; --mb-link: #ff8552; --mb-content-width: 880px; --mb-font-sans: 'Inter', system-ui; } :root[data-theme="dark"] { --mb-accent: #ff9b80; } ``` Wire it into `markbook.config.ts`: ```ts export default defineConfig({ css: ['./markbook.css'], }); ``` Done. Markbook inlines your CSS after `BASE_CSS` so your overrides win. No HTML changes needed. ## Layer 2 — Drop the built-in chrome (`disableBaseCss`) When you want every selector to be yours: ```ts export default defineConfig({ css: ['./my-styles.css'], disableBaseCss: true, }); ``` Markbook now ships **zero CSS** for the chrome. Your stylesheet is responsible for everything — header layout, nav, content typography, code blocks, the lot. Markbook still emits its stable DOM contract (`.markbook-header`, `.markbook-sidebar`, `.markbook-content`, `.markbook-toc`, `[data-pagefind-body]`, `[data-markbook-theme-toggle]`, ...) — you just write all the rules. This is usually paired with layer 3 below, since at that point you might as well swap the shell too. ## Layer 3 — HTML layouts (`layoutsDir`) When the docs shell isn't what you want at all — marketing site, blog, portfolio — write your own HTML. Set up a layouts directory: ```ts export default defineConfig({ layoutsDir: 'layouts', // string or string[] layout: 'default', // default layout for every page }); ``` Create `layouts/default.html`: ```html {{ browserTitle }} {{ head }}
My Site {{ search }} {{ themeToggle }}
{{ pageActions }}
{{ content }}
{{ bodyEnd }} ``` Per-page opt-in via frontmatter: ```yaml --- layout: landing # uses layouts/landing.html instead of the default --- ``` Set `layout: false` in frontmatter to force the built-in shell on a single page even when `config.layout` is set. Layouts are validated strictly: - Missing `{{ content }}` placeholder → throws. - More than one `{{ content }}` → throws. - Unknown placeholder name (typo guard) → throws. - Named layout file not found → throws (no silent fallback). - HTML comments are preserved verbatim — placeholders inside `` are NOT substituted. See [placeholder reference →](../reference/placeholders.html) for the full token list. ## Layer 4 — Post-process (`transformHtml`) Sometimes you need to mutate the final HTML — inject an analytics snippet, rewrite a specific attribute, add JSON-LD structured data. `transformHtml` runs last, after either the built-in shell or your layout has produced output: ```ts export default defineConfig({ transformHtml: async (html, page) => { // Add Google Analytics return html.replace( '', ``, ); }, }); ``` The callback gets: - `html` — the fully-rendered HTML string - `page` — `{ relPath, htmlRelPath, title, frontmatter }` for per-page decisions Use sparingly. If you find yourself doing more than a regex or two, you probably want a layout instead. :::callout{type=info} **You probably don't need `transformHtml` for SEO URLs or titles.** The three most common uses are now automatic: - The canonical URL and `og:url` collapse `index.html` to the directory URL — the homepage is `https://site.com/`, a section index is `https://site.com/guides/` (no more string-replacing `/index.html` away). - The `` / `og:title` drops the duplicated site title, so a homepage whose title equals `config.title` is `My Site`, not `My Site — My Site`. - The built-in `<meta name="description">` is skipped when your layout already hand-writes one (no duplicate tag). See [Search & SEO](./search-and-seo.html). Reach for `transformHtml` only for things Markbook can't know about — analytics snippets, custom structured data, third-party widget markup. ::: ## A worked example [`examples/marketing-demo/`](https://github.com/doidor/markbook/tree/main/examples/marketing-demo) in the Markbook repo combines layers 2 + 3: - `disableBaseCss: true` — Markbook ships zero CSS. - `layoutsDir: 'layouts'` + `layout: 'default'` — replaces the docs shell with a marketing layout (top nav + content + footer). - Per-page frontmatter `layout: landing` on the home page only — opts into a hero-style variant. - A hand-rolled `cumulus.css` with ~560 lines covering everything. Result: a navy + coral marketing site for a fictional cloud platform. Same engine, totally different output. ## Next steps - [Search & SEO →](./search-and-seo.html) — what the SEO defaults give you. - [Config reference →](../reference/config.html) — full options. - [Placeholder reference →](../reference/placeholders.html) — every layout token.