Dossier d'Architecture Technique (DAT)
Dossier d'Architecture Technique (DAT)
Documentation technique
Couche 01 : Gestion et Stockage du Contenu
Projet : Réarchitecture du Site Web (Vitrine, Blog, Portfolio, Documentation)
Statut : Version Initiale
Date : 1er Septembre 2026
Équipe : AMN
1. Introduction & Objectifs
Ce document décrit les spécifications techniques de la Couche 01 (Contenu). L’objectif de cette couche est d’assurer le stockage, la modélisation, la sécurisation et la mise à disposition des données textuelles et médias du site web.
Cette couche est découplée des couches de Présentation (Couche 02) et de Logiciel/Backend (Couche 03) selon les principes d’une architecture Jamstack / Headless.
2. Architecture Conceptuelle de la Couche Contenu
La couche contenu est hybride. Elle sépare le contenu dynamique (orienté marketing) du contenu technique (orienté produit/développeur) :
┌───────────────────────────────────────┐ │ COUCHE 01 : CONTENU │ └───────────────────┬───────────────────┘ │ ┌────────────────────┴────────────────────┐ ▼ ▼ [ Données Dynamiques ] [ Données Techniques ] Vitrine, Blog, Portfolio Documentation │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Sanity.io │ │ Fichiers Git │ │ (CMS Headless)│ │ (Markdown/MDX)│ └───────┬───────┘ └───────┬───────┘ │ │ └────────────────────┬────────────────────┘ │ (Phase de Build) ▼ ┌───────────────────────────────────────┐ │ COUCHE 02 : PRÉSENTATION │ │ (Générateur Astro) │ └───────────────────────────────────────┘
3. Spécifications Techniques par Composant
3.1. Composant CMS Headless : Sanity.io1/2
Dédié à la gestion du contenu pour la Vitrine, le Blog et le Portfolio.
- Modèle de Données (Content Schema) : Déclaré sous forme de code (Schema-as-Code) en TypeScript au sein du sous-projet
sanity-studio. - Format de Stockage : JSON propriétaire orienté graphe (Sanity Content Lake).
3.1. Composant CMS Headless : Sanity.io2/2
- Gestion des Médias : Tous les fichiers médias (images, vidéos, PDF, documents) sont stockés exclusivement sur Cloudflare R2 (et Cloudflare Stream pour les vidéos). Sanity ne conserve que les métadonnées et les URLs pointant vers R2/Stream (voir §4.8).
- Interface d’édition (Sanity Studio) : Déployée de manière indépendante ou intégrée en tant que route d’administration. Elle offre une prévisualisation en temps réel aux contributeurs (Draft Mode, voir §4.7).
3.2. Composant Fichiers Locaux : Astro Starlight
Dédié à la gestion de la documentation technique.
-
Format de Stockage : Fichiers locaux
.md(Markdown) et.mdx(composants interactifs dans le contenu). -
Arborescence cible :
src/└── content/└── docs/├── index.mdx├── guide/│ ├── installation.md│ └── configuration.md└── api/└── reference.md -
Cycle de Vie : Gestion exclusive via Git. Toute modification de la documentation nécessite une Pull Request et passe par le pipeline CI/CD standard du projet.
4. Modélisation et Schémas des Données (Sanity)
Afin de garantir la robustesse du typage lors de la récupération dans Astro, les schémas Sanity sont définis en Schema-as-Code (TypeScript). Voici la structure complète et enrichie.
4.1. Schéma utilitaire : author
{ name: 'author', title: 'Auteur', type: 'document', fields: [ { name: 'name', title: 'Nom complet', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'name' } }, { name: 'image', title: 'Photo', type: 'image', options: { hotspot: true } }, { name: 'bio', title: 'Biographie', type: 'text' }, { name: 'role', title: 'Rôle / Fonction', type: 'string' }, { name: 'social', title: 'Réseaux sociaux', type: 'object', fields: [ { name: 'twitter', type: 'url' }, { name: 'linkedin', type: 'url' }, { name: 'github', type: 'url' } ]} ]}4.2. Schéma utilitaire : category
{ name: 'category', title: 'Catégorie', type: 'document', fields: [ { name: 'title', title: 'Nom', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }, { name: 'description', title: 'Description', type: 'text' } ]}4.3. Schéma : Blog (post)
{ name: 'post', title: 'Article de blog', type: 'document', fields: [ { name: 'title', title: 'Titre', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' }, validation: Rule => Rule.required() }, { name: 'publishedAt', title: 'Date de publication', type: 'datetime' }, { name: 'status', title: 'Statut', type: 'string', options: { list: [ { title: 'Brouillon', value: 'draft' }, { title: 'Publié', value: 'published' }, { title: 'Archivé', value: 'archived' } ] }, initialValue: 'draft' }, { name: 'mainImage', title: 'Image principale', type: 'image', options: { hotspot: true } }, { name: 'excerpt', title: 'Extrait / Résumé', type: 'text', rows: 3 }, { name: 'body', title: 'Contenu', type: 'array', of: [{ type: 'block' }] }, { name: 'author', title: 'Auteur', type: 'reference', to: [{ type: 'author' }] }, { name: 'categories', title: 'Catégories', type: 'array', of: [{ type: 'reference', to: [{ type: 'category' }] }] }, { name: 'tags', title: 'Tags', type: 'array', of: [{ type: 'string' }] }, { name: 'seo', title: 'SEO', type: 'object', fields: [ { name: 'metaTitle', title: 'Meta Title', type: 'string' }, { name: 'metaDescription', title: 'Meta Description', type: 'text', rows: 3 }, { name: 'ogImage', title: 'Image Open Graph', type: 'image' } ]} ]}4.4. Schéma : Portfolio (project)
{ name: 'project', title: 'Projet Portfolio', type: 'document', fields: [ { name: 'title', title: 'Titre du projet', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }, { name: 'client', title: 'Client', type: 'string' }, { name: 'description', title: 'Description courte', type: 'text' }, { name: 'projectUrl', title: 'URL du projet', type: 'url' }, { name: 'technologies', title: 'Technologies', type: 'array', of: [{ type: 'string' }] }, { name: 'gallery', title: 'Galerie', type: 'array', of: [{ type: 'image', options: { hotspot: true } }] }, { name: 'featured', title: 'Projet mis en avant', type: 'boolean', initialValue: false }, { name: 'publishedAt', title: 'Date', type: 'datetime' }, { name: 'status', title: 'Statut', type: 'string', options: { list: [ { title: 'Brouillon', value: 'draft' }, { title: 'Publié', value: 'published' }, { title: 'Archivé', value: 'archived' } ] }, initialValue: 'draft' }, { name: 'seo', title: 'SEO', type: 'object', fields: [ { name: 'metaTitle', title: 'Meta Title', type: 'string' }, { name: 'metaDescription', title: 'Meta Description', type: 'text' }, { name: 'ogImage', title: 'Image Open Graph', type: 'image' } ]} ]}4.5. Schéma : Vitrine (page)
{ name: 'page', title: 'Page Vitrine', type: 'document', fields: [ { name: 'title', title: 'Titre de la page', type: 'string', validation: Rule => Rule.required() }, { name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' }, validation: Rule => Rule.required() }, { name: 'status', title: 'Statut', type: 'string', options: { list: [ { title: 'Brouillon', value: 'draft' }, { title: 'Publié', value: 'published' } ] }, initialValue: 'draft' }, { name: 'pageBuilder', title: 'Constructeur de page', type: 'array', of: [ { type: 'hero' }, { type: 'features' }, { type: 'testimonials' }, { type: 'cta' }, { type: 'richText' }, { type: 'imageGallery' } ] }, { name: 'seo', title: 'SEO', type: 'object', fields: [ { name: 'metaTitle', title: 'Meta Title', type: 'string' }, { name: 'metaDescription', title: 'Meta Description', type: 'text' }, { name: 'ogImage', title: 'Image Open Graph', type: 'image' } ]} ]}4.6. Schémas des blocs PageBuilder
4.6.1. Bloc hero
{ name: 'hero', title: 'Hero', type: 'object', fields: [ { name: 'heading', title: 'Titre principal', type: 'string' }, { name: 'subheading', title: 'Sous-titre', type: 'text' }, { name: 'backgroundImage', title: 'Image de fond', type: 'image', options: { hotspot: true } }, { name: 'ctaLabel', title: 'Libellé du bouton', type: 'string' }, { name: 'ctaLink', title: 'Lien du bouton', type: 'url' } ]}4.6.2. Bloc features
{ name: 'features', title: 'Fonctionnalités', type: 'object', fields: [ { name: 'title', title: 'Titre de la section', type: 'string' }, { name: 'items', title: 'Liste des fonctionnalités', type: 'array', of: [{ type: 'object', fields: [ { name: 'title', title: 'Titre', type: 'string' }, { name: 'description', title: 'Description', type: 'text' }, { name: 'icon', title: 'Icône (nom ou image)', type: 'string' } ] }]} ]}4.6.3. Bloc testimonials
{ name: 'testimonials', title: 'Témoignages', type: 'object', fields: [ { name: 'title', title: 'Titre de la section', type: 'string' }, { name: 'items', title: 'Témoignages', type: 'array', of: [{ type: 'object', fields: [ { name: 'quote', title: 'Citation', type: 'text' }, { name: 'authorName', title: 'Nom de l’auteur', type: 'string' }, { name: 'authorRole', title: 'Rôle / Entreprise', type: 'string' }, { name: 'authorImage', title: 'Photo', type: 'image' } ] }]} ]}4.6.4. Bloc cta
{ name: 'cta', title: 'Call to Action', type: 'object', fields: [ { name: 'heading', title: 'Titre', type: 'string' }, { name: 'text', title: 'Texte d’accompagnement', type: 'text' }, { name: 'buttonLabel', title: 'Libellé du bouton', type: 'string' }, { name: 'buttonLink', title: 'Lien du bouton', type: 'url' }, { name: 'style', title: 'Style', type: 'string', options: { list: [ { title: 'Primaire', value: 'primary' }, { title: 'Secondaire', value: 'secondary' } ] }} ]}4.6.5. Bloc richText
{ name: 'richText', title: 'Texte riche', type: 'object', fields: [ { name: 'content', title: 'Contenu', type: 'array', of: [{ type: 'block' }] } ]}4.6.6. Bloc imageGallery
{ name: 'imageGallery', title: 'Galerie d’images', type: 'object', fields: [ { name: 'title', title: 'Titre de la galerie', type: 'string' }, { name: 'images', title: 'Images', type: 'array', of: [{ type: 'image', options: { hotspot: true }, fields: [ { name: 'alt', title: 'Texte alternatif', type: 'string' }, { name: 'caption', title: 'Légende', type: 'string' } ] }]} ]}4.7. Stratégie de Preview (Draft Mode)
La prévisualisation des contenus en brouillon (draft) est un besoin critique pour les contributeurs marketing. La stratégie retenue respecte strictement le découplage décidé en Couche 03 : toute la logique d’activation et de validation de la preview est portée par le Middleware Cloudflare indépendant, et non par Astro.
4.7.1. Principes
- Activation via un secret token passé en query parameter depuis Sanity Studio.
- Validation et pose du cookie de preview effectuées exclusivement par le Cloudflare Middleware Worker.
- Astro se contente de détecter la présence du cookie (ou d’un header injecté) et d’adapter le client Sanity en conséquence.
- Aucun contenu draft n’est jamais exposé sans cookie/header valide.
- La documentation Starlight n’est pas concernée (elle reste 100 % Git-based).
4.7.2. Flux de fonctionnement
[Sanity Studio] │ (bouton "Open Preview") ▼URL de preview : https://votresite.com/api/preview?secret=XXX&slug=/blog/mon-article │ ▼[Cloudflare Middleware Worker] │ 1. Valide le secret │ 2. Pose un cookie httpOnly + Secure : `sanity-preview=true` │ 3. (Optionnel) Injecte le header `x-sanity-preview: true` │ 4. Redirige vers la page demandée ▼[Astro – Pages Functions / SSR] │ Détecte le cookie ou le header │ Configure le client Sanity : │ perspective: 'previewDrafts' │ useCdn: false │ token: SANITY_API_READ_TOKEN (lecture drafts) ▼Affichage du contenu draft dans le vrai design du site4.7.3. Règles de sécurité
- Le secret de preview est stocké en variable d’environnement (Cloudflare + Sanity).
- Cookie :
httpOnly,Secure,SameSite=Lax, durée de vie limitée (ex. 1 heure). - Le token d’API utilisé en mode preview possède uniquement les droits de lecture des drafts.
- Une route de sortie (
/api/exit-preview) permet de supprimer le cookie et de revenir en mode publié. - Les webhooks de rebuild ne sont déclenchés que sur l’événement
publish(jamais sur les sauvegardes de draft).
4.7.4. Responsabilités par couche
| Responsabilité | Couche / Composant |
|---|---|
| Validation du secret + pose du cookie | Cloudflare Middleware Worker (L3) |
| Détection du mode preview | Astro (lecture cookie/header) |
| Requêtage des drafts (GROQ) | Astro (client Sanity conditionnel) |
| Rendu de la page | Astro (SSR via Pages Functions) |
4.8. Gestion des Médias (Cloudflare R2 + Stream)
Tous les fichiers médias du projet (images, vidéos, PDF, documents) sont stockés exclusivement sur Cloudflare R2. Sanity ne conserve que les métadonnées et les URLs pointant vers R2. Cette approche garantit la cohérence avec l’écosystème Cloudflare (Couche 03), un contrôle total des coûts et une performance Edge native.
4.8.1. Architecture de stockage
[Upload depuis Sanity Studio] │ ▼[Worker d’upload (Cloudflare)] │ • Réception du fichier │ • Stockage structuré dans R2 │ • Retour de l’URL publique (ou signée) ▼[Sanity] │ Enregistre uniquement les métadonnées + l’URL R2 ▼[Astro / Site] │ Consomme les URLs R2 (images) ou Cloudflare Stream (vidéos)4.8.2. Organisation des chemins R2
medias/├── images/│ ├── blog/│ ├── portfolio/│ └── pages/├── videos/ ← fichiers sources (si besoin)├── documents/│ ├── pdf/│ └── autres/└── temp/ ← uploads temporaires (nettoyage automatique)4.8.3. Traitement par type de média
| Type de média | Solution | Détails |
|---|---|---|
| Images | Cloudflare R2 + CDN | Optimisation possible via Cloudflare Images / Polish. Les métadonnées (alt, hotspot, dimensions) restent gérées dans Sanity. |
| Vidéos | Cloudflare Stream | Upload vers Stream (transcoding, adaptive bitrate, lecteur intégré). L’UID Stream est stocké dans Sanity. |
| PDF & Documents | Cloudflare R2 | Lien direct ou prévisualisation. Miniatures générables via Worker si nécessaire. |
| Autres fichiers | Cloudflare R2 | Stockage générique. |
4.8.4. Politique de nettoyage (Garbage Collection)
- Fichiers orphelins : tout fichier présent dans R2 qui n’est plus référencé par aucun document Sanity depuis 30 jours est considéré comme orphelin.
- Job de nettoyage : Worker planifié (Cron Trigger) exécuté 1 fois par semaine. Il compare le contenu de R2 avec les URLs réellement utilisées dans Sanity (via GROQ) et supprime les orphelins.
- Dossier
temp/: nettoyage automatique tous les 7 jours. - Archivage optionnel : les fichiers supprimés peuvent être déplacés vers un bucket d’archive pendant 90 jours avant suppression définitive.
- Audit : chaque suppression est journalisée (Logpush ou logs Worker).
4.8.5. Sécurité et accès
- Buckets R2 configurés en accès contrôlé.
- URLs publiques pour les médias destinés au site.
- URLs signées (expirantes) pour les documents sensibles si nécessaire.
- Les credentials R2 et Stream sont injectés via
wrangler secret(jamais dans le code source).
4.9. Gouvernance, Rôles, Backup & Alerting
4.9.1. Rôles et permissions dans Sanity Studio
| Rôle | Droits principaux | Public cible |
|---|---|---|
| Administrator | Accès total (schémas, settings, tous les documents, gestion des utilisateurs) | Équipe Architecture / Tech Lead |
| Editor | Création, modification et publication de tous les contenus (posts, projects, pages, authors, categories) | Équipe Marketing / Contenu |
| Contributor | Création et modification de contenus, sans droit de publication (reste en draft) | Rédacteurs externes ou juniors |
| Viewer | Lecture seule | Stakeholders / relecture |
- L’accès au Studio est protégé par authentification obligatoire (OAuth Google / GitHub).
- Seuls les Administrators peuvent modifier les schémas et configurer les webhooks.
4.9.2. Stratégie de Backup
| Élément | Solution | Fréquence / Rétention |
|---|---|---|
| Contenu Sanity (documents) | Export automatisé via l’API Sanity vers un bucket R2 dédié backups/sanity/ |
Quotidien – rétention 30 jours |
| Médias (R2) | Versioning activé sur les buckets R2 | Versioning natif |
| Schémas Sanity | Versionnés dans le dépôt Git (sanity-studio) |
À chaque commit |
| Documentation Starlight | Déjà dans Git | Standard Git |
Un Worker planifié (ou GitHub Action) exécute quotidiennement l’export du dataset production et purge les archives de plus de 30 jours.
4.9.3. Alerting Webhook & Opérations
| Événement | Action |
|---|---|
| Échec du webhook de rebuild (Sanity → hébergeur) | Alerte immédiate |
Taux d’erreur élevé sur cms-webhook-receiver |
Alerte |
Échec du job media-cleanup |
Alerte |
| Échec du job de backup Sanity | Alerte |
Les alertes sont envoyées par email et/ou canal Slack/Teams.
Le Worker cms-webhook-receiver (voir DAT L3) est responsable de détecter les échecs de déclenchement de rebuild et de lever l’alerte.
5. Flux d’Intégration et API1/2
La communication entre la Couche 01 et la Couche 02 s’effectue principalement au moment du Build de l’application (Static Site Generation - SSG).
- Protocole de Requêtage (Sanity) : Utilisation du langage GROQ (Graph-Relational Object Queries) via le client officiel
@sanity/client. - Gestion des Fichiers (Starlight) : Utilisation des
getCollection('docs')natifs d’Astro pour lire et valider les fichiers Markdown locaux via les schémas d’Astro (Zod).
5. Flux d’Intégration et API2/2
- Mise à jour du contenu (Webhooks) :
- Une modification dans Sanity déclenche un webhook HTTP POST vers la plateforme d’hébergement (ex: Vercel, Netlify).
- Ce webhook force le redéploiement et le re-build automatique du site Astro pour intégrer le nouveau contenu.
- Recherche unifiée : Le contenu Sanity (une fois rendu en HTML) est indexé avec la documentation Starlight par Pagefind lors du build (voir DAT L2 §8).
6. Sécurité et Performances
6.1. Sécurité
- Jetons d’accès (Tokens) : Les requêtes GROQ en lecture lors du build utilisent un token d’API en lecture seule, injecté comme variable d’environnement (
SANITY_API_READ_TOKEN). L’accès public à l’API du Content Lake est désactivé. - Authentification Studio : L’accès à l’interface d’édition Sanity Studio est restreint via une authentification obligatoire (OAuth2 / Google / GitHub) configurée sur le projet Sanity.
6.2. Performances
- Mise en cache (Caching) : Le contenu étant massivement statique (généré au build), l’impact de la latence de l’API Sanity est nul pour l’utilisateur final.
- Optimisation CDN : Les médias sont servis exclusivement depuis Cloudflare R2 (images et documents) et Cloudflare Stream (vidéos), avec cache Edge agressif et optimisations natives (Polish / Cloudflare Images si activé).