Référence de configuration
Tout ce que comprend pullpress.config.yml : le seul fichier qui transforme votre dépôt en CMS.
Le fichier de configuration se trouve à la racine de votre dépôt et est versionné comme tout le reste. Contrairement aux modifications de contenu (qui ouvrent une demande de modification à relire), enregistrer la configuration est une action d’administrateur qui commite directement sur votre branche par défaut : un nouveau modèle de contenu prend effet aussitôt (et il est validé avant d’être écrit). Il comporte quelques clés de premier niveau, une liste de collections et un petit ensemble de types de champs.
Vous préférez modifier dans l’application ? L’éditeur de configuration intégré propose un mode visuel dont le sélecteur de types de champs regroupe chaque type avec une description d’une ligne, et son mode YAML valide avant l’enregistrement : une faute de syntaxe est signalée à sa ligne et sa colonne exactes, et les problèmes de structure sont listés par chemin de champ. Le nom du fichier est pullpress.config.yml (l’extension .yaml fonctionne aussi).


Clés de premier niveau
| Clé | Description |
|---|---|
version | Version du schéma de configuration. Actuellement toujours 1. |
media.folder | Chemin du dépôt où les images envoyées sont commitées, par exemple public/uploads. |
media.public_path | Préfixe d’URL sous lequel le site publié sert ce dossier, par exemple /uploads. Sert à écrire des liens d’images corrects dans votre Markdown. |
collections | La liste des collections de contenu avec lesquelles les rédacteurs travaillent. Chaque collection a un nom, un libellé, un type et des champs. |
components | Facultatif. Une table de groupes de champs nommés et réutilisables, référencés par les champs de type: component. Définissez un groupe une fois, réutilisez-le dans plusieurs collections. |
Types de collections
Une collection décrit un type de contenu. Son type décide de la correspondance entre les entrées et les fichiers du dépôt.
folder : un fichier par entrée
Chaque entrée est son propre fichier Markdown dans le dossier configuré. Les rédacteurs peuvent créer, modifier et supprimer des entrées : idéal pour les articles de blog, les actualités ou les membres de l’équipe.
collections:
- name: blog
label: Blog posts
type: folder
folder: content/blog
fields:
- { name: title, label: Title, type: string, required: true }
- { name: date, label: Date, type: date, default: now }
- { name: body, label: Body, type: markdown }Formats de frontmatter. Les trois formats sont conservés : YAML entre des lignes ---, TOML entre des lignes +++ (le format par défaut de Hugo) et un objet JSON nu en haut du fichier. Les entrées existantes gardent toujours leur format, l’ordre de leurs clés et les clés que PullPress ne gère pas. Le frontmatter YAML garde aussi ses # comments, ses ancres et ses chaînes en bloc |/> à l’enregistrement : modifier un champ produit un diff qui ne touche que ce champ. (Les frontmatters TOML et JSON gardent les valeurs et l’ordre des clés, mais les lignes de commentaire TOML ne sont pas conservées.) Mettez format: toml (ou json) sur une collection pour créer les nouvelles entrées dans ce format ; la détection le renseigne pour vous quand votre contenu existant l’utilise.
Les sites qui utilisent un dossier par entrée (content collections d’Astro, leaf bundles de Hugo : content/my-post/index.md) mettent structure: bundle. Ajoutez media: colocated pour ranger les images d’une entrée dans son propre dossier, référencées de façon relative (./photo.jpg), pour que la chaîne d’images de votre générateur les prenne en charge. La détection du dépôt vous propose tout cela : elle reconnaît les content collections d’Astro, les leaf et branch bundles de Hugo (plus contentDir et les fichiers de data/), les collections Jekyll de _config.yml, les dossiers de contenu Next.js et les bundles imbriqués par date, et écrit les libellés générés dans la langue de votre interface :
collections:
- name: projects
label: Projects
type: folder
folder: redesign/content
structure: bundle # one folder per entry: content/my-post/index.md
media: colocated # images live inside the entry folder (./photo.jpg)
slug: "{{date}}-{{slug:title}}"
fields:
- { name: title, label: Title, type: string, required: true }
- { name: date, label: Date, type: date, default: now }
- { name: body, label: Body, type: markdown }files : des pages fixes
Un ensemble fixe de fichiers que vous listez explicitement. Les rédacteurs peuvent les modifier mais pas en ajouter ni en retirer : parfait pour des pages uniques comme À propos ou Contact. Chaque fichier a ses propres fields (imbriqués sous ce fichier, pas partagés au niveau de la collection), donc chaque page peut avoir une forme différente.
collections:
- name: pages
label: Pages
type: files
files:
- name: about
label: About page
file: content/about.md
fields:
- { name: title, label: Title, type: string, required: true }
- { name: body, label: Body, type: markdown }
- name: contact
label: Contact page
file: content/contact.md
fields:
- { name: heading, label: Heading, type: string, required: true }
- { name: body, label: Body, type: markdown }La détection l’utilise aussi pour les pages de section : le _index.md d’un branch bundle de Hugo (la page d’accueil d’une section) n’est pas une entrée de la collection folder, donc la détection les rassemble tous dans une collection files générée « Section pages », modifiable comme n’importe quelle page, sans câblage manuel.
data : un seul fichier de données structurées
Un fichier YAML, JSON ou TOML modifié entièrement via un formulaire, sans corps Markdown. Utilisez-le pour les données structurées que vos templates lisent directement : horaires d’ouverture, menus de navigation, réglages du site. Les clés du fichier absentes de la configuration des champs survivent à chaque enregistrement, dans les trois formats. Les fichiers YAML gardent aussi leurs commentaires ; les fichiers TOML et JSON gardent leurs clés et leur ordre mais sont reformatés.
collections:
- name: opening_hours
label: Opening hours
type: data
file: data/opening-hours.yml
fields:
- name: hours
label: Weekly hours
type: list
of:
type: object
fields:
- { name: day, label: Day, type: select, options: [mon, tue, wed, thu, fri, sat, sun] }
- { name: open, label: Opens, type: string }
- { name: close, label: Closes, type: string }
- { name: closed, label: Closed all day, type: boolean, default: false }Certains fichiers de données ne sont pas une table mais une grande liste : un tableau en haut du fichier, comme celui que lit le loader file() d’Astro (src/data/authors.json contenant [ …, … ]), ou les fichiers de données en liste de Hugo/Jekyll/Eleventy. Mettez root: list et déclarez exactement un champ de type list : ses éléments sont le tableau de premier niveau du fichier, modifié comme un formulaire répétable. Les éléments que vous ne touchez pas sont réécrits tels quels (l’ordre de leurs clés et leurs clés supplémentaires sont conservés) ; TOML ne peut pas représenter un tableau de premier niveau, donc cela fonctionne pour les fichiers .json, .yaml et .yml. La détection propose automatiquement cette forme.
Les fichiers CSV et TSV (Jekyll _data/*.csv, exports de tableur) fonctionnent de la même façon : ils sont toujours tabulaires, donc faites pointer une collection data en root: list vers le fichier et listez les colonnes comme champs string de l’unique champ list. La ligne d’en-tête et l’ordre des colonnes sont conservés ; chaque valeur fait l’aller-retour en texte.
collections:
- name: authors
label: Authors
type: data
file: src/data/authors.json # the file's root is [ … ], not { … }
root: list
fields: # exactly one field, of type list
- name: items
label: Authors
type: list
of:
type: object
fields:
- { name: name, label: Name, type: string, required: true }
- { name: avatar, label: Avatar, type: image }folder d’entrées de données : sans corps Markdown
Parfois chaque entrée n’est que des données structurées, sans corps d’article : auteurs, membres de l’équipe, produits, listes de liens. Donnez à une collection folder une extension de données (json, yaml, yml ou toml) et chaque entrée devient un seul fichier de données que les rédacteurs remplissent comme un formulaire : la même création, modification, suppression, relecture et le même historique que pour le Markdown, simplement sans corps. C’est exactement ainsi que fonctionnent les collections Astro type: 'data', et la détection vous les propose.
collections:
- name: authors
label: Authors
type: folder
folder: src/content/authors
extension: json # each entry is a .json data file (no body)
slug: "{{slug:name}}"
fields:
- { name: name, label: Name, type: string, required: true }
- { name: bio, label: Bio, type: text }
- { name: avatar, label: Avatar, type: image }Le fichier entier constitue les données (son format suit l’extension), et, comme pour une collection data, l’ordre des clés et les clés absentes de votre liste de champs sont conservés à chaque enregistrement. Il n’y a pas de champ de corps, n’en ajoutez donc pas ; les relations peuvent aussi pointer vers ces collections.
Options des collections
En plus de name, label, folder et fields, une collection folder accepte ces clés facultatives. La détection en renseigne la plupart pour vous.
| Clé | Description |
|---|---|
structure | flat (par défaut) garde un fichier par entrée ; bundle utilise un dossier par entrée (folder/slug/index.md). |
media | site (par défaut) envoie les fichiers dans le dossier des médias ; colocated les range dans le dossier bundle de l’entrée, référencés comme ./photo.jpg. colocated exige structure: bundle. |
extension | Type de fichier des entrées : md (par défaut), markdown ou mdx pour des pages Markdown ; html, adoc ou org pour du contenu non Markdown (le frontmatter est toujours conservé ; déclarez le corps en type: code pour qu’il soit modifié en texte brut) ; ou json, yaml, yml ou toml pour faire de chaque entrée un fichier de données sans corps (un dossier d’entrées de données, comme les data collections d’Astro). |
format | Format du frontmatter des nouvelles entrées : yaml (par défaut), toml ou json. Les entrées existantes gardent leur format. |
nested | true rassemble aussi les entrées des sous-dossiers (jusqu’à cinq niveaux), groupées par dossier. Désactivé par défaut. |
locales | Codes de langue supplémentaires (par exemple [fr, en]) qui rendent chaque entrée modifiable par langue, organisées selon la clé i18n. |
i18n | Où se trouvent les traductions : suffix (par défaut) utilise des fichiers voisins (post.fr.md) ; subfolder utilise un dossier par langue (Hugo content/<lang>/, Astro/Starlight docs/<lang>/). Avec subfolder, faites pointer folder vers le dossier de la langue par défaut et nommez ce segment dans defaultLocale ; par exemple folder: content/en, defaultLocale: en, locales: [fr, de] place les entrées françaises dans content/fr. |
slug | Modèle de nom de fichier pour les nouvelles entrées. Par défaut {{slug:title}}. Jetons : {{slug:field}} (champ converti en slug), {{field}}, {{date}}, {{year}}, {{month}}, {{day}}. |
sort | Ordre de la liste des entrées : { field, order } avec order asc ou desc. Par défaut le champ de date, le plus récent en premier. |
Une collection files liste plutôt chaque fichier avec ses propres fields (et un format facultatif par fichier) ; une collection data pointe vers un seul file et partage un seul ensemble de fields.
Types de champs
Chaque champ d’une collection devient un contrôle de formulaire dans l’éditeur. Les options communes label, required et default fonctionnent sur tous les types. Deux autres aident vos rédacteurs : un texte help affiche une indication sous le champ, et un placeholder affiche un texte d’exemple dans un champ vide. Les champs obligatoires reçoivent un astérisque rouge et, à la soumission, un récapitulatif aimable de ce qui manque.
Champs en lecture seule. Ajoutez readOnly: true pour afficher un champ avec sa valeur enregistrée sans que les rédacteurs puissent la changer : le nom d’un document de réglages, un slug verrouillé, un identifiant externe. Il est verrouillé dans l’éditeur web comme dans l’application iOS, et le serveur restaure la valeur enregistrée à chaque enregistrement : un champ en lecture seule ne peut jamais dériver, quelle que soit la façon dont arrive une modification.
- name: title
type: string
label: Name
readOnly: true # shown, but editors can't change itTextes par langue. L’interface s’adapte à la langue choisie par chaque rédacteur, et vous pouvez faire de même pour vos propres textes label, help et placeholder. La clé simple est affichée à tous ; ajoutez un suffixe _<language> (parmi en, nl, fr, de, es) pour la remplacer pour les rédacteurs qui utilisent cette langue. Rien n’est traduit automatiquement : vous écrivez chaque variante.
- name: title
type: string
label: Titel # shown to everyone
help: De titel van je pagina
help_en: Your page title # only for editors using English| Type | Description | Exemple |
|---|---|---|
string | Une seule ligne de texte. | { name: title, type: string, required: true } |
text | Texte brut sur plusieurs lignes, affiché dans une zone de texte. | { name: summary, type: text } |
markdown | Texte mis en forme dans l’éditeur épuré, stocké en Markdown. | { name: body, type: markdown } |
number | Un nombre entier ou décimal. | { name: price, type: number } |
boolean | Un interrupteur oui/non. | { name: featured, type: boolean, default: false } |
date | Une date du calendrier. default: now remplit la date du jour. | { name: date, type: date, default: now } |
datetime | Une date avec une heure. | { name: published_at, type: datetime } |
select | Un choix dans une liste fixe, via options. | { name: category, type: select, options: [news, recipes] } |
multiselect | Plusieurs choix dans une liste fixe, sous forme de cases à cocher. | { name: categories, type: multiselect, options: [news, events] } |
url | Une adresse web, validée comme https://… | { name: website, type: url } |
email | Une adresse e-mail validée. | { name: contact, type: email } |
color | Un sélecteur de couleur, stocké en #rrggbb. | { name: accent, type: color } |
image | Une image envoyée, commitée dans le dossier des médias. | { name: cover, type: image } |
list | Une liste répétable d’éléments ; of est la définition de champ du type d’élément. | { name: tags, type: list, of: { type: string } } |
object | Un groupe de champs imbriqués, définis avec fields. | { name: cta, type: object, fields: [...] } |
relation | Une référence à une entrée d’une autre collection folder (stocke son slug). | { name: author, type: relation, collection: team } |
blocks | Des sections empilables de formes différentes (chaque élément porte une clé _type ; changez la clé avec typeKey). | { name: sections, type: blocks, blocks: [...] } |
hidden | Non affiché aux rédacteurs ; écrit avec une valeur fixe (par exemple une clé de mise en page). | { name: layout, type: hidden, default: post } |
code | Un bloc de code en police à chasse fixe, avec une indication de langage facultative. | { name: snippet, type: code, language: js } |
map | Un lieu, modifié en latitude/longitude (stocké en 'lat,lng'). | { name: location, type: map } |
compute | Une valeur en lecture seule dérivée d’un modèle ; chaque {{token}} est le nom d’un champ voisin. | { name: ref, type: compute, template: '{{brand}}-{{sku}}' } |
keyvalue | Des paires clé/valeur libres, par exemple des métadonnées supplémentaires. | { name: meta, type: keyvalue } |
seo | Panneau SEO : titre + description (avec indications de longueur), URL canonique, image OG et noindex, avec un aperçu de l’extrait de recherche et du JSON-LD. | { name: seo, type: seo } |
component | Déploie un groupe de champs réutilisable de la table components de premier niveau. | { name: cta, type: component, component: callToAction } |
list et object se combinent : une list d’object donne des groupes de champs répétables, comme les horaires hebdomadaires de l’exemple ci-dessus.
Règles de validation
En plus de required, un champ peut reprendre exactement les contraintes qu’impose le schéma de votre générateur : les erreurs sont ainsi repérées dans l’éditeur avec un message aimable, et non par un build qui échoue. Les champs numériques acceptent min, max et integer: true ; les champs texte (string, text, markdown, code) acceptent minLength, maxLength et une expression régulière pattern ; list et multiselect acceptent minItems et maxItems. Les erreurs s’affichent à côté du champ pendant la saisie (un champ facultatif peut rester vide ; ses règles s’appliquent dès qu’on y tape quelque chose), et les mêmes règles tournent côté serveur à chaque enregistrement.
- { name: title, type: string, required: true, maxLength: 70 }
- { name: rating, type: number, min: 1, max: 5, integer: true }
- { name: sku, type: string, pattern: "^[A-Z]{2}-\\d{4}$" }
- { name: tags, type: list, of: { type: string }, minItems: 1, maxItems: 8 }Définir des blocks
Un champ blocks permet aux rédacteurs d’empiler des sections de formes différentes (un hero, une citation, une galerie…). Listez les types de sections sous blocks ; chacun est un groupe nommé de fields (avec un label facultatif), comme un object :
- name: sections
label: Page sections
type: blocks
blocks:
- name: hero
label: Hero
fields:
- { name: heading, type: string, required: true }
- { name: image, type: image }
- name: quote
label: Pull quote
fields:
- { name: text, type: text, required: true }
- { name: source, type: string }Chaque élément ajouté par un rédacteur enregistre de quel bloc il s’agit sous une clé _type (hero, quote, …), pour que votre générateur sache comment afficher chaque section. Si votre schéma attend une autre clé (le z.discriminatedUnion('type', …) d’Astro, ou du contenu importé de Decap CMS, qui la stocke en type), mettez typeKey sur le champ (typeKey: type) et PullPress lit, écrit et valide cette clé à la place. L’import Decap le règle automatiquement, donc les entrées importées fonctionnent sans changement.
Champs conditionnels (showIf)
Tout champ peut déclarer showIf pour n’apparaître que lorsqu’un champ voisin correspond. Les champs masqués sont vidés (ils n’atterrissent donc jamais dans le frontmatter), et la règle required d’un champ conditionnel ne s’applique que tant qu’il est visible.
fields:
- { name: hasCta, label: Add a call to action?, type: boolean }
- name: ctaUrl
label: Button URL
type: url
required: true
showIf: { field: hasCta, truthy: true } # equals / oneOf also workCollections imbriquées
Mettez nested: true sur une collection folder pour rassembler aussi les entrées des sous-dossiers (jusqu’à cinq niveaux) et les afficher groupées par dossier : pratique pour des arborescences de documentation. La recherche et les sélecteurs de relations prennent en compte les entrées imbriquées automatiquement.
collections:
- name: docs
label: Documentation
type: folder
folder: content/docs
nested: true # gather entries from subfolders too
fields:
- { name: title, type: string, required: true }
- { name: body, type: markdown }Composants de champs réutilisables
Définissez une fois un groupe de champs nommé sous la table components de premier niveau, puis insérez-le dans n’importe quelle collection avec un champ component. Au chargement, il se déploie en un groupe d’objet ordinaire, donc une modification du groupe met à jour chaque collection qui l’utilise.
components:
callToAction:
- { name: label, type: string }
- { name: url, type: url }
collections:
- name: pages
type: folder
folder: content/pages
fields:
- { name: title, type: string, required: true }
- { name: cta, type: component, component: callToAction }Où vivent les médias (et pourquoi pas S3)
Les fichiers envoyés sont commités dans votre dépôt, à côté du contenu qui les référence. C’est un choix délibéré, pas une fonction manquante : le dépôt reste la seule source de vérité, les images suivent leurs articles à travers les branches, la relecture et l’historique, et cloner le dépôt donne le site complet, sans second système à sauvegarder ou à payer.
Besoin d’un CDN ou de redimensionnement d’images ? Laissez votre générateur ou votre hébergeur s’en charger au build (chaînes d’images d’Astro et de Next.js, CDN d’images de Netlify, URL fetch de Cloudinary). PullPress garde l’original dans Git ; votre build le rend rapide. Les fichiers sont redimensionnés au format web dans le navigateur avant l’envoi, même une photo de téléphone de 20 à 50 Mo, ce qui garde en pratique les dépôts très légers. Un stockage d’objets externe (S3 et consorts) casserait la garantie « votre dépôt est tout », donc nous ne le prévoyons pas.
Découvrabilité IA (llms.txt)
PullPress peut générer un llms.txt à la racine de votre dépôt : un index en texte brut de votre site (son nom, son URL et, pour chaque collection folder, une liste de titres d’entrées) que les moteurs de réponse IA lisent pour résumer et citer correctement votre site. Il est construit de façon déterministe à partir de votre modèle de contenu (aucun appel IA, rien ne quitte votre dépôt).
Les administrateurs le trouvent dans le panneau Paramètres → Découvrabilité IA (GEO) d’un site, qui affiche aussi un score de préparation : si l’URL de votre site est renseignée, si llms.txt est présent, si vous avez du contenu publié et des champs de description/SEO, et, si l’URL de votre site est joignable, si la page d’accueil expose une meta description, des balises Open Graph et du JSON-LD. Cliquez sur Mettre à jour llms.txt pour (re)générer le fichier ; il est commité comme toute autre modification : via la relecture, ou directement sur votre branche par défaut sur les sites en publication directe.
brandvoice.md ci-dessous : le ton de marque guide la façon dont l’IA écrit pour vous, tandis que llms.txt aide l’IA à trouver et citer ce que vous avez déjà publié. Il ne gère ni robots.txt ni votre sitemap : laissez-les à votre générateur.Ton de marque pour les agents IA
Déposez un brandvoice.md facultatif à la racine de votre dépôt pour décrire le ton du site, les mots préférés et proscrits, et le niveau de langue. Les agents IA connectés via MCP peuvent le lire (avec l’outil get_style_guide) et le suivre en rédigeant. Comme c’est un fichier ordinaire de votre dépôt, il est versionné et relu comme tout autre contenu, et propre à chaque client.