Themes erstellen
Alles, was du in FolioPress schreibst — Seiten, Beiträge, Tags — lebt getrennt davon, wie es dargestellt wird. Diese Darstellungsschicht ist ein Theme, und dieser Leitfaden nimmt eines auseinander: den Ordner, die Templates, die Einstellungsdatei und genau die Daten, die jedes Template bekommt. Wer mit HTML und CSS vertraut ist, hat alles, was nötig ist.
Ein Theme ist ein Ordner
Wechsle das Theme, und dieselben Seiten, Beiträge und das Menü werden neu gezeichnet — am Inhalt ändert sich nichts, nur an seiner Kleidung. Ein Theme ist ein Ordner mit drei Dingen: einer Einstellungsdatei, Templates und statischen Dateien.
Vier Templates sind Pflicht — base.html, home.html, page.html und post.html —, ein Theme ohne sie lädt nicht. Alles andere ist optional: ein Tag-Archiv, eine Suchseite, eigene Fehlerseiten, ein eigenes Layout für eine bestimmte Seite und ein Ordner locales/ für die Texte des Themes selbst. Themes lassen sich direkt in der Verwaltung unter Themes bearbeiten, als Zip exportieren und in eine andere Website importieren.
Von einer URL zu einer Seite
Es hilft zu sehen, was FolioPress mit einer Anfrage tut, bevor dein Template überhaupt läuft — denn es erklärt, warum Templates so kurz sind.
Zwei Dinge fallen auf. Die Sprache wird aus dem URL-Präfix gelesen, sodass /pt-pt/sobre und /about dieselbe Seite in zwei Sprachen sind, und der Kontext, den dein Template bekommt, ist bereits in der richtigen — jeder Titel, Slug, Menüeintrag und Inhalt. Und das Template wird für dich gewählt: Für eine Seite, deren Slug in der Hauptsprache about lautet, sucht FolioPress zuerst page-about.html und fällt auf page.html zurück. Nichts muss verdrahtet werden.
Ein Gerüst, viele Seiten
Templates sind Jinja2 — HTML mit zwei zusätzlichen Klammerarten. {{ page.title }} gibt einen Wert aus. {% if page.poster_image_url %} … {% endif %} ist eine Anweisung: Schleifen, Bedingungen und Vererbung.
base.html enthält alles, was jede Seite teilt — den <head>, die Navigation, den Sprachumschalter, die Fußzeile, die Skripte — mit einer einzigen Lücke in der Mitte. Jedes andere Template beginnt gleich und füllt diese Lücke:
{% extends "base.html" %}
{% block content %}
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
{% endblock %}
Alles, was sich über Templates hinweg wiederholt — eine Beitragskarte, eine Seitennavigation — gehört nach partials/ und wird mit {% include "partials/post-card.html" %} eingebunden.
theme.yml: das Einstellungsfeld
Hier beschreibt sich ein Theme selbst und legt ein paar Verhaltensweisen fest, ohne ein Template anzufassen. So nutzen es die mitgelieferten Themes:
title: "Ink"
description: "Ein klares, minimalistisches Theme"
version: "1.0.0"
author: "FolioPress"
meta:
supports_dark_mode: true
responsive: true
categories: [minimal, portfolio]
settings: # frei; in Templates als theme_settings verfügbar
primary_color: "#2563eb"
home:
posts: 10 # wie viele Beiträge die Startseite bekommt
order_by: "-published_at" # neueste zuerst
has_pagination: false # ein Abschnitt, oder echte Seitenlinks?
image_ratios:
poster: "16:9" # was der Bildzuschnitt der Verwaltung hier anbietet
gallery: "free"
page: { posts: 10, order_by: "-published_at", has_pagination: false }
post: { posts: 3, order_by: "-published_at" } # verwandte Beiträge unter einem Beitrag
tag: { posts: 12, order_by: "-published_at", has_pagination: true }
page-about: # Überschreibung für eine Seite, per Slug in der Hauptsprache
image_ratios: { poster: "1:1" }
post-tutorials: # Überschreibung für Beiträge, deren erster Tag diesen Slug hat
image_ratios: { poster: "4:3" }
| Schlüssel | Was er steuert |
|---|---|
title, description, version, author |
Werden auf der Seite Themes der Verwaltung angezeigt. |
meta.supports_dark_mode, meta.responsive, meta.categories |
Beschreibend; für die Theme-Galerie. |
meta.social_image |
Eine Datei im static/ des Themes, 1200x630, als Vorschaubild verwendet, wenn eine Seite kein eigenes Titelbild hat. Ohne sie wird eine solche Seite mit dem 180x180-Icon geteilt. |
settings |
Was immer du möchtest. Wird jedem Template als theme_settings übergeben, sodass eine Farbe oder ein Slogan hier statt im CSS leben kann. |
home, page, post, tag |
Die Beitragsliste, die dieser Template-Typ bekommt: posts (wie viele), order_by, und ob has_pagination echte Seitenlinks ergänzt oder die Liste nur abschneidet. |
order_by |
-published_at (neueste zuerst) oder published_at; ebenso created_at und updated_at, mit oder ohne führendes -; oder random. |
image_ratios |
Was der Bildzuschnitt der Verwaltung beim Bearbeiten dieses Inhaltstyps anbietet: ein Verhältnis wie 16:9, eine Liste davon oder free. Getrennte Werte für poster und gallery. |
page-<slug>, post-<tag-slug> |
Dieselben Schlüssel, die page/post für eine Seite (per Slug in der Hauptsprache) oder für Beiträge überschreiben, deren erster Tag diesen Slug trägt. |
Was jedes Template sehen kann
Ein Template holt nie etwas selbst. Es bekommt ein Bündel gebrauchsfertiger Werte — FolioPress nennt es den Kontext — und gibt aus, was es braucht. Ein Teil des Bündels ist auf jeder Seite gleich; der Rest hängt davon ab, welches Template gerendert wird.
Allen Templates gemeinsam
| Variable | Was sie ist |
|---|---|
page |
Die angezeigte Seite oder der Beitrag, bereits in der aktuellen Sprache (siehe unten). None im Tag-Archiv und in der Suche. |
site_title, site_description, footer |
Aus den Website-Einstellungen, in der aktuellen Sprache. footer ist bereits aus Markdown gerendert. |
navigation_menu |
Das in der Verwaltung konfigurierte Menü: eine Liste von {title, url, slug, page_id}, jede url fertig für diese Sprache. |
languages |
Die Sprachen der Website, nach Code. Jeder Eintrag: code, name, native_name, short_code, menu_label, flag. |
current_language, current_language_code, default_language_code |
Die gerenderte Sprache (gleiche Form wie ein languages-Eintrag), ihr Code und die Hauptsprache der Website. |
language_switch_urls |
Nach Sprachcode: die URL derselben Seite in dieser Sprache, übersetzter Slug inklusive. |
home_url, url_language_code |
Die Startseite in der aktuellen Sprache (/ oder /pt-pt) und das kleingeschriebene Präfix für jede URL, die du selbst baust ('' für die Hauptsprache). |
static_url, media_url |
Woher die Dateien dieses Themes ausgeliefert werden (/static/<theme>) und wo hochgeladene Bilder liegen. |
page_title, meta_description, canonical_url, hreflang_urls, social_image_url, twitter_card_type, json_ld |
Alles, was der <head> braucht: ein sinnvoller Tab-Titel, absolute kanonische und sprachalternative URLs, ein Open-Graph-Bild und strukturierte Daten — alles vorberechnet. |
theme_name, theme_info, theme_settings |
Der Name des aktiven Themes, seine komplette theme.yml und ihr settings-Block. |
t |
Der Übersetzer des Themes: {{ t('Read more') }} schlägt den Satz in locales/<sprache>.json nach (siehe unten). |
request |
Die FastAPI-Anfrage, für den seltenen Fall, dass du Pfad oder Query-Parameter brauchst. |
Das page-Objekt
Dieselbe Form, ob Seite, Beitrag oder Eintrag in einer beliebigen Beitragsliste:
| Feld | Hinweise |
|---|---|
title, slug, url |
In der aktuellen Sprache. url ist vollständig, inklusive Sprachpräfix oder eigener Domain der Sprache. |
content |
Bereits von Markdown in HTML umgewandelt — ausgeben mit {{ page.content | safe }}. |
excerpt, meta_description |
Kurzformen, in der aktuellen Sprache. |
poster_image_url |
Vollständige URL des Titelbilds, oder None. |
gallery, content_gallery |
Bildlisten: {id, url, title, width, height, original_filename}. image.url direkt verwenden. |
tags |
{slug, title, url} für jeden öffentlichen Tag. Private Tags werden nie aufgenommen. |
is_post, is_featured, published_at, created_at, updated_at |
Ob es ein Beitrag ist, ob hervorgehoben, und die Daten. |
id |
Die Datenbank-ID — praktisch für Anker und Cache-Schlüssel. |
Beitragslisten
| Template | Variable | Inhalt |
|---|---|---|
home.html |
posts (Alias recent_posts), pages, is_home |
Beiträge, die einen Tag mit der Startseite teilen, in Anzahl und Reihenfolge aus dem home-Abschnitt der theme.yml. Eine Startseite ohne Tags bekommt keine Beiträge — tagge sie, um sie zu füllen. |
page.html |
posts (Alias page_posts) |
Beiträge, die einen Tag mit dieser Seite teilen, aus dem page-Abschnitt. So listet eine „Blog“-Seite ihre Beiträge. |
post.html |
related_posts, is_post |
Andere Beiträge, die einen Tag mit diesem teilen, aus dem post-Abschnitt. |
tag.html |
tag (title, slug), posts |
Jeder veröffentlichte Beitrag mit diesem Tag, aus dem tag-Abschnitt. |
search.html |
search_query, search_results, results_count |
Der eingegebene Begriff und die passenden Seiten und Beiträge. |
Jede Beitragsliste kommt mit einer Paginierung gleicher Form: has_pagination, current_page, total_pages, has_previous, has_next, previous_page, next_page und pagination_urls mit first, previous, next und last — None, wo es nicht weitergeht. Ist `has_pagination` in der `theme.yml` aus, wird die Liste einfach bei `posts` abgeschnitten und die Flags bleiben falsch.
Sprachen und der Umschalter
Zwei der Sprachfelder werden für dich berechnet. short_code ist nur der Sprachcode — EN —, es sei denn, die Website hat auch eine andere Variante derselben Sprache aktiviert; dann behält er seine Region: EN-US und EN-GB. menu_label folgt derselben Regel mit Namen: English, oder English (US) und English (UK). Das ist genau die Regel des Sprachumschalters der Verwaltung, sodass Website und Verwaltung sich nie über eine Beschriftung uneinig sind. Ein vollständiger Umschalter ist deshalb kurz:
{% if languages | length > 1 %}
<button type="button" aria-label="{{ t('Change language') }}">
{{ current_language.short_code }}
</button>
<ul>
{% for lang in languages.values() | sort(attribute='menu_label') %}
<li>
{% if lang.code == current_language.code %}
<span aria-current="true">{{ lang.menu_label }}</span>
{% else %}
<a href="{{ language_switch_urls[lang.code] }}" hreflang="{{ lang.code }}">{{ lang.menu_label }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
language_switch_urls zeigt bereits auf dieselbe Seite in jeder Sprache — auf den übersetzten Slug oder, falls diese Übersetzung noch fehlt, auf die Startseite —, sodass ein Umschalter nie selbst eine URL baut.
Die Texte des Themes selbst
Inhalte übersetzt der Autor in der Verwaltung. Die eigenen Texte eines Themes — „Weiterlesen“, „Suche“, „Alle Rechte vorbehalten“ — übersetzt das Theme, in templates/locales/<sprache>.json:
{
"Read more": "Weiterlesen",
"Search": "Suche",
"Change language": "Sprache wechseln"
}
Umschließe den Satz mit {{ t('Read more') }}, und er erscheint in der Sprache des Besuchers. Fehlt ein Satz in der Datei dieser Sprache, fällt er auf die Datei der Hauptsprache zurück, dann auf den Satz selbst — ein halb übersetztes Theme funktioniert also weiter, nur auf Englisch, wo noch keine Übersetzung geschrieben wurde. Die Dateien reisen beim Export mit dem Theme.
Eine Seite mit eigenem Layout
Eine Landingpage, ein Kontaktformular, ein Portfolio-Raster — manchmal verdient eine Seite ein Layout, das keine andere braucht. Lege ein Template namens page-<slug>.html an, wobei <slug> der Slug dieser Seite in der Hauptsprache ist, und FolioPress nutzt es für diese Seite in jeder Sprache. Lösche die Datei, und die Seite kehrt still zu page.html zurück.
{# templates/page-about.html #}
{% extends "base.html" %}
{% block content %}
<section class="about">
<img src="{{ page.poster_image_url }}" alt="">
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</section>
{% endblock %}
Die Seite, die ein Administrator als Kontaktformular-Seite festlegt, bekommt etwas extra: is_contact_page ist wahr, contact_form_action ist das Ziel des Formulars, und contact_guard_token / contact_honeypot_field sind zwei versteckte Felder, die Bots fernhalten. Nach dem Absenden sagt contact_status, ob ein Dank oder ein Fehler anzuzeigen ist.
Beginne mit einem Theme, das funktioniert
Der schnellste Weg zu einem Theme ist, eines der mitgelieferten zu öffnen — jedes hat eine Live-Demo — und es neben dieser Seite zu lesen. Dupliziere es in der Verwaltung, ändere das CSS, dann die Templates, und du hast etwas Eigenes, ohne je mit einer leeren Datei begonnen zu haben.
Zwei Gewohnheiten, die Zeit sparen. Gib die Werte aus, die der Kontext liefert, statt sie nachzubauen — page.url, tag.url und language_switch_urls kennen Sprachpräfixe und eigene Domains bereits. Und lass das Gerüst in base.html: Wer sich dabei ertappt, einen Header in ein zweites Template zu kopieren, weiß, wohin er gehört.