Designing template

Typlog themes are powered by Jinja template language. If you are not familiar with Jinja, you can learn the syntax at the offical documentation.


We don't use {% extends %} in Typlog themes, each template inherits its own base layout which contains many usefull code in <head>. What we actually need are blocks:


Redesign the document title:

{% block title %}{{ page.title }}{% endblock %}


{% block lang %} is defined for the lang attribute on <html> tag:

<html lang="{% block lang %}{% endblock %}">

By default, it will use site.primary_lang or page.lang.


Adding more <meta>, <link> tag in meta block:

{% block meta %}
<meta />
{% endblock %}


Adding css in style block:

{% block style %}
<link href=",400,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ static_url }}hello.css">
{% endblock }


This is the main block for your theme. All content should be in this block.

{% block body %}
{% endblock %}


This block is located at the bottom of <body> tag, we can add some scripts here if needed:

{% block script %}
{% endblock %}


This block is located just below <body> tag, the default HTML is:

{% block head_html %}
{% if features.head_html %}{{ features.head_html|safe }}{% endif %}
{% endblock %}


This block is located above {% block script %}, the default HTML is:

{% block foot_html %}
{% if features.foot_html %}{{ features.foot_html|safe }}{% endif %}
{% endblock %}


In the templates, we can use variables to render the real data into HTML. Here are all the variables:


This is a global variable, it contains the information about your site. The detail structure:

id: integer
name: string
slug: string
primary_lang: string
languages: [string, string]
base_url: uri
active: boolean
summary?: string
favicon?: uri
src: uri
width: integer
height: integer
src: uri
width: integer
height: integer
src: uri
width: integer
height: integer
license?: string
primary_links: [{ url, title, blank }]
secondary_links: [{ url, title, blank }]
color: string
socials: {}
sponsors: {}
list_url: string (e.g. /archive/)
posts_url?: string (e.g. /posts/, this attribute can be undefined)
audios_url?: string (e.g. /episodes/, this attribute can be undefined)
podcast?: # if enabled and has podcast information
title: string
subtitle: string
description: string
explicit: boolean
categories: []
type: string
image: uri
email: string
author: string
homepage: uri
keywords: string
copyright: string
links?: [ { icon, url, title, blank } ]

Please note, ? means this field may be None. For example:

{% if site.logo %}
{{ site.logo.src }}
{% endif %}


This is a global variable, it contains methods to query posts, episodes and etc.

.latest_subjects(subject_type=None, count=10, contains=None)

This will list the recent posts or episodes. This method is usually used in home.j2 template. The subject_type value can be post and audio.

Here is an example in home.j2:

{% block body %}
{% for post in query.latest_subjects('post') %}
<li>{{ post.title }}</li>
{% endfor %}
{% endblock %}

contains is a list, which can contain: authors, tags, and content.

If there is no need to show authors, tags or full content of a post, we can pass nothing to contains which will improve the performance of rendering.

.previous_subjects(subject, count=1, same_type=True, contains=None)

This method can list the previous posts or episodes of the given subject(post or episode). This is usually used in item.j2 template:

{% block body %}
<div class="previous">
{% for post in query.previous_subjects(page, count=2) %}
<a href="{{ post.url }}>{{ post.title }}</a>
{% endfor %}
{% endblock %}

.previous_subject(subject, same_type=True, contains=None)

This method will return only the nearest previous subject. It is used in item.j2 template.

.next_subjects(subject, count=1, same_type=True, contains=None)

This method is the same as query.previous_subjects, but for next.

.next_subject(subject, same_type=True, contains=None)

This method is the same as query.previous_subject, but for next.


This method will list all the tags in your site:

{% block body %}
{% for tag in query.tags() %}
<a href="{{ tag.url }}">{{ tag.title }}</a>
{% endfor %}
{% endblock %}


This method will list all the years of your posts or episodes.


This method will list all the authors, including hosts and guests in your site.


This is a PRO feature, it will load the JSON assets named with _config/{name}. The name should be the theme name you registered in our theme registry. If the site doesn't has PRO plan, this method will return None.

Let's take an example. You are going to create a theme named paper:

{% set theme_conf = query.config('paper') %}
{% block style %}
{% if theme_conf and theme_conf.css_files %}
{% for src in theme_conf.css_files %}
<link rel="stylesheet" href="{{ src }}">
{% endfor %}
{% endif %}
{% endblock %}

To use this feature, the site has to create an asset file _config/paper in JSON format:

