This is the detailed guide on how to use zola-theme-serene. You should also check zola's documentation.
Create a zola site and add serene theme (assuming your site is called myblog):
zola init myblog
cd myblog
git init
git submodule add -b latest https://github.com/isunjn/serene.git themes/sereneCopy the content of myblog/themes/serene/config.example.toml to myblog/config.toml.
There is a sections config option in your config.toml, which enumerates the sections your site has. You should have at least one blog section.
The name and path can be changed, be noticed that if you changed the blog section path (e.g. from /posts to /blog), then you should also change blog_section_path option.
For home page, create myblog/content/_index.md:
+++
template = 'home.html'
[extra]
lang = 'en'
# Show footer in home page
footer = false
# If you don't want to display id/bio/avatar, simply comment out that line
name = "Jhon Wick"
id = "jhonwick"
bio = "dog person, killer"
avatar = "img/avatar.webp"
links = [
{ name = "GitHub", icon = "github", url = "https://github.com/<your-username>" },
{ name = "Email", icon = "email", url = "mailto:<your-email-address>" },
{ name = "Twitter", icon = "twitter", url = "https://twitter.com/<your-username>" },
{ name = "Mastodon", icon = "mastodon", url = "https://mastodon.social/<your-username>", rel_me = true },
]
# Show a few recent posts in home page
recent = false
recent_max = 15
recent_more_text = "more »"
date_format = "%b %-d, %Y"
+++
Hi, I'm ...
For blog section, create myblog/content/posts/_index.md:
+++
title = "My Blog"
description = "My blog site."
sort_by = "date"
template = "blog.html"
page_template = "post.html"
insert_anchor_links = "right"
generate_feeds = true
[extra]
lang = "en"
title = "Posts"
subtitle = "I write about ...."
date_format = "%b %-d, %Y"
categorized = false # posts can be categorized
back_to_top = true # show back-to-top button
toc = true # show table-of-contents
comment = false # enable comment
copy = true # show copy button in code block
outdate_alert = false
outdate_alert_days = 12
outdate_alert_text_before = "This article was last updated "
outdate_alert_text_after = " days ago and may be out of date."
+++
Blog section is defined by blog.html and post.html. Serene also has a special template called prose.html, it applies the same styles of blog post page. You can use it as a section template for a custom section page, for example if you want a separate about page, you can add a { name = "about", path = "/about", is_external = false } to the sections and create a myblog/content/about/_index.md:
+++
title = "About me"
description = "About page of ..."
template = "prose.html"
insert_anchor_links = "none"
[extra]
lang = 'en'
title = "Posts"
subtitle = "I write about ...."
math = false
mermaid = false
copy = false
comment = false
reaction = false
+++
Hi, My name is ....
The default date format is "%b %-d, %Y", e.g. "Dec 13, 2025", check this page if you want to customize it, for example change to "%Y-%-m-%-d", e.g. "2025-2-13".
Now the myblog directory may look like this:
├── config.toml
├── content/
│ ├── posts/
│ │ └── _index.md
│ ├── about/
│ │ └── _index.md
│ └── _index.md
├── sass/
├── static/
├── templates/
└── themes/
└── serene/
Create a new directory img under myblog/static, put favicon related files here, you can use tools like favicon.io to generate those files. If you want to display avatar in home page, also put your avatar picture file avatar.webp here, webp format is recommended.
...
├── static/
│ └── img/
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ └── avatar.webp
...
The default icons are placed in myblog/themes/serene/static/icon, the icon value in links of home section is the file name of the svg file.
To customize, find the svg file you want, modify (in case you don't kown, a svg file is just a plian text file) its width and height to 18, and the color to currentColor:
... width="18" height="18" ... fill="currentColor" ...
and then put it in myblog/static/icon, file in this folder with the same name will override the default one.
The default icons mostly came from Remix Icon.
By default there is theme toggle button to switch between light and dark mode, you can set force_theme in config.toml to force a specific mode only.
Zola's default feed file is located in the root directory of the site, set generate_feeds = true in config.toml, feed_filenames can be set to ["atom.xml"] or ["rss.xml"] , corresponding to two different rss file standards, you should also set generate_feeds = false in myblog/content/posts/_index.md
The serene theme looks more like a personal website, the posts are in the /posts directory, you may want the feed file to be in the /posts directory instead of the root directory, this requires you to set generate_feeds = false feed_filenames = ["feed.xml"] in config.toml, and set generate_feeds = true in myblog/content/posts/_index.md.
feed.xml uses title and description from myblog/content/posts/_index.md, the other two use config.toml's.
To add scripts for analytics tools (such as Google Analytics, Umami, etc.), you can create myblog/templates/_head_extend.html. The content of this file will be added to the html head of each page.
Copy myblog/themes/serene/templates/_custom_css.html to myblog/templates/_custom_css.html, variables in this file are used to control styles, such as the theme color --primary-color, modify them as you want.
If you want to customize more, you need to copy that file under the templates, static, sass directory in the corresponding themes/serene to the same name directory of myblog, and modify it. Be careful not to directly modify the files under the serene directory, because these modifications may cause conflicts if the theme is updated.
If you want to use a custom font, create a new myblog/templates/_custom_font.html and put the font link tags (for example, from google fonts) into it, and then modify --main-font or --code-font in myblog/sass/templates/_custom_css.html. For performance reasons, you may want to self-host font files, but it's optional:
- Open google-webfonts-helper and choose your font.
- Modify
Customize folder prefixof step 3 to/font/and then copy the css. - Replace the content of
myblog/templates/_custom_font.htmlwith a<style> </style>tag, with the css you just copied. - Download step 4 font files and put them in
myblog/static/font/folder.
The content inside the two +++ at the top of the post markdown file is called front matter, used to provide metadata and config options of that post. Supported options:
+++
title = ""
description = ""
date = 2022-01-01
updated = 2025-01-01
draft = true
[taxonomies]
categories = ["one"]
tags = ["one", "two", "three"]
[extra]
lang = "en"
toc = true
comment = false
copy = true
outdate_alert = true
outdate_alert_days = 120
math = false
mermaid = false
featured = false
reaction = false
+++
new post about something...Some of these options can also be configured in myblog/content/posts/_index.md, as the default value for all posts.
If you set blog_categorized = true, posts will be sorted alphabetically by default, you can manually set the order by adding a prefix __[0-9]{2}__ in front of the category name, for example, categories = ["__01__CatXXX"]
Set toc = true to display of table-of-contents.
Set math = true to enable formula rendering with KaTeX.
Set mermaid = true to enable chart rendering with Mermaid.
Set featured = true to display an asterisk(*) mark in front of the title.
If one of your posts has strong timeliness, you can display an outdate alert after certain days.
Set outdate_alert and outdate_alert_days to enable the alert.
In myblog/content/posts/_index.md, options outdate_alert_text_before and outdate_alert_text_after are the text content of the alert.
You can use giscus as the comment system.
To enable it, you need to create myblog/templates/_giscus_script.html and put the script configured on the giscus website into it, then change the value of data-theme to https://<your-domain-name>/giscus_light.css, replace <your-domain-name> with you domain name, same as base_url in config.toml, if you set force_theme to dark, replace giscus_light.css with giscus_dark.css.
Then set comment = true to enable comment.
This theme supports a feature called anonymous emoji reaction, visitors of you site can react to your post with emojis, without the need to log in or register.
You need to setup a backend api endpoint to enable it. Your endpoint should handle both GET and POST request:
-
GETRequest query:
slug: the slug of the postResponse:
-
POSTRequest body:
{ "slug": "post-slug", "target": "👍", "reacted": true }Response:
{ "success": true }
For conivence, you can use one template repo to setup your own endpoint:
-
isunjn/reaction: All you need is a Cloudflare account. The free tier is good enough for a low-traffic personal blog.
-
mildronize/reaction: Specific to Azure platform.
-
sorokya/reaction: A self-contained one, you can run it with docker.
After you setup your endpoint, set reaction_endpoint = "<your-endpoint>" and reaction = true to enable it.
Giscus also support a reaction feature, but it requires visitors to log in to GitHub, you can disable it in giscus's settings.
Zola supports some annotations for code blocks.
Shortcodes are some special templates.
-
Use
figureto add caption or width/height to an image,altcaptionwidthheightare all optional:{{ figure(src="/path/to/img", alt="alt text", caption="caption text", width="600", height="400") }}The caption is parsed as markdown so you can use bold / italic / link, for example
caption="[via](https://example.com)"Adding height to an image is always recommended, as this can avoid page layout shift. When you use
[](https://exmaple.com/img.png), browser cannot determine the image's dimensions before it loads. -
Use
quoteto display a special quote block,citeis optional:{% quote(cite="") %} // content... {% end %} -
Use
detailto add an expandable detail block,default_openis optional:{% detail(title="", default_open=false) %} // content... {% end %} -
As you can see in this page of the demo site, callouts are special blockquote blocks, just like github's. There are currently 5 types:
notetipimportantwarningcaution.titleis optional:{% note(title="Note") %} note text {% end %}Update: github callout/alert syntax is supported since zola v0.21 (however it doesn't display icon and title), the callout shortcodes will be deprecated in this theme's next major release
-
Use
mermaidto add a mermaid chart:{% mermaid() %} flowchart LR A[Hard] -->|Text| B(Round) B --> C{Decision} C -->|One| D[Result 1] C -->|Two| E[Result 2] {% end %} -
Use
youtubeto embed a youtube video,autoplayis optional, default tofalse:{{ youtube(id="<youtube-video-id>", autoplay=true) }}
This theme has several special shortcodes for creating a collection of items. These collections can be used to showcase various types of your list, such as projects, publications, blogroll, bookmarks, etc. Check this page on demo site to see some examples.
Currently, there are 7 types of collection item:
-
card[[collection]] type = "card" title = "Title" subtitle = "Subtitle" # optional; supports Markdown date = "Date" # optional link = "https://example.com" # optional icon = "https://example.com/image.png" # optional content = "Content" # supports Markdown tags = ["tag1", "tag2"] # optional featured = false # optional
-
card_simple[[collection]] type = "card_simple" title = "Title" date = "Date" # optional link = "https://example.com" # optional icon = "https://example.com/image.png" # optional content = "Content" # supports Markdown featured = false # optional
-
entry[[collection]] type = "entry" title = "Title" # optional subtitle = "Subtitle" # optional link = "https://example.com" # optional icon = "https://example.com/image.png" # optional
-
box[[collection]] type = "box" title = "Title" subtitle = "Subtitle" # optional link = "https://example.com" # optional img = "https://example.com/image.png" # optional
-
art[[collection]] type = "art" title = "Title" subtitle = "Subtitle" # optional; support Markdown link = "https://example.com" # optional img = "https://example.com/image.png" content = "Content" # optional; supports Markdown footer = "Footer" # optional; supports Markdown
-
art_simple[[collection]] type = "art_simple" title = "Title" subtitle = "Subtitle" # optional; supports Markdown link = "https://example.com" # optional img = "https://example.com/image.png"
List your items in a toml file and then use a collection shortcode to render them.
For example, to create a "projects" section page:
-
Create
myblog/content/projects/projects.toml:[[collection]] type = "card" title = "Tokio" link = "https://example.com" content = "Tokio is an asynchronous runtime for the Rust programming language. It provides the building blocks needed for writing network applications. It gives the flexibility to target a wide range of systems, from large servers with dozens of cores to small embedded devices." tags = ["rust", "async", "runtime"] [[collection]] type = "card" title = "Kubernetes" link = "https://example.com" content = "Kubernetes, also known as K8s, is an open source system for managing containerized applications across multiple hosts. It provides basic mechanisms for the deployment, maintenance, and scaling of applications." tags = ["k8s", "golang"] [[collection]] type = "card" title = "Next.js" link = "https://example.com" content = "Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations." tags = ["typescript", "react", "frontend"]
-
Create
myblog/content/projects/_index.md:+++ title = "My projects" description = "Projects page of ..." template = "prose.html" [extra] title = "Projects" subtitle = "Some cool projects I made" +++ {{ collection(file="projects.toml") }} -
Add projects section in
sectionsofconfig.tomlsections = [ # ... { name = "projects", path = "/projects", is_external = false }, ]
Local preview:
zola serveBuild the site:
zola buildTo deploy a static site, refer to zola's documentation about deployment.
Check the CHANGELOG.md on github for breaking changes before you update.
If you copied some files from myblog/themes/serene to myblog/ for customization, such as _custom_css.html or main.scss, then you should record what you have modified before you update, re-copy those files and re-apply your modification after updating. The config.toml should be re-copied too.
You can watch (watch > custom > releases > apply) this project on github to be reminded of a new release.
git submodule update --remote themes/serene
{ "👍": [123, true], // emoji: [count, reacted] "👀": [456, false] }