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 parSLUG-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 :
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.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é (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.
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.