Creating a Theme
This guide walks through building a minimal theme from scratch, then extending it with layouts, components, and MDX support.
1. Create the directory structure
themes/
└── my-theme/
├── theme.yaml
└── templates/
└── default.tsx
That's the minimum viable theme — a manifest and one template.
2. Write the manifest
themes/my-theme/theme.yaml:
name: my-theme
version: 1.0.0
description: "My custom theme"
author: "Your Name"
All fields except name are optional. Set parent: default to inherit templates from another theme (see Theme Inheritance).
3. Write your first template
themes/my-theme/templates/default.tsx:
import type { TemplateProps } from "@dune/core";
export default async function DefaultTemplate({ page, children, site }: TemplateProps) {
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{page.frontmatter.title} | {site.title}</title>
</head>
<body>
<main>
<h1>{page.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: await page.html() }} />
</main>
</body>
</html>
);
}
children contains the pre-rendered page HTML as a JSX element — using it keeps the template synchronous. You can also call await page.html() directly if you need the raw string, but then the template must be declared async function, as above.
Only the top-level template component may be async: Dune awaits the template itself before rendering, but everything it returns renders synchronously. Nested components — including the Layout a template wraps itself in — must stay synchronous, and an async template cannot use hooks.
4. Activate the theme
In config/site.yaml (via your site's system config or dune.config.ts):
# dune.config.ts
export default {
theme: {
name: "my-theme",
},
};
Run dune dev — your theme is active.
5. Add a layout component
Extract shared HTML into a layout to avoid repetition across templates:
themes/my-theme/components/layout.tsx:
import type { TemplateProps } from "@dune/core";
import type { ComponentChildren } from "preact";
interface LayoutProps extends Omit<TemplateProps, "children"> {
children: ComponentChildren;
}
export default function Layout({ page, site, children }: LayoutProps) {
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{page.frontmatter.title} | {site.title}</title>
<link rel="stylesheet" href="/themes/my-theme/static/styles.css" />
</head>
<body>
<header>
<a href="/">{site.title}</a>
</header>
<main>{children}</main>
<footer>
<p>© {new Date().getFullYear()} {site.title}</p>
</footer>
</body>
</html>
);
}
Then use it in templates via the Layout prop (loaded dynamically for hot-reload) with a static import as fallback:
import type { TemplateProps } from "@dune/core";
import StaticLayout from "../components/layout.tsx";
export default function DefaultTemplate({ page, site, config, nav, Layout, children }: TemplateProps) {
// Layout prop is reloaded on each request in dev mode — prefer it over the static import
const LayoutComponent = Layout ?? StaticLayout;
return (
<LayoutComponent page={page} site={site} config={config} nav={nav}>
{children}
</LayoutComponent>
);
}
Hot-reload note: Always use
Layout ?? StaticLayout— never import layout directly as the only option. Direct imports are module-cached and won't reflect changes during development. Dune logs a warning if it detects a static-only layout import in a template.
6. Add static assets
Place CSS, fonts, and other static files in themes/my-theme/static/. They're served at their full namespaced path:
themes/my-theme/static/styles.css → GET /themes/my-theme/static/styles.css
themes/my-theme/static/fonts/Inter.woff2 → GET /themes/my-theme/static/fonts/Inter.woff2
The namespace ensures themes don't clobber each other's assets. Reference files using the full path in your layout — e.g. href="/themes/my-theme/static/styles.css". Use your theme's name from theme.yaml for the segment.
The site-level static/ directory is separate: files there are served directly at / without namespacing (static/favicon.ico → /favicon.ico).
7. Add a navigation component
Access the nav prop (top-level pages) to render a site navigation:
// In your layout:
<nav>
{nav.map((item) => (
<a key={item.route} href={item.route}>{item.navTitle}</a>
))}
</nav>
item.navTitle uses the page's nav_title frontmatter field if set, falling back to title.
8. Add MDX component support (optional)
If your site uses .mdx content files and you want custom components available inside them, create themes/my-theme/mdx-components.ts:
// themes/my-theme/mdx-components.ts
import { Alert } from "./components/Alert.tsx";
import { Callout } from "./components/Callout.tsx";
export default { Alert, Callout };
Dune loads this file automatically at startup. See MDX Content for details.
9. Add a collection template
For listing pages (blog index, docs index, etc.), use the collection prop:
// themes/my-theme/templates/blog.tsx
import type { TemplateProps } from "@dune/core";
import StaticLayout from "../components/layout.tsx";
export default function BlogTemplate({ page, site, config, nav, Layout, collection, children }: TemplateProps) {
const LayoutComponent = Layout ?? StaticLayout;
return (
<LayoutComponent page={page} site={site} config={config} nav={nav}>
<h1>{page.frontmatter.title}</h1>
{children}
<ul>
{collection?.items.map((post) => (
<li key={post.route}>
<a href={post.route}>{post.frontmatter.title}</a>
{post.frontmatter.date && <time>{post.frontmatter.date}</time>}
</li>
))}
</ul>
{collection?.hasNext && (
<a href={`${page.route}/page:${(collection.page ?? 1) + 1}`}>Older →</a>
)}
</LayoutComponent>
);
}
The content file controls collection query settings via frontmatter — the template just renders whatever collection contains.
Theme development tips
- Run
dune dev— templates reload automatically on file change; no restart needed theme.customconfig — pass theme-specific settings viaconfig.theme.custom(type-unsafe; validate in your template)- Error template — add
templates/error.tsxto customize 404 and 500 pages; it receives{ statusCode, message }in frontmatter - Watch the console — Dune logs warnings for static layout imports and MDX load failures during startup