Skip to content



To develop your own theme, you need to grab an API token at first. You can create one in your account system. Click to create one.

create token


You will also use serve-theme tool to develop your theme, get the executable binary file from Download the one matching your OS system, rename it ot serve-theme.

This tool can create a localhost site for previewing your current theme.


Let's create an example theme called hello. First, we create a folder hello, then we add some files into this folder:

$ cd hello
$ ls  home.j2  item.j2  list.j2  hello.css  theme.json

In this folder, we have 3 templates:

  1. home.j2 - this is your homepage
  2. list.j2 - template for posts, episodes list/archive page
  3. item.j2 - detail page for a post, episode or page.

Another required file is theme.json, which contains information about your theme.

Then we can start our dev server with serve-theme:

$ export TOKEN=pt_xxxx   # this token is created in preparation
$ export SITE=123        # find your site ID in Typlog
$ ../serve-theme         # assuming this bin file is located in the parent folder
2021/09/21 20:54:44 Listening . on port 7000

Then open your browser and visit http://localhost:7000.

Let's add some code into home.j2 file:

{% block body %}
<h1>Hello Typlog</h1>
{% endblock %}

Then refresh your browser, you will see the changes in http://localhost:7000. But the above code is useless, you have to learn the variables provided by Typlog to add content into your templates.

These templates are Jinja templates. 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:


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 %}


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
  nav_links: [{ url, title, blank }]
  color: string
  socials: {}
  sponsors: {}
  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 } ]


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: collaborators, 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 global variable, it contains the features this site supports:

  post: boolean
  audio: boolean
  photo: boolean
  comment: boolean
  subscriber: boolean
  algolia: boolean


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 == 'episode_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
  collaborators: 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


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.collaborators()
  2. page.topic of author type in list.j2
  3. page.collaborators in item.j2

The structure of a collaborator:

  username: string
  name: string
  type: string  # host, guest
    src: uri
    width: integer
    height: integer
  avatar: uri
  bio: html
  metadata: {}
  url: path


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

  1. xmldatetime: used to format .published_at

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

  2. thumbnail: used to resize image

    jinja <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


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_collaborators %}

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_collaborators(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..."
{% if features.algolia %}data-id="{{ site.algolia_id }}" data-key="{{ site.algolia_key }}"{% endif %}>

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" %}

Submit your theme

When you have finished developing your theme, fill all the information in theme.json. The required fields are:


And then register your theme with a pull request to