Thema’s maken

Alles wat je in FolioPress schrijft — pagina’s, berichten, tags — staat los van hoe het wordt getoond. Die presentatielaag is een thema, en deze gids haalt er een uit elkaar: de map, de sjablonen, het instellingenbestand en precies welke gegevens elk sjabloon krijgt. Wie vertrouwd is met HTML en CSS heeft alles wat nodig is.

Een thema is een map

Wissel van thema en dezelfde pagina’s, berichten en hetzelfde menu worden opnieuw getekend — aan de inhoud verandert niets, alleen aan de kleding. Een thema is een map met drie dingen erin: een instellingenbestand, sjablonen en statische bestanden.

Een thema is een map verplicht optioneel my-theme/ theme.yml naam, kleuren, hoeveel berichten te tonen… templates/ base.html het gedeelde raamwerk van elke pagina home.html de startpagina page.html één pagina (Over, Contact…) post.html één blogbericht tag.html een tag-archief search.html zoekresultaten 404.html · 500.html page-<slug>.html een eigen lay-out voor één pagina partials/ fragmenten die andere templates insluiten locales/pt-PT.json … static/ styles.css · scripts.js favicon.svg · apple-touch-icon.png
Vier templates en theme.yml zijn genoeg; al het andere is optionele afwerking.

Vier sjablonen zijn verplicht — base.html, home.html, page.html en post.html —, een thema zonder deze laadt niet. Al het andere is optioneel: een tagarchief, een zoekpagina, eigen foutpagina’s, een eigen lay-out voor één specifieke pagina en een map locales/ voor de eigen teksten van het thema. Thema’s bewerk je rechtstreeks in het beheer onder Thema’s, exporteer je als zip en importeer je in een andere website.

Van een URL naar een pagina

Het helpt om te zien wat FolioPress met een verzoek doet voordat je sjabloon überhaupt draait, want het verklaart waarom sjablonen zo kort zijn.

1 Een bezoeker opent een URL /nl-nl/over 2 FolioPress vindt de taal en de pagina nl-NL · “Over” 3 Het bouwt de context page, site_title, languages, navigation… 4 Het kiest een template page-over.html? anders page.html 5 Gerenderd in base.html HTML naar de browser Elke waarde komt al vertaald in de taal van de bezoeker aan; het template drukt alleen af wat het krijgt.
Eén verzoek van begin tot eind. In stap 3 en 4 doet het thema zijn werk.

Twee dingen vallen op. De taal wordt uit het URL-voorvoegsel gelezen, dus /nl-nl/over-ons en /about zijn dezelfde pagina in twee talen, en de context die je sjabloon krijgt, is al in de juiste taal — elke titel, slug, menu-item en inhoud. En het sjabloon wordt voor je gekozen: voor een pagina waarvan de slug in de hoofdtaal about is, zoekt FolioPress eerst page-about.html en valt terug op page.html. Er hoeft niets aangesloten te worden.

Eén casco, veel pagina’s

Sjablonen zijn Jinja2 — HTML met twee extra soorten accolades. {{ page.title }} drukt een waarde af. {% if page.poster_image_url %} … {% endif %} is een instructie: lussen, voorwaarden en overerving.

base.html het gedeelde raamwerk van elke pagina <head>, navigatie, taalkeuze {% block content %} de ene opening die elk template vult voettekst, scripts Elk van deze vult de opening: home.html {% extends %} page.html {% extends %} post.html {% extends %} tag.html {% extends %} search.html {% extends %}
Kop- en voettekst staan op één plek; elk ander template schrijft alleen het midden.

base.html bevat alles wat elke pagina deelt — de <head>, de navigatie, de taalkiezer, de voettekst, de scripts — met één gat in het midden. Elk ander sjabloon begint hetzelfde en vult dat gat:

{% extends "base.html" %}

{% block content %}
  <h1>{{ page.title }}</h1>
  {{ page.content | safe }}
{% endblock %}

Alles wat zich over sjablonen heen herhaalt — een berichtkaart, een paginering — hoort in partials/ en wordt ingevoegd met {% include "partials/post-card.html" %}.

theme.yml: het instellingenpaneel

Hier beschrijft een thema zichzelf en stelt het een paar gedragingen in, zonder een sjabloon aan te raken. Dit is wat de meegeleverde thema’s gebruiken:

title: "Ink"
description: "Een schoon, minimalistisch thema"
version: "1.0.0"
author: "FolioPress"

meta:
  supports_dark_mode: true
  responsive: true
  categories: [minimal, portfolio]

settings:                    # vrij in te vullen; in sjablonen beschikbaar als theme_settings
  primary_color: "#2563eb"

