Mon premier article de blog

Problématique :
La plateforme ne dispose d’aucun canal de content marketing. Un blog permettrait à chaque tenant de publier des articles multilingues optimisés pour le SEO, rédigés dans Strapi CMS et rendus dans l’application SvelteKit — pour générer du trafic organique, asseoir l’autorité de marque et soutenir le pipeline i18n existant de 13 locales.
Critères de réussite
- Les articles de blog rédigés dans Strapi sont affichés sur le frontend en respectant l’identité de marque du tenant
- Les articles sont traduits automatiquement dans les 13 locales via le pipeline de webhook existant (aucune modification de l’infrastructure de traduction)
- Les pages de blog sont bien référencées dans les moteurs de recherche (données structurées, sitemap, hreflang, og:article)
- Les tenants sans le feature flag
blogne voient aucune route de blog (404)
User Stories
- En tant que créateur de contenu, je veux rédiger des articles de blog dans Strapi avec du markdown enrichi, des images et des vidéos intégrées afin de pouvoir publier du contenu sans toucher au code
- En tant que visiteur du site, je veux parcourir les articles de blog filtrés par catégorie afin de trouver du contenu pertinent
- En tant que visiteur du site, je veux une table des matières sur les longs articles afin de pouvoir naviguer rapidement
- En tant que visiteur du site, je veux lire les articles dans ma langue, avec une traduction automatique depuis l’anglais
- En tant que visiteur du site, je veux partager les articles sur les réseaux sociaux
- En tant que visiteur du site, je veux découvrir des articles similaires et en savoir plus sur l’auteur
- En tant que responsable SEO, je veux que chaque article dispose de balises meta correctes, de données structurées et d’entrées dans le sitemap
Exigences fonctionnelles
Indispensable (MVP)
- Types de contenu Strapi :
blog-posts,blog-authors,blog-categories— tous avec les champs tenant, locale, inherit - Page de liste de blog (
/blog) — grille paginée d’articles, filtre par catégorie via paramètre de requête - Page de détail d’un article (
/blog/[slug]) — rendu markdown complet avec sommaire, carte auteur, articles liés, partage social - Page catégorie (
/blog/category/[slug]) — liste filtrée réutilisant les mêmes composants de listing - Page auteur (
/blog/author/[slug]) — bio de l’auteur + ses articles - Table des matières — générée automatiquement à partir des titres h2/h3, barre latérale fixe sur desktop, repliable sur mobile, mise en évidence du titre actif via Intersection Observer
- Intégrations YouTube/Vimeo — les URLs brutes dans le markdown sont automatiquement converties en iframes responsives (youtube-nocookie.com pour la confidentialité)
- Temps de lecture — calculé côté serveur à partir du nombre de mots (Math.ceil(words / 200))
- Boutons de partage social — X, Facebook, LinkedIn, e‑mail, copie du lien (Web Share API lorsque disponible)
- Articles liés — même catégorie, exclure l’article courant, limite 3, triés par publishedAt desc
- Flux RSS (
/blog/rss.xml) — RSS 2.0, locale via paramètre de requête - SEO — SeoComponent par article, balises og:article, schéma JSON-LD BlogPosting, URLs canoniques
- Sitemap — URLs dynamiques des articles / catégories / auteurs ajoutées au sitemap existant avec hreflang
- Feature flag —
blog: booleandans TenantConfig.features, garde de layout renvoyant une 404 quand désactivé - i18n — clés de messages Paraglide pour toutes les chaînes UI (navigation, libellés, pagination), contenu Strapi auto‑traduit via le webhook existant
- Pagination — basée sur les pages en utilisant la pagination native de Strapi (9 articles par page)
- Article mis en avant — booléen
featuredsur les blog-posts ; l’article mis en avant est affiché comme carte « hero » en haut de la page de liste/blog(carte plus grande, pleine largeur, au‑dessus de la grille)
Optionnel (post-MVP)
- Recherche en texte intégral sur les articles du blog
- Séries d’articles / articles en plusieurs parties
- Barre de progression de lecture
- Commentaires (probablement via une intégration tierce)
- CTA d’inscription à la newsletter dans les articles
- Compteur de vues / intégration analytics
Exigences techniques
Modèle de données (Types de contenu Strapi)
blog-posts (api::blog-post.blog-post) :
| Champ | Type | Localisé | Notes |
|---|---|---|---|
| title | Texte (obligatoire) | Oui | Mis en correspondance par la config de traduction |
| slug | UID (à partir de title) | Non | Exclu de la traduction |
| excerpt | Texte | Oui | Pour les cartes + meta description |
| body | Rich Text (Markdown) | Oui | Rendu via marked |
| featuredImage | Média (simple) | Non | URL + alternativeText |
| category | Relation (manyToOne -> blog-category) | Non | |
| author | Relation (manyToOne -> blog-author) | Non | |
| featured | Booléen (false par défaut) | Non | Article mis en avant affiché comme carte héro sur la liste |
| seo | Composant (shared.seo) | Oui | Réutilise le SeoComponent existant |
| tenant | Texte | Non | Filtre tenant standard |
| inherit | Booléen (true par défaut) | Non | Indicateur d’auto‑traduction |
blog-authors (api::blog-author.blog-author) :
| Champ | Type | Localisé | Notes |
|---|---|---|---|
| name | Texte (obligatoire) | Oui | |
| slug | UID (à partir de name) | Non | |
| bio | Rich Text (Markdown) | Oui | |
| avatar | Média (simple) | Non | |
| socialLinks | JSON | Non | { twitter?, linkedin?, website? } |
| tenant | Texte | Non | |
| inherit | Booléen (true par défaut) | Non |
blog-categories (api::blog-category.blog-category) :
| Champ | Type | Localisé | Notes |
|---|---|---|---|
| name | Texte (obligatoire) | Oui | |
| slug | UID (à partir de name) | Non | |
| description | Texte | Oui | |
| color | Texte | Non | Couleur hex pour les badges UI |
| tenant | Texte | Non | |
| inherit | Booléen (true par défaut) | Non |
Interfaces TypeScript
Voir src/lib/services/strapi-types.ts pour BlogPost, BlogAuthor, BlogCategory.
Routes
Toutes sous src/routes/(marketing)/blog/ :
| Route | Server Load | Objectif |
|---|---|---|
blog/+layout.server.ts |
Feature guard + chargement des catégories | Porte d’accès + données de navigation |
blog/+page.server.ts |
Articles paginés, filtre ?category= optionnel |
Listing |
blog/[slug]/+page.server.ts |
Article par slug + articles associés + calcul du temps de lecture | Détail |
blog/category/[slug]/+page.server.ts |
Articles filtrés par catégorie | Listing par catégorie |
blog/author/[slug]/+page.server.ts |
Auteur + ses articles | Page auteur |
blog/rss.xml/+server.ts |
XML RSS 2.0, paramètre ?locale= |
Flux |
Composants UI
Tous dans src/lib/components/blog/ :
| Composant | Props | Notes |
|---|---|---|
BlogPostCard.svelte |
post: BlogPost |
Carte avec image, badge de catégorie, titre, extrait, auteur, date, temps de lecture |
BlogPostContent.svelte |
body: string |
Markdown vers HTML via marked avec extension d’intégration vidéo + IDs de titres |
TableOfContents.svelte |
entries: TocEntry[] |
Barre latérale fixe, Intersection Observer pour l’état actif, repliable sur mobile |
AuthorCard.svelte |
author: BlogAuthor |
Avatar, nom, bio, liens sociaux |
SocialShare.svelte |
url: string, title: string |
Boutons de partage avec repli via Web Share API |
RelatedPosts.svelte |
posts: BlogPost[] |
Grille de BlogPostCard |
BlogPagination.svelte |
pagination: StrapiPagination, basePath: string |
Précédent/suivant + numéros de page |
Sécurité & Authentification
- Le blog est public (aucune authentification requise) — contenu marketing
- Accès en lecture à l’API Strapi via le jeton
STRAPI_JWTexistant - Les intégrations vidéo utilisent
youtube-nocookie.compour la conformité RGPD
Cas limites & gestion des erreurs
- Article introuvable : 404 via SvelteKit
error() - Aucun article dans la catégorie : état vide avec message
blog_no_posts - Image à la une manquante : la carte s’affiche sans image
- Traduction non disponible :
loadContentWithFallback()revient par défaut à l’anglais - Langues RTL : la mise en page respecte
dir="rtl"(ar, he, ur)
Plan de test E2E
Les fichiers de tests se trouvent dans tests/blog/. Suivre les modèles existants : paramétrés selon les viewports, Axe pour l’accessibilité (a11y), networkidle attend que le contenu Strapi soit prêt.
Tests du parcours critique
- Chargement de la liste des articles —
/blogaffiche une grille de cartes d’articles avec titre, extrait, badge de catégorie, auteur, date et temps de lecture - Pagination fonctionnelle —
/blog?page=2affiche la deuxième page de résultats ; les boutons précédent/suivant naviguent correctement ; la page 1 n’a pas de bouton « Précédent » - Filtre par catégorie — cliquer sur un chip de catégorie filtre les articles ; l’URL est mise à jour vers
?category=<slug>; le chip « Tous » supprime le filtre - Affichage du détail d’un article —
/blog/[slug]affiche le titre, l’image à la une, le corps mis en forme, la carte auteur, le temps de lecture et la date de publication - Table des matières — la page de détail d’un article génère une table des matières à partir des titres h2/h3 ; cliquer sur une entrée de la table des matières fait défiler la page jusqu’au titre correspondant
- Articles connexes — la page de détail affiche jusqu’à 3 articles connexes de la même catégorie (en excluant l’article courant)
- Page catégorie —
/blog/category/[slug]affiche le nom de la catégorie, sa description et uniquement les articles de cette catégorie - Page auteur —
/blog/author/[slug]affiche la biographie de l’auteur, son avatar, ses liens sociaux et ses articles - Boutons de partage social — les boutons de partage sont visibles sur la page de détail ; « Copier le lien » copie l’URL et affiche un texte de confirmation
- Flux RSS —
GET /blog/rss.xmlrenvoie un XML valide avecContent-Type: application/rss+xmlet contient des entrées d’articles - Navigation — le lien « Blog » dans le menu du site mène à
/blog; le fil d’Ariane affiche la bonne hiérarchie - Article mis en avant —
/blogaffiche l’article en vedette sous forme de grande carte « hero » au-dessus de la grille standard
Tests de cas limites et d’erreurs
- Article introuvable —
/blog/nonexistent-slugrenvoie une page 404 - Catégorie vide — une catégorie sans articles affiche le message d’état vide
blog_no_posts - Image à la une manquante — la carte d’article s’affiche correctement sans image (aucune
<img>cassée) - Auteur/catégorie manquant·e — un article sans auteur ou sans catégorie s’affiche simplement sans ces éléments (sans crash)
- Feature flag désactivé — lorsque
blog: false,/blogrenvoie une 404
Tests transverses
- i18n : changement de langue — naviguer vers
/fr/blog, vérifier que les chaînes d’interface (titre de page, « Lire la suite », libellés de pagination) sont en français ; le contenu de l’article revient à l’anglais s’il n’est pas traduit - i18n : mise en page RTL — naviguer vers
/ar/bloget/ar/blog/[slug], vérifier quedir="rtl"est défini et que la mise en page s’affiche correctement (table des matières du bon côté) - SEO : balises meta — la page de détail d’un article contient
og:type=article,og:title,og:description,article:published_timeet un lien canonique - SEO : JSON-LD — la page de détail d’un article contient un
<script type="application/ld+json">avec@type: BlogPosting, le titre, l’auteur et la datePublished - SEO : sitemap —
/sitemap.xmlinclut les URL des articles de blog avec les alternances hreflang - Accessibilité — lancer des audits Axe sur
/blog,/blog/[slug],/blog/author/[slug]pour les viewports mobile et desktop (en suivant le modèle de tests a11y existant) - Navigation clavier — les entrées de la table des matières et les boutons de pagination sont focalisables et actionnables au clavier ; les indicateurs de focus sont visibles
Hors périmètre
- Commentaires sur les articles de blog
- Recherche en texte intégral
- Intégration de newsletters au sein des articles
- Analyses / nombre de vues
Ludo est un enseignant et entrepreneur dans le domaine de la tech, animé par une conviction simple : une éducation de qualité devrait être accessible à tous.