Créer des thèmes

Tout ce que vous écrivez dans FolioPress — pages, articles, étiquettes — vit à part de la façon dont c’est affiché. Cette couche de présentation est un thème, et ce guide en démonte un : le dossier, les gabarits, le fichier de réglages et exactement quelles données chaque gabarit reçoit. Si vous êtes à l’aise avec HTML et CSS, vous avez tout ce qu’il faut.

Un thème est un dossier

Changez de thème et les mêmes pages, articles et menu sont redessinés — rien dans le contenu ne change, seulement sa tenue. Un thème est un dossier contenant trois choses : un fichier de réglages, des gabarits et des ressources statiques.

Un thème est un dossier obligatoire facultatif my-theme/ theme.yml nom, couleurs, nombre d’articles à lister… templates/ base.html le squelette commun à toutes les pages home.html la page d’accueil page.html une page (À propos, Contact…) post.html un article de blog tag.html l’archive d’une étiquette search.html résultats de recherche 404.html · 500.html page-<slug>.html une mise en page propre à une page partials/ fragments inclus depuis d’autres gabarits locales/pt-PT.json … static/ styles.css · scripts.js favicon.svg · apple-touch-icon.png
Quatre gabarits et theme.yml suffisent ; tout le reste est un plus facultatif.

Quatre gabarits sont obligatoires — base.html, home.html, page.html et post.html — et un thème qui en manque ne se charge pas. Tout le reste est facultatif : une archive d’étiquette, une page de recherche, des pages d’erreur personnalisées, une mise en page dédiée à une page précise, et un dossier locales/ pour les mots du thème lui-même. Les thèmes se modifient directement dans l’administration, sous Thèmes, s’exportent en zip et s’importent sur un autre site.

D’une URL à une page

Il est utile de voir ce que FolioPress fait d’une requête avant même que votre gabarit ne s’exécute, car cela explique pourquoi les gabarits sont si courts.

1 Un visiteur ouvre une URL /fr-fr/a-propos 2 FolioPress trouve la langue et la page fr-FR · « À propos » 3 Il construit le contexte page, site_title, languages, navigation… 4 Il choisit un gabarit page-a-propos.html ? sinon page.html 5 Rendu dans base.html HTML envoyé au navigateur Chaque valeur arrive déjà traduite dans la langue du visiteur ; le gabarit ne fait qu’afficher ce qu’on lui donne.
Une requête du début à la fin. Les étapes 3 et 4 sont celles où le thème fait son travail.

Deux choses à retenir. La langue est lue dans le préfixe de l’URL, si bien que /fr-fr/a-propos et /about sont la même page en deux langues, et le contexte remis à votre gabarit est déjà dans la bonne — chaque titre, slug, libellé de menu et contenu. Et le gabarit est choisi pour vous : pour une page dont le slug en langue principale est about, FolioPress cherche d’abord page-about.html et se rabat sur page.html. Il n’y a rien à câbler.

Un squelette, plusieurs pages

Les gabarits sont du Jinja2 — du HTML avec deux sortes d’accolades en plus. {{ page.title }} affiche une valeur. {% if page.poster_image_url %} … {% endif %} est une instruction : boucles, conditions et héritage.

base.html le squelette commun à toutes les pages <head>, navigation, sélecteur de langue {% block content %} le seul emplacement que chaque gabarit remplit pied de page, scripts Chacun de ceux-ci remplit l’emplacement : home.html {% extends %} page.html {% extends %} post.html {% extends %} tag.html {% extends %} search.html {% extends %}
En-tête et pied de page vivent à un seul endroit ; les autres gabarits n’écrivent que le milieu.

base.html contient tout ce que les pages ont en commun — le <head>, la navigation, le sélecteur de langue, le pied de page, les scripts — avec un seul emplacement vide au milieu. Tous les autres gabarits commencent de la même façon et remplissent cet emplacement :

{% extends "base.html" %}

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

Tout ce qui se répète d’un gabarit à l’autre — une carte d’article, une barre de pagination — va dans partials/ et s’inclut avec {% include "partials/post-card.html" %}.

theme.yml : le panneau de réglages

C’est ici qu’un thème se décrit et configure quelques comportements, sans toucher à un gabarit. Voici ce qu’utilisent les thèmes fournis :

title: "Ink"
description: "Un thème sobre et minimaliste"
version: "1.0.0"
author: "FolioPress"

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

settings:                    # libre ; disponible dans les gabarits sous theme_settings
  primary_color: "#2563eb"

