Lektor

Ernesto Rico Schmidt am 14. Juni 2018

Bald wird hinter dieser Seite keine Django-Applikation (und keine Datenbank) mehr sein, sondern ein statischer Generator: Lektor

denklab mit Django

In den letzten Jahren etablierte sich für einige Zeit eine Blog-Web-Applikation als de-facto Benchmark für die Einfachheit von Web-Frameworks, als prominentes Teil des Portfolios und so entstand, vor fast zehn Jahren, denklab die erste Iteration dieser Website mit Django.

Und vor nicht einmal sechs Monaten folgte denklab2, die zweite Iteration, diesmal als Reaktion auf das Einstellen von Typed.

Mir war aber schon vom Anfang an klar, dass es ein mit Kanonen auf Spatzen schießen war, für diese Seite eine Datenbank im Hintergrund zu haben. Eine unnötige Ressourcenverschwendung ist. Abgesehen von der Bürde, diese, wenn auch sehr kleine und einfache, Applikation und deren Abhängigkeiten aktuell und sicher zu halten.

Statische Generatoren

Es ist auch wieder ein Trend: Statische Generatoren zu benutzen, zwecks Ressourcenschonung, aber auch wegen den einfache Wartung die so ein System benötigt. Wenn die Inhalte allesamt statisch sind, ist das Generieren von Inhalt die einzige Wartung, die man machen muss.

Ich habe schon früher, mehrmals, überlegt eine Seite mit einem statischen Generator zu machen, und schnell auch wieder die Idee aufgegeben, weil ich nie hundert Prozent zufrieden war mit der Lösung. So was wie ein statischer CMS gab es nicht.

Eins der Gründe dafür, dass das Mikro-Framework Flask keine Datenbank oder keinen Objekt-Relationaler-Mapper hat und nie haben wird, wird in der Dokumentation einfach und direkt erklärt: nicht jede Web-Applikation braucht eine Datenbank. Und nicht jede Applikation, die eine braucht, benötigt eine SQL-Datenbank.

Und genau dort kommt Lektor ins Spiel und schließt den Kreis bzw. mein Suche. Nicht zufällig auch von Armin Ronacher, mit Hilfe von Click, Flask und Jinja entwickelt, um genau dieses Problem zu lösen.

Installation

Die Installation ist sehr, sehr einfach: Man führt in der Mac- oder Linux-Konsole einfach den Installer aus:

$ curl -sf https://www.getlektor.com/install.sh | sh

Für Windows führt man das Pendant in der Konsole aus:

@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1'))" && SET PATH=%PATH%;%LocalAppData%\lektor-cli

bzw. in der PowerShell:

iex ((new-object net.webclient).DownloadString('https://getlektor.com/install.ps1'))

Wenn man sich aber ein wenig unwohl fühlt, etwas aus dem Internet mehr oder weniger direkt auszuführen, und lieber Lektor und alle seine Abhängigkeiten isoliert in einer eigenen virtuellen Umgebung haben will, dann kann man das machen.

Für einen Schnellstart läst man Lektor die Struktur von einem einfachen Projekt erstellen:

$ lektor quickstart --name Beispiel --path Website

Das Ergebnis ist das Projekt Beispiel im Verzeichnis Website. Darunter die Verzeichnisse assets, content, models und templatesund die Datei Beispiel.lektorproject.

Modelle

Ein Modell in Lektor ist einfach eine .ini-Datei, im Verzeichnis models.

Nach dem Schnellstart haben wir drei Modelle: blog.ini, blog-post.ini für einen Blog und page.ini für einfache Seiten:

models/
├── blog.ini
├── blog-post.ini
└── page.ini

Das Modell für eine Seite hat zwei Felder: einen title, der ein String ist und einen body, vom Typ Markdown.

[model]
name = Page
label = {{ this.title }}

[fields.title]
label = Title
type = string

[fields.body]
label = Body
type = markdown

Das Modell für den Blog enthält ein Feld title, einen String, und kann mehrere Kinder, Eiträge, vom Typ blog-post, enthalten, die nach dem Veröffentlichungsdatum (pub_date) und dem title sortiert werden. Ausserdem enthält der Blog zehn Posts pro Seite ([pagination]).

[model]
name = Blog
label = Blog
hidden = yes

[fields.title]
label = Title
type = string

[children]
model = blog-post
order_by = -pub_date, title

[pagination]
enabled = yes
per_page = 10

Das Model für eine Blog-Post enthält die Felder title, author, twitter_handle, vom Typ String, pub_date, ein Datum, und body vom Typ Markdow, mit jeweils der Breite, die sie in der Administrationsoberfläche haben werden.

[model]
name = Blog Post
label = {{ this.title }}
hidden = yes

[fields.title]
label = Title
type = string
size = large

[fields.author]
label = Author
type = string
width = 1/2

[fields.twitter_handle]
label = Twitter Handle
type = string
width = 1/4
addon_label = @

