Templates & Layouts
Stati uses Eta as its templating engine, providing a powerful yet familiar syntax for creating dynamic layouts and templates. Combined with Stati’s layout inheritance system, you can create flexible and maintainable site designs.
Template Engine: Eta
Eta is a fast, lightweight templating engine with a syntax similar to EJS but with better performance and TypeScript support.
Dynamic Attribute Values
Eta requires every HTML attribute value to be fully resolved by the time it is rendered. You cannot splice template expressions into the middle of a static value (for example
class="bg-<%= color %>-500").
Use template literals (or the helper described below) to make the entire attribute value dynamic:
<!-- ❌ Invalid: partial interpolation -->
<button class="bg-<%= color %>-500">Click me</button>
<!-- ✅ Valid: full dynamic value -->
<button class="<%= `bg-${color}-500` %>">Click me</button>
For more complex combinations, prefer the stati.propValue() helper provided by Stati. It builds attribute values by accepting strings, arrays, or objects and works best for attributes that support space-separated tokens:
<a
class="<%= stati.propValue('btn', `btn-${variant}`, isActive && 'is-active') %>"
data-analytics="<%= stati.propValue('cta', campaign, isActive && 'active') %>"
>
Learn more
</a>
stati.propValue() joins all truthy values with spaces (similar to classnames). For attributes that need a single concatenated string, use a template literal instead:
data-id="<%= `item-${id}` %>"
Basic Syntax
<!-- Variables -->
<h1><%= stati.page.title %></h1>
<p><%= stati.page.description %></p>
<!-- Raw HTML (unescaped) -->
<div><%~ stati.content %></div>
<!-- Conditionals -->
<% if (stati.page.author) { %>
<p>Written by <%= stati.page.author %></p>
<% } else { %>
<p>Author unknown</p>
<% } %>
<!-- Loops -->
<ul>
<% (stati.collection?.pages || []).forEach(page => { %>
<li><a href="<%= page.url %>"><%= page.title %></a></li>
<% }); %>
</ul>
<!-- Comments -->
<%/* This is a comment */%>
Available Data
In your templates, you have access to:
<!-- Page data -->
<%= stati.page.title %> <!-- From front matter -->
<%= stati.page.description %> <!-- From front matter -->
<%= stati.content %> <!-- Rendered markdown content -->
<%= stati.page.url %> <!-- Current page URL -->
<%= stati.page.date %> <!-- Page date (if specified) -->
<!-- Site data -->
<%= stati.site.title %> <!-- Site title from config -->
<%= stati.site.baseUrl %> <!-- Site base URL -->
<%= stati.site.defaultLocale %> <!-- Default locale (optional) -->
<!-- Generator data -->
<%= stati.generator.version %> <!-- Stati core version -->
<!-- Page frontmatter data -->
<%= stati.page.layout %> <!-- Layout name from frontmatter -->
<%= stati.partials.header %> <!-- Partial templates -->
Layout System
Layout Inheritance
Stati supports hierarchical layouts that inherit from parent directories:
site/
├── layout.eta # Root layout (all pages)
└── blog/
├── layout.eta # Blog layout (inherits from root)
├── index.md
└── posts/
├── layout.eta # Post layout (inherits from blog)
└── my-post.md
Template Resolution Order
Stati looks for templates in this order:
- Named template specified in front matter (e.g.,
layout: 'article') - Directory-specific
layout.eta(cascades from current to root) - Root
layout.etaas final fallback
Example: For /blog/tech/post.md, Stati checks:
- Front matter
layoutfield first /blog/tech/layout.eta/blog/layout.eta/layout.eta(root fallback)
---
# This overrides automatic template discovery
layout: 'article'
title: 'My Post'
---
The first match wins, providing maximum flexibility while maintaining sensible defaults.
Root Layout Example
site/layout.eta:
<!DOCTYPE html>
<html lang="<%= stati.site.defaultLocale || 'en-US' %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= stati.page.title ? `${stati.page.title} | ${stati.site.title}` : stati.site.title %></title>
<% if (stati.page.description) { %>
<meta name="description" content="<%= stati.page.description %>">
<% } %>
<!-- Favicon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<!-- Styles -->
<link rel="stylesheet" href="/styles.css">
<!-- SEO Meta Tags -->
<meta property="og:title" content="<%= stati.page.title || stati.site.title %>">
<% if (stati.page.description) { %>
<meta property="og:description" content="<%= stati.page.description %>">
<% } %>
<meta property="og:type" content="website">
<meta property="og:url" content="<%= stati.site.baseUrl + stati.page.url %>">
</head>
<body>
<%~ stati.partials.header %>
<main>
<%~ stati.content %>
</main>
<%~ stati.partials.footer %>
</body>
</html>
Section Layout Example
site/blog/layout.eta:
<!-- This extends the root layout -->
<div class="blog-container">
<aside class="blog-sidebar">
<%~ stati.partials.blogNav %>
</aside>
<article class="blog-content">
<% if (stati.page.title) { %>
<header class="post-header">
<h1><%= stati.page.title %></h1>
<% if (stati.page.date) { %>
<time datetime="<%= stati.page.date %>">
<%= new Date(stati.page.date).toLocaleDateString() %>
</time>
<% } %>
<% if (stati.page.tags) { %>
<div class="tags">
<% stati.page.tags.forEach(tag => { %>
<span class="tag"><%= tag %></span>
<% }); %>
</div>
<% } %>
</header>
<% } %>
<div class="prose">
<%~ stati.content %>
</div>
</article>
</div>
Partial Templates
Partials are reusable template components stored in _partials/ directories.
Partial Discovery Rules
Stati uses a strict convention for partial auto-discovery:
- Partials MUST be placed in folders starting with
_(underscore) - Multiple underscore folders are supported:
_partials/,_components/,_includes/ - Partials can be nested within underscore folders:
_partials/components/button.eta - Folders without underscore prefix are NOT scanned for partials
- Files within underscore folders are automatically available as partials
site/
├── _partials/ ✅ Auto-discovered
│ ├── header.eta
│ └── components/ ✅ Nested partials supported
│ └── button.eta
├── _components/ ✅ Multiple underscore folders
│ └── card.eta
├── _includes/ ✅ Any underscore folder name
│ └── analytics.eta
└── partials/ ❌ NOT discovered (no underscore)
└── ignored.eta
Creating Partials
site/_partials/header.eta:
<header class="site-header">
<nav class="main-nav">
<div class="nav-brand">
<a href="/">
<img src="/logo.svg" alt="<%= stati.site.title %>">
<%= stati.site.title %>
</a>
</div>
<ul class="nav-links">
<li><a href="/" class="<%= stati.page.url === '/' ? 'active' : '' %>">Home</a></li>
<li><a href="/blog/" class="<%= stati.page.url.startsWith('/blog/') ? 'active' : '' %>">Blog</a></li>
<li><a href="/docs/" class="<%= stati.page.url.startsWith('/docs/') ? 'active' : '' %>">Docs</a></li>
<li><a href="/about/" class="<%= stati.page.url === '/about/' ? 'active' : '' %>">About</a></li>
</ul>
</nav>
</header>
Using Partials
Partials are automatically available in templates:
<!-- Include a partial -->
<%~ stati.partials.header %>
<%~ stati.partials.footer %>
<%~ stati.partials.sidebar %>
Passing Data to Partials
Partials can accept custom data, making them reusable like components. This is perfect for cards, buttons, or any content that varies slightly each time you use it.
Basic Usage
<!-- Simple: no data needed -->
<%~ stati.partials.header %>
<!-- With custom data -->
<%~ stati.partials.hero({
title: 'Welcome to Stati',
subtitle: 'Build fast static sites'
}) %>
<!-- In loops: render a card for each post -->
<% posts.forEach(post => { %>
<%~ stati.partials.card({
title: post.title,
description: post.description,
url: post.url
}) %>
<% }) %>
How it works:
- Use partials as-is when no custom data is needed:
<%~ stati.partials.header %> - Pass data by calling the partial like a function:
stati.partials.name({ ... }) - Access passed data inside the partial via
stati.props.propertyName - All Stati data remains available:
stati.site,stati.page,stati.content, etc.
Creating a Reusable Partial
Let’s create a card component that accepts custom data.
Example partial (_partials/card.eta):
<article class="card">
<h3><%= stati.props.title || 'Untitled' %></h3>
<% if (stati.props.description) { %>
<p><%= stati.props.description %></p>
<% } %>
<% if (stati.props.url) { %>
<a href="<%= stati.props.url %>">Read more →</a>
<% } %>
</article>
Using the card:
<!-- Without data: uses defaults -->
<%~ stati.partials.card %>
<!-- Output: <article class="card"><h3>Untitled</h3></article> -->
<!-- With custom data -->
<%~ stati.partials.card({
title: 'Custom Title',
description: 'A great article',
url: '/blog/article'
}) %>
<!-- Output: full card with all fields -->
<!-- Render multiple cards from a collection -->
<div class="posts-grid">
<% stati.collection?.pages.forEach(post => { %>
<%~ stati.partials.card({
title: post.title,
description: post.description,
url: post.url
}) %>
<% }) %>
</div>
Accessing Data Inside Partials
Inside your partials, any data you pass is available via stati.props:
<!-- Inside _partials/hero.eta -->
<section class="hero">
<h1><%= stati.props.title %></h1>
<p><%= stati.props.subtitle %></p>
<!-- You still have access to site data -->
<small>© <%= stati.site.title %></small>
<!-- And page data -->
<small>Current page: <%= stati.page.title %></small>
</section>
Mixing Custom and Site Data
You can combine data passed to the partial with Stati’s global data:
<!-- _partials/page-header.eta -->
<header>
<!-- Use custom title if provided, otherwise fall back to site title -->
<h1><%= stati.props.customTitle || stati.site.title %></h1>
<!-- Always show current page -->
<p>You're on: <%= stati.page.title %></p>
<!-- Use custom subtitle if provided -->
<% if (stati.props.subtitle) { %>
<p class="subtitle"><%= stati.props.subtitle %></p>
<% } %>
</header>
<!-- Usage -->
<%~ stati.partials.pageHeader({
customTitle: 'Special Event',
subtitle: 'Join us for an amazing experience'
}) %>
Practical tips:
- Use
stati.propsfor data that changes each time you use the partial - Use
stati.sitefor global site information (title, URL, etc.) - Use
stati.pagefor the current page’s information - Provide sensible defaults with
||for optional properties
Hierarchical Partials
Partials inherit from parent directories:
site/
├── _partials/
│ ├── header.eta # Available everywhere
│ └── footer.eta
└── blog/
├── _partials/
│ ├── sidebar.eta # Available in blog/ and subdirectories
│ └── tagList.eta
└── posts/
└── _partials/
└── meta.eta # Available only in posts/
Template Data and Context
Front Matter Integration
Front matter from your markdown files is available in templates:
---
title: 'My Blog Post'
description: 'A great post about Stati'
date: '2024-01-15'
tags: ['stati', 'markdown', 'templates']
author: 'John Doe'
featured: true
---
# Content goes here...
Template usage:
<article>
<h1><%= stati.page.title %></h1>
<% if (stati.page.featured) { %>
<div class="featured-badge">Featured Post</div>
<% } %>
<div class="post-meta">
<span>By <%= stati.page.author %></span>
<time><%= new Date(stati.page.date).toLocaleDateString() %></time>
</div>
<div class="tags">
<% stati.page.tags.forEach(tag => { %>
<a href="/tags/<%= tag %>/" class="tag">#<%= tag %></a>
<% }); %>
</div>
<div class="content">
<%~ stati.content %>
</div>
</article>
Custom Filters and Helpers
Date Formatting
<!-- Format dates -->
<time><%= new Date(stati.page.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) %></time>
<!-- Relative time -->
<%
const now = new Date();
const postDate = new Date(stati.page.date);
const diffTime = Math.abs(now - postDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
%>
<span class="relative-time">
<% if (diffDays === 0) { %>
Today
<% } else if (diffDays === 1) { %>
Yesterday
<% } else if (diffDays < 7) { %>
<%= diffDays %> days ago
<% } else { %>
<%= new Date(stati.page.date).toLocaleDateString() %>
<% } %>
</span>
Text Processing
<!-- Truncate text -->
<%
function truncate(text, length = 150) {
if (text.length <= length) return text;
return text.slice(0, length) + '...';
}
%>
<p class="excerpt"><%= truncate(stati.page.description) %></p>
<!-- Slugify text -->
<%
function slugify(text) {
return text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
%>
<a href="/tags/<%= slugify(tag) %>/" class="tag-link">#<%= tag %></a>
Performance Considerations
Template Caching
Stati automatically caches compiled templates, but you can optimize further:
- Keep partials small and focused
- Avoid complex logic in templates
- Use front matter for data that doesn’t change
- Cache expensive computations in build process
Best Practices
-
Separate logic from presentation
- Use front matter for data
- Keep complex logic in build scripts
- Use simple helpers for common tasks
-
Optimize partial usage
- Don’t over-fragment templates
- Reuse partials across sections
- Keep partial dependencies clear
-
Performance-friendly patterns
- Minimize nested loops
- Use conditionals to skip unnecessary work
- Cache computed values when possible
Generator Information
Displaying Version Information
You can access Stati’s version information in templates using stati.generator.version. This is useful for attribution, debugging, or technical information display:
<!-- Simple footer attribution -->
<footer>
<p>Generated with Stati v<%= stati.generator.version %></p>
</footer>
Understanding templates and layouts is crucial for creating maintainable Stati sites. Next, learn about the Markdown Pipeline to understand how your content gets processed.