📍 Article principal de la série : Directus 2026 : guide pratique.
Backend Directus prêt, frontend à connecter. Ce tutoriel détaille l’intégration dans Next.js 15 (App Router) et Astro 4, avec SDK TypeScript officiel, types auto-générés, SSG/ISR pour performance maximale.
Prérequis
- Directus en production avec collections + données.
- Next.js 15 ou Astro 4.
- API key Directus créée pour le frontend.
- Niveau attendu : intermédiaire.
- Temps estimé : 1-2 heures.
Setup Next.js 15
Installation
npm install @directus/sdk
# Pour types auto : npx directus-sdk-typegen
Client Directus
// lib/directus.ts
import { createDirectus, rest, staticToken } from '@directus/sdk';
import type { Schema } from '@/types/directus';
export const directus = createDirectus<Schema>(process.env.NEXT_PUBLIC_DIRECTUS_URL!)
.with(rest())
.with(staticToken(process.env.DIRECTUS_TOKEN!));
Server Component liste produits
// app/produits/page.tsx
import { directus } from '@/lib/directus';
import { readItems } from '@directus/sdk';
export const revalidate = 60; // ISR 60s
export default async function ProductsPage() {
const products = await directus.request(readItems('products', {
fields: ['*', 'category.name', 'images.*', 'translations.*'],
filter: { status: { _eq: 'published' } },
sort: ['-date_created'],
limit: 24,
}));
return (
<div className="grid grid-cols-3 gap-6">
{products.map(p => (
<article key={p.id}>
<img src={`${process.env.NEXT_PUBLIC_DIRECTUS_URL}/assets/${p.images[0]?.id}?width=400`} />
<h3>{p.name}</h3>
<p>{p.price.toLocaleString()} {p.currency}</p>
</article>
))}
</div>
);
}
Page produit dynamique
// app/produits/[slug]/page.tsx
export async function generateStaticParams() {
const products = await directus.request(readItems('products', {
fields: ['slug'],
filter: { status: { _eq: 'published' } },
}));
return products.map(p => ({ slug: p.slug }));
}
export default async function ProductPage({ params }: { params: { slug: string } }) {
const = await directus.request(readItems('products', {
fields: ['*', 'category.*', 'images.*', 'variants.*', 'reviews.*'],
filter: { slug: { _eq: params.slug } },
limit: 1,
}));
if (!product) notFound();
return <ProductDetail product={product} />;
}
Webhook ISR Revalidate
Configurer dans Directus Settings → Webhooks → Add :
- URL :
https://votre-site.com/api/revalidate?secret=... - Triggers : items.update, items.create.
- Collections : products.
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(req: Request) {
const { searchParams } = new URL(req.url);
if (searchParams.get('secret') !== process.env.REVALIDATE_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
revalidatePath('/produits');
return Response.json({ revalidated: true });
}
Setup Astro 4
Installation
npm install @directus/sdk
Client Directus pour Astro
// src/lib/directus.ts
import { createDirectus, rest, staticToken } from '@directus/sdk';
export const directus = createDirectus(import.meta.env.DIRECTUS_URL)
.with(rest())
.with(staticToken(import.meta.env.DIRECTUS_TOKEN));
Page liste articles
---
// src/pages/blog/index.astro
import { directus } from '@/lib/directus';
import { readItems } from '@directus/sdk';
const articles = await directus.request(readItems('articles', {
fields: ['*', 'author.name', 'cover_image.*'],
filter: { status: { _eq: 'published' } },
sort: ['-date_created'],
}));
---
<Layout>
{articles.map(a => (
<article>
<a href={`/blog/${a.slug}`}>{a.title}</a>
<p>par {a.author.name}</p>
</article>
))}
</Layout>
Page article dynamique
---
// src/pages/blog/[slug].astro
import { directus } from '@/lib/directus';
import { readItems } from '@directus/sdk';
export async function getStaticPaths() {
const articles = await directus.request(readItems('articles', { fields: ['slug'] }));
return articles.map(a => ({ params: { slug: a.slug } }));
}
const { slug } = Astro.params;
const [article] = await directus.request(readItems('articles', {
filter: { slug: { _eq: slug } },
fields: ['*', 'author.*', 'cover_image.*'],
limit: 1,
}));
---
<article set:html={article.content} />
Webhook rebuild Astro
Astro étant statique, redéploiement nécessaire à chaque update. Webhook Directus → Vercel/Netlify Build Hook.
Performance et caching
CDN Cloudflare devant
Mettre Cloudflare en façade frontend + Directus. Cache assets Directus (images) avec Cache-Control 1 an. API responses cache via SWR Next.js ou Astro static.
Image transformations
// Auto resize/compress côté Directus
<img src="https://cms.../assets/IMG_ID?width=400&height=300&fit=cover&format=webp" />
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| API 401 | Token non valide | Régénérer token |
| Relations vides | fields= manque populate | fields: ['*', 'relation.*'] |
| Images CORS | CORS Directus pas configuré | CORS_ENABLED=true + CORS_ORIGIN |
| ISR ne marche pas | revalidate manquant | export const revalidate = 60 |
| Build Astro lent | Trop de pages générées | Limiter ou pagination |
| Types TypeScript erreurs | Pas de Schema généré | npx directus-sdk-typegen |
Au-delà du standard occidental : adaptations locales
Trois précisions. CDN Cloudflare gratuit : essentiel pour servir images Directus avec faible latence depuis Dakar/Abidjan. WebP + AVIF : Directus génère auto, gain 50% taille images, crucial mobile 4G. SSG vs SSR : Astro SSG pour blog + Next.js ISR pour e-commerce dynamique.
Tutoriels frères
FAQ
GraphQL ou REST ? REST plus simple. GraphQL pour relations complexes uniquement.
SDK obligatoire ? Non, fetch standard fonctionne aussi. SDK fournit types + helpers.
Real-time updates frontend ? WebSocket Directus + Next.js Suspense pour live data.
SEO sur Astro ? SSG = HTML statique, parfait SEO. Schema.org markup recommandé.
Coût Vercel + Directus ? Vercel gratuit jusqu’à 100 Go/mois bandwidth. Directus 4,51 €. Total < 5 €/mois.
Pour étoffer le tableau
- 🔝 Retour au guide général : guide pratique Directus 2026
- SDK : docs.directus.io/guides/sdk
Étape 1 — Provisionner Directus en mode headless production
Avant de connecter un frontend, il faut un Directus 11 stable avec PostgreSQL 15 et Redis pour le cache des permissions. Sur un VPS Hetzner CX22 à Falkenstein (4 EUR/mois, 2 624 FCFA), déployez Directus avec un docker-compose explicite plutôt que la commande npx, qui n’est pas adaptée à la production.
# docker-compose.yml extrait
services:
database:
image: postgres:15-alpine
environment:
POSTGRES_DB: directus
POSTGRES_USER: directus
POSTGRES_PASSWORD: ${PG_PASS}
cache:
image: redis:7-alpine
directus:
image: directus/directus:11
environment:
KEY: ${DIRECTUS_KEY}
SECRET: ${DIRECTUS_SECRET}
ADMIN_EMAIL: admin@itskillscenter.io
ADMIN_PASSWORD: ${ADMIN_PASS}
DB_CLIENT: pg
DB_HOST: database
CACHE_ENABLED: true
CACHE_STORE: redis
REDIS: redis://cache:6379
Lancez avec docker compose up -d. Indicateur que tout est en place : https://cms.itskillscenter.io/admin affiche le formulaire de login et l’API REST /server/health renvoie un JSON contenant status: ok avec uptime non nul.
Étape 2 — Modéliser une collection articles avec relations
Ouvrez Settings → Data Model → Create Collection et créez la collection articles. Évitez de tout mettre dans un blob JSON : Directus excelle quand chaque champ est typé. Cela permet aux dev frontend d’avoir un typage TypeScript propre généré automatiquement.
Champs articles :
- id (uuid, primary)
- title (string, required, 200)
- slug (string, unique, lowercase)
- excerpt (text, 320 chars)
- body (richtext markdown)
- cover (file relation, image)
- author (M2O vers directus_users)
- category (M2O vers categories)
- tags (M2M vers tags)
- status (string, choices: draft|published|archived)
- published_at (timestamp)
La preuve que ça tourne : la création d’un article via l’admin Directus produit un payload JSON propre via GET /items/articles?fields=*,author.first_name,category.name,tags.tags_id.name, prêt à être consommé par n’importe quel frontend.
Étape 3 — Configurer les permissions publiques fines
Par défaut, Directus refuse tout accès non authentifié. Pour un blog public consommé par Next.js ou Astro, créez un rôle Public avec lecture seule sur articles filtrée sur status = published. Ne donnez jamais l’accès All sur la collection : c’est l’erreur classique qui expose les brouillons.
# Settings → Roles & Permissions → Public
Collection: articles
Action: Read
Fields: id, title, slug, excerpt, body, cover, author.first_name,
category.name, tags.tags_id.name, published_at
Filter: { status: { _eq: "published" } }
Le marqueur de succès : curl https://cms.itskillscenter.io/items/articles sans token retourne uniquement les articles publiés et n’expose ni les brouillons ni les emails d’auteurs. Tous les autres endpoints (users, files privés) répondent 403.
Étape 4 — Brancher Next.js 15 App Router avec fetch et SSG
Sur un projet Next.js 15 (Node 22 LTS), créez un client REST minimal et utilisez la cache fetch native. Pour un blog peu fréquemment mis à jour, ISR avec revalidate de 60 secondes donne le meilleur ratio fraîcheur/coût.
// lib/directus.ts
const BASE = process.env.DIRECTUS_URL!;
export async function getArticles() {
const r = await fetch(`${BASE}/items/articles?fields=id,title,slug,excerpt,published_at&sort=-published_at`, {
next: { revalidate: 60 }
});
if (!r.ok) throw new Error('Directus fetch failed');
return (await r.json()).data;
}
// app/blog/page.tsx
import { getArticles } from '@/lib/directus';
export default async function Blog() {
const articles = await getArticles();
return <ul>{articles.map(a => <li key={a.id}><a href={`/blog/${a.slug}`}>{a.title}</a></li>)}</ul>;
}
Comment vérifier le bon fonctionnement : npm run dev ouvre http://localhost:3000/blog avec la liste des articles, et un changement de titre dans Directus est visible sous 60 secondes sans rebuild manuel grâce à l’ISR.
Étape 5 — Brancher Astro 5 avec content collections
Astro 5 favorise une approche différente : on précharge le contenu au build via une content collection externe. C’est plus rapide à servir mais nécessite un rebuild à chaque publication. Pour un blog itskillscenter.io qui publie 2 fois par jour, déclenchez le rebuild via webhook Directus vers Vercel ou Netlify.
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
const articles = defineCollection({
loader: async () => {
const r = await fetch(`${import.meta.env.DIRECTUS_URL}/items/articles?fields=*&limit=-1`);
return (await r.json()).data;
},
schema: z.object({
id: z.string(),
title: z.string(),
slug: z.string(),
excerpt: z.string(),
body: z.string(),
published_at: z.string()
})
});
export const collections = { articles };
Validation pratique : npm run build génère un dossier dist/blog/ avec un fichier HTML par slug, et le score Lighthouse en mobile dépasse 95 sur la page d’accueil. Pour Astro, l’optimisation d’images se fait via <Image> qui transforme les uploads Directus en WebP responsive.
Étape 6 — Sécuriser le webhook de revalidation côté frontend
Pour rebuild automatiquement quand un article est publié, Directus émet un webhook. Ce webhook doit pointer vers une route protégée du frontend, sinon n’importe qui peut déclencher des rebuilds et faire monter votre facture Vercel ou Netlify.
// app/api/revalidate/route.ts (Next.js)
import { revalidatePath } from 'next/cache';
export async function POST(req: Request) {
const secret = req.headers.get('x-directus-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
revalidatePath('/blog');
return Response.json({ revalidated: true });
}
// Côté Directus → Settings → Webhooks
URL: https://itskillscenter.io/api/revalidate
Method: POST
Headers: x-directus-secret = <secret-partagé>
Triggers: items.update on articles, items.create on articles
Validation pratique : publier un article dans Directus déclenche le webhook, le frontend invalide la page /blog et la nouvelle version apparaît sous 5 secondes pour les visiteurs à Dakar.
Étape 7 — Servir les images Directus via CDN avec transformation
Servir une image 4K via le VPS Directus à Falkenstein vers un mobile à Bouaké en 4G coûte 2 à 3 secondes de chargement. Directus expose un endpoint /assets/<id>?width=800&format=webp&quality=80 qui transforme à la volée. Cachez ce endpoint via Cloudflare gratuit pour réduire la latence à moins de 200 ms partout en Afrique de l’Ouest.
# Cloudflare Page Rule
URL: https://cms.itskillscenter.io/assets/*
Cache Level: Cache Everything
Edge Cache TTL: 1 month
# Frontend Next.js Image
<Image src={`https://cms.itskillscenter.io/assets/${article.cover}?width=1200&format=webp&quality=85`}
alt={article.title} width={1200} height={630} />
Comment vérifier le bon fonctionnement : un test WebPageTest depuis Lagos affiche un LCP inférieur à 1,5 seconde sur la page d’accueil du blog, et le hit ratio Cloudflare sur /assets/* dépasse 90 % après 24h.
Étape 8 — Mettre en production et auditer
Avant le go-live, lancez un audit complet. Vérifiez que les permissions publiques n’exposent que ce qui est attendu, que le rate limit Directus est activé (variable RATE_LIMITER_ENABLED=true) et que les sauvegardes PostgreSQL tournent quotidiennement vers Scaleway Object Storage Paris pour rester en conformité RGPD.
# Audit minimal
curl -I https://cms.itskillscenter.io/items/articles
# 200 OK
curl -I https://cms.itskillscenter.io/users
# 403 Forbidden (correct, jamais accessible publiquement)
curl -I https://cms.itskillscenter.io/items/articles?filter[status][_eq]=draft
# Retour vide ou 200 avec [] si bien filtré
# Backup quotidien 3h UTC
0 3 * * * docker exec directus_database_1 pg_dump -U directus directus | gzip | rclone rcat scaleway:directus-bk/$(date +\%F).sql.gz
Le test concluant : le blog Next.js ou Astro est en production sur itskillscenter.io/blog, le LCP est sous 2 secondes en 4G Dakar, les permissions sont fermées par défaut et un test de restauration mensuel passe en moins de 15 minutes. Vous avez maintenant un Directus headless propre, blindé et prêt à servir indépendamment Next.js et Astro avec un excellent ratio coût/performance pour le marché ouest-africain francophone.
Étape 9 — Générer un client TypeScript typé pour ne plus deviner les schémas
Au-delà de 5 collections, écrire à la main les types TypeScript de chaque réponse Directus devient une source de bugs. Le SDK officiel @directus/sdk v17 supporte la génération automatique de types depuis le schéma de production. Vous obtenez l’autocomplétion IDE sur tous les champs, les filtres, les relations et les statuts d’articles.
// schema.ts
import type { CoreSchema } from '@directus/sdk';
export interface Article {
id: string;
title: string;
slug: string;
excerpt: string;
body: string;
status: 'draft' | 'published' | 'archived';
published_at: string;
}
export interface Schema extends CoreSchema {
articles: Article[];
}
// usage côté Next.js
import { createDirectus, rest, readItems } from '@directus/sdk';
const client = createDirectus<Schema>(process.env.DIRECTUS_URL!).with(rest());
const articles = await client.request(
readItems('articles', { fields: ['id','title','slug'], filter: { status: { _eq: 'published' } } })
);
Validation pratique : votre IDE TypeScript signale immédiatement une faute de frappe sur un nom de champ ou un statut invalide, et la commande tsc --noEmit passe sans warning sur l’ensemble du frontend. Cette discipline évite 80 % des bugs en production sur un projet headless multi-frontends Dakar-Abidjan-Paris.