Ce que vous saurez faire
À la fin de ce tutoriel, vous serez en mesure de concevoir et déployer une architecture Headless CMS moderne pour une PME sénégalaise (média, e-commerce, ONG, agence) qui sépare clairement le back-office de gestion de contenu (Strapi, Directus, Payload ou Sanity) du front-end de présentation (Next.js, Astro ou Nuxt). Vous saurez choisir le bon CMS headless selon le budget (gratuit auto-hébergé à 25 000 FCFA/mois en SaaS), modéliser des Content Types souples, exposer une API REST et GraphQL, gérer la diffusion de contenu multilingue (français, wolof, anglais) et le stockage des médias sur un bucket S3 compatible (Backblaze B2 ou Wasabi à 4 USD/To). Vous mettrez en place le rendu statique incrémental (ISR) avec revalidation, le caching CDN via Cloudflare, et un workflow éditorial avec rôles (rédacteur, relecteur, publication). À la fin, votre site charge en moins de 1 seconde sur une connexion 3G dakaroise et tient 100 000 visiteurs/mois pour moins de 18 000 FCFA.
Étape 1 : Comprendre la différence avec un CMS classique
Un CMS traditionnel (WordPress monolithique) couple le contenu, la base de données et la présentation. Un Headless CMS expose UNIQUEMENT une API :
WordPress classique : Admin -> DB -> Theme PHP -> HTML servi
Headless CMS : Admin -> DB -> API JSON -> Frontend separe (Next.js, mobile, etc.)
Avantages : un même contenu nourrit le site web, l'app mobile, un écran d'affichage en agence, et un export newsletter. Inconvénient : plus complexe à mettre en place pour un éditeur seul.
Étape 2 : Choisir le bon CMS headless
Comparatif honnête pour une PME sénégalaise :
CMS | Open Source | Hebergement | Cout/mois | Difficulte
----------|-------------|-----------------|--------------|------------
Strapi | Oui | Auto ou Cloud | 0 - 60 USD | Moyenne
Directus | Oui | Auto ou Cloud | 0 - 25 USD | Facile
Payload | Oui | Auto-heberge | 0 | Moyenne
Sanity | Non (free) | SaaS uniquement | 0 - 99 USD | Facile
Contentful| Non | SaaS | 489 USD | Tres facile
Recommandation : Directus si vous voulez du SQL natif (PostgreSQL existant), Strapi si vous voulez une grosse communauté et plugins, Payload si TypeScript first.
Étape 3 : Installer Strapi en local
Prérequis : Node.js 20+, PostgreSQL 14+. Lancez la commande :
npx create-strapi-app@latest mon-cms --quickstart
cd mon-cms
npm run develop
L'interface admin s'ouvre sur http://localhost:1337/admin. Créez votre compte super-admin avec un mot de passe fort (12+ caractères).
Étape 4 : Modéliser les Content Types
Pour un média en ligne sénégalais, créez via Content-Type Builder :
Article
- title (Text, requis)
- slug (UID base sur title)
- cover (Media, single)
- body (Rich Text - Markdown)
- excerpt (Text long, max 250)
- category (Relation -> Category)
- tags (Relation many -> Tag)
- publishedAt (Date)
- locale (fr, wo, en)
Author
- name, photo, bio, role
- articles (Relation reverse)
Activez la publication brouillon/publié et l'internationalisation (i18n) dans les paramètres.
Étape 5 : Configurer les permissions API
Dans Settings > Users & Permissions > Roles > Public :
- Article : find, findOne (autorise)
- Article : create, update, delete (REFUSE)
- Author : find, findOne
Pour le frontend authentifié (admin), créez un rôle "Editor" avec accès complet, puis générez un API Token longue durée dans Settings > API Tokens.
Étape 6 : Tester l'API REST
Vérifiez que l'API répond :
curl http://localhost:1337/api/articles?populate=cover,author
curl http://localhost:1337/api/articles/1?populate=*
curl "http://localhost:1337/api/articles?filters[category][slug][$eq]=politique&sort=publishedAt:desc&pagination[pageSize]=10"
Strapi expose automatiquement aussi GraphQL si vous installez le plugin :
npm install @strapi/plugin-graphql
Étape 7 : Stocker les médias sur S3 compatible
Le filesystem local est insuffisant en production. Configurez Backblaze B2 :
npm install @strapi/provider-upload-aws-s3
Dans config/plugins.js :
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
accessKeyId: env('B2_KEY_ID'),
secretAccessKey: env('B2_APP_KEY'),
endpoint: env('B2_ENDPOINT'),
params: { Bucket: env('B2_BUCKET') }
}
}
}
});
Étape 8 : Créer le frontend Next.js
Dans un nouveau dossier :
npx create-next-app@latest site-public
cd site-public
npm install
Créez un client API simple lib/cms.js :
const API = process.env.CMS_URL;
export async function getArticles(locale = 'fr') {
const r = await fetch(
`${API}/api/articles?locale=${locale}&populate=cover&sort=publishedAt:desc`,
{ next: { revalidate: 60 } }
);
const json = await r.json();
return json.data;
}
Étape 9 : Rendu statique incrémental (ISR)
Dans app/articles/[slug]/page.js :
export const revalidate = 300; // re-genere apres 5 minutes
export async function generateStaticParams() {
const articles = await getArticles();
return articles.map(a => ({ slug: a.attributes.slug }));
}
export default async function Article({ params }) {
const article = await getArticleBySlug(params.slug);
return (
<article>
<h1>{article.attributes.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.attributes.body }} />
</article>
);
}
Étape 10 : Webhooks pour publication instantanée
Au lieu d'attendre la revalidation de 5 minutes, déclenchez la rebuild dès la publication. Dans Strapi Settings > Webhooks :
URL : https://site-public.sn/api/revalidate?secret=XXX
Events : entry.publish, entry.unpublish
Côté Next.js dans app/api/revalidate/route.js :
import { revalidatePath } from 'next/cache';
export async function POST(request) {
const { searchParams } = new URL(request.url);
if (searchParams.get('secret') !== process.env.REVAL_SECRET) {
return Response.json({ error: 'invalid' }, { status: 401 });
}
revalidatePath('/');
revalidatePath('/articles/[slug]', 'page');
return Response.json({ revalidated: true });
}
Étape 11 : Multilingue (français, wolof, anglais)
Activez i18n dans Strapi puis dans Next.js, créez la structure de routes :
app/
[locale]/
page.js
articles/
[slug]/page.js
middleware.js // detecte la langue depuis Accept-Language
Pour le wolof, ajoutez le code wo dans Strapi (locale custom). N'oubliez pas les balises hreflang SEO :
<link rel="alternate" hreflang="fr" href="https://media.sn/fr/article" />
<link rel="alternate" hreflang="wo" href="https://media.sn/wo/article" />
Étape 12 : Workflow éditorial
Dans Strapi Enterprise (ou via plugin gratuit pour communauté), configurez les rôles :
Redacteur : peut creer/editer SES articles, statut DRAFT
Relecteur : peut relire, demander corrections, statut READY
Editeur : peut publier (statut PUBLISHED)
Admin : tout
Ajoutez un champ editorial_status et utilisez les hooks Strapi pour notifier par email à chaque changement.
Étape 13 : Performance et CDN
Mettez Cloudflare devant tout :
1. Domaine -> Cloudflare DNS (gratuit)
2. Cache niveau Standard
3. Page Rule : *.media.sn/api/* -> Bypass Cache
4. Page Rule : *.media.sn/* -> Cache Everything (1h)
5. Auto Minify : HTML, CSS, JS
6. Brotli : ON
7. Polish (images) : Lossless
Mesurez avec WebPageTest depuis un noeud africain : objectif Time To First Byte < 400ms, LCP < 2.5s.
Étape 14 : Déploiement et coûts en FCFA
Architecture finale recommandée pour 100 000 visiteurs/mois :
Composant | Service | Cout mensuel
------------------|------------------|-------------
Strapi (back) | VPS Contabo 4Go | 6 000 FCFA
PostgreSQL | Inclus VPS | 0
Frontend Next.js | Vercel Hobby | 0 (gratuit)
Stockage medias | Backblaze B2 | 3 600 FCFA (1To)
CDN | Cloudflare Free | 0
Domaine .sn | Annuel | 1 250 FCFA/mois
TOTAL | | ~12 000 FCFA
Comparaison : un Contentful équivalent coûterait 489 USD/mois soit 295 000 FCFA. Économie : 96 % avec un setup auto-hébergé maîtrisé.
Erreurs
Erreur 1 : Modèle de données trop rigide. Si vous mettez 50 champs obligatoires, vos rédacteurs ne publieront jamais. Démarrez minimaliste, ajoutez au besoin.
Erreur 2 : Oublier la pagination. Charger 5000 articles d'un coup tue le serveur. Toujours paginer (10-20 par page).
Erreur 3 : Médias non optimisés. Une photo de 8 Mo en première page = 30 secondes de chargement en 3G. Imposez WebP/AVIF avec compression.
Erreur 4 : API exposée publiquement avec écriture. Vérifiez TROIS FOIS que les permissions Public sont en lecture seule.
Erreur 5 : Pas de cache HTTP. Sans Cache-Control headers, votre CMS encaisse tous les visiteurs au lieu du CDN.
Erreur 6 : Backup oublié. Sauvegardez la base PostgreSQL ET le bucket S3 séparément. Une suppression accidentelle de média n'est pas dans la base.
Erreur 7 : Vendor lock-in SaaS. Si vous démarrez avec Contentful, exporter 100 000 entrées vers un autre CMS est un projet de 2 mois. Préférez open source.
Checklist
- CMS headless choisi selon budget et compétences
- Strapi/Directus installé avec PostgreSQL
- Content Types modélisés simplement
- Permissions Public en lecture seule uniquement
- API Tokens créés pour chaque consommateur
- Médias stockés sur S3 compatible (B2 ou Wasabi)
- Frontend Next.js avec ISR et revalidate < 5 min
- Webhooks de publication configurés
- i18n actif pour fr/wo/en avec hreflang
- Workflow éditorial avec rôles définis
- Cloudflare CDN en frontal
- WebPageTest validé : LCP < 2.5s en 3G Dakar
- Backup automatisé base + médias
- Coût mensuel total < 18 000 FCFA confirmé