home:
  posts: 10                  # hoeveel berichten de startpagina krijgt
  order_by: "-published_at"  # nieuwste eerst
  has_pagination: false      # afkappen, of echte paginalinks?
  image_ratios:
    poster: "16:9"           # wat de bijsnijder in het beheer hier aanbiedt
    gallery: "free"

page:  { posts: 10, order_by: "-published_at", has_pagination: false }
post:  { posts: 3,  order_by: "-published_at" }   # gerelateerde berichten onder een bericht
tag:   { posts: 12, order_by: "-published_at", has_pagination: true }

page-about:                  # overschrijving voor één pagina, via haar slug in de hoofdtaal
  image_ratios: { poster: "1:1" }
post-tutorials:              # overschrijving voor berichten waarvan de eerste tag deze slug heeft
  image_ratios: { poster: "4:3" }
Sleutel Wat hij regelt
title, description, version, author Getoond op de pagina Thema’s in het beheer.
meta.supports_dark_mode, meta.responsive, meta.categories Beschrijvend; gebruikt voor de themagalerij.
meta.social_image Een bestand in de static/ van het thema, 1200x630, gebruikt als voorbeeldafbeelding wanneer een pagina geen eigen omslagafbeelding heeft. Zonder dit wordt zo'n pagina gedeeld met het 180x180-icoon.
settings Wat je maar wilt. Wordt aan elk sjabloon doorgegeven als theme_settings, zodat een kleur of een slogan hier kan wonen in plaats van in de CSS.
home, page, post, tag De berichtenlijst die dat sjabloontype krijgt: posts (hoeveel), order_by, en of has_pagination echte paginalinks toevoegt of de lijst gewoon afkapt.
order_by -published_at (nieuwste eerst) of published_at; hetzelfde voor created_at en updated_at, met of zonder de - vooraan; of random.
image_ratios Wat de bijsnijder in het beheer aanbiedt bij het bewerken van dat inhoudstype: een verhouding zoals 16:9, een lijst daarvan, of free. Aparte waarden voor poster en gallery.
page-<slug>, post-<tag-slug> Dezelfde sleutels, die page/post overschrijven voor één pagina (via haar slug in de hoofdtaal) of voor berichten waarvan de eerste tag die slug heeft.

Wat elk sjabloon kan zien

Een sjabloon haalt nooit zelf iets op. Het krijgt een bundel kant-en-klare waarden — FolioPress noemt dat de context — en drukt af wat het nodig heeft. Een deel van de bundel is op elke pagina hetzelfde; de rest hangt af van welk sjabloon wordt gerenderd.

Elk template krijgt deze bundel page site_title site_description navigation_menu languages current_language language_switch_urls footer static_url media_url home_url canonical_url hreflang_urls t() …plus, afhankelijk van het template: home.html posts pages is_home page.html posts is_contact_page post.html related_posts is_post tag.html tag posts search.html search_query search_results results_count
De gedeelde bundel is op elke pagina gelijk; de extra’s maken elk templatetype anders.

Gemeenschappelijk voor alle sjablonen

Variabele Wat het is
page De getoonde pagina of het bericht, al in de huidige taal (zie hieronder). None in het tagarchief en bij zoeken.
site_title, site_description, footer Uit de Website-instellingen, in de huidige taal. footer is al vanuit Markdown gerenderd.
navigation_menu Het menu zoals ingesteld in het beheer: een lijst van {title, url, slug, page_id}, elke url klaar voor deze taal.
languages De talen van de website, op code. Elk item: code, name, native_name, short_code, menu_label, flag.
current_language, current_language_code, default_language_code De taal die wordt gerenderd (zelfde vorm als een languages-item), de code ervan en de hoofdtaal van de website.
language_switch_urls Op taalcode: de URL van deze zelfde pagina in die taal, vertaalde slug inbegrepen.
home_url, url_language_code De startpagina in de huidige taal (/ of /pt-pt) en het kleingeschreven voorvoegsel voor elke URL die je zelf bouwt ('' voor de hoofdtaal).
static_url, media_url Waar de bestanden van dit thema vandaan worden geleverd (/static/<thema>) en waar geüploade afbeeldingen staan.
page_title, meta_description, canonical_url, hreflang_urls, social_image_url, twitter_card_type, json_ld Alles wat de <head> nodig heeft: een zinnige tabbladtitel, absolute canonieke en taalalternatieve URL’s, een Open Graph-afbeelding en gestructureerde gegevens — alles vooraf berekend.
theme_name, theme_info, theme_settings De naam van het actieve thema, zijn volledige theme.yml en het settings-blok daarvan.
t De vertaler van het thema zelf: {{ t('Read more') }} zoekt de zin op in locales/<taal>.json (zie hieronder).
request Het FastAPI-verzoek, voor het zeldzame geval dat je het pad of een queryparameter nodig hebt.

Het page-object

Dezelfde vorm, of het nu een pagina, een bericht of een item in een berichtenlijst is:

