Building a static blog with Lektor
Setup
Install lektor from the command line by pasting the following command. This will collect and build all the dependencies necessary for Lektor.
curl -sf https://www.getlektor.com/install.sh | 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.
Configuration
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.
First Run
The landing page is generated from the contents.lr
file at the root of content
folder. Inside the content
folder, you also have folders for about, blog and projects with a contents.lr
file inside each of them. As explained earlier, these contents.lr
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/contents.lr
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 https://b.anmsh.net/ having a blog like this would give the blog a URL https://b.anmsh.net/blog, which is redundant. I felt it best to move the blog to the root of the site.
To do this, first update the root contents.lr
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 contents.lr
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 contents.lr
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 page
Title on the browser tab now shows up as — | animeshdotblog-model
. You can fix this by updating the blog model blog.ini
:
[model]
name = Blog
label = {{ this.title }}
hidden = yes
[fields.title]
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 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.
Add post
This is the edit view of the post just created:
Edit 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.
Preview post
Now check your landing page again. You have a new post:
New post
Customization
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 page
I wanted a summary of the post below the post title, generated automatically, in this fashion.
Simple 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
:
[fields.excerpt]
label = Excerpt
type = markdown
size = normal
This creates a new field that can accept markdown text.
New excerpt field in admin
Add some text there and save the post. The contents.lr
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 %}
<div>
{{ post.excerpt }}
</div>
<div>
{{ post.body }}
</div>
{% endif %}
That is all. You have posts with excerpts.
Posts 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
:
[packages]
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
[blog]
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 = https://animeshb.github.io
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 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">
<p>contents</p>
<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 %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
Refresh the blog post and now you have a table of contents:
TOC demo after
Categories
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.
blog-categories.ini
[model]
name = Blog Categories
label = Blog Categories
hidden = yes
protected = yes
[children]
model = blog-category
order_by = name
blog-category.ini
[model]
name = Blog Category
label = {{ this.name }}
hidden = yes
[fields.name]
label = Name
type = string
[children]
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.
[fields.categories]
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 contents.lr 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.
Blog Categories link on the left
Click on it and you can see that it is just a regular page on the site.
Categories Page Edit View
Click on the plus sign on the top left to add a sub page to Blog Categories
Add 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:
Template 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) %}
<ol>
{% for post in posts.order_by('-pub_date') %}
<li><a href="{{ post|url }}">{{ post.title }}</a></li>
{% endfor %}
</ol>
{% endmacro %}
{% macro render_cat_nav(active=none) %}
{% set post_count = site.query('/blog-categories').count() %}
<h2>{{post_count}} Categories</h2>
<div>
<ul>
{% for category in site.query('/blog-categories') %}
<li{% if category._id == active %} class="active"{% endif
%}><a href="{{ category|url }}">{{ category.name }}</a>
({{ category.children.count() }})</li>
{% endfor %}
</ul>
</div>
{% 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.
blog-categories.html
{% 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 %}
blog-category.html
{% extends "layout.html" %}
{% from "macros/categories.html" import render_post_list %}
{% block title %}Category {{ this.name }} {{super.title}}{% endblock %}
{% block body %}
<h2>Category: {{ this.name }}</h2>
<h4>Posts</h4>
{% if this.children %}
{{ render_post_list(this.children) }}
{%else%}
<p>No posts in category {{this.name}} yet. May be there is a draft in the works.</p>
{% endif %}
<br>
<p>
<a href="/categories">Show all Categories</a>
</p>
{% 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 %}
<div>Categories:
<ul>
{% for category in post.categories %}
<li><a href="/categories/{{ category }}">{{category}}</a>{% if not loop.last %},{% endif %}</li>
{% endfor %}
</ul>
</div>
{%endif%}
Open up the blog posts in admin and select categories as needed and save them.
Categories on post
Also, add the categories link in the navigation. Now you have the categories ready.
Categories Page
Click on any category to see the list of posts in that category.
Specific category
Comments
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.
References
- Repository for my static blog, anmsh.net, is at https://git.sr.ht/~animesh/anmsh.net