Creating a Blog with Zola: A Practical Guide
Building a blog with Zola is straightforward, but there are patterns and decisions that aren't immediately obvious from the documentation. This guide walks through creating a real blog, covering the practical choices you'll face and the solutions that work well.
Getting Started: Installation and Project Setup
First, install Zola. Download the binary for your platform from the releases page, or use a package manager:
# macOS
brew install zola
# Linux
snap install zola --edge
# Or download the binary directly
Create your project:
zola init my-blog
cd my-blog
Zola will ask a few questions. For a blog, answer:
- URL: Your production URL (you can change this later)
- Sass compilation: Yes
- Syntax highlighting: Yes
Your directory structure looks like this:
my-blog/
├── config.toml
├── content/
├── sass/
├── static/
├── templates/
└── themes/
Configuration: The Foundation
Open config.toml and configure the essentials:
base_url = "https://yourdomain.com"
title = "Your Blog Name"
description = "A brief description of your blog"
# Compile Sass files
compile_sass = true
# Enable syntax highlighting for code blocks
highlight_code = true
highlight_theme = "base16-ocean-dark"
# Generate RSS feed
generate_feed = true
feed_filename = "rss.xml"
# Taxonomies (tags and categories)
taxonomies = [
{name = "tags", feed = true},
{name = "categories", feed = true},
]
[markdown]
# Enable smart punctuation and other features
smart_punctuation = true
highlight_code = true
[extra]
# Add any custom variables you need
author = "Your Name"
github = "https://github.com/yourusername"
twitter = "yourusername"
This configuration gives you a solid foundation: RSS feeds, taxonomies for organizing content, and code highlighting.
Content Structure: Organizing Your Posts
Create a blog section in content/blog/:
mkdir -p content/blog
Create content/blog/_index.md:
+++
title = "Blog"
sort_by = "date"
template = "blog.html"
page_template = "blog-page.html"
paginate_by = 10
+++
Welcome to my blog where I write about web development, programming, and technology.
This file defines how the blog section behaves:
sort_by = "date"orders posts by publication datepaginate_by = 10creates pagination every 10 poststemplatedefines the list viewpage_templatedefines individual post view
Creating Your First Post
Create content/blog/hello-world.md:
+++
title = "Hello World: Starting a New Blog"
date = 2025-11-16
description = "My first post on this new blog built with Zola"
[taxonomies]
tags = ["meta", "blogging"]
categories = ["General"]
[extra]
featured = true
+++
Welcome to my blog! This is my first post built with Zola, and I'm excited to share what I'm learning.
<!-- more -->
## Why Zola?
I chose Zola for several reasons:
- Fast builds
- Zero dependencies
- Simple configuration
- Great multilingual support
## What's Next?
I plan to write about web development, share tutorials, and document my learning journey.
Stay tuned for more content!
The <!-- more --> tag creates a summary break. Text before it appears in post previews.
Templates: Building the Blog Layout
Create templates/base.html as your foundation:
<!DOCTYPE html>
<html lang="{{ lang | default(value="en") }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<meta name="description" content="{% block description %}{{ config.description }}{% endblock %}">
{% if config.generate_feed %}
<link rel="alternate" type="application/rss+xml" title="RSS" href="#">
{% endif %}
<link rel="/">
</head>
<body>
<header>
<nav>
<a href="#">{{ config.title }}</a>
<a href="#">Blog</a>
<a href="#">About</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ now() | date(format="%Y") }} {{ config.extra.author }}</p>
</footer>
</body>
</html>
Create templates/blog.html for the blog list:
{% extends "base.html" %}
{% block title %}{{ section.title }} | {{ config.title }}{% endblock %}
{% block description %}{{ section.description | default(value=config.description) }}{% endblock %}
{% block content %}
<div class="blog-list">
<h1>{{ section.title }}</h1>
{{ section.content | safe }}
{% if section.pages %}
<div class="posts">
{% for page in section.pages %}
<article class="post-preview">
<h2><a href="{{ page.permalink }}">{{ page.title }}</a></h2>
<time datetime="{{ page.date }}">{{ page.date | date(format="%B %d, %Y") }}</time>
{% if page.summary %}
<p>{{ page.summary | safe }}</p>
{% elif page.description %}
<p>{{ page.description }}</p>
{% endif %}
{% if page.taxonomies.tags %}
<div class="tags">
{% for tag in page.taxonomies.tags %}
<a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">#{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
<a href="{{ page.permalink }}">Read more →</a>
</article>
{% endfor %}
</div>
{% endif %}
{% if paginator %}
<nav class="pagination">
{% if paginator.previous %}
<a href="{{ paginator.previous }}">← Newer</a>
{% endif %}
<span>Page {{ paginator.current_index }} of {{ paginator.number_pagers }}</span>
{% if paginator.next %}
<a href="{{ paginator.next }}">Older →</a>
{% endif %}
</nav>
{% endif %}
</div>
{% endblock %}
Create templates/blog-page.html for individual posts:
{% extends "base.html" %}
{% block title %}{{ page.title }} | {{ config.title }}{% endblock %}
{% block description %}{{ page.description | default(value=page.summary) }}{% endblock %}
{% block content %}
<article class="blog-post">
<header>
<h1>{{ page.title }}</h1>
<div class="post-meta">
<time datetime="{{ page.date }}">{{ page.date | date(format="%B %d, %Y") }}</time>
{% if page.extra.reading_time %}
<span>• {{ page.reading_time }} min read</span>
{% endif %}
</div>
{% if page.taxonomies.tags %}
<div class="tags">
{% for tag in page.taxonomies.tags %}
<a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">#{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</header>
<div class="post-content">
{{ page.content | safe }}
</div>
{% if page.earlier or page.later %}
<nav class="post-navigation">
{% if page.earlier %}
<a href="{{ page.earlier.permalink }}" class="prev">← {{ page.earlier.title }}</a>
{% endif %}
{% if page.later %}
<a href="{{ page.later.permalink }}" class="next">{{ page.later.title }} →</a>
{% endif %}
</nav>
{% endif %}
</article>
{% endblock %}
Styling with Sass
Create sass/style.scss:
// Variables
$primary-color: #2563eb;
$text-color: #1f2937;
$background: #ffffff;
$border-color: #e5e7eb;
$max-width: 800px;
// Base styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: $text-color;
background: $background;
}
// Layout
header {
border-bottom: 1px solid $border-color;
padding: 1rem 0;
nav {
max-width: $max-width;
margin: 0 auto;
padding: 0 1rem;
display: flex;
gap: 2rem;
a {
text-decoration: none;
color: $text-color;
font-weight: 500;
&:hover {
color: $primary-color;
}
&:first-child {
font-weight: 700;
margin-right: auto;
}
}
}
}
main {
max-width: $max-width;
margin: 2rem auto;
padding: 0 1rem;
}
footer {
border-top: 1px solid $border-color;
padding: 2rem 1rem;
text-align: center;
color: #6b7280;
margin-top: 4rem;
}
// Blog styles
.post-preview {
margin-bottom: 3rem;
h2 {
margin-bottom: 0.5rem;
a {
color: $text-color;
text-decoration: none;
&:hover {
color: $primary-color;
}
}
}
time {
color: #6b7280;
font-size: 0.9rem;
}
p {
margin: 1rem 0;
}
}
.blog-post {
.post-content {
margin: 2rem 0;
h2, h3 {
margin: 2rem 0 1rem;
}
p {
margin: 1rem 0;
}
code {
background: #f3f4f6;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}
pre {
background: #1f2937;
color: #f3f4f6;
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
}
}
.tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 1rem 0;
a {
background: #f3f4f6;
padding: 0.25rem 0.75rem;
border-radius: 3px;
text-decoration: none;
color: $text-color;
font-size: 0.9rem;
&:hover {
background: $primary-color;
color: white;
}
}
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin: 3rem 0;
padding: 1rem 0;
border-top: 1px solid $border-color;
}
// Responsive
@media (max-width: 768px) {
header nav {
flex-direction: column;
gap: 1rem;
}
}
Adding Homepage
Create content/_index.md:
+++
title = "Home"
template = "index.html"
+++
Create templates/index.html:
{% extends "base.html" %}
{% block content %}
<div class="home">
<h1>Welcome to {{ config.title }}</h1>
<p class="intro">{{ config.description }}</p>
{% set blog_section = get_section(path="blog/_index.md") %}
{% if blog_section.pages %}
<section class="recent-posts">
<h2>Recent Posts</h2>
<div class="posts">
{% for page in blog_section.pages | slice(end=5) %}
<article class="post-preview">
<h3><a href="{{ page.permalink }}">{{ page.title }}</a></h3>
<time datetime="{{ page.date }}">{{ page.date | date(format="%B %d, %Y") }}</time>
{% if page.description %}
<p>{{ page.description }}</p>
{% endif %}
</article>
{% endfor %}
</div>
<a href="#" class="view-all">View all posts →</a>
</section>
{% endif %}
</div>
{% endblock %}
Running Your Blog
Start the development server:
zola serve
Visit http://127.0.0.1:1111 to see your blog. Changes to content and templates reload automatically.
Building for Production
Build your site for deployment:
zola build
The static site is generated in the public/ directory. Upload these files to any static hosting service.
Deployment to GitHub Pages
Create .github/workflows/deploy.yml:
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Zola
run: |
wget https://github.com/getzola/zola/releases/download/v0.18.0/zola-v0.18.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf zola-v0.18.0-x86_64-unknown-linux-gnu.tar.gz
sudo mv zola /usr/local/bin/
- name: Build
run: zola build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
Push to GitHub, enable GitHub Pages in your repository settings, and your blog deploys automatically on every push.
Next Steps
Your blog is now functional. Consider adding:
Featured posts: Add a featured field to front matter and filter in templates
Reading time: Use {{ page.reading_time }} to show estimated reading time
Table of contents: Enable insert_anchor_links = "left" in config for heading anchors
Search: Implement search with Zola's search index feature
Comments: Integrate a commenting system like utterances or Disqus
Analytics: Add analytics with a simple script include
Key Takeaways
Creating a blog with Zola is refreshingly straightforward:
- Content is just Markdown with front matter—no complex APIs or abstractions
- Templates are Tera—if you know Jinja2, you already know 90% of what you need
- Built-in features eliminate plugins—Sass, syntax highlighting, RSS, and taxonomies work out of the box
- Fast iteration—changes appear instantly during development
- Simple deployment—static files can be hosted anywhere
The key to productive Zola development is embracing its opinions. The structure might feel restrictive at first, but it eliminates decision fatigue and lets you focus on content.
Start simple, write content, and expand as you need more features. Zola gets out of your way and lets you build.