skip to main content

Building a static blog with Lektor


Install lektor from the command line by pasting the following command. This will collect and build all the dependencies necessary for Lektor.

curl -sf | sh

Create a new project

lektor quickstart

Step 1:
> Project Name: animeshdotblog-model

Step 2:
> Author Name [Animesh,,,]: Animesh Bulusu

Step 3:
> Project Path [/home/animesh/projects/animeshdotblog-model]: 

Step 4:
> Add Basic Blog [Y/n]: Y

That's all. Create project? [Y/n] Y

It will ask you the project name, author name, project path and if you want to add a basic blog. I chose the project name animeshdotblog-model and it will automatically assume the project path will be a folder with the project name the current path. Of course, you can change it.


Change into the new project path and open the editor.

cd animeshdotblog-model
code .

Init a git repository, add a .gitignore file and add the /public to it. By default, the /public folder holds the artifacts after the build and we don't need it in version control. Add and commit all the files.

git init
touch .gitignore
echo "/public" > .gitignore
git add .
git commit -m "First commit"

Start the website.

lektor server 

This will run the site on port 5000. You could specify a port by adding -p PORT at the end. To automatically open the browser after serving the site, add --browse at the end.

Lektor first runFirst Run

The landing page is generated from the file at the root of content folder. Inside the content folder, you also have folders for about, blog and projects with a file inside each of them. As explained earlier, these files at the root of each folder are just markdown files that Lektor expects to build into the index.html for that folder.

The about/ file is built into about/index.html so that when you visit http://localhost:5000/about you would see the contents of your about page. Pretty easy and intuitive right?

The blog is accessible at the URL http://localhost:5000/blog. For my domain having a blog like this would give the blog a URL, which is redundant. I felt it best to move the blog to the root of the site.

To do this, first update the root as such:

_model: blog

title: Home

Then, update the blog model in blog.ini by adding the following in the pagination section:

items = this.children.filter(F._model == 'blog-post')

You will now see the contents of about and projects on the landing page. This is because they are inside the root folder and its has the model set to blog.

Then, move the folder blog/first-post into the content. Now you can see the post 'Hello Website' also appears on the landing page.

Set the _model of about and projects folders in their file.

_model: page

title: About

Finally, update the contents of nav tag in layout.html to this:

<li{% if this._path == '/' %} class="active"{% endif
    %}><a href="{{ '/'|url }}">Home</a></li>
{% for href, title in [
    ['/projects', 'Projects'],
    ['/about', 'About']
] %}
    <li{% if this.is_child_of(href) %} class="active"{% endif
    %}><a href="{{ href|url }}">{{ title }}</a></li>
{% endfor %}

Now this is your landing page:

Blog in the landing pageBlog in the landing page

Title on the browser tab now shows up as — | animeshdotblog-model. You can fix this by updating the blog model blog.ini:

name = Blog
label = {{ this.title }}
hidden = yes

label = Title
type = string

We have added a new field title and updated the label attribute in [model] section to {{ this.title}}. I am not really sure why the title is empty when it is just label = Blog.

Now we can move onto writing our first post.

Authoring posts

Click the pencil icon in the corner to navigate to the admin.

Lektor adminLektor admin

Lektor has a very minimalistic admin designed around the actions necessary for the current view. Lektor admin doesn't support theming at the moment, but it gets its job done. You can see the sub pages of the root About, Hello Website and Projects. Though Hello Website is the only blog post, it still shows the other sub folders of the root.

Click on the plus icon beside the Home link in the top left corner to add a blog post. Enter the title of the post and the ID (id or URL or slug) field automatically builds off of the title. Click on the Add Child Page button to open the post for editing.

Lektor admin - add postAdd post

This is the edit view of the post just created:

Lektor adminEdit post

Fill in your author and twitter handle. Unless your template displays these two fields you don't have to fill them. Select publication date and click on Save Changes. Your post is now created and you will be shown the post preview page inside admin.

Lektor adminPreview post

Now check your landing page again. You have a new post:

