Architecture
Comment notACMS fonctionne en interne — pipeline de contenu, système de routage et processus de build.
Pipeline de Contenu
Chaque build suit une pipeline en quatre étapes :
1. sass:build → Compiler SCSS vers CSS
2. asset-map:compile → Hacher et copier les assets
3. app:build → Rendre toutes les pages HTML depuis le contenu + les templates
4. pagefind → Créer l'index de recherche
L'étape 3 est contrôlée par BuildStaticSiteCommand dans src/Command/, qui scanne le contenu, rend les templates, génère les images responsives et écrit le HTML statique dans public/static/. L'étape 4 exécute npx pagefind comme processus séparé.
Les URLs sont dérivées du champ frontmatter slug, pas des chemins de répertoire. Un article dans blog/releases/release-1-0-0/en.md avec slug: "blog/release-1-0-0" génère /blog/release-1-0-0/.
Système de Routage
Les routes sont générées à partir de l'arborescence de contenu. Chaque ContentItem a une URL définie par son champ frontmatter slug.
Le LocalizedRouteLoader lit local/content/_routes.yaml et enregistre les routes Symfony pour chaque locale. La locale par défaut (EN) n'a pas de préfixe. Les autres locales obtiennent un préfixe /{locale}/, sauf si surchargé.
Exemple de génération d'URL :
| Fichier de Contenu | Slug Frontmatter | Locale | URL Générée |
|---|---|---|---|
pages/about/en.md |
about |
EN | /about/ |
pages/about/de.md |
about |
DE | /de/about/ |
blog/releases/release-1-0-0/en.md |
blog/release-1-0-0 |
EN | /blog/release-1-0-0/ |
blog/releases/release-1-0-0/pl.md |
blog/release-1-0-0 |
PL | /wpisy/release-1-0-0/ |
Services Core
ContentService
La façade principale pour charger et interroger le contenu. Délègue la construction de l'arborescence à ContentTreeBuilder et implémente ContentCacheInterface, permettant ainsi à invalidateCache($locale) d'invalider les arbres mis en cache lorsque le contenu change.
$posts = $contentService->getPosts($locale, 1, 5);
$recent = $contentService->getRecentPosts($locale, 5);
$item = $contentService->findByUrl('/about/', $locale);
ContentItem
Représente un élément de contenu unique. Expose des méthodes pour les champs frontmatter :
$item->title() // string
$item->slug() // string — dernier segment du slug frontmatter (ex. "my-post" de "blog/my-post")
$item->url() // string (chemin URL complet, ex. /blog/my-post/)
$item->date() // ?DateTimeImmutable
$item->updatedDate() // ?DateTimeImmutable
$item->description() // string
$item->tags() // string[]
$item->category() // ?string
$item->image() // ?string
$item->imageAlt() // string (valeur par défaut : title)
$item->template() // string
$item->isDraft() // bool
$item->isScheduled() // bool
$item->isPinned() // bool
$item->isDynamic() // bool
$item->isFeatured() // bool
$item->hasToc() // bool
$item->series() // ?string
$item->seriesOrder() // ?int
$item->directoryKey() // ?string
$item->relatedSlugs() // string[]
$item->menuWeight() // int
$item->isIndex() // bool
$item->readingTime() // int
$item->wordCount() // int
$item->excerpt() // string
$item->htmlContent // chaîne HTML rendue (propriété publique)
SiteConfigService
Lit local/content/_site.yaml et expose les paramètres du site en tant que service. Est injecté dans les contrôleurs et les globales Twig via SiteConfigExtension.
ContentTree
Représentation interne de la hiérarchie de contenu pour une seule locale. Utilisée pour la navigation, le calcul précédent/suivant et la reconnaissance des séries. ContentService l'enveloppe comme une façade, donc les mêmes noms de méthodes de recherche (findByUrl, findPostBySlug, findScheduledPostBySlug) existent aux deux niveaux : la variante service accepte $locale et délègue à l'arbre de la locale correspondante.
Méthodes importantes :
getAdjacentPosts()— retourne le Value ObjectAdjacentPostsavec->prevet->nextgetSeriesPosts()— retourne les articles de la même série, triés parseries_order
Autres Services
| Service | Objectif |
|---|---|
ImageResizer |
Wrapper ImageMagick : resize() génère des variantes responsives ; optimize() recompresse sur place |
ResponsiveImageService |
Calcule les largeurs de variantes responsives et crée les valeurs d'attribut srcset |
SrcsetExtension |
Filtre Twig srcset_media — post-traite le HTML pour insérer les attributs srcset/sizes dans les balises <img> |
MediaFileResolver |
Résout les chemins de fichiers média à travers les locales |
RelatedPostsService |
Trouve les articles connexes via les tags communs et le frontmatter related manuel |
SessionToggleService |
Générateur de bascule booléenne basée sur session ; dépendance commune des deux services de prévisualisation ci-dessous |
DraftPreviewService |
Gère la prévisualisation du contenu brouillon avec des bascules basées sur session |
ScheduledPreviewService |
Gère la prévisualisation des articles planifiés futurs |
TranslationMapBuilder |
Crée les cartes de traduction d'URL à travers les locales pour le sélecteur de langue |
ContactFormConfig |
Fournit la configuration du formulaire de contact (destinataire, Turnstile, etc.) |
MarkdownParser |
Convertit Markdown en HTML via CommonMark |
SidebarDataProvider |
Assemble les données de la barre latérale (catégories, tags, derniers articles) |
TagTranslationService |
Traduit les slugs de tags entre les locales |
Extensions Twig
Cinq extensions Twig fournissent des données pour les templates :
| Extension | Type | Fournit |
|---|---|---|
SiteConfigExtension |
Globals | site_name, site_base_url, site_description, site_social, site_author, site_locales, site_locales_list, site_default_locale, image_variant_widths, new_post_days, coming_soon_reveal_days, meta_description_length |
TranslationMapTwigExtension |
Global | translation_map — mapping {directoryKey: {locale: url}} pour le sélecteur de langue et les tags hreflang |
ContentTwigExtension |
Function | content_url(directoryKey, locale) — résout l'URL pour un élément de contenu par clé de répertoire |
LangSwitcherExtension |
Function | lang_switch_urls(otherLocales) — résout les URLs du sélecteur de langue par locale : translation map, override contrôleur, puis fallbacks (archive → paginé → liste blog → accueil) |
SrcsetExtension |
Filter | srcset_media — insère les attributs srcset/sizes dans les balises <img> pour les images responsives |
De plus, cf_analytics_token est enregistré comme globale Twig dans config/packages/twig.yaml, lié à la variable d'environnement CF_ANALYTICS_TOKEN.
Flux de Requête (Mode Live)
Browser → nginx → PHP-FPM → Symfony Kernel → LocaleListener → Router
→ Controller → ContentService → Twig → HTML-Response
→ Twig-Renderer (appliquer template)
→ HTML-Response
Les contrôleurs sont minces. Toute la logique métier réside dans les services.
Stratégie de Test
ddev test # Exécuter toute la suite de tests
ddev code-check # PHPStan Level 6 + PHP CS Fixer
- Centaines de méthodes de test dans les suites d'intégration et unitaires.
- PHPStan : Analyse statique au niveau 6. Tous les services sont entièrement typés.
- PHP CS Fixer : Applique PSR-12 + Standards de Codage Symfony.
- Tests d'intégration : Couvrent le routage, le chargement de contenu, les contrôleurs, la sortie de build, le traitement d'images, les services de prévisualisation et les extensions Twig.
- Tests unitaires : Couvrent
ContentItem,ContentTree, Value Objects,RelatedPostsService,TagTranslationServiceetResponsiveImageService.
Voir docs/TESTING.md pour les détails.
Ajout de Fonctionnalités
notACMS suit le pattern du conteneur de services Symfony. Pour ajouter un nouveau type de contenu ou une étape de build :
- Créer une interface dans
src/Service/(pour les contributions core) oulocal/src/(pour les extensions spécifiques au site) - Créer la classe d'implémentation dans le même répertoire
- Symfony auto-discover et autowire automatiquement — aucun enregistrement manuel
services.yamlrequis - Injecter dans le contrôleur ou commande pertinent
- Surcharger les templates dans
local/templates/si nécessaire
Astuce : Tous les services sont d'abord des interfaces. Cela facilite le remplacement des implémentations — par exemple, le chargeur de système de fichiers local contre une source S3.
Runtime PHP (Optionnel)
Par défaut, le site construit est purement statique — Nginx sert public/static/ directement et PHP n'est pas impliqué.
La configuration Docker de production dans local/docker/ contient un conteneur PHP optionnel. Définissez RUNTIME_PHP_ENABLED=true dans votre environnement de production pour le démarrer. Lorsqu'activé, Nginx redirige /api/ (et /{locale}/api/) vers le conteneur PHP — tout le reste continue d'être servi comme fichier statique.
Le cas d'utilisation standard est le formulaire de contact (ContactController via SMTP), accessible sous /api/contact.
Note : Le développement local (
ddev start) exécute l'application Symfony complète dynamiquement. C'est une fonctionnalité de confort dev-only — elle ne fait pas partie du runtime PHP de production.
Architecture de Déploiement
Serving Static-First
Nginx sert les fichiers pré-construits directement depuis public/static/. PHP n'est impliqué que pour :
/api/contact— lorsqueRUNTIME_PHP_ENABLED=true- Mode développement (
ddev start)
Flux de trafic de production :
Browser → Nginx → public/static/ (HTML, CSS, JS)
→ Proxy vers le conteneur PHP (uniquement pour /api/*)
Build vs Runtime
| Phase | Commande | Sortie | Durée |
|---|---|---|---|
| 1. Compiler | sass:build |
public/assets/app-<hash>.css |
~2s |
| 2. Assets | asset-map:compile |
Copies d'assets hachées dans public/assets/ |
~1s |
| 3. Build | app:build (BuildStaticSiteCommand) |
public/static/**/*.html |
~5s |
| 4. Index | pagefind |
Index de recherche public/pagefind/ |
~3s |
Premier déploiement complet : ~2-3 minutes (Build Docker) Mise à jour de contenu : ~10 secondes
Profils de Conteneur
| Profil | Conteneurs | Cas d'utilisation |
|---|---|---|
| (aucun) | nginx uniquement | Sites statiques, pas de formulaire de contact |
--profile runtime |
nginx + php | Formulaire de contact activé |
Les profils sont basculés via RUNTIME_PHP_ENABLED dans .env.local.