Examples
Concrete, copy-paste-ready patterns for the most common Blog-Doc theme-building tasks.
Concrete, copy-paste-ready patterns for the most common theme-building tasks. Each example is a working snippet you can drop into your own templates.
Minimal theme
The smallest valid theme that activates without errors:
my-theme/
├── theme.json
├── screenshot.webp
└── templates/
├── home.html
└── 404.html
theme.json
{
"name": "My Theme",
"id": "my-theme",
"version": "1.0.0"
}
home.html
<!doctype html>
<html lang="{{ site.language | defaults('en') }}">
<head>
<meta charset="UTF-8" />
<title>{{ site.title }}</title>
</head>
<body>
<h1>{{ site.title }}</h1>
<p>{{ site.description }}</p>
</body>
</html>
404.html
<!doctype html>
<html lang="{{ site.language | defaults('en') }}">
<head>
<meta charset="UTF-8" />
<title>{{ title }} — {{ site.title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<a href="/">Go home</a>
</body>
</html>
Rendering a page or post
A single content.html template can serve both pages and posts by branching on route flags:
<!-- content.html = page.html + post.html -->
<!doctype html>
<html lang="{{ site.language | defaults('en') }}">
<head>
<meta charset="UTF-8" />
{{#if pageRoute}}
<title>{{ page.title }} — {{ site.title }}</title>
{{#elseif postRoute}}
<title>{{ post.title }} — {{ site.title }}</title>
{{/if}}
</head>
<body>
{{#if pageRoute}}
<h1>{{ page.title }}</h1>
{{#elseif postRoute}}
<h1>{{ post.title }}</h1>
<time>{{ post.date }}</time>
{{/if}}
<main>{{ html_content }}</main>
</body>
</html>
Blog index loop
<!-- blog.html -->
{{#each posts}}
<article>
<h2><a href="/blog/{{ slug }}">{{ title }}</a></h2>
<time>{{ date }}</time>
<span>By {{ author }}</span>
{{#if category}}
<a href="/categories/{{ category }}">{{ categoryName }}</a>
{{/if}}
{{#if tags}}
{{#each tags}}
<a href="/tags/{{ this | slugify }}">{{ this }}</a>
{{/each}}
{{/if}}
<p>{{ excerpt }}</p>
</article>
{{/each}}
Pagination controls
{{#if pagination.hasPrev}}
<a href="/blog/page/{{ pagination.prevPage }}">← Newer</a>
{{/if}}
<span>Page {{ pagination.currentPage }} of {{ pagination.totalPages }}</span>
{{#if pagination.hasNext}}
<a href="/blog/page/{{ pagination.nextPage }}">Older →</a>
{{/if}}
On the home page, hasPrev is always false (it's always page 1), so you only need to check hasNext:
{{#if pagination.hasNext}}
<a href="/blog/page/{{ pagination.nextPage }}">See older posts →</a>
{{/if}}
A generic pagination covering homeRoute, blogRoute and taxonomyRoute:
<nav aria-label="pagination" id="pagination">
<ul class="pagination">
{{#if pagination.hasNext}}
<!-- Older Posts (only if there's a next page) -->
<li>
<a href="{{pagination.lastPageHref}}" title="Last page" aria-label="Go to the last page">
<span aria-hidden="true">
<img src="{{theme.assetsUrl}}/images/chevrons-left.svg" alt="Last page" />
</span>
<span class="hidden">Last page</span>
</a>
</li>
<li>
<a class="button-default prev" href="{{pagination.nextPageHref}}" aria-label="Older Posts">
<span aria-hidden="true">Older Posts</span>
<span class="hidden">Go to older posts</span>
</a>
</li>
{{/if}}
{{#if pagination.hasPrev}}
<!-- Newer Posts (only if there's a previous page) -->
<li>
<a class="button-default next" href="{{pagination.prevPageHref}}" aria-label="Newer Posts">
<span aria-hidden="true">Newer Posts</span>
<span class="hidden">Go to newer posts</span>
</a>
</li>
<li>
<a href="{{pagination.firstPageHref}}" title="First page" aria-label="Go to the first page">
<span aria-hidden="true">
<img src="{{theme.assetsUrl}}/images/chevrons-right.svg" alt="Last page" />
</span>
<span class="hidden">First page</span>
</a>
</li>
{{/if}}
</ul>
</nav>
Navigation from post
<nav aria-label="pagination" id="pagination">
<ul class="pagination">
{{#if prevPost}}
<!-- Previous Post Link (only if there's a previous post) -->
<li>
<a class="post-link prev" role="button" href="/blog/{{prevPost.slug}}" aria-label="Go to the previous post">
<span aria-hidden="true">Older: {{prevPost.title}}</span>
<span class="hidden">Go to the previous post</span>
</a>
</li>
{{/if}}
{{#if nextPost}}
<!-- Next Post Link (only if there's a next post) -->
<li>
<a class="post-link next" role="button" href="/blog/{{nextPost.slug}}" aria-label="Go to the next post">
<span aria-hidden="true">Newer: {{nextPost.title}}</span>
<span class="hidden">Go to the next post</span>
</a>
</li>
{{/if}}
</ul>
<nav/>
Related Posts
Display related posts to the curent one:
<section class="related-posts">
<h3>You Might Also Like</h3>
<div class="related-posts-grid">
{{#each relatedPosts}}
<div class="related-post-card">
{{#if featured_image}}
<a href="/blog/{{slug}}" class="related-post-image">
<img src="{{featured_image}}" alt="Featured image for the post: {{title}}" />
</a>
{{/if}}
<div class="related-post-content">
<h4><a href="/blog/{{slug}}">{{title}}</a></h4>
{{#if description}}
<p class="related-post-excerpt">{{description}}</p>
{{/if}}
</div>
</div>
{{/each}}
</div>
</section>
Navigation from menu
<nav>
{{#each menus["primary"]}}
<a href="{{ url }}">{{ label }}</a>
{{#if children | length}}
<ul>
{{#each1 children}}
<li><a href="{{ url }}">{{ label }}</a></li>
{{/each1}}
</ul>
{{/if}}
{{/each}}
</nav>
Note: To avoid menu name collisions and accidental overwriting, prefix menu names with the theme
id. For example, use"default-primary"and"default-footer"instead of generic names like"primary"or"footer". Then reference the prefixed menu in your template, for example:{{#each menus["default-primary"]}}.
Navigation from pages list
<nav>
{{#each pages}}
{{#if root}}
<a href="/{{ slug }}">{{ title }}</a>
{{#else}}
<a href="/pages/{{ slug }}">{{ title }}</a>
{{/if}}
{{/each}}
</nav>
Shared partial with route flags
<!-- partials/meta.html -->
{{#if postRoute}}
<title>{{ post.title }} — {{ site.title }}</title>
<meta name="description" content="{{ post.description | defaults(site.description) }}" />
{{#elseif pageRoute}}
<title>{{ page.title }} — {{ site.title }}</title>
<meta name="description" content="{{ page.description | defaults(site.description) }}" />
{{#elseif notFoundRoute}}
<title>{{ title }} — {{ site.title }}</title>
{{#else}}
<title>{{ site.title }}</title>
<meta name="description" content="{{ site.description }}" />
{{/if}}
Include it from any template:
{{#include("partials/meta.html")}}
Featured image with fallback
Using an {{#if}} block:
{{#if post.featured_image}}
<img src="{{ post.featured_image }}" alt="{{ post.title }}" />
{{#else}}
<img src="/assets/images/default.webp" alt="{{ post.title }}" />
{{/if}}
Or using the defaults filter:
<img src="{{ post.featured_image | defaults('/assets/images/default.webp') }}" alt="{{ post.title }}" />
Taxonomy page
<!-- taxonomy.html -->
<h1>
{{#if taxonomyType == 'category'}}
Posts in {{ categoryName }}
{{#else}} Posts tagged {{ tagName }}
{{/if}}
</h1>
{{#each posts}}
<article>
<h2><a href="/blog/{{ slug }}">{{ title }}</a></h2>
<p>{{ excerpt }}</p>
</article>
{{/each}}
{{#if pagination.hasPrev}}
<a href="?page={{ pagination.prevPage }}">← Previous</a>
{{/if}}
{{#if pagination.hasNext}}
<a href="?page={{ pagination.nextPage }}">Next →</a>
{{/if}}
Detecting static build
{{#if isGenerateStatic}}
<!-- Rendered in static build output only -->
<script>
console.log("static build")
</script>
{{#else}}
<!-- Rendered during live development only -->
<script src="/app/themes/my-theme/assets/js/live-reload.js"></script>
{{/if}}
Consuming the search index
The search index is served at /data/search.json — same URL in both development and after a static build.
async function initSearch(inputEl, resultsEl) {
const res = await fetch("/data/search.json")
const index = await res.json()
inputEl.addEventListener("input", function () {
const q = inputEl.value.trim().toLowerCase()
if (!q) {
resultsEl.innerHTML = ""
return
}
const hits = index.filter(function (entry) {
return (
entry.title.toLowerCase().includes(q) ||
(entry.description || "").toLowerCase().includes(q) ||
(entry.body || "").toLowerCase().includes(q)
)
})
resultsEl.innerHTML = hits
.slice(0, 8)
.map(function (h) {
return '<li><a href="' + h.url + '">' + h.title + "</a></li>"
})
.join("")
})
}
Each entry in the index:
{
"type": "page",
"title": "About",
"description": "About this site",
"url": "/about/",
"body": "First 300 chars of plain text..."
}
Posts additionally include date, category, and tags.