Templates
Templates are JSX/TSX components that receive a page object and render it. Each content file maps to a template by filename convention.
Template props
Every template receives TemplateProps:
import type { TemplateProps } from "@dune/core";
import StaticLayout from "../components/layout.tsx";
export default function PostTemplate({ page, pageTitle, site, config, nav, Layout, children }: TemplateProps) {
const LayoutComponent = Layout ?? StaticLayout;
return (
<LayoutComponent site={site} config={config} nav={nav} page={page} pageTitle={pageTitle}>
<article>
<h1>{page.frontmatter.title}</h1>
<time datetime={page.frontmatter.date}>
{new Date(page.frontmatter.date).toLocaleDateString()}
</time>
<div>{children}</div>
{page.frontmatter.taxonomy?.tag?.map((tag) => (
<a key={tag} href={`/tag/${tag}`}>{tag}</a>
))}
</article>
</LayoutComponent>
);
}
What's in TemplateProps
| Prop | Type | Description |
|---|---|---|
page |
Page |
The full page object (frontmatter, content, media, relations) |
pageTitle |
string |
Pre-formatted title: "Title - Descriptor | Site Name" |
site |
SiteConfig |
Site configuration (title, URL, metadata) |
config |
DuneConfig |
Full merged configuration |
nav |
PageIndex[] |
Top-level navigation pages |
navAll |
PageIndex[]? |
Full site navigation for the page's language — every page in the index with depth, order, parentPath (same data as /api/nav) |
translations |
PageTranslation[]? |
Languages the current page exists in, with the URL for each — for language switchers and hreflang links (see below) |
Layout |
Component? |
Dynamically loaded layout component (see below) |
collection |
Collection? |
Collection results if page defines one |
children |
Element? |
Pre-rendered content (HTML wrapped in a div) |
searchQuery |
string? |
Set when rendering the /search page. The raw query string from ?q=. |
searchResults |
SearchResult[]? |
Set when rendering the /search page. Ranked results from the search engine. |
Using the Layout prop
The router passes a Layout prop that is loaded dynamically on each request. This ensures layout changes take effect during development without restarting the server. Templates should prefer the Layout prop over a static import:
import StaticLayout from "../components/layout.tsx";
export default function MyTemplate({ Layout, ...props }: TemplateProps) {
// Layout prop is fresh on every request; StaticLayout is the build-time fallback
const LayoutComponent = Layout ?? StaticLayout;
return (
<LayoutComponent {...props}>
{/* content */}
</LayoutComponent>
);
}
If a template only uses import Layout from "../components/layout.tsx" directly, layout file changes won't appear until the server restarts. Dune logs a warning when it detects this pattern during development.
Language switchers and hreflang links
On multilingual sites (more than one entry in config.system.languages.supported), templates receive a translations prop: one entry per language the current page actually exists in. Each entry has:
| Field | Description |
|---|---|
lang |
Language code, e.g. "de" |
route |
Canonical route shared across languages, e.g. "/about/" |
url |
Root-relative URL for that language, e.g. "/de/about/" |
URLs respect config.system.languages.include_default_in_url — the default language is unprefixed unless that option is set. The list includes the page's own language; mark it active or filter it out. On single-language sites translations is an empty array, so switchers naturally render nothing.
A language switcher:
{translations && translations.length > 1 && (
<nav class="lang-switcher">
{translations.map((t) => (
<a href={t.url} class={t.lang === page.language ? "active" : ""}>
{t.lang.toUpperCase()}
</a>
))}
</nav>
)}
Hreflang alternate links in the document <head> (prefix with site.url for absolute URLs, as search engines expect):
{translations?.map((t) => (
<link rel="alternate" hreflang={t.lang} href={site.url + t.url} />
))}
What's in Page
| Property | Type | Description |
|---|---|---|
page.frontmatter |
PageFrontmatter |
All frontmatter fields |
page.route |
string |
URL path: /blog/hello-world |
page.format |
ContentFormat |
"md", "tsx", or "mdx" |
page.template |
string |
Template name: "post" |
page.media |
MediaFile[] |
Co-located media files |
page.html() |
Promise<string> |
Rendered HTML (Markdown pages) |
page.summary() |
Promise<string> |
Auto-generated excerpt |
page.children() |
Promise<Page[]> |
Child pages |
page.parent() |
Promise<Page|null> |
Parent page |
page.siblings() |
Promise<Page[]> |
Sibling pages |
Note: html(), children(), parent(), and siblings() are lazy — they only load data when called.
Async templates
These lazy accessors return Promises, so a template that calls them must be declared async function. Dune awaits the top-level template component before rendering; everything the template returns renders synchronously. Two constraints follow: nested components (including Layout) must be synchronous, and an async template cannot use hooks. If you only need the rendered body, prefer the children prop — it's the pre-rendered HTML and keeps the template synchronous.
Template naming convention
| Content file | Template used |
|---|---|
default.md |
templates/default.tsx |
post.md |
templates/post.tsx |
blog.md |
templates/blog.tsx |
item.md |
templates/item.tsx |
Override with the template frontmatter field:
template: landing # uses templates/landing.tsx instead
Template names affect routing
Template names do more than select a component — they also determine how Dune routes content in plain (non-numeric) folders. When Dune builds the page index, if a content file's stem matches a template defined in templates/, the parent folder is treated as a page folder and the folder's path becomes the route:
blog/my-post/post.md → /blog/my-post (because templates/post.tsx exists)
news/launch/article.md → /news/launch (because templates/article.tsx exists)
Files whose stems don't match any template — and aren't reserved stems (default, index) — are treated as flat content files at their own routes:
articles/first.md → /articles/first (no templates/first.tsx)
This means adding a new template to your theme can change how existing content files are routed. Keep template names purposeful: verbs and nouns that describe the content type (post, article, project, event), not structural words that might collide with content filenames.
Reserved template names
The following template names are used by Dune's built-in routes:
| Template | Route | When rendered |
|---|---|---|
search |
/search |
When a visitor submits a search query. Receives searchQuery and searchResults in props. If not present, Dune falls back to a built-in standalone page. |
See Search for a full example.
Blog listing template example
import StaticLayout from "../components/layout.tsx";
export default function BlogTemplate({ page, pageTitle, site, config, nav, Layout, collection, children }: TemplateProps) {
const LayoutComponent = Layout ?? StaticLayout;
return (
<LayoutComponent site={site} config={config} nav={nav} page={page} pageTitle={pageTitle}>
<h1>{page.frontmatter.title}</h1>
<div>{children}</div>
{collection && (
<ul>
{collection.items.map((post) => (
<li key={post.route}>
<a href={post.route}>
<h2>{post.frontmatter.title}</h2>
<time>{post.frontmatter.date}</time>
</a>
</li>
))}
</ul>
)}
{collection?.hasNext && (
<a href={`${page.route}/page:${collection.page + 1}`}>
Older posts →
</a>
)}
</LayoutComponent>
);
}