ADR-017 — Type slug et module URL-slug canonique¶
Statut¶
Acceptée
Décision-cadre de la Phase 2 de la roadmap beta.13. Implémentée par
SLUG-CORE-001(module),SLUG-VALIDATION-001,SLUG-SQL-CRUD-001,SLUG-ROUTING-001.
Contexte¶
Les URLs publiques générées par Forge restent faibles sans slug
(/articles/42 plutôt que /articles/premier-contact). Le slug est
nécessaire pour les pages publiques, catalogues, blogs : URLs lisibles,
partageables, SEO-friendly.
L'audit de l'existant (BETA13-EXISTING-AUDIT-001) révèle un terrain
partiellement défriché mais incohérent :
forge_cli/public_page._slugify— un validateur strict (kebab-case,[a-z0-9-]) qui rejette les accents (ValueErrorsur « Écrire ») ; ce n'est pas un générateur. 1 seul usage.forge_cli/entities/migrations.slugify_migration_name— un transformeur snake_case (_) pour les noms de fichiers de migration : concept différent d'une URL slug (format et finalité distincts).- type
slugdéjà reconnu dans le contrat d'entité (forge_cli/entities/validation.py) +SlugFielddans le CRUD, sans logique de génération.
Trois objets « slug » coexistent donc, avec des contrats divergents — risque de duplication (charte §11) si on ajoute naïvement une 4ᵉ fonction.
Décision¶
D1 — Deux concepts distincts, nommés clairement¶
- URL slug (kebab-case
-, pour les URLs publiques) → un seul module canoniquecore/http/slug.py. - Nom de fichier de migration (snake_case
_) → resteslugify_migration_name, séparé. Ce n'est pas une URL slug ; un commentaire le précise pour éviter toute fusion future.
D2 — Module canonique core/http/slug.py (runtime, stdlib seul)¶
Deux fonctions publiques, dépendances stdlib uniquement (unicodedata,
re) — respect du runtime minimal (charte) :
def slugify(text: str, *, max_length: int = 180) -> str
def is_valid_slug(value: str, *, max_length: int = 180) -> bool
slugifytransforme n'importe quel texte en URL slug : translittération des accents viaunicodedata.normalize("NFKD", …)(« Écrire avec Forge ! » →ecrire-avec-forge), minuscules, tout caractère non[a-z0-9]→-, tirets compactés, bordures retirées, longueur bornée. Ne lève que si le résultat est vide (entrée sans aucun caractère exploitable).is_valid_slugvalide un slug existant : path-safe (rejette/,\,..), correspond à[a-z0-9]+(?:-[a-z0-9]+)*, longueur ≤max_length.
Le runtime (contrôleurs/modèles générés) et le CLI (codegen) importent ce même
module. forge_cli/public_page._slugify est remplacé par core.http.slug
(§11 : une seule implémentation URL-slug).
D3 — Le type slug dans le contrat d'entité¶
slug est un type de champ de premier rang du contrat d'entité JSON
(au même titre que string, email…), validé par le contrat (ADR-013) :
Sémantique : identifiant URL-safe d'une ressource. La génération SQL produit
VARCHAR(180) + contrainte UNIQUE visible (charte « SQL visible »).
D4 — Unicité : rejet des doublons (pas de suffixe auto en b13)¶
Le CRUD généré refuse un slug en doublon avec une erreur claire. Le
suffixe automatique (mon-article-2) introduit une complexité de
concurrence (race sur insert) non justifiée en dernière beta → reporté
après 1.0.
D5 — Périmètre b13¶
Inclus : module canonique, validation, type d'entité, génération SQL/CRUD,
routing public par slug (SLUG-ROUTING-001).
Exclus (post-1.0) : suffixe auto, slug_history, redirections 301,
sitemap, slugs multilingues.
Conséquences¶
public_page._slugifychange de comportement : il translittère les accents au lieu de les rejeter (plus utile) — ses tests sont ajustés.slugify_migration_nameest inchangé mais documenté comme distinct.- Le contrat d'entité gagne un type de premier rang
slug→ garde-fou de complétude (§10) : SQL, validation, CRUD, doc cohérents. - Nouveau module runtime
core/http/slug.pytesté indépendamment.
Alternatives considérées¶
Fusionner les 3 « slug » en une fonction. Écarté : slugify_migration_name
produit du snake_case pour des noms de fichiers, pas des URLs — fusionner
casserait les migrations. Les deux concepts restent séparés (D1).
Option « slug » comme simple drapeau sur un champ string
({"type": "string", "slug": true}). Écarté au profit d'un type dédié
(D3) : plus explicite pour les générateurs, les tests et la doc, conforme à
« une seule façon » (§11).
Dépendance externe (python-slugify, unidecode). Écarté :
unicodedata (stdlib) suffit pour la translittération NFKD ; le runtime
minimal de Forge n'accueille pas de dépendance pour ça.
Suffixe automatique -2/-3 dès b13. Écarté : race conditions sur insert
concurrent, complexité non justifiée en dernière beta (D4).
Liens¶
- Charte : §10 (contrat de complétude), §11 (une seule façon), « SQL visible », runtime minimal.
- ADR-013 — politique du contrat d'entité (nullable/required).
- Roadmap beta.13 — Phase 2.