Templates
Learn how templates define page-specific content and are automatically routed by Nimbu
Templates define the content and structure of specific page types in your Nimbu theme. While layouts provide the global HTML structure, templates focus on the unique content for each page.
How Templates Work
When a request comes to your Nimbu site:
- Nimbu matches the URL to a route
- Selects the appropriate template
- Renders the template inside the layout
- Returns the complete HTML
Template Types
Nimbu uses these standard template types:
| Template | Route | Purpose |
|---|---|---|
index.liquid | / | Homepage |
page.liquid | /about, /contact | Generic content pages |
blog.liquid | /blog | Blog listing page |
article.liquid | /blog/post-title | Individual blog posts |
shop/index.liquid | /shop | Shop homepage |
shop/product.liquid | /products/product-slug | Product details |
shop/collection.liquid | /shop/collection-slug | Product collections |
customers/login.liquid | /login | Customer login |
customers/register.liquid | /register | Customer registration |
customers/account.liquid | /account | Customer dashboard |
customers/oauth2_consent.liquid | /oauth2/authorize | Consent screen when your site is an OpenID Connect provider (details) |
search.liquid | /search?q=... | Search results (details) |
404.liquid | Unknown pages, products and collections | Optional themed not-found page (details) |
robots.txt.liquid | /robots.txt | Optional extra robots.txt rules, rendered without layout (details) |
Template-Specific Variables
Each template type has access to specific variables:
Product Template
{{ product.name }}
{{ product.price }}
{{ product.description }}
{{ product.images }}
{{ product.variants }}
{{ product.on_sale }}Page Template
{{ page.title }}
{{ page.url }}
{{ page.updated_at }}
{{ page.translations }}Article Template
{{ article.title }}
{{ article.content }}
{{ article.author }}
{{ article.published_at }}
{{ article.blog }}Blog Template
{{ blog.name }}
{{ blog.articles }}
{{ blog.tags }}See Global Variables for complete variable reference.
Creating Templates
Homepage (templates/index.liquid)
<section class="hero">
<div class="container">
<h1>{% editable_field hero_title %}Welcome to {{ site.name }}{% endeditable_field %}</h1>
<div class="hero-content">
{% editable_text hero_text %}
<p>Build amazing websites with Nimbu.</p>
{% endeditable_text %}
</div>
<a href="/shop" class="cta-button">
{% translate 'cta.shop_now', default: 'Shop Now' %}
</a>
</div>
</section>
<section class="featured-products">
<div class="container">
<h2>Featured Products</h2>
<div class="product-grid">
{% for product in products.featured limit: 4 %}
{% include 'product-card', product: product %}
{% endfor %}
</div>
</div>
</section>
<section class="content-blocks">
<div class="container">
{% editable_canvas blocks %}
{% repeatable "Text Block" %}
{% include 'repeatables/text-block' %}
{% endrepeatable %}
{% repeatable "Image Gallery" %}
{% include 'repeatables/gallery' %}
{% endrepeatable %}
{% endeditable_canvas %}
</div>
</section>Page Template (templates/page.liquid)
Generic template for content pages:
<article class="page">
<header class="page-header">
<div class="container">
{% breadcrumbs %}
<h1>{{ page.title }}</h1>
{% if page.excerpt %}
<p class="lead">{{ page.excerpt }}</p>
{% endif %}
</div>
</header>
<div class="page-content">
<div class="container">
{% editable_canvas content %}
{% repeatable "Full Width Text" %}
{% include 'repeatables/full-text' %}
{% endrepeatable %}
{% repeatable "Text & Image" %}
{% include 'repeatables/text-image' %}
{% endrepeatable %}
{% repeatable "Cards" %}
{% include 'repeatables/cards' %}
{% endrepeatable %}
{% repeatable "Call to Action" %}
{% include 'repeatables/cta' %}
{% endrepeatable %}
{% endeditable_canvas %}
</div>
</div>
</article>Product Template (templates/shop/product.liquid)
Full product page with images, variants, and add to cart:
<div class="product-page">
<div class="container">
{% breadcrumbs %}
<div class="product-main">
<!-- Product Images -->
<div class="product-images">
{% if product.images.size > 0 %}
<div class="main-image">
<img src="{{ product.images.first.url | filter, width: '800px' }}"
alt="{{ product.name }}">
</div>
{% if product.images.size > 1 %}
<div class="image-thumbnails">
{% for image in product.images %}
<img src="{{ image.url | filter, width: '100px' }}"
alt="{{ product.name }}"
data-full="{{ image.url | filter, width: '800px' }}">
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
<!-- Product Info -->
<div class="product-info">
<h1>{{ product.name }}</h1>
<div class="product-price">
{% if product.on_sale %}
<span class="sale-price">{{ product.on_sale_price | money_with_currency }}</span>
<span class="original-price">{{ product.price | money_with_currency }}</span>
{% else %}
<span class="price">{{ product.price | money_with_currency }}</span>
{% endif %}
</div>
<div class="product-description">
{{ product.description }}
</div>
<!-- Add to Cart Form -->
{% if product.status != 'sold_out' %}
{{ product | add_to_cart, btn_class: 'btn btn-primary', add_label: 'Add to Cart' }}
{% else %}
<p class="sold-out">This product is currently sold out.</p>
{% endif %}
<!-- Product Details -->
<div class="product-details">
<h3>Details</h3>
<dl>
<dt>SKU</dt>
<dd>{{ product.sku }}</dd>
{% if product.vendor %}
<dt>Brand</dt>
<dd>{{ product.vendor }}</dd>
{% endif %}
{% if product.type %}
<dt>Type</dt>
<dd>{{ product.type }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
<!-- Related Products -->
<div class="related-products">
<h2>You May Also Like</h2>
<div class="product-grid">
{% scope product_type_name == product.product_type_name %}
{% for related_product in products.random limit: 4 %}
{% unless related_product.id == product.id %}
{% include 'product-card', product: related_product %}
{% endunless %}
{% endfor %}
{% endscope %}
</div>
</div>
</div>
</div>Blog Template (templates/blog.liquid)
<div class="blog">
<div class="container">
<header class="blog-header">
<h1>{{ blog.name }}</h1>
{% if blog.description %}
<p class="lead">{{ blog.description }}</p>
{% endif %}
</header>
<!-- Article List -->
<div class="articles">
{% paginate blog.articles by 10 %}
{% for article in paginate.collection %}
<article class="article-preview">
{% if article.thumbnail.url %}
<a href="{{ article.url }}" class="article-image">
<img src="{{ article.thumbnail.url | filter, width: '400px' }}" alt="{{ article.title }}">
</a>
{% endif %}
<div class="article-content">
<h2><a href="{{ article.url }}">{{ article.title }}</a></h2>
<p class="article-meta">
{{ article.published_at | localized_date: 'long' }}
{% if article.author %} · By {{ article.author }}{% endif %}
</p>
<p class="article-excerpt">{{ article.excerpt }}</p>
<a href="{{ article.url }}" class="read-more">
{% translate 'blog.read_more', default: 'Read More' %} →
</a>
</div>
</article>
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}
</div>
<!-- Sidebar -->
<aside class="blog-sidebar">
<!-- Tags -->
{% if blog.tags.size > 0 %}
<div class="widget">
<h3>Topics</h3>
<ul class="tag-list">
{% for tag in blog.tags %}
<li><a href="{{ blog.url }}?tag={{ tag | url_encode }}">{{ tag }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Recent Articles -->
<div class="widget">
<h3>Recent Posts</h3>
<ul class="recent-articles">
{% for article in blog.articles.latest limit: 5 %}
<li><a href="{{ article.url }}">{{ article.title }}</a></li>
{% endfor %}
</ul>
</div>
</aside>
</div>
</div>Article Template (templates/article.liquid)
<article class="article">
<header class="article-header">
<div class="container">
<p class="article-category">
<a href="{{ article.blog.url }}">{{ article.blog.name }}</a>
</p>
<h1>{{ article.title }}</h1>
<p class="article-meta">
{{ article.published_at | localized_date: 'long' }}
{% if article.author %} by {{ article.author }}{% endif %}
</p>
</div>
</header>
{% if article.header_image.url %}
<div class="article-featured-image">
<img src="{{ article.header_image.url | filter, width: '1200px' }}" alt="{{ article.title }}">
</div>
{% endif %}
<div class="article-content">
<div class="container">
{{ article.content }}
</div>
</div>
<footer class="article-footer">
<div class="container">
<!-- Tags -->
{% if article.tags.size > 0 %}
<div class="article-tags">
{% for tag in article.tags %}
<a href="{{ article.blog.url }}?tag={{ tag | url_encode }}" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
<!-- Previous/Next Navigation -->
<nav class="article-navigation">
{% if article.previous %}
<a href="{{ article.previous.url }}" class="prev">
← {{ article.previous.title }}
</a>
{% endif %}
{% if article.next %}
<a href="{{ article.next.url }}" class="next">
{{ article.next.title }} →
</a>
{% endif %}
</nav>
</div>
</footer>
</article>Customer Login (templates/customers/login.liquid)
{%- layout 'minimal' -%}
<div class="login-page">
<div class="login-container">
<h1>{% translate 'customer.login.title', default: 'Sign In' %}</h1>
{% form customers.login, class: 'login-form' %}
{% error_messages_for form_model %}
{% input 'email', as: 'email', label: 'Email', required: true, autofocus: true %}
{% input 'password', as: 'password', label: 'Password', required: true %}
<div class="form-actions">
{% submit_tag 'Sign In', class: 'btn btn-primary' %}
</div>
<div class="form-links">
<a href="/password/new">
{% translate 'customer.login.forgot_password', default: 'Forgot password?' %}
</a>
<a href="/register">
{% translate 'customer.login.create_account', default: 'Create account' %}
</a>
</div>
{% endform %}
</div>
</div>Themed 404 Page
Add templates/404.liquid to give "not found" responses your own design. The template is opt-in: a theme without it keeps the platform behaviour, which redirects the visitor to the homepage once and then shows a bare 404 page (for missing products and collections: a redirect to /shop).
When the live theme has templates/404.liquid, Nimbu renders it inside the default layout with HTTP status 404 for:
- unknown pages
- Cloud Code routes that respond with
notfound - missing or hidden products
- missing or non-public collections
Only GET and HEAD requests that accept HTML (including a bare Accept: */*, as many crawlers send) get the themed page. Other methods and formats such as .json keep their existing responses, and GET /404 itself always shows the platform page. If the default layout is missing, Nimbu falls back to the platform behaviour.
Available variables:
| Variable | Value |
|---|---|
page.title | "Page not found", from the translation html.titles.not_found. Override it in your translations. |
path | The requested path. |
url.current_path | The requested path including the query string. |
template | "404" |
path and url.current_path contain whatever URL the visitor typed, so always print them with | escape:
<section class="not-found">
<h1>{{ page.title }}</h1>
<p>We could not find <code>{{ path | escape }}</code>.</p>
<form action="{{ locale_url_prefix }}/search" method="get">
<input type="search" name="q" placeholder="Search the site">
<button type="submit">Search</button>
</form>
<a href="{{ locale_url_prefix }}/">Back to the homepage</a>
</section>Things to keep in mind:
- There is no current page, so
{% nav %},{% nav page %}and{% nav parent %}render nothing. Use{% nav site %}or a menu instead. - The response carries
X-Robots-Tag: noindexandcontent_for_headeroutputs<meta name="robots" content="noindex">without canonical or hreflang links. nimbu serverpicks up your localtemplates/404.liquid, so you can preview it before pushing the theme.- In layouts, use
{% if template == '404' %}to hide elements that don't make sense on an error page.
Template Routing
Nimbu automatically routes URLs to templates:
| URL Pattern | Template | Variables |
|---|---|---|
/ | index.liquid | site |
/about | page.liquid | page |
/products/shoes | shop/product.liquid | product |
/shop/summer | shop/collection.liquid | collection |
/blog | blog.liquid | blog |
/blog/hello-world | article.liquid | article, blog |
Custom Templates
Create custom templates for specific pages by matching the page slug:
templates/contact.liquid for /contact:
<div class="contact-page">
<div class="container">
<h1>{{ page.title }}</h1>
<div class="contact-grid">
<div class="contact-info">
{% editable_text contact_info %}
<p>Get in touch with us!</p>
{% endeditable_text %}
</div>
<div class="contact-form">
{% form channels.contact, class: 'form' %}
{% input 'name', label: 'Name', required: true %}
{% input 'email', as: 'email', label: 'Email', required: true %}
{% text_area 'message', label: 'Message', rows: 5, required: true %}
{% submit_tag 'Send Message', class: 'btn btn-primary' %}
{% endform %}
</div>
</div>
</div>
</div>Template Best Practices
1. Keep Templates Focused
Each template should handle one page type. Don't try to make a template do too much.
2. Use Snippets for Shared Components
Extract repeated elements into snippets:
<!-- In product.liquid -->
{% include 'product-images', product: product %}
{% include 'product-info', product: product %}
{% include 'related-products', product: product %}3. Make Content Editable
Use editable regions so clients can update content:
{% editable_canvas content %}
{% repeatable "Section" %}
{% include 'section' %}
{% endrepeatable %}
{% endeditable_canvas %}4. Handle Empty States
Always check if data exists:
{% if products.size > 0 %}
<!-- Show products -->
{% else %}
<p>No products found.</p>
{% endif %}5. Optimize Performance
Use caching for expensive operations:
{% cache 'product-grid', expires_in: 3600 %}
{% for product in products.all %}
{% include 'product-card', product: product %}
{% endfor %}
{% endcache %}Next Steps
- Snippets - Create reusable components
- Pages - Build flexible canvas-based pages
- Webshop - Complete e-commerce templates
- Global Variables - All available variables
Continue to Snippets to learn about creating reusable components.