Modèle de données

Table lrvo_pages_page, hiérarchie par parent_id, règles de slug, visibilité et migrations appliquées au lrvo-db init.

Le module crée une seule table, lrvo_pages_page. Comprendre sa forme est nécessaire pour écrire un template, appeler l'API ou diagnostiquer une URL qui ne résout pas.

Table lrvo_pages_page

ColonneTypeNotes
idSERIALClé primaire.
parent_idINTEGERRéférence auto vers lrvo_pages_page(id), ON DELETE SET NULL. NULL pour une page racine.
slugVARCHAR(200)Un seul segment d'URL. Voir les règles ci-dessous.
titleVARCHAR(500)Requis, jamais vide.
templateVARCHAR(100)Identifiant de template, 'default' par défaut.
blocksJSONBTableau de blocs du BlockEditor, [] par défaut.
seoJSONBObjet SEO partagé, forme EMPTY_SEO de core/seo, {} par défaut.
visibilityVARCHAR(20)'public' ou 'private', 'public' par défaut.
created_atTIMESTAMPTZÉditable depuis l'admin, dans le passé uniquement.
updated_atTIMESTAMPTZMis à jour à chaque enregistrement.

Index créés : un sur visibility, un sur parent_id, et un index unique sur (COALESCE(parent_id, 0), slug).

Hiérarchie et URL

Une page ne stocke pas son chemin. Elle stocke un segment dans slug et un lien vers son parent. Le chemin public est reconstruit à la lecture en remontant la chaîne des parents.

Une page de slug equipe dont le parent a le slug a-propos est servie à /a-propos/equipe. Les pages vivent à la racine du site : aucun préfixe de type n'est ajouté.

La résolution descend segment par segment depuis la racine. Si un seul segment de la chaîne ne correspond pas, la page n'est pas trouvée, même si le dernier segment existe ailleurs dans l'arbre.

Le module refuse d'assigner un parent qui créerait un cycle, en vérifiant toute la chaîne d'ancêtres avant d'enregistrer.

Règles de slug

Le slug envoyé n'est jamais enregistré tel quel. Il est d'abord normalisé : minuscules, accents retirés, espaces convertis en tirets, tirets multiples réduits, tout caractère hors [a-z0-9-] supprimé. La validation porte ensuite sur ce slug calculé, pas sur la valeur envoyée.

Deux conséquences pratiques :

  • Un slug vide est dérivé du titre. Si le titre ne produit rien d'utilisable, le slug vaut page.
  • Un slug qui contient / ou $ n'est pas rejeté, ses caractères sont retirés. Envoyer a-propos/equipe produit la page a-proposequipe, à plat, et non une hiérarchie. La hiérarchie passe par le parent, jamais par le chemin.

Un slug ne peut donc pas violer la forme attendue, ^[a-z0-9][a-z0-9-]*$ : la normalisation la garantit. Le seul refus qu'un appelant rencontre en pratique est celui des slugs réservés.

Slugs réservés à la racine

À la racine seulement, ces slugs sont refusés parce qu'ils entrent en collision avec les routes statiques du projet :

admin, auth, lrvo, api, _next, static

La restriction ne s'applique pas aux pages enfants. Une page /documentation/api est valide, une page /api ne l'est pas.

Unicité

Le slug est unique par parent, pas globalement. Deux pages de parents différents peuvent porter le même slug. C'est ce que garantit l'index unique sur (COALESCE(parent_id, 0), slug) : en PostgreSQL, une contrainte UNIQUE classique ne suffirait pas, puisque NULL n'y est jamais égal à NULL.

Quand un slug est déjà pris chez le même parent, le module suffixe automatiquement : contact, puis contact-2, puis contact-3.

Déplacer une page

Changer le parent d'une page rejoue la validation et l'unicité du slug, même quand le slug lui-même n'est pas modifié. Deux effets à connaître :

  • le slug peut changer, s'il entre en collision chez le nouveau parent ;
  • remonter une page à la racine peut être refusé, si son slug figure dans la liste réservée.

Visibilité

ValeurEffet
publicLa page est servie sur son URL et apparaît dans les listes publiques.
privateLa page reste éditable dans l'admin, mais le rendu public répond 404.

Toute autre valeur est rejetée à l'écriture.

Date de création éditable

created_at est une colonne structurelle éditable. Dans l'éditeur, « Créé le » se transforme en sélecteur de date au clic. Une date future est refusée, côté client comme côté serveur.

updated_at reste en lecture seule et continue d'être mis à jour à chaque enregistrement.

Migrations

createTables court-circuite quand la table existe déjà. Pour que les bases déployées reçoivent quand même les évolutions de schéma, deux migrations idempotentes sont exécutées inconditionnellement à chaque lrvo-db init :

MigrationEffet
VisibilitéRenomme l'ancienne colonne status en visibility et convertit les valeurs : published devient public, tout le reste devient private. Renomme l'index associé.
SEOAjoute la colonne seo si elle est absente.

Les deux sont gardées et ré-exécutables sans effet. Un projet créé avant l'introduction de ces colonnes est donc migré par un simple npx lrvo-db init.

Notes

  • Supprimer une page ne supprime pas ses enfants : leur parent_id passe à NULL et elles remontent à la racine. Vérifier qu'aucun slug d'enfant ne heurte alors un slug réservé.
  • Les références médias des blocs sont détachées avant la suppression de la ligne, via le registre de médias du core. Le type de source utilisé est pages.
  • blocks est volontairement absent des listes. Les fonctions de liste renvoient des lignes légères ; charger une page par son identifiant ou par ses segments pour obtenir son contenu.

Sur cette page