TidyPress keeps content and routing in the engine while exposing the website as a public presentation contract. Start with tokens and CSS, add components through stable slots, replace a stable boundary when necessary, and use a custom layout for a page that needs complete control.
All project module paths start with ./ and are resolved from the publish root (site/ in a generated project). Parent traversal with .. is rejected.
Complete example
import { defineConfig } from 'tidypress'
export default defineConfig({
name: 'PocketStation',
capabilities: { enable: ['themingCustom'] },
theme: {
mode: 'custom',
fonts: {
body: { family: 'Geist Mono', source: 'google' },
heading: 'Geist Mono',
mono: 'JetBrains Mono',
},
tokens: {
light: {
background: '#f4f3ef',
foreground: '#0d0f14',
accent: '#f5a623',
},
dark: {
background: '#0d0f14',
foreground: '#f4f3ef',
accent: '#f5a623',
},
},
variables: { '--runtime-ok': '#35c46a' },
css: ['./src/theme/pocketstation.css'],
},
ui: {
components: {
Navbar: './src/theme/Navbar.astro',
Footer: { component: './src/theme/FooterFrame.astro', mode: 'wrap' },
},
slots: {
'navbar.end': ['./src/components/GitHubLink.astro'],
'docs.right.before': ['./src/components/CoreStatus.astro'],
},
layouts: {
benchmark: './src/layouts/BenchmarkLayout.astro',
},
},
mdx: {
components: {
RuntimeGraph: './src/components/RuntimeGraph.astro',
Benchmark: './src/components/Benchmark.astro',
},
},
navigation: {
header: [
{ label: 'Docs', href: '/docs' },
{
type: 'dropdown',
label: 'Resources',
items: [{ label: 'API', href: '/docs/api' }],
},
{ type: 'search', position: 'end' },
],
sidebar: [
{
type: 'group',
label: 'Runtime',
children: [
{ type: 'page', page: 'architecture' },
{ type: 'page', page: 'backpressure', tag: 'core' },
],
},
],
},
})Existing sites do not need any of these fields. Legacy nav and docs.sidebar configuration remains supported.
Tokens, fonts, CSS, and scripts
Theme token names are open-ended semantic names. Every token becomes a CSS custom property by converting camel case to kebab case: accentStrong becomes --tp-accent-strong. The built-in aliases bg, background, fg, foreground, muted, border, surface, codeBg, and codeFg also feed the existing engine variables.
Use theme.variables for an exact custom-property name. Keys must start with --.
Font entries accept a family string or a descriptor:
theme: {
fonts: {
body: { family: 'Inter', source: 'google', display: 'swap' },
heading: { family: 'Acme Sans', source: 'local', url: '/fonts/acme.woff2' },
mono: { family: 'IBM Plex Mono', source: 'external', url: 'https://example.com/fonts.css' },
},
}Sources are system, google, local, and external. Local URLs emit @font-face; Google and external definitions emit stylesheet links.
Configured project CSS is compiled by Vite. Root-relative and HTTP(S) stylesheets are linked directly:
styles: ['./src/styles/brand.css', '/styles/print.css', 'https://cdn.example.com/widget.css'],
scripts: ['./src/scripts/runtime.ts', { src: 'https://cdn.example.com/widget.js', defer: true }],theme.css and top-level styles are combined. Project scripts are bundled as browser modules; public and external scripts retain their type, async, and defer settings.
Stable slots
A slot adds one or more Astro components without taking ownership of its surrounding layout. Components render in array order.
Stable shell slots:
navbar.before,navbar.start,navbar.center,navbar.end,navbar.afterfooter.before,footer.afterdocs.sidebar.before,docs.sidebar.afterdocs.content.before,docs.content.afterdocs.right.before,docs.right.after
Each component receives context. Shell context contains site, route, locale, and theme state. Docs slots also receive the collection and entry summary.
---
const { context } = Astro.props
---
<span data-path={context.route.pathname}>core online</span>Component overrides
These names are public override boundaries:
RouteView,RouteViewShellDocument,DocumentHead,Analytics,ThemeBoot,BodyScripts,RootShellNavbar,Footer,HomeDocLayout,DocHeader,DocSidebar,TocSearch,CodeBlock,PageLayout,NotFound
A string replaces the default. The object form selects replace or wrap:
ui: {
components: {
Search: './src/theme/Search.astro',
Footer: {
component: './src/theme/FooterFrame.astro',
mode: 'wrap',
},
},
}An override receives the original component props and an Original component reference. A wrapper receives the rendered original as its default slot:
---
const { Original, ...props } = Astro.props
---
<div class="footer-frame">
<slot />
</div>RootShell owns visible body chrome inside TidyPress’s document. Replacing it
does not require rebuilding the HTML <head>.
Document can replace the complete HTML document. The default document is split
deliberately: DocumentHead owns metadata, font
and theme assets, analytics, and the pre-paint theme boot; BodyScripts owns
configured browser scripts. Analytics and ThemeBoot can be replaced
individually. RootShell owns only visible body chrome. This lets a theme pack
change policy without copying Base.astro or losing the document skeleton.
The built-in analytics adapters only translate the supported provider name into
its script attribute. Replace Analytics or use configured/project plugin
scripts for another provider. Google fonts likewise accept an explicit url, so
a theme pack is not required to use the baseline Google stylesheet resolver.
DocHeader receives title, description, part, the original markdown body, and the localized copy-action labels. The baseline header puts Copy page (readable rendered text) and Copy Markdown (title, description, and original MDX/Markdown source) in one compact Copy menu. A replacement header can preserve those actions, move them, or provide a different interaction.
RouteView is the complete route-runtime boundary. Its default resolves the
configured presentation, source entry, Markdown rendering, and MDX map.
RouteViewShell is the narrower boundary after that data has been resolved; it
receives the view bundle, rendered content, headings, and MDX components. Custom
collection and doc-form views registered through render.views still take
priority over the baseline shell catalog. A registered view that cannot load is
an explicit build error, not a silent fallback.
Project MDX components
Register components once, then use them in any .mdx entry without a local import:
mdx: {
components: {
Benchmark: './src/components/Benchmark.astro',
},
}<Benchmark scenario="fanout-8" />Names must be PascalCase. Direct relative imports inside MDX continue to work for one-off components. Registered components are merged after TidyPress’s built-ins, so registering Card, Steps, or any other baseline name replaces it everywhere. This is the component-level theming contract used by project theme packs. See Components for the complete baseline pack and its public styling surfaces.
Page layouts
Set layout in docs, writing, projects, or page frontmatter:
---
title: Runtime architecture
layout: wide
---Built-in values:
| Layout | Result |
|---|---|
docs | normal collection/docs chrome |
wide | wider content canvas |
center | centered reading canvas without the desktop docs sidebar |
bare | content without a TidyPress canvas wrapper |
custom | use the layout registered as custom |
Register any named layout through ui.layouts, then use its name in frontmatter. A direct project path such as layout: ./src/layouts/Lab.astro is also accepted. Custom layouts receive title, description, and the page content as their default slot.
Theme the built-in geometry
The built-in layouts provide the default structure, but their presentation is not locked into layout-component utility strings. Project CSS and theme packs can target the semantic classes or change their variables:
| Surface | Semantic class | Main variables |
|---|---|---|
| docs canvas | .tidy-doc-layout | --tidy-doc-max-width, --tidy-doc-content-width, --tidy-doc-gap, --tidy-doc-padding-x, --tidy-doc-padding-y |
| left and right rails | .tidy-doc-sidebar, .tidy-doc-right-rail | --tidy-doc-sidebar-width, --tidy-doc-rail-width |
| page canvas | .tidy-page-canvas | --tidy-page-canvas-max-width, --tidy-page-canvas-padding-x, --tidy-page-canvas-padding-y |
| header actions | .tidy-doc-actions | --tidy-doc-actions-radius |
Layout variants are data, not generated class names. Use
[data-page-layout="docs|wide|center"]; manual pages also expose
[data-doc-form="manual"]. The baseline hides center-layout rails in CSS, so a
theme can restore or reposition them without changing Astro markup.
/* src/theme/brand.css */
.tidy-doc-layout {
--tidy-doc-max-width: 88rem;
--tidy-doc-content-width: 46rem;
--tidy-doc-gap: 2.5rem;
}
.tidy-doc-layout[data-page-layout='wide'] {
--tidy-doc-content-width: 58rem;
}This is the theme-pack path for restyling the standard composition. Use DocLayout, PageLayout, or RootShell when a project needs different markup or ownership rather than different geometry.
Recursive navigation
navigation.header accepts link, dropdown, search, theme-toggle, and project component nodes. Nodes can choose start, center, or end position.
navigation.sidebar accepts recursive group, page, link, separator, component, and autogenerated nodes. A group can set page to make its heading a linked overview. Page values are docs slugs relative to the collection root. autogenerated can optionally select a directory, including inside a linked folder group.
Navbar chrome is independent of its contents:
navbar: {
position: 'sticky',
hideOnScroll: true,
width: 'full',
background: 'color-mix(in srgb, var(--bg) 90%, transparent)',
}Markdown pipeline
Project-local Unified plugins extend the built-in Markdown/MDX pipeline:
markdown: {
remarkPlugins: ['./src/markdown/remark-directives.ts'],
rehypePlugins: ['./src/markdown/rehype-api-links.ts'],
}Each module exports a remark or rehype plugin function. TidyPress keeps its syntax highlighting, internal-link resolution, external-link handling, and inline-code cleanup around those additions.
Presentation plugins
Use one plugin when an integration contributes several presentation features:
// src/plugins/runtime.ts
import { definePlugin } from 'tidypress'
export default definePlugin({
name: 'runtime',
setup(ctx) {
ctx.addMdxComponent('RuntimeGraph', './src/components/RuntimeGraph.astro')
ctx.addSlot('navbar.end', './src/components/Status.astro')
ctx.addThemeComponent('Footer', './src/theme/Footer.astro', { mode: 'wrap' })
ctx.addLayout('benchmark', './src/layouts/Benchmark.astro')
ctx.addStyle('./src/styles/runtime.css')
ctx.addScript('./src/scripts/runtime.ts')
ctx.addRemarkPlugin('./src/markdown/remark-runtime.ts')
ctx.addRehypePlugin('./src/markdown/rehype-runtime.ts')
ctx.addSearchProvider('./src/search/runtime.ts')
},
})plugins: ['./src/plugins/runtime.ts'],Plugin setup runs before Astro configuration is finalized. Contributions enter the same generated manifest as direct config, so build and dev use one extension path.
Search providers
Pagefind remains the zero-config default. A project provider replaces only the query backend while retaining TidyPress’s accessible search trigger, modal, collection filters, and result UI:
// src/search/provider.ts
import { defineSearchProvider } from 'tidypress'
export default defineSearchProvider({
async search(query, { collection } = {}) {
const response = await fetch('/api/search', {
method: 'POST',
body: JSON.stringify({ query, collection }),
})
return response.json()
},
})search: {
provider: './src/search/provider.ts',
shortcuts: [
{ label: 'Quickstart', href: '/docs/getting-started', description: 'Install and run locally' },
],
}Providers return { url, title, excerpt?, collection? }[]. Shortcuts render before a query and yield completely to provider results while typing. Replace the Search UI component as well when the backend needs a different interaction model.