[fields.pub_date]
label = Publication date
type = date
width = 1/4

[fields.body]
label = Body
type = markdown

Inhalte

Die Inhalte, die die Modelle nutzen, und die Website dann füllen, sind einfache Verzeichnisse, die eine contents.lr-Datei und etwaige weitere Dateien (z.B. Bilder) enthalten, und innerhalb des Verzeichnis content sind:

content/
├── about
│   └── contents.lr
├── blog
│   ├── contents.lr
│   └── first-post
│       └── contents.lr
├── contents.lr
└── projects
    └── contents.lr

Die Startseite, die contents.lr-Datei, enthält dann einen einfachen Text und ist automatisch vom Typ page, sie enthält den title und den body getrennt durch drei Bindestriche:

title: Welcome to Website!

body:

This is a basic demo website that shows how to use     
Lektor for a basic  website with some pages and a blog.

Der Inhalt für den Blog befindet sich im Verzeichnis blog, die Datei blog/contents.lr muss zwar explizit das Modell angeben, die Kinder dafür nicht mehr. Sie sind automatisch blog-post. Diese können entweder über die Konsole in die Verzeichnisstruktur erzeugt werden, oder Über die Administrationsoberfläche hinzugefügt werden.

Templates

Die Templates, im dazugehörigen Verzeichnis, sind mit der Jinja2-Engine gemacht, was einige sehr mächtige Funktionen, wie Makros oder Vererbung mit sich bringt.

Zu jedem Modell gehört ein gleichnamiges Template.

templates/
├── blog.html
├── blog-post.html
├── layout.html
├── macros
│   ├── blog.html
│   └── pagination.html
└── page.html

Zum Beispiel, erweitert das Template page.html die Basis von layout.html:

{% extends "layout.html" %}
{% block title %}{{ this.title }}{% endblock %}
{% block body %}
  <h2>{{ this.title }}</h2>
  {{ this.body }}
{% endblock %}

So werden die Blöcke title mit dem Inhalt title und body mit dem Inhalt body der Seite ersetzt.

Das Basis-Template layout.html ist ein wenig komplexer, aber für diesen Artikel nicht weiter von Interesse, es definiert die Blöcke title und body, und legt das Layout von allen Seiten und Blog-Posts.

Die Liste von Blog-Posts des Blogs (blog.html) ist da ein wenig interessanter, denn es verwendet Makros, um einen Blog-Post render_blog_post und um die Pagination anzuzeigen render_pagination.

{% extends "layout.html" %}
{% from "macros/blog.html" import render_blog_post %}
{% from "macros/pagination.html" import render_pagination %}
{% block title %}{{ this.title }}{% endblock %}
{% block body %}
  {% for child in this.pagination.items %}
    {{ render_blog_post(child, from_index=true) }}
  {% endfor %}

  {{ render_pagination(this.pagination) }}
{% endblock %}

Das Template für einen Blog-Post ist dank der Struktur und das Makro render_blog_post noch einfacher:

{% extends "layout.html" %}
{% from "macros/blog.html" import render_blog_post %}
{% block title %}{{ this.title }}{% endblock %}
{% block body %}
  {{ render_blog_post(this) }}
{% endblock %}

Makros

Mit Makros in Jinja lassen sich ähnlich wie mit Funktionen, Templates und die Generierung von Inhalt (in diesem Fall HTML) vereinfachen.

Das Makro render_blog_post zeigt, abhängig davon, ob es sich um die Blog-Index Seite oder nicht, den Titel des Posts mit einem Link zum Post, oder nur den Titel. Außerdem wird der Twitter-Handle mit einem Link angezeigt, wenn der Post diesen enthält, oder nur den Autor.

{% macro render_blog_post(post, from_index=false) %}
  <div class="blog-post">
  {% if from_index %}
    <h2><a href="{{ post|url }}">{{ post.title }}</a></h2>
  {% else %}
    <h2>{{ post.title }}</h2>
  {% endif %}
  <p class="meta">
    written by
    {% if post.twitter_handle %}
      <a href="https://twitter.com/{{ post.twitter_handle
        }}">{{ post.author or post.twitter_handle }}</a>
    {% else %}
      {{ post.author }}
    {% endif %}
    on {{ post.pub_date }}
  </p>
  {{ post.body }}
  </div>
{% endmacro %}

Administrationsoberfläche

Ja, richtig gelesen: Es gibt einen Administrationsoberfläche!

Und genau das ist der Grund, der mich dazu bewogen hat, endgültig den Schritt zu machen, einen statischen Generator und genau diesen zu verwenden.

Lektor Adminstrationsoberfläche

Und das schöne daran: Wenn die Seite, oder der Blog-Post bereit sind, dann kann man den Inhalt einfach zum einem FTP-Server, GitHub Pages, GitLab Pages, einem Server über rsync ausrollen.

$ lektor deploy