Développement Web

Connecter Directus à Next.js et Astro : tutoriel frontend 2026

12 min de lecture

📍 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

É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.

Partager