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.