Veld Opmerkingen
title, slug, url In de huidige taal. url is volledig, inclusief taalvoorvoegsel of het eigen domein van de taal.
content Al omgezet van Markdown naar HTML — afdrukken met {{ page.content | safe }}.
excerpt, meta_description De korte vormen, in de huidige taal.
poster_image_url Volledige URL van de omslagafbeelding, of None.
gallery, content_gallery Afbeeldingslijsten: {id, url, title, width, height, original_filename}. Gebruik image.url rechtstreeks.
tags {slug, title, url} voor elke openbare tag. Privétags worden nooit opgenomen.
is_post, is_featured, published_at, created_at, updated_at Of het een bericht is, of het uitgelicht is, en de datums.
id Het database-id — handig voor ankers en cachesleutels.

Berichtenlijsten

Sjabloon Variabele Inhoud
home.html posts (alias recent_posts), pages, is_home Berichten die een tag delen met de startpagina, in aantal en volgorde uit de sectie home van theme.yml. Een startpagina zonder tags krijgt geen berichten — geef haar een tag om haar te vullen.
page.html posts (alias page_posts) Berichten die een tag delen met deze pagina, uit de sectie page. Zo toont een “Blog”-pagina haar berichten.
post.html related_posts, is_post Andere berichten die een tag delen met dit bericht, uit de sectie post.
tag.html tag (title, slug), posts Elk gepubliceerd bericht met deze tag, uit de sectie tag.
search.html search_query, search_results, results_count De gezochte term en de overeenkomende pagina’s en berichten.

Elke berichtenlijst komt met een paginering van dezelfde vorm: has_pagination, current_page, total_pages, has_previous, has_next, previous_page, next_page en pagination_urls met first, previous, next en last — None waar er niets meer is. Staat `has_pagination` in `theme.yml` uit, dan wordt de lijst gewoon afgekapt op `posts` en blijven de vlaggen onwaar.

Talen en de kiezer

Twee van de taalvelden worden voor je berekend. short_code is alleen de taalcode — EN — tenzij de website ook een andere variant van dezelfde taal heeft ingeschakeld; dan behoudt hij zijn regio: EN-US en EN-GB. menu_label volgt dezelfde regel met namen: English, of English (US) en English (UK). Dat is precies de regel van de taalkiezer in het beheer, zodat website en beheer het nooit oneens zijn over een label. Een volledige kiezer is daarom kort:

{% 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 wijst al naar dezelfde pagina in elke taal — de vertaalde slug, of de startpagina als die vertaling nog niet bestaat —, dus een kiezer bouwt nooit zelf een URL.

De eigen teksten van het thema

Inhoud vertaalt de auteur in het beheer. De eigen teksten van een thema — “Lees verder”, “Zoeken”, “Alle rechten voorbehouden” — vertaalt het thema, in templates/locales/<taal>.json:

{
  "Read more": "Lees verder",
  "Search": "Zoeken",
  "Change language": "Taal wijzigen"
}

Omsluit de zin met {{ t('Read more') }} en hij verschijnt in de taal van de bezoeker. Ontbreekt een zin in het bestand van die taal, dan valt hij terug op het bestand van de hoofdtaal, en daarna op de zin zelf — een half vertaald thema blijft dus werken, alleen in het Engels waar de vertaling nog niet is geschreven. De bestanden reizen mee met het thema bij het exporteren.

Een pagina met een eigen lay-out

Een landingspagina, een contactformulier, een portfoliorooster — soms verdient één pagina een lay-out die geen andere nodig heeft. Voeg een sjabloon toe met de naam page-<slug>.html, waarbij <slug> de slug van die pagina in de hoofdtaal is, en FolioPress gebruikt het voor die pagina in elke taal. Verwijder het bestand en de pagina keert stilletjes terug naar page.html.

{# 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 %}

De pagina die een beheerder als contactformulierpagina kiest, krijgt iets extra’s: is_contact_page is waar, contact_form_action is waar het formulier naartoe moet, en contact_guard_token / contact_honeypot_field zijn twee verborgen velden die bots buiten houden. Na een inzending zegt contact_status of je een bedankje of een fout moet tonen.

Begin met een thema dat werkt

De snelste manier om een thema te bouwen is een van de meegeleverde te openen — elk heeft een live demo — en het naast deze pagina te lezen. Dupliceer het in het beheer, pas de CSS aan, dan de sjablonen, en je hebt iets eigens zonder ooit met een leeg bestand te zijn begonnen.

Twee gewoontes die tijd besparen. Druk de waarden af die de context je geeft in plaats van ze na te bouwen — page.url, tag.url en language_switch_urls kennen taalvoorvoegsels en eigen domeinen al. En houd het casco in base.html: wie zichzelf betrapt op het plakken van een header in een tweede sjabloon, weet waar die thuishoort.