home:
  posts: 10                  # combien d’articles la page d’accueil reçoit
  order_by: "-published_at"  # les plus récents d’abord
  has_pagination: false      # une coupure, ou de vrais liens de page ?
  image_ratios:
    poster: "16:9"           # ce que propose l’outil de recadrage de l’administration ici
    gallery: "free"

page:  { posts: 10, order_by: "-published_at", has_pagination: false }
post:  { posts: 3,  order_by: "-published_at" }   # articles liés sous un article
tag:   { posts: 12, order_by: "-published_at", has_pagination: true }

page-about:                  # surcharge pour une page, par son slug en langue principale
  image_ratios: { poster: "1:1" }
post-tutorials:              # surcharge pour les articles dont la première étiquette a ce slug
  image_ratios: { poster: "4:3" }
Clé Ce qu’elle contrôle
title, description, version, author Affichés sur la page Thèmes de l’administration.
meta.supports_dark_mode, meta.responsive, meta.categories Descriptifs ; utilisés pour la galerie de thèmes.
meta.social_image Un fichier dans le static/ du thème, en 1200x630, utilisé comme image d'aperçu quand une page n'a pas d'image à la une. Sans lui, cette page est partagée avec l'icône de 180x180.
settings Ce que vous voulez. Transmis à chaque gabarit sous theme_settings, pour qu’une couleur ou un slogan puisse vivre ici plutôt que dans le CSS.
home, page, post, tag La liste d’articles que reçoit ce type de gabarit : posts (combien), order_by, et si has_pagination ajoute de vrais liens de page ou coupe simplement la liste.
order_by -published_at (les plus récents d’abord) ou published_at ; de même created_at et updated_at, avec ou sans le - initial ; ou random.
image_ratios Ce que l’outil de recadrage de l’administration propose lors de l’édition de ce type de contenu : un ratio comme 16:9, une liste de ratios, ou free. Valeurs distinctes pour poster et gallery.
page-<slug>, post-<tag-slug> Les mêmes clés, surchargeant page/post pour une page (par son slug en langue principale) ou pour les articles dont la première étiquette porte ce slug.

Ce que chaque gabarit peut voir

Un gabarit ne va jamais rien chercher. On lui remet un ensemble de valeurs prêtes à l’emploi — FolioPress l’appelle le contexte — et il affiche ce dont il a besoin. Une partie de l’ensemble est identique sur chaque page ; le reste dépend du gabarit en cours de rendu.

Chaque gabarit reçoit cet ensemble 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() …et en plus, selon le gabarit : 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
L’ensemble commun est le même sur chaque page ; les extras sont ce qui distingue chaque type de gabarit.

Commun à tous les gabarits

Variable Ce que c’est
page La page ou l’article consulté, déjà dans la langue courante (voir plus bas). None sur l’archive d’étiquette et la recherche.
site_title, site_description, footer Issus des Réglages du site, dans la langue courante. footer est déjà rendu depuis Markdown.
navigation_menu Le menu configuré dans l’administration : une liste de {title, url, slug, page_id}, chaque url prête à l’emploi dans cette langue.
languages Les langues du site, indexées par code. Chaque entrée : code, name, native_name, short_code, menu_label, flag.
current_language, current_language_code, default_language_code La langue en cours de rendu (même forme qu’une entrée de languages), son code, et la langue principale du site.
language_switch_urls Indexé par code de langue : l’URL de cette même page dans cette langue, slug traduit compris.
home_url, url_language_code La page d’accueil dans la langue courante (/ ou /pt-pt), et le préfixe en minuscules à placer devant toute URL que vous construisez vous-même ('' pour la langue principale).
static_url, media_url D’où sont servis les fichiers de ce thème (/static/<thème>) et où vivent les images téléversées.
page_title, meta_description, canonical_url, hreflang_urls, social_image_url, twitter_card_type, json_ld Tout ce dont le <head> a besoin : un titre d’onglet sensé, des URL canonique et alternatives par langue absolues, une image Open Graph et des données structurées — le tout précalculé.
theme_name, theme_info, theme_settings Le nom du thème actif, son theme.yml complet et son bloc settings.
t Le traducteur du thème lui-même : {{ t('Read more') }} cherche la phrase dans locales/<langue>.json (voir plus bas).
request La requête FastAPI, pour le rare cas où vous avez besoin du chemin ou d’un paramètre.

L’objet page

La même forme, qu’il s’agisse d’une page, d’un article ou d’un élément de n’importe quelle liste d’articles :

