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/types";
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 |
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.
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.
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>
);
}