Landing pageNew post


The quickstart that Lektor provides is an extremely simple website. We made a few modifications to this to understand how the content is used and organized by Lektor. Let us make a few customizations.

A few customizations below use Lektor plugins.

Post Summary

Initially, I used to have only the post title in my static site.

Simplest landing pageSimplest landing page

I wanted a summary of the post below the post title, generated automatically, in this fashion.

My landing pageSimple landing page

I found a plugin for this called markdown-excerpt which will extract the first paragraph from the body of the post. We have to update the part where the body of a post is rendered in the blog.html macro or wherever you have the blog post template with the filter excerpt like this:

{{ post.body|excerpt }}

When I started importing old posts from my previous blogs, I found that some posts did not have a good first paragraph. To change the first paragraphs for all these posts is tedious and some posts have to be adjusted for the change in the first paragraph.

Though it is not a big deal, this also means that {{ post.body|excerpt }} is parsing the entire posts' body N times on my landing page, given N posts on the landing page. It felt a bit excessive. I wanted another option.

I created a new field, excerpt in the blog post model blog-post.ini:

label = Excerpt
type = markdown
size = normal

This creates a new field that can accept markdown text.

ExcerptNew excerpt field in admin

Add some text there and save the post. The for the post has an updated field now

excerpt: First Post

Then surround the {{ post.body }} part in the blog.html macro with the following HTML:

{% if from_index %}
{{ post.excerpt }}
{% else %}
    {{ post.excerpt }}
    {{ post.body }}            
{% endif %}

That is all. You have posts with excerpts.

Simplified landing pagePosts with excerpts

RSS Feed

Every blog needs an RSS feed. There is a plugin lektor-atom to do this.

To add a plugin to the site, run the following command from command line:

lektor plugins add lektor-atom

The plugin can also be added manually in the project file animeshdotblog-model.lektorproject:

lektor-atom = 0.2

Stop the running Lektor instance by pressing Ctrl-C and run the lektor server command again.

Create a new folder configs in the root and create a file atom.ini inside it. Add the following lines to atom.ini

name = animeshdotblog-model
source_path = /
url_path = /feed.xml
items = site.query('/')
item_model = blog-post

Now you will notice the following error in the terminal:

E feed.xml (RuntimeError: To use absolute URLs you need to configure the URL in the project config.)

To fix this, you must supply an absolute URL in your project file:

url_style = absolute
url =

Finally, add a list element in the nav tag.

<li><a href="/feed.xml">RSS</a>

Table of Contents

A table of contents is useful for long articles like this one. There is an official plugin for this called markdown-header-anchors.

Install the plugin:

lektor plugins add markdown-header-anchors

Create a new post with a few headers:

TOC demoTOC demo before

Place this piece of HTML in your blog.html macro somewhere above the {{post.body}}.

{% if not from_index %}
    {% if post.body.toc %}
        <div class="toc-container">
            <ul class="toc">
                {% for item in post.body.toc recursive %}
                    <li><a href="#{{ item.anchor }}">{{ item.title }}</a>
                        {%if item.children %}
                            <ul>{{ loop(item.children) }}</ul>
                        {% endif %}
                {% endfor %}
    {% endif %}
{% endif %}

Refresh the blog post and now you have a table of contents:

TOC DemoTOC demo after


The official guide does a good job explaining how to go about setting up categories for your projects. We can easily adapt this for blog posts.

First, we need to create two new models, one for blog categories blog-categories.ini which will represent all categories available and the other for blog category blog-category.ini which will represent the individual category. Blog categories model will be the parent of blog category.


name = Blog Categories
label = Blog Categories
hidden = yes
protected = yes

model = blog-category
order_by = name


name = Blog Category
label = {{ }}
hidden = yes

label = Name
type = string

replaced_with = site.query('/').filter(F.categories.contains(this))

Since Lektor considers any sub folder under current folder as its child, categories would not have its associated posts as its children by default. To get around this, we use an attribute replaced_with in the [children] section which makes a query to the landing page path and returns a set of items that have this category.