Champ Notes
title, slug, url Dans la langue courante. url est complète, préfixe de langue ou domaine propre à la langue compris.
content Déjà converti de Markdown en HTML — affichez-le avec {{ page.content | safe }}.
excerpt, meta_description Les formes courtes, dans la langue courante.
poster_image_url L’URL complète de l’image de couverture, ou None.
gallery, content_gallery Des listes d’images : {id, url, title, width, height, original_filename}. Utilisez image.url directement.
tags {slug, title, url} pour chaque étiquette publique. Les étiquettes privées ne sont jamais incluses.
is_post, is_featured, published_at, created_at, updated_at S’il s’agit d’un article, s’il est mis en avant, et ses dates.
id L’identifiant en base — pratique pour les ancres et les clés de cache.

Listes d’articles

Gabarit Variable Contenu
home.html posts (alias recent_posts), pages, is_home Les articles partageant une étiquette avec la page d’accueil, en nombre et ordre définis par la section home de theme.yml. Une page d’accueil sans étiquette ne reçoit aucun article — étiquetez-la pour la remplir.
page.html posts (alias page_posts) Les articles partageant une étiquette avec cette page, selon la section page. C’est ainsi qu’une page « Blog » liste ses articles.
post.html related_posts, is_post Les autres articles partageant une étiquette avec celui-ci, selon la section post.
tag.html tag (title, slug), posts Tous les articles publiés portant cette étiquette, selon la section tag.
search.html search_query, search_results, results_count Le terme saisi et les pages et articles correspondants.

Chaque liste d’articles est accompagnée d’une pagination de même forme : has_pagination, current_page, total_pages, has_previous, has_next, previous_page, next_page, et pagination_urls avec first, previous, next et last — None là où il n’y a nulle part où aller. Avec `has_pagination` désactivé dans `theme.yml`, la liste est simplement coupée à `posts` et les indicateurs restent à faux.

Les langues et le sélecteur

Deux des champs de langue sont calculés pour vous. short_code est le code de langue seul — EN — sauf si le site active aussi une autre variante de la même langue, auquel cas il garde sa région : EN-US et EN-GB. menu_label suit la même règle avec les noms : English, ou English (US) et English (UK). C’est exactement la règle qu’utilise le sélecteur de langue de l’administration elle-même, si bien que le site et l’administration ne se contredisent jamais sur un libellé. Un sélecteur complet est donc court :

{% 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 pointe déjà vers la même page dans chaque langue — le slug traduit, ou la page d’accueil si cette traduction n’existe pas encore —, de sorte qu’un sélecteur ne construit jamais d’URL lui-même.

Les mots du thème lui-même

Le contenu est traduit par l’auteur dans l’administration. Les mots propres au thème — « Lire la suite », « Rechercher », « Tous droits réservés » — sont traduits par le thème, dans templates/locales/<langue>.json :

{
  "Read more": "Lire la suite",
  "Search": "Rechercher",
  "Change language": "Changer de langue"
}

Entourez la phrase de {{ t('Read more') }} et elle s’affiche dans la langue du visiteur. Une phrase absente du fichier de cette langue se rabat sur le fichier de la langue principale, puis sur la phrase elle-même — un thème à moitié traduit fonctionne donc toujours, simplement en anglais là où la traduction n’a pas encore été écrite. Les fichiers voyagent avec le thème lors de l’export.

Une page, sa propre mise en page

Une page d’atterrissage, un formulaire de contact, une grille de portfolio — parfois une page mérite une mise en page dont aucune autre n’a besoin. Ajoutez un gabarit nommé page-<slug>.html, où <slug> est le slug de cette page dans la langue principale, et FolioPress l’utilise pour cette page dans toutes les langues. Supprimez le fichier et la page revient discrètement à 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 %}

La page qu’un administrateur désigne comme page du formulaire de contact reçoit un petit supplément : is_contact_page est vrai, contact_form_action indique où le formulaire doit envoyer, et contact_guard_token / contact_honeypot_field sont deux champs cachés qui tiennent les robots à l’écart. Après un envoi, contact_status vous dit s’il faut afficher un remerciement ou une erreur.

Partez d’un thème qui fonctionne

Le moyen le plus rapide de construire un thème est d’ouvrir l’un des thèmes fournis — chacun a une démonstration en direct — et de le lire à côté de cette page. Dupliquez-le dans l’administration, modifiez le CSS, puis les gabarits, et vous aurez quelque chose à vous sans jamais être parti d’un fichier vide.

Deux habitudes qui font gagner du temps. Affichez les valeurs que le contexte vous donne plutôt que de les reconstruire — page.url, tag.url et language_switch_urls connaissent déjà les préfixes de langue et les domaines personnalisés. Et gardez le squelette dans base.html : si vous vous surprenez à coller un en-tête dans un second gabarit, il a sa place dans la base.