"css_files": [


This is a global variable, it contains the features this site supports:

post: boolean
audio: boolean
photo: boolean
comment: boolean
subscriber: boolean
algolia: boolean
head_html: string # inject code for head_html
foot_html: string # inject code for foot_html


page is a context aware variable. It is different in each view.

In list.j2, the structure is:

title: string
url: path
type: string
topic: object
items: []
prev_year?: integer
prev_url?: path
next_year?: integer
next_url?: path

A topic refers to a model about the list page. For instance, in a tag page: /tags/web, this topic is the tag object, which means you can set:

{% set tag = page.topic %}

This topic is usually used together with type. Here is an example of how to use it:

{% macro render_tag(tag) %}
{% endmacro }
{% macro render_user(author) %}
{% endmacro }
{% block body %}
{% if page.type == 'tag' %}
{{ render_tag(page.topic) }}
{% elif page.type == 'author' %}
{{ render_tag(page.topic) }}
{% elif page.type == 'audio_list' %}
{{ render_podcast_info(site) }}
{% else %}
{{ render_site_info(site) }}
{% endif %}
{% endblock }

items is a list of posts and episodes of current visiting page.

In item.j2, page refers to a post, an episode, or a page object.

id: integer
type: string # post, audio, page
slug: string
lang: string
title: string
src: uri
width: integer
height: integer
subtitle?: string
content: string
url: path
tags: array of tags
authors: array of authors
published_at: datetime
updated_at: datetime
comment: string # open, closed, disabled
visibility: string # public, password, member
render_html: function # render_html(lazy=True) -> HTML
# only available with "post" type
license: string
# only available with "audio" type
image: uri
explicit: boolean
season: integer
episode: integer
episode_type: string
duration: string
src: uri
size: integer
duration: integer
mimetype: string

In episode post, page contains two more attributes:

# ....
hosts: array of authors
guests: array of authors


This variable will be a jsdelivr link. In development, serve-theme will replace this variable to the local css file. It is usually used in:

{% block style %}
<link rel="stylesheet" href="{{ static_url }}hello.css">
{% endblock %}


This is not a variable, but it may be used via:

  1. query.tags()
  2. page.topic of tag type in list.j2
  3. page.tags in item.j2

The structure of a tag:

id: integer
slug: string
title: string
summary: string
src: uri
width: integer
height: integer
metadata: {}
url: path


This object may be one of these:

  1. query.authors()
  2. page.topic of author type in list.j2
  3. page.authors in item.j2

The structure of a author:

username: string
name: string
src: uri
width: integer
height: integer
avatar: uri
bio: html
metadata: {}
url: path
# only in page.authors
role: string # primary, secondary


We can use all the built-in filters provided by Jinja. And in Typlog, there are two more filters:


This filter is used to produce ISO formattted datetime, it is usually used to format .published_at:

<time datetime="{{ page.published_at|xmldatetime }}">

This is equal to:

<time datetime="{{ page.published_at.strftime('%Y-%m-%dT%H:%M:%SZ') }}">


This filter is used to resize the images, it accepts several image sizes:

<img src="{{ site.logo.src|thumbnail('ss') }}">

Available sizes:

  • s: small size
  • m: middle size
  • l: large size
  • ss: crop to squared small size
  • sm: squared middle size
  • sl: squared large size


This filter will convert markdown text into HTML. When using this filter, make sure to apply the safe filter:

{{ my_text|markdown|safe }}


There are some built-in macros, which can be used to help you rendering the templates.

{% from "macros.j2" import render_social_icons %}
This macro is used for:
{{ render_social_icons(site) }}
{% from "macros.j2" import render_item, render_navigation %}
The `render_item` is usually used in `home.j2` and `list.j2`.
{% for item in query.latest_subjects() %}
{{ render_item(site, item) }}
{% endfor %}
The `render_navigation` macro is used in `list.j2` to render the year navigation:
{{ render_navigation(site, page) }}
{% from "macros.j2" import render_subject_visibility, render_subject_content %}
{% from "macros.j2" import render_subject_license, render_review_subject, render_subject_authors %}
These macros are used in `item.j2`. The `render_subject_content` is used to render the content part, it will
handle visibility automatically.
{{ render_subject_content(site, page) }}
Parameters for other macros:
{{ render_subject_visibility(site, page) }}
{{ render_subject_license(site, page) }}
{% if %}
{{ render_review_subject( }}
{% endif %}
{{ render_subject_authors(site, page) }}


Here are the built-in snippets which we can {% include %} them:

this is typlog brand foot
{% include "_snippets/brand_foot.j2" }
this is your site foot
{% include "_snippets/site_foot.j2" }

Used in item.j2:

{% include "_snippets/subject_share.j2" %}
{% include "_snippets/subject_foot.j2" %}

JavaScript hooks

In the rendered html, Typlog will inject a typlog.js script. This script has many features, we can use js-? class to connect the hooks.

js-search: popup the search overlay

<input class="js-search" type="text" placeholder="Search...">

js-subscribe: popup the subscribe dialog

<button class="js-subscribe">Subscribe</button>

js-audio: render the player UI

<div class="entry-audio js-audio">
<audio src="{{ }}" preload="none" controls
{% if page.image %}data-image="{{ page.image|thumbnail('ss') }}"{% endif %}>
This hook is already included in
{{ render_subject_content(site, page) }}

js-enjoy: click to send like event

<button class="js-enjoy">Enjoy</button>
This hook is already included in snippet:
{% include "_snippets/subject_share.j2" %}

Next step

Now, your theme looks pretty, and you are eager to use this theme, you have to submit the theme to Typlog theme registry.

Read more