Next, we update the blog post model blog-post.ini to have a new field, categories, so that it shows up in the admin when a post is in the edit view.

label = Categories
type = checkboxes
source = site.query('/blog-categories')

Here we are using site.query to fill the categories from the folder blog-categories.

Now that models are ready, let us create a folder blog-categories and set its as such:

_model: blog-categories

_slug: /categories

The _slug will be /blog-categories by default, but you can change it to categories to have a simpler URL. Go to admin and now you can see a sub page for categories.

CategoriesBlog Categories link on the left

Click on it and you can see that it is just a regular page on the site.

CategoriesCategories Page Edit View

Click on the plus sign on the top left to add a sub page to Blog Categories

Add CategoryAdd Category

Add a few more categories in this way and you can see them on the side. After adding each category we see an error like this:

Categories ErrorTemplate not found

This is because we do not have templates setup for blog category and categories.

Before adding the templates, we need to add two macros, one to fetch a list of posts belonging to a category, render_post_list, and the other to render a list of all categories available, render_cat_nav. Save them into a new categories.html in the templates/macros folder.

{% macro render_post_list(posts) %}
{% for post in posts.order_by('-pub_date') %}
<li><a href="{{ post|url }}">{{ post.title }}</a></li>
{% endfor %}
{% endmacro %}

{% macro render_cat_nav(active=none) %}
{% set post_count = site.query('/blog-categories').count() %}
<h2>{{post_count}} Categories</h2>
{% for category in site.query('/blog-categories') %}
    <li{% if category._id == active %} class="active"{% endif
    %}><a href="{{ category|url }}">{{ }}</a>
    ({{ category.children.count() }})</li>
{% endfor %}
{% endmacro %}

In macros, we can use the set statement to assign a variable to an expression. In the render_cat_nav macro, we get the count of all categories available. In the render_cat_nav macro, while looping over the available posts in the current category, we are also getting a count of the available posts for that category.

Add two templates blog-categories.html and blog-category.html. After adding these two empty templates, we don't see the error anymore. We need to inherit from layout.html and update them as follows.


{% extends "layout.html" %}
{% from "macros/categories.html" import render_cat_nav %}
{% block title %}Tags {{ super.title }}{% endblock %}
{% block body %}
{{ render_cat_nav(active=none) }}
{% endblock %}


{% extends "layout.html" %}
{% from "macros/categories.html" import render_post_list %}
{% block title %}Category {{ }} {{super.title}}{% endblock %}
{% block body %}
<h2>Category: {{ }}</h2>
{% if this.children %}
    {{ render_post_list(this.children) }}
    <p>No posts in category {{}} yet. May be there is a draft in the works.</p>
{% endif %}
    <a href="/categories">Show all Categories</a>
{% endblock %}

The code render_post_list(this.children) in blog category template passes a list of children to the render_post_list macro which just loops over and displays them.

To have a list of categories inside a blog post, add the following piece somewhere in your blog.html macro.

{%if post.categories %}
        {% for category in post.categories  %}
            <li><a href="/categories/{{ category }}">{{category}}</a>{% if not loop.last %},{% endif %}</li>
        {% endfor %}

Open up the blog posts in admin and select categories as needed and save them.

Categories on postCategories on post

Also, add the categories link in the navigation. Now you have the categories ready.

Categories PageCategories Page

Click on any category to see the list of posts in that category.

Category PageSpecific category


I had setup comments on my static site using the official plugin lektor-disqus. It is fairly easy to setup comments once you have your account ready with Disqus. Identify the short name from the Disqus admin and add into the config file for it as per the documentation.

I have removed the comments recently as I am experimenting with having comments using gitlab issues API.


This example

The quickstart example, animeshdotblog-model, we worked on in this post is available at:

github: animeshdotblog-model

This quickstart example now can be viewed at:

I have setup Github pages on it and updated it as per Lektor's official documentation on deploying to Github pages.

My static blog

Repository for my static blog,, is at: