
Creating Zola Static Sites
- 7 installs
- 1 repo stars
- Updated January 17, 2026
- spillwavesolutions/creating-zola-static-sites-plugin
Helps with ai & agent building tasks during AI-assisted development.
About
creating-zola-static-sites is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- creating-zola-static-sites
- AI & Agent Building
- AI-coding skill
Creating Zola Static Sites by the numbers
- 7 all-time installs (skills.sh)
- Ranked #12,545 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/creating-zola-static-sites-plugin --skill creating-zola-static-sitesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 1 |
| Last updated | January 17, 2026 |
| Repository | spillwavesolutions/creating-zola-static-sites-plugin ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Creating Zola Static Sites
Single-binary static site generator. Built-in Sass, image processing, Tera templating.
Quick Start
New Project:
- [ ] zola init my-site && cd my-site
- [ ] Edit config.toml: set base_url
- [ ] IF using theme: skip next step (theme provides templates)
- [ ] IF no theme: copy templates from skill assets/
- [ ] zola serve → http://127.0.0.1:1111 loads
- [ ] zola check → exit code 0Commands
| Command | Purpose | Verify Success |
|---|---|---|
zola init | Create project | ls config.toml exists |
zola serve | Dev server :1111 | Browser loads site |
zola build | Output to public/ | ls public/index.html exists |
zola check | Validate links | Exit 0, no errors printed |
Core: Sections vs Pages
`_` prefix = section (listing); no prefix = page (content):
| Filename | Type | Template | Example URL |
|---|---|---|---|
_index.md | section | section.html | /blog/ (listing) |
index.md | page | page.html | /about/ (with assets) |
post.md | page | page.html | /blog/post/ |
content/
├── _index.md # section: /
├── about.md # page: /about/
└── blog/
├── _index.md # section: /blog/
└── post.md # page: /blog/post/Frontmatter
section (_index.md):
+++
title = "Blog"
sort_by = "date" # date|title|weight|none
paginate_by = 10
+++page (*.md):
+++
title = "My Post"
date = 2024-01-15 # NO QUOTES or sorting breaks
[taxonomies]
tags = ["rust"]
+++Internal Links
Build-validated with @/ prefix:
[About](@/about.md)
[Blog](@/blog/_index.md)Workflows
Create Blog
Blog Setup:
- [ ] Create content/blog/_index.md with sort_by="date"
- [ ] Create post: content/blog/2024-01-15-hello.md
- [ ] IF multilingual: add blog/_index.fr.md per language
- [ ] zola serve → /blog/ shows listing (empty if no posts yet)
- [ ] zola serve → /blog/hello/ shows post
- [ ] IF pagination: add more posts than paginate_by to test
- [ ] zola check → passesAdd Taxonomies
Taxonomy Setup:
- [ ] config.toml: taxonomies = [{name = "tags", feed = true}]
- [ ] Page frontmatter: [taxonomies] tags = ["value"]
- [ ] IF need categories: add {name = "categories"} to array
- [ ] IF term has spaces: use slugified URL (/tags/my-tag/)
- [ ] zola serve → /tags/ lists tags
- [ ] zola serve → /tags/rust/ shows postsDeploy
Production Deploy:
- [ ] zola check → passes (use --skip-external-links if slow)
- [ ] zola build → no errors
- [ ] ls public/index.html → exists
- [ ] IF feeds enabled: ls public/atom.xml → existsReference Files
| Task | Reference | Load When User Asks About |
|---|---|---|
| Config | config-reference.md | Feeds, taxonomies, highlighting, search, link checker |
| Templates | tera-templates.md | Filters, loops, variables, macros, shortcodes |
| Content | content-organization.md | _index.md vs index.md, frontmatter, multilingual |
| Deploy | deployment-guides.md | Netlify, Cloudflare, GitHub Actions, Vercel, Firebase |
| Hybrid | astro-integration.md | Zola+Astro, Firebase export, shared navigation |
Assets
Copy from skill assets/ directory:
| Asset | Use For |
|---|---|
templates/base.html | Base layout |
templates/section.html | Listings with pagination |
templates/page.html | Articles with TOC |
templates/404.html | Error page |
templates/shortcodes/ | youtube.html, figure.html |
config-templates/blog.toml | Blog preset |
config-templates/docs.toml | Docs preset |
Common Mistakes
| Wrong | Right | Why |
|---|---|---|
date = "2024-01-15" | date = 2024-01-15 | Quoted dates are strings, break sorting |
[About](about.md) | [About](@/about.md) | Missing @/ means no build validation |
index.md in blog/ | _index.md in blog/ | Without _ it's a page, not a section |
templates/blog.html | templates/section.html | Zola looks for section.html by default |
Troubleshooting
| Error | Fix | Verify |
|---|---|---|
| Template not found | Check spelling; ensure templates/ exists | ls templates/*.html shows files |
| Dates not sorting | Use date = 2024-01-15 (no quotes) | Posts appear in date order |
| Broken @/ links | Path from content/, include .md extension | zola check passes |
| Slow check | Skip external: zola check --skip-external-links | Completes in <10s |
Debug: {{ __tera_context }} in template shows all variables.
Verbose: RUST_LOG=zola=info zola build
When Not to Use
- Other SSGs: Hugo, Jekyll, Eleventy use different syntax
- General Markdown: Questions without Zola context
- Pure Astro: Projects without Zola integration
# Zola Blog Configuration Template
# Replace YOUR_DOMAIN with your actual domain
base_url = "https://YOUR_DOMAIN.com"
title = "My Blog"
description = "A personal blog about technology and life"
default_language = "en"
# Build options
compile_sass = true
minify_html = true
generate_sitemap = true
generate_robots_txt = true
# Feeds
generate_feeds = true
feed_filenames = ["atom.xml"]
feed_limit = 20
# Search
build_search_index = true
[search]
include_title = true
include_description = true
include_content = true
truncate_content_length = 300
# Markdown
[markdown]
render_emoji = true
smart_punctuation = true
external_links_target_blank = true
external_links_class = "external"
lazy_async_image = true
insert_anchor_links = "heading"
[markdown.highlighting]
highlight_code = true
style = "class"
light_theme = "github-light"
dark_theme = "github-dark"
# Link checking
[link_checker]
internal_level = "error"
external_level = "warn"
# Taxonomies
taxonomies = [
{name = "tags", feed = true},
{name = "categories", paginate_by = 10},
]
# Translations
[translations]
read_more = "Read more →"
posted_on = "Published"
updated_on = "Updated"
tags = "Tags"
# Custom variables
[extra]
# Site branding
logo = "/images/logo.svg"
favicon = "/favicon.ico"
# Content display
show_reading_time = true
show_word_count = false
show_toc = true
# Navigation
nav_items = [
{label = "Home", url = "/"},
{label = "Blog", url = "/blog/"},
{label = "About", url = "/about/"},
]
# Social links
[extra.social]
github = "YOUR_USERNAME"
twitter = "YOUR_USERNAME"
email = "your@email.com"
# Analytics (optional)
# [extra.analytics]
# google = "G-XXXXXXXXXX"
# Zola Documentation Site Configuration Template
# Replace YOUR_DOMAIN with your actual domain
base_url = "https://docs.YOUR_DOMAIN.com"
title = "Documentation"
description = "Project documentation and guides"
default_language = "en"
# Build options
compile_sass = true
minify_html = true
generate_sitemap = true
generate_robots_txt = true
# No feeds for docs (typically)
generate_feeds = false
# Search is essential for docs
build_search_index = true
[search]
include_title = true
include_description = true
include_content = true
truncate_content_length = 500
index_format = "elasticlunr_json"
# Markdown
[markdown]
render_emoji = false
smart_punctuation = true
external_links_target_blank = true
lazy_async_image = true
insert_anchor_links = "left"
definition_list = true
github_alerts = true
[markdown.highlighting]
highlight_code = true
style = "class"
light_theme = "github-light"
dark_theme = "github-dark"
error_on_missing_language = true
# Strict link checking for docs
[link_checker]
internal_level = "error"
external_level = "error"
# No taxonomies needed for most docs
taxonomies = []
# Translations
[translations]
toc = "On this page"
edit_page = "Edit this page"
last_updated = "Last updated"
# Custom variables
[extra]
# Site branding
logo = "/images/logo.svg"
favicon = "/favicon.ico"
# Documentation features
show_toc = true
show_last_updated = true
show_edit_link = true
repo_url = "https://github.com/YOUR_ORG/YOUR_REPO"
repo_branch = "main"
docs_path = "content"
# Navigation
nav_items = [
{label = "Docs", url = "/"},
{label = "API", url = "/api/"},
{label = "Examples", url = "/examples/"},
{label = "GitHub", url = "https://github.com/YOUR_ORG/YOUR_REPO"},
]
# Sidebar sections (for custom sidebar rendering)
[extra.sidebar]
sections = [
{title = "Getting Started", path = "getting-started/"},
{title = "Guides", path = "guides/"},
{title = "Reference", path = "reference/"},
]
# Version selector (optional)
# [extra.versions]
# current = "1.0"
# available = ["1.0", "0.9", "0.8"]
{% extends "base.html" %}
{% block title %}Page Not Found | {{ config.title }}{% endblock %}
{% block content %}
<div class="error-page">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>The page you're looking for doesn't exist or has been moved.</p>
<a href="{{ config.base_url }}" class="button">Go Home</a>
</div>
{% endblock content %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block head %}
<title>{% block title %}{{ config.title }}{% endblock %}</title>
{% if page.description %}
<meta name="description" content="{{ page.description }}">
{% elif section.description %}
<meta name="description" content="{{ section.description }}">
{% elif config.description %}
<meta name="description" content="{{ config.description }}">
{% endif %}
<link rel="stylesheet" href="{{ get_url(path='css/style.css', cachebust=true) }}">
{% if config.generate_feeds %}
<link rel="alternate" type="application/atom+xml" title="Atom Feed" href="{{ get_url(path='atom.xml', trailing_slash=false) }}">
{% endif %}
{% endblock head %}
</head>
<body>
{% block header %}
<header>
<nav>
<a href="{{ config.base_url }}" class="logo">{{ config.title }}</a>
{% if config.extra.nav_items %}
<ul>
{% for item in config.extra.nav_items %}
<li>
<a href="{{ item.url }}" {% if current_path == item.url %}class="active"{% endif %}>
{{ item.label }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</nav>
</header>
{% endblock header %}
<main>
{% block content %}{% endblock content %}
</main>
{% block footer %}
<footer>
<p>© {{ now() | date(format="%Y") }} {{ config.title }}</p>
</footer>
{% endblock footer %}
{% block scripts %}{% endblock scripts %}
</body>
</html>
{% extends "base.html" %}
{% block title %}{{ page.title }} | {{ config.title }}{% endblock %}
{% block head %}
{{ super() }}
{% if page.extra.cover_image %}
<meta property="og:image" content="{{ get_url(path=page.path ~ page.extra.cover_image) }}">
{% endif %}
{% endblock head %}
{% block content %}
<article class="page">
<header class="page-header">
<h1>{{ page.title }}</h1>
<div class="meta">
{% if page.date %}
<time datetime="{{ page.date | date(format='%Y-%m-%d') }}">
{{ page.date | date(format="%B %d, %Y") }}
</time>
{% endif %}
{% if page.updated and page.updated != page.date %}
<span class="updated">
Updated: {{ page.updated | date(format="%B %d, %Y") }}
</span>
{% endif %}
{% if page.reading_time %}
<span class="reading-time">{{ page.reading_time }} min read</span>
{% endif %}
</div>
{% if page.taxonomies.tags %}
<ul class="tags">
{% for tag in page.taxonomies.tags %}
<li><a href="{{ get_taxonomy_url(kind='tags', name=tag) }}">{{ tag }}</a></li>
{% endfor %}
</ul>
{% endif %}
</header>
{# Table of Contents #}
{% if page.toc and page.extra.toc | default(value=true) %}
<nav class="toc">
<h2>Contents</h2>
<ul>
{% for h1 in page.toc %}
<li>
<a href="{{ h1.permalink }}">{{ h1.title }}</a>
{% if h1.children %}
<ul>
{% for h2 in h1.children %}
<li>
<a href="{{ h2.permalink }}">{{ h2.title }}</a>
{% if h2.children %}
<ul>
{% for h3 in h2.children %}
<li><a href="{{ h3.permalink }}">{{ h3.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
<div class="content">
{{ page.content | safe }}
</div>
{# Previous/Next navigation #}
{% if page.lower or page.higher %}
<nav class="post-nav">
{% if page.lower %}
<a href="{{ page.lower.permalink }}" class="prev">
<span>← Previous</span>
<strong>{{ page.lower.title }}</strong>
</a>
{% endif %}
{% if page.higher %}
<a href="{{ page.higher.permalink }}" class="next">
<span>Next →</span>
<strong>{{ page.higher.title }}</strong>
</a>
{% endif %}
</nav>
{% endif %}
{# Translations #}
{% if page.translations %}
<div class="translations">
<span>Also available in:</span>
{% for trans in page.translations %}
<a href="{{ trans.permalink }}">{{ trans.lang | upper }}</a>
{% endfor %}
</div>
{% endif %}
</article>
{% endblock content %}
{% extends "base.html" %}
{% block title %}{{ section.title }} | {{ config.title }}{% endblock %}
{% block content %}
<section class="section-listing">
<h1>{{ section.title }}</h1>
{% if section.description %}
<p class="description">{{ section.description }}</p>
{% endif %}
{% if section.content %}
<div class="section-content">
{{ section.content | safe }}
</div>
{% endif %}
{# Use paginator if pagination is enabled, otherwise use section.pages #}
{% if paginator %}
{% set pages = paginator.pages %}
{% else %}
{% set pages = section.pages %}
{% endif %}
{% if pages %}
<ul class="post-list">
{% for page in pages %}
<li class="post-item">
<article>
<h2><a href="{{ page.permalink }}">{{ page.title }}</a></h2>
{% if page.date %}
<time datetime="{{ page.date | date(format='%Y-%m-%d') }}">
{{ page.date | date(format="%B %d, %Y") }}
</time>
{% endif %}
{% if page.description %}
<p>{{ page.description }}</p>
{% elif page.summary %}
<p>{{ page.summary | striptags | truncate(length=200) }}</p>
{% endif %}
{% if page.taxonomies.tags %}
<ul class="tags">
{% for tag in page.taxonomies.tags %}
<li><a href="{{ get_taxonomy_url(kind='tags', name=tag) }}">{{ tag }}</a></li>
{% endfor %}
</ul>
{% endif %}
</article>
</li>
{% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}
{# Pagination navigation #}
{% if paginator %}
<nav class="pagination">
{% if paginator.previous %}
<a href="{{ paginator.previous }}" class="prev">← Newer</a>
{% else %}
<span class="prev disabled">← Newer</span>
{% endif %}
<span class="page-info">
Page {{ paginator.current_index }} of {{ paginator.number_pagers }}
</span>
{% if paginator.next %}
<a href="{{ paginator.next }}" class="next">Older →</a>
{% else %}
<span class="next disabled">Older →</span>
{% endif %}
</nav>
{% endif %}
{# Subsections #}
{% if section.subsections %}
<h2>Sections</h2>
<ul class="subsections">
{% for subsection_path in section.subsections %}
{% set subsection = get_section(path=subsection_path) %}
<li>
<a href="{{ subsection.permalink }}">{{ subsection.title }}</a>
{% if subsection.description %}
<p>{{ subsection.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% endblock content %}
<figure class="figure {% if class %}{{ class }}{% endif %}">
{% if resize %}
{% set img = resize_image(path=src, width=resize | int, op="fit_width", format="webp") %}
<img src="{{ img.url }}"
width="{{ img.width }}"
height="{{ img.height }}"
alt="{{ alt | default(value='') }}"
loading="lazy">
{% else %}
<img src="{{ get_url(path=src) }}"
alt="{{ alt | default(value='') }}"
loading="lazy">
{% endif %}
{% if caption %}
<figcaption>{{ caption | markdown(inline=true) | safe }}</figcaption>
{% endif %}
</figure>
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/{{ id }}"
title="{{ title | default(value='Video') }}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
loading="lazy">
</iframe>
</div>
Zola + Astro Hybrid Architecture
Patterns for combining Zola content with Astro interactive components.
Contents
- When to Use
- Directory Structure
- Configuration
- Build Scripts
- Shared Navigation
- Firebase Export
- CI/CD Pipeline
- Troubleshooting
---
When to Use
- Zola for content-heavy sections (blogs, docs)
- Astro for interactive components (dashboards, forms)
- Gradual migration between frameworks
- Different team expertise
---
Directory Structure
project/
├── astro/
│ ├── src/
│ ├── public/
│ │ └── docs/ # Zola output
│ └── astro.config.mjs
├── zola/
│ ├── content/
│ ├── templates/
│ └── config.toml
├── package.json
└── shared/
└── navigation.json---
Configuration
zola/config.toml:
base_url = "https://example.com/docs"
output_dir = "../astro/public/docs"astro/astro.config.mjs:
export default defineConfig({
site: 'https://example.com',
trailingSlash: 'always',
build: { format: 'directory' },
});---
Build Scripts
package.json:
{
"scripts": {
"build": "npm-run-all build:zola build:astro",
"build:zola": "cd zola && zola build",
"build:astro": "cd astro && npm run build",
"dev": "npm-run-all --parallel dev:zola dev:astro",
"dev:zola": "cd zola && zola serve --port 1111",
"dev:astro": "cd astro && npm run dev"
}
}Build Checklist:
- [ ] Build Zola first: cd zola && zola build
- [ ] Verify: ls ../astro/public/docs/index.html exists
- [ ] Build Astro: cd ../astro && npm run build
- [ ] Verify: ls dist/index.html exists
- [ ] Verify: ls dist/docs/index.html exists
- [ ] Test locally: npx serve dist---
Shared Navigation
shared/navigation.json:
{
"main": [
{"label": "Home", "href": "/"},
{"label": "Docs", "href": "/docs/"},
{"label": "Blog", "href": "/blog/"}
]
}In Zola:
{% set nav = load_data(path="../shared/navigation.json") %}
{% for item in nav.main %}
<a href="{{ item.href }}">{{ item.label }}</a>
{% endfor %}In Astro:
---
import nav from '../../shared/navigation.json';
---
{nav.main.map(item => <a href={item.href}>{item.label}</a>)}---
Firebase Export
Export Firestore to Zola Markdown:
scripts/export.ts:
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import * as fs from 'fs';
initializeApp({ credential: cert('./service-account.json') });
const db = getFirestore();
interface Post {
title: string;
content: string;
date: Date;
tags?: string[];
}
function toFrontmatter(post: Post): string {
const lines = [
'+++',
`title = "${post.title.replace(/"/g, '\\"')}"`,
`date = ${post.date.toISOString().split('T')[0]}`,
];
if (post.tags?.length) {
lines.push('[taxonomies]');
lines.push(`tags = [${post.tags.map(t => `"${t}"`).join(', ')}]`);
}
lines.push('+++');
return lines.join('\n');
}
async function exportPosts(outputDir: string) {
const snapshot = await db.collection('posts').orderBy('date', 'desc').get();
fs.mkdirSync(outputDir, { recursive: true });
for (const doc of snapshot.docs) {
const post = doc.data() as Post;
const slug = post.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
const date = post.date.toISOString().split('T')[0];
const content = `${toFrontmatter(post)}\n\n${post.content}`;
fs.writeFileSync(`${outputDir}/${date}-${slug}.md`, content);
}
}
exportPosts('./zola/content/blog');---
CI/CD Pipeline
.github/workflows/deploy.yml:
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.2
- name: Build Zola
run: cd zola && zola build
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build Astro
run: cd astro && npm ci && npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./astro/dist
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
steps:
- uses: actions/deploy-pages@v4---
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| 404 on Zola content | Wrong output path | Verify Zola output in astro/public/docs |
| Broken cross-links | Relative paths | Use absolute paths (/docs/, not docs/) |
| Trailing slash mismatch | Different settings | Set trailingSlash: 'always' in both |
| Build order issue | Astro before Zola | Always build Zola first |
URL consistency:
- Both use trailing slashes
- Base URLs match deployment path
- Use absolute paths for cross-system links
Zola config.toml Reference
Only base_url is required. All other options have sensible defaults.
Contents
- Required
- Site Metadata
- Build Options
- Markdown
- Syntax Highlighting
- Search
- Link Checker
- Feeds
- Taxonomies
- Slugification
- Languages
- Extra Variables
- Complete Example
---
Required
base_url = "https://example.com"
# For subpath: base_url = "https://example.com/blog"---
Site Metadata
title = "My Site"
description = "Site description"
default_language = "en"
author = "John Doe"Access in templates: {{ config.title }}
---
Build Options
output_dir = "public"
compile_sass = true
minify_html = true
hard_link_static = false
generate_sitemap = true
generate_robots_txt = true
ignored_content = ["*.draft.md", "temp/*"]
ignored_static = ["*.psd"]---
Markdown
[markdown]
render_emoji = true
smart_punctuation = true
external_links_target_blank = true
external_links_no_follow = false
external_links_class = "external"
lazy_async_image = true
bottom_footnotes = true
github_alerts = true
definition_list = true
insert_anchor_links = "heading" # "none", "left", "right", "heading"---
Syntax Highlighting
[markdown.highlighting]
highlight_code = true
style = "class" # "class" or "inline"
light_theme = "github-light"
dark_theme = "github-dark"
error_on_missing_language = trueThemes: base16-ocean-dark, dracula, github-dark, github-light, gruvbox-dark, monokai, nord, one-dark, solarized-dark
---
Search
build_search_index = true
[search]
include_title = true
include_description = true
include_content = true
truncate_content_length = 300
index_format = "elasticlunr_json"Formats: elasticlunr_json, elasticlunr_javascript, fuse_json, fuse_javascript
---
Link Checker
[link_checker]
skip_prefixes = ["http://localhost"]
skip_anchor_prefixes = ["https://caniuse.com/"]
internal_level = "error"
external_level = "warn"Run: zola check or zola check --skip-external-links
---
Feeds
generate_feeds = true
feed_filenames = ["atom.xml", "rss.xml"]
feed_limit = 20---
Taxonomies
taxonomies = [
{name = "tags"},
{name = "categories", feed = true},
{name = "authors", paginate_by = 10},
]Options: name (required), feed, paginate_by, paginate_path, render
In page frontmatter:
[taxonomies]
tags = ["rust", "web"]---
Slugification
[slugify]
paths = "on" # "on", "safe", "off"
taxonomies = "on"
anchors = "on"
paths_keep_dates = false---
Languages
Single Language
default_language = "en"
[translations]
read_more = "Read more"Access: {{ trans(key="read_more") }}
Multiple Languages
default_language = "en"
[languages.fr]
title = "Mon Site"
generate_feeds = true
taxonomies = [{name = "tags"}]
[languages.fr.translations]
read_more = "Lire la suite"Files: about.md (default), about.fr.md (French)
---
Extra Variables
[extra]
logo = "/images/logo.svg"
show_reading_time = true
nav_items = [
{label = "Home", url = "/"},
{label = "Blog", url = "/blog/"},
]
[extra.social]
github = "username"
twitter = "username"Access: {{ config.extra.logo }}, {{ config.extra.social.github }}
---
Complete Example
base_url = "https://myblog.com"
title = "My Blog"
description = "Tech articles"
default_language = "en"
compile_sass = true
minify_html = true
generate_sitemap = true
generate_feeds = true
feed_filenames = ["atom.xml"]
build_search_index = true
[search]
include_title = true
include_content = true
truncate_content_length = 300
[markdown]
render_emoji = true
smart_punctuation = true
external_links_target_blank = true
insert_anchor_links = "heading"
[markdown.highlighting]
highlight_code = true
style = "class"
light_theme = "github-light"
dark_theme = "github-dark"
[link_checker]
internal_level = "error"
external_level = "warn"
taxonomies = [
{name = "tags", feed = true},
{name = "categories", paginate_by = 10},
]
[translations]
read_more = "Read more →"
[extra]
logo = "/images/logo.svg"
show_reading_time = true
nav_items = [
{label = "Home", url = "/"},
{label = "Blog", url = "/blog/"},
]
[extra.social]
github = "username"Content Organization Reference
Guide to organizing content in Zola: sections, pages, taxonomies, frontmatter.
Contents
- Sections vs Pages
- section Frontmatter
- page Frontmatter
- Asset Colocation
- URL Generation
- Internal Links
- Taxonomies
- Multilingual
- Common Structures
---
Sections vs Pages
| Filename | Type | Template | Purpose |
|---|---|---|---|
_index.md | Section | section.html | Container/listing |
index.md | Page | page.html | Page with colocated assets |
*.md | Page | page.html | Single content |
content/
├── _index.md # Section: /
├── about.md # Page: /about/
├── blog/
│ ├── _index.md # Section: /blog/
│ ├── post.md # Page: /blog/post/
│ └── featured/
│ ├── index.md # Page: /blog/featured/
│ └── hero.jpg # Colocated asset
└── docs/
├── _index.md # Section: /docs/
└── getting-started/
├── _index.md # Section: /docs/getting-started/
└── install.md # Page: /docs/getting-started/install/---
section Frontmatter
+++
title = "Blog"
description = "My articles"
# Sorting
sort_by = "date" # "date", "title", "weight", "slug", "none"
# Pagination
paginate_by = 10
paginate_path = "page" # /blog/page/2/
# Templates
template = "blog.html" # Override section template
page_template = "post.html" # Default for child pages
# Content
render = true # false = data-only
transparent = false # Pass pages to parent
insert_anchor_links = "left"
in_search_index = true
# Feeds
generate_feeds = true
# Redirects
aliases = ["/articles/"]
[extra]
featured_image = "header.jpg"
+++Transparent sections: transparent = true passes pages to parent section. Useful for year-based organization without yearly listings.
---
page Frontmatter
+++
title = "My Post"
description = "SEO description"
date = 2024-01-15 # NO QUOTES!
updated = 2024-02-01
authors = ["Alice"]
# URL Control
slug = "custom-url" # Override filename
path = "custom/full/path" # Override entire path
aliases = ["/old-url/"] # Redirects
# Visibility
draft = false
render = true
# Sorting
weight = 10 # Lower = first
# Template
template = "custom.html"
# Search
in_search_index = true
[taxonomies]
tags = ["rust", "web"]
categories = ["programming"]
[extra]
featured = true
cover_image = "cover.jpg"
+++Date formats:
date = 2024-01-15 # Correct
date = 2024-01-15T10:30:00Z # With time
date = "2024-01-15" # WRONG - string breaks sortingDate from filename: 2024-01-15-hello.md → date = 2024-01-15, slug = "hello"
---
Asset Colocation
Use index.md (not _index.md) for pages with assets:
content/blog/my-post/
├── index.md # The page
├── hero.jpg # Image
├── diagram.svg # SVG
└── data.json # DataIn Markdown:
In templates:
{% for asset in page.assets %}
{% if asset is ending_with(".jpg") %}
{% set img = resize_image(path=asset, width=800) %}
<img src="{{ img.url }}">
{% endif %}
{% endfor %}---
URL Generation
| Content Path | URL |
|---|---|
content/about.md | /about/ |
content/blog/_index.md | /blog/ |
content/blog/post.md | /blog/post/ |
content/blog/my-post/index.md | /blog/my-post/ |
content/2024-01-15-hello.md | /hello/ |
Slug vs Path:
# Original: content/blog/my-file.md → /blog/my-file/
slug = "custom"
# Result: /blog/custom/
path = "tutorials/intro"
# Result: /tutorials/intro/ (ignores section)---
Internal Links
Use @/ prefix for validated links:
[About](@/about.md)
[Blog](@/blog/_index.md)
[Post](@/blog/post.md)
[Anchor](@/blog/post.md#section)Build fails if target doesn't exist.
---
Taxonomies
Config:
taxonomies = [
{name = "tags", feed = true},
{name = "categories", paginate_by = 10},
]Page frontmatter:
[taxonomies]
tags = ["rust", "web"]Generated URLs:
/tags/ # All tags
/tags/rust/ # Posts tagged "rust"
/tags/rust/atom.xml # Feed (if enabled)Templates:
List (templates/tags/list.html or taxonomy_list.html):
{% for term in terms %}
<a href="{{ term.permalink }}">{{ term.name }} ({{ term.pages | length }})</a>
{% endfor %}Single (templates/tags/single.html or taxonomy_single.html):
<h1>{{ term.name }}</h1>
{% for page in term.pages %}
<a href="{{ page.permalink }}">{{ page.title }}</a>
{% endfor %}In page templates:
{% for tag in page.taxonomies.tags %}
<a href="{{ get_taxonomy_url(kind='tags', name=tag) }}">{{ tag }}</a>
{% endfor %}---
Multilingual
Files: about.md (default), about.fr.md (French)
Config:
default_language = "en"
[languages.fr]
title = "Mon Site"
taxonomies = [{name = "tags"}]
[languages.fr.translations]
read_more = "Lire la suite"Language switcher:
{% if page.translations %}
{% for trans in page.translations %}
<a href="{{ trans.permalink }}">{{ trans.lang | upper }}</a>
{% endfor %}
{% endif %}---
Common Structures
Blog
content/
├── _index.md
└── blog/
├── _index.md # sort_by = "date", paginate_by = 10
├── 2024-01-15-post.md
└── featured/
├── index.md
└── cover.jpgVerify: zola serve → /blog/ shows posts sorted by date, /blog/featured/ shows page with image.
Documentation
content/
├── _index.md
├── getting-started/
│ ├── _index.md # weight = 1
│ ├── install.md # weight = 1
│ └── quick-start.md # weight = 2
├── guides/
│ ├── _index.md # weight = 2
│ └── advanced/
│ └── _index.md
└── reference/
├── _index.md # weight = 3
└── api.mdUse sort_by = "weight" for manual ordering.
Verify: zola serve → sections appear in weight order, not alphabetical.
Portfolio
content/
├── _index.md
├── projects/
│ ├── _index.md # sort_by = "weight"
│ ├── project-a/
│ │ ├── index.md # weight = 1
│ │ └── screenshot.png
│ └── project-b/
│ └── index.md # weight = 2
└── about.mdVerify: zola serve → /projects/ shows project-a before project-b.
Zola Deployment Guides
Platform-specific deployment configurations.
Contents
---
Netlify
netlify.toml:
[build]
publish = "public"
command = "zola build"
[build.environment]
ZOLA_VERSION = "0.19.2"
[context.deploy-preview]
command = "zola build --base-url $DEPLOY_PRIME_URL"Setup Checklist:
- [ ] Push to Git repository
- [ ] Connect repository in Netlify
- [ ] Build command: zola build
- [ ] Publish directory: public
- [ ] Environment: ZOLA_VERSION=0.19.2
- [ ] Deploy → check build logs for errors
- [ ] Visit deploy URL → site loads correctly---
Cloudflare Pages
Settings:
- Framework preset: Zola
- Build command:
zola build - Output directory:
public - Environment:
ZOLA_VERSION=0.19.2
Setup Checklist:
- [ ] Connect Git repository
- [ ] Select Zola preset
- [ ] Add ZOLA_VERSION=0.19.2 variable
- [ ] Deploy → check build logs
- [ ] Visit *.pages.dev URL → site loads---
GitHub Pages
.github/workflows/deploy.yml:
name: Deploy Zola
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Zola
uses: taiki-e/install-action@v2
with:
tool: zola@0.19.2
- name: Build
run: zola build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v4Setup Checklist:
- [ ] Create .github/workflows/deploy.yml
- [ ] Settings → Pages → Source: GitHub Actions
- [ ] Push to main branch
- [ ] Actions tab → workflow completes green
- [ ] Visit github.io URL → site loads---
Vercel
vercel.json:
{
"trailingSlash": true,
"cleanUrls": true
}package.json:
{
"scripts": {
"install-zola": "curl -sL https://github.com/getzola/zola/releases/download/v0.19.2/zola-v0.19.2-x86_64-unknown-linux-gnu.tar.gz | tar xz",
"build": "./zola build"
}
}Setup Checklist:
- [ ] Connect repository
- [ ] Framework: Other
- [ ] Build: npm run install-zola && npm run build
- [ ] Output: public
- [ ] Deploy → check build logs
- [ ] Visit *.vercel.app URL → site loads---
Firebase
firebase.json:
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*"],
"rewrites": [{"source": "**", "destination": "/404.html"}],
"headers": [
{
"source": "**/*.@(js|css)",
"headers": [{"key": "Cache-Control", "value": "max-age=31536000"}]
}
]
}
}Setup Checklist:
- [ ] npm install -g firebase-tools
- [ ] firebase login
- [ ] firebase init hosting
- [ ] zola build
- [ ] firebase deploy
- [ ] Visit firebase URL → site loads---
Docker
Dockerfile:
FROM ghcr.io/getzola/zola:v0.19.1 as builder
COPY . /project
WORKDIR /project
RUN ["zola", "build"]
FROM nginx:alpine
COPY --from=builder /project/public /usr/share/nginx/html
EXPOSE 80docker build -t my-site .
docker run -p 8080:80 my-site---
Common Patterns
Base URL override:
zola build --base-url $DEPLOY_URLInclude drafts:
zola build --draftsLink checking in CI:
- name: Check links
run: zola check --skip-external-linksCache processed images:
cache:
paths:
- public/processed_images/Tera Templates Reference
Complete Tera templating reference for Zola.
Contents
- Syntax
- Variables
- Control Structures
- Filters
- Template Inheritance
- Macros
- Zola Variables
- Zola Functions
- Shortcodes
- Common Patterns
---
Syntax
{{ variable }} {# Output #}
{% statement %} {# Control #}
{# comment #} {# Comment #}Whitespace control: {%- trim -%}, {{- trim -}}
---
Variables
{{ name }}
{{ user.name }}
{{ items[0] }}
{{ data[key] }}Operators:
{{ a + b }} {{ a - b }} {{ a * b }} {{ a / b }}
{{ a == b }} {{ a != b }} {{ a < b }} {{ a > b }}
{{ a and b }} {{ a or b }} {{ not a }}
{{ "Hello " ~ name }} {# String concat #}Set variables:
{% set name = "value" %}
{% set_global counter = counter + 1 %}---
Control Structures
Conditionals:
{% if condition %}
...
{% elif other %}
...
{% else %}
...
{% endif %}Tests:
{% if value is defined %}
{% if items is iterable %}
{% if text is containing("word") %}
{% if text is matching("^[a-z]+$") %}Loops:
{% for item in items %}
{{ loop.index }} {# 1-indexed #}
{{ loop.first }} {# true if first #}
{{ loop.last }} {# true if last #}
{% else %}
No items.
{% endfor %}
{% for key, value in object %}
{{ key }}: {{ value }}
{% endfor %}---
Filters
String filters with examples:
| Filter | Input | Output |
|---|---|---|
lower | "Hello" | "hello" |
upper | "Hello" | "HELLO" |
title | "hello world" | "Hello World" |
slugify | "Hello World!" | "hello-world" |
truncate(length=10) | "Hello World" | "Hello W..." |
{{ text | lower }}
{{ text | upper }}
{{ text | title }}
{{ text | truncate(length=100) }}
{{ text | replace(from="old", to="new") }}
{{ text | split(pat=",") }}
{{ array | join(sep=", ") }}
{{ text | slugify }}
{{ html | striptags }}
{{ html | safe }}Number:
{{ num | round(precision=2) }}
{{ bytes | filesizeformat }}Array:
{{ items | length }}
{{ items | first }}
{{ items | last }}
{{ items | reverse }}
{{ items | sort(attribute="date") }}
{{ items | filter(attribute="draft", value=false) }}
{{ items | map(attribute="title") }}
{{ items | slice(start=0, end=5) }}
{{ items | json_encode() }}Date filters with examples:
| Format | Input (2024-01-15) | Output |
|---|---|---|
%Y-%m-%d | date | 2024-01-15 |
%B %d, %Y | date | January 15, 2024 |
%A | date | Monday |
{{ date | date(format="%Y-%m-%d") }}
{{ date | date(format="%B %d, %Y") }}Codes: %Y (2024), %m (01-12), %d (01-31), %B (January), %A (Monday)
Default:
{{ value | default(value="fallback") }}---
Template Inheritance
Base template:
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}{{ config.title }}{% endblock %}</title>
{% endblock head %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>Child template:
{% extends "base.html" %}
{% block title %}{{ page.title }}{% endblock %}
{% block head %}
{{ super() }} {# Include parent #}
<link rel="stylesheet" href="/css/page.css">
{% endblock head %}
{% block content %}
<article>{{ page.content | safe }}</article>
{% endblock %}---
Macros
Define:
{% macro post_card(post, show_date=true) %}
<article>
<h2><a href="{{ post.permalink }}">{{ post.title }}</a></h2>
{% if show_date %}<time>{{ post.date | date(format="%B %d, %Y") }}</time>{% endif %}
</article>
{% endmacro %}Use:
{% import "macros.html" as macros %}
{{ macros::post_card(post=post) }}Include:
{% include "header.html" %}
{% include "optional.html" ignore missing %}---
Zola Variables
Global (all templates):
{{ config.base_url }}
{{ config.title }}
{{ config.extra.logo }}
{{ current_path }}
{{ current_url }}
{{ lang }}Page (page.html):
{{ page.title }}
{{ page.description }}
{{ page.content | safe }}
{{ page.summary | safe }}
{{ page.date }}
{{ page.updated }}
{{ page.permalink }}
{{ page.word_count }}
{{ page.reading_time }}
{{ page.toc }}
{{ page.taxonomies.tags }}
{{ page.extra.* }}
{{ page.lower }} {# Previous page #}
{{ page.higher }} {# Next page #}
{{ page.translations }}Section (section.html):
{{ section.title }}
{{ section.content | safe }}
{{ section.pages }}
{{ section.subsections }}Paginator:
{{ paginator.pages }}
{{ paginator.current_index }}
{{ paginator.number_pagers }}
{{ paginator.previous }}
{{ paginator.next }}Taxonomy list:
{{ taxonomy.name }}
{% for term in terms %}
{{ term.name }}
{{ term.permalink }}
{{ term.pages | length }}
{% endfor %}---
Zola Functions
URLs:
{{ get_url(path="blog/post") }}
{{ get_url(path="@/blog/_index.md") }}
{{ get_url(path="js/app.js", cachebust=true) }}Content:
{% set page = get_page(path="pages/about.md") %}
{% set section = get_section(path="blog/_index.md") %}
{% set section = get_section(path="blog/_index.md", metadata_only=true) %}Taxonomy:
{% set tags = get_taxonomy(kind="tags") %}
{{ get_taxonomy_url(kind="tags", name="rust") }}Data:
{% set data = load_data(path="data/config.json") %}
{% set data = load_data(url="https://api.example.com", format="json") %}Images:
{% set img = resize_image(path="photo.jpg", width=800, op="fit_width", format="webp") %}
<img src="{{ img.url }}" width="{{ img.width }}" height="{{ img.height }}">Operations: fill, fit_width, fit_height, fit, scale Formats: auto, jpg, png, webp, avif
Hash:
integrity="sha384-{{ get_hash(path='static/js/app.js', sha_type=384, base64=true) }}"---
Shortcodes
HTML shortcode (templates/shortcodes/youtube.html):
<iframe src="https://www.youtube-nocookie.com/embed/{{ id }}" allowfullscreen></iframe>Usage: {{ youtube(id="dQw4w9WgXcQ") }}
Body shortcode (templates/shortcodes/note.html):
<div class="note note-{{ type | default(value='info') }}">{{ body }}</div>Usage:
{% note(type="warning") %}
Important content.
{% end %}---
Common Patterns
Navigation:
{% for item in config.extra.nav_items %}
<a href="{{ item.url }}" {% if current_path == item.url %}class="active"{% endif %}>
{{ item.label }}
</a>
{% endfor %}Table of contents:
{% for h1 in page.toc %}
<li>
<a href="{{ h1.permalink }}">{{ h1.title }}</a>
{% if h1.children %}
<ul>{% for h2 in h1.children %}
<li><a href="{{ h2.permalink }}">{{ h2.title }}</a></li>
{% endfor %}</ul>
{% endif %}
</li>
{% endfor %}Pagination:
{% if paginator.previous %}<a href="{{ paginator.previous }}">← Newer</a>{% endif %}
<span>Page {{ paginator.current_index }} / {{ paginator.number_pagers }}</span>
{% if paginator.next %}<a href="{{ paginator.next }}">Older →</a>{% endif %}Debug:
{{ __tera_context }}