Static site generators can be complex. Trakai makes it simple to get a blog up and running without harming other static content.

There's plenty of static site generators out there. Most of them are focused on building sites for the Next Big Thing with hundreds of pages. But what if you've got a few HTML files with your resumé on them, and you want to create a blog article on the latest programming trend?

When designing my own blog, I wanted something simple - that did just what I needed it to. I found the excellent Makesite by Sunaina Pai, but that was too focused on general site-making, and not blog-posting. So I adapted his work and added a bit of my bad code, with the goal of making it more powerful and effective. And it still can't do the fancy stuff. But it does what I need.

Trakai is currently in alpha. Code is still incomplete and unpolished, and features may change. It's primarily made for my own use, so I can't guarantee that it'll ever be in a better state.

Requires Python 3.6+, Python-Markdown, and Jinja 2.


Install using pip: pip install trakai
...and run as a command-line tool. trakai path/to/your/site

Trakai operates on the assumption that you have a bunch of static files already created - and a blog that you want to dynamically generate. Rather than editing all your files in the main site directory, and then uploading a build directory, Trakai content is stored in a resources directory that is exempt from upload, and generates content directly in a folder of the main site directory.

This approach means that using Trakai requires a bit more caution than most SSGs. Ensure that you don't have any pre-existing content that is at risk of being overwritten.

Writing Content

Blog content is written as individual Markdown files. All content in the posts_path directory of your site (default: resources/content) is transformed into a blog post, sorted by newest first, with the same file name as the Markdown file.


Metadata is written like below, with the date being highly recommended (if not provided, the last modified date will be used, which isn't recommended, as you may want to correct something). The blank line separates metadata from the content.

title: My Blog Post
summary: This is a post that I'm writing.
date: 2020-1-31

Blog content lorem ipsum dolor amet...

Tags (enabled with the has_tags configuration item) allow you to categorise your posts. Any page without a tags item has no tags.

tags: Lithium, Rhodonite, Prismarine

For exact Markdown details, see the Python-Markdown documentation (metadata is handled by its meta extension). And for advanced Markdown functionality, you can install more extensions by modifying the markdown_extensions configuration item.

Ensure that metadata names don't conflict with pre-defined variables or configuration items (see below).


Rather than letting you define millions of your own templates, Trakai limits the range to four, all stored in the templates_path directory (default: resources/templates). If you don't define any of your own, it provides sensible defaults.

Any (or all) of these templates can inherit from additional files of your own making as long as they are in the same directory. The default templates inherit from a page.html template; allowing you to define your site's design and let Trakai subsitute in the {{ content }}.

Templates are written in Jinja 2. For a lot of templating tasks, you'll find it best to work off the included templates (in the example folder of the repo) and customise them to your own liking. For more advanced tasks, Jinja has its own templating reference. Of course, this doesn't cover blog content...

Post Variables

These are accessible directly from post.html, and inside a list of dictionary posts from listings.

The metadata of each post is accessible via the same names as their definitions. This allows you to define custom variables, and use them on both post and listing pages. Using advanced Jinja template functionality, you can do some neat stuff with them.
The content of the post, outputted into HTML. This can easily be used to generate a summary as well:
{% for item in posts %}{{ item.prose | striptags | truncate(230) }}{% endfor %}
The date metadata printed in a more human-readable fashion - e.g. 2 January, 2020. Currently, this cannot be customised.
The date metadata printed in the RFC 822/2822 format. Handy for RSS.
The date metadata printed in the RFC 3399 format. Handy for Atom. As date doesn't include time, it is set to midnight.
Only extant if you place an <!-- nvpr --> comment on a line of your post. Contains any content before it verbatim, so you can place a preview of it in your listing.
It's a good idea to make a fallback in your template in case you forget (or can't be bothered) to add a comment:
{%- if item.preview -%}{{ item.preview }}{%- else -%}...

Global Variables

These are accessible from every template.

