Theme ¶
The default theme renders a header, optional sidebar, content area, optional outline, footer navigation, edit link, last-updated timestamp, search, social links, and theme toggle.
Sidebar ¶
Use hand-written groups when order matters.
sidebar: {
'/guide/': [
{
text: 'Guide',
items: [{ text: 'Getting Started', link: '/guide/getting-started' }],
},
{
text: 'Reference',
collapsed: true,
items: [{ text: 'CLI', link: '/reference/cli' }],
},
],
}Generated groups collect pages by route prefix and sort by order, index page first, then title.
sidebar: {
'/blog/': [{ text: 'Blog', generate: '/blog/' }],
}Outline ¶
Set themeConfig.outline.level globally, then override a page with frontmatter:
outline: deep
aside: false
sidebar: falseLogo And Social Icons ¶
themeConfig: {
logo: '/logo.svg',
socialLinks: [
{ icon: 'github', link: 'https://github.com/markup-carve/carve-press' },
],
}Built-in icons include github, gitlab, x, mastodon, bluesky, discord, slack, npm, linkedin, youtube, and rss. Custom SVG icons are sanitized before being inlined.
Custom CSS ¶
Use theme.extraCss when you only need overrides:
theme: {
extraCss: ['docs/public/site.css'],
}Use theme.css only when replacing the default theme completely.
Custom Layouts ¶
Register a layout function by name:
import type { Layout } from '@markup-carve/carve-press'
const landing: Layout = (ctx) => ctx.rendered.html
export default defineConfig({
title: 'Docs',
layouts: { landing },
})Then select it in page frontmatter with layout: landing.
Version Switcher ¶
A project that publishes each major release at its own base URL can offer a switcher and a banner. Both are off unless themeConfig.versions is set.
themeConfig: {
versions: {
current: '5.x',
banner: 'You are reading the documentation for an older release.',
items: [
{ text: '5.x (latest)', link: 'https://book.example.com/5.x/', current: true },
{ text: '4.x', link: 'https://book.example.com/4.x/' },
],
},
}The switcher renders in the header using the same dropdown as the primary nav. The banner appears only when this build’s own hostname and base match an entry that is not the one marked current. If the build cannot be matched to any entry - no hostname, for instance - no banner renders at all: telling a reader they are on old documentation when they are not is worse than saying nothing.
Both strings go through the locale label map, so a translated site can translate them.
Assets And Cache Busting ¶
CarvePress copies the theme stylesheet and its small scripts into assets/ with a content hash in the filename - style.61f743c0.css - and references those names from every page. A deploy therefore cannot be served from a stale cache, which is the failure that makes a site look broken for exactly the readers whose cache is warmest.
Turn it off when something outside the build references the plain names:
assets: { hash: false }Bringing Your Own Bundler ¶
There is deliberately no asset pipeline here. The theme is plain CSS and a handful of vanilla scripts, which is why npm install puts no toolchain underneath your site.
When a site outgrows that - SCSS, TypeScript, Tailwind, npm component libraries - run your own bundler and point CarvePress at its output:
theme: { extraCss: ['build/site.css'] },
head: [['script', { type: 'module', src: '/site.js', defer: '' }]],Build your assets into publicDir (or into a path you reference above) before running carve-press build, and let your bundler own hashing for the files it emits. CarvePress hashes only what it emits itself.
Islands ¶
A page can mount an interactive component without this project shipping a framework to do it. Declare the module, then mount it:
islands: {
counter: { module: 'islands/counter.js', hydrate: 'visible' },
}{name="counter" props="{\"start\": 3}"}
::: island
Counter unavailable without JavaScript.
:::The block’s own content is the fallback: it renders statically and stays on screen until the module replaces it, so an island degrades to what the author wrote rather than to an empty box.
Your module is copied as-is - not bundled, not transpiled - and imported lazily. It default-exports a function that receives the element and the parsed props:
export default (element, props) => {
let count = props.start ?? 0
element.textContent = String(count)
element.addEventListener('click', () => {
count += 1
element.textContent = String(count)
})
}hydrate is load (default), idle, or visible. The loader and the registry are emitted only for pages that actually contain an island, so the rest of the site downloads nothing extra.
Route Rewrites ¶
A source tree organized for editing rarely matches the URL tree a reader should see:
rewrites: {
'packages/a/docs/*': '/a/*',
'legacy/handbook.crv': '/handbook/',
}Keys are content-relative source paths. A trailing /* on both sides moves a directory and keeps the tail; the longest matching pattern wins, so config order never decides the result. A rewrite that matches no source file fails the build - otherwise a typo is indistinguishable from a page that moved.
Last updated