Configuration variables
The configuration settings as detailed in Configuration.
If you want tag or archive listings displayed differently, this allows you to use Jinja conditionals with them. Is regular for standard listings, archive for archives, tags for tag listings, feed for feeds, and posts for individual posts.
Requires the has_tags configuration item to be enabled. A listing of all tags as strings.
The output path of the current item, relative to the site root.
Intended as a unique name for CSS styling and such. Based on the file path for index pages, and the page's file name for blog posts.

Listing Variables

These are accessible from every listing, including feed.xml.

A listing of each post displayed on that page, represented as a dictionary of their post variables. When has_pagination or has_tag_pagination is disabled, this is every post on the main index page and every tag page respectively; otherwise it is each post on that page as dictated by page_limit. For archive and feed pages, it is always every post. Sorted by newest first.
Requires has_pagination or has_tag_pagination; only applicable for paginated pages. A list comprising the series of pages, represented by file path, that the current page is included in.
Requires has_pagination or has_tag_pagination; only applicable for paginated pages. The total amount of pages in the series that the current page is included in.
Requires has_pagination or has_tag_pagination; only applicable for paginated pages. The current page's position (or page number) in the series that it is included in.

Internal Variables

These are intended for internal/debugging usage. Like global variables, these are accessible from every template.

The path of the site on your disk.
Whether informational output is enabled for Trakai.


Configuration is achieved via a trakai.json file in the resources folder of your site.

The path to a folder containing blog posts in Markdown format. resources/content
The path to a folder containing Jinja templates. resources/templates
The path to a folder containing backups of the index.html page; only required if has_preview is enabled. resources/backup
A path indicating the output folder of blog content, relative to
The URI/URL that visitors use to access your site's content.
The description for Atom (or other) feeds.Placeholder Description
A value for templating designed to indicate the current year.The current year.
Whether index pages are "paginated" - i.e. divided into multiple pages that the user has to navigate to. False
Whether tag pages are paginated; requires has_tags. False
How many articles are on each page when has_tags and/or has_tag_pagination are enabled. 5
A list of extensions that Python-Markdown loads. Using this can provide lots of additional Markdown functionality with the correct extensions; for more information, see Python-Markdown's extensions page. ['def_list','admonition','tables']

The built-in extension meta is always loaded regardless of these settings, as Trakai requires it to retrieve page information.

If true, replaces content on the main page with a blog preview derived from the excerpt.html template. The first element with preview_class is modified to have the template's contents. False

Needless to say, this should only be used with extreme caution. Ensure that page content is properly backed up before making major changes; never, if possible, rely solely on backup_path.

If true, generate a separate archive.html page in the blog directory output_path, which is always devoid of pagination. False
Whether to include tags. Tags are specified by a tags metadata entry, which contains a list of comma-separated values representing each tag. An individual listing for each tag is generated in the blog directory output_path. False
Whether to build a syndication feed (e.g. RSS or Atom) of all posts using the feed.xml template. True
Whether to build a cache to avoid generating pages whose content hasn't changed. This only affects posts, and does not monitor external files (e.g. CSS) that pages depend on. False

Since caching maintains the contents of previous builds, its usage can result in the accretion of old or malformed content unless care is taken.

The location where the post cache is stored. resources/backup/trakai_cache.json
Custom values
Since configuration settings are exposed to templates, you can define custom flags here and use them for debugging, among other purposes.

Command Line

When running trakai from your favourite terminal, there are a few options to customise things.

path (positional argument)
The path of the site that is being operated upon. The current directory.
-h, --help
Show a help message and exit.
-v, --version
Output the installed version and exit.
-s, --silent
Run Trakai without outputting informational messages to stdout.
-a, --cache
Always use the cache to avoid generating pages whose content hasn't changed, regardless of configured settings.
-n, --nocache
Never use the cache, and always generate pages whose content hasn't changed regardless of configured settings.
-c path, --config path
Use an alternate JSON file specified by path to load configuration data.resources/trakai.json


Trakai can also be imported into a Python project as a module. Said functionality is currently experimental and undocumented.