Après plusieurs projets Next.js pour des clients avec un catalogue éditorial simple, un studio de céramique nous a demandé quelque chose de plus léger : un site rapide, sans complexité inutile côté gestion d’état, où chaque page interroge WordPress au moment où elle est demandée. Remix s’est imposé pour sa philosophie : les données se chargent côté serveur, dans une fonction dédiée à chaque route, sans magie cachée.
Ce choix a simplifié beaucoup de choses qu’on avait l’habitude de bricoler ailleurs, en particulier le cache et les formulaires.
Le loader : une fonction, une route, une requête
Chaque fichier de route Remix exporte une fonction loader exécutée côté serveur avant le rendu. C’est elle qui interroge WPGraphQL et transmet les données au composant :
export async function loader({ params }) {
const data = await requeteWPGraphQL(REQUETE_PIECE, { slug: params.slug });
if (!data.piece) {
throw new Response('Introuvable', { status: 404 });
}
return json(data.piece);
}
Pas de useEffect, pas d’état de chargement à gérer manuellement : au moment où le composant s’exécute, la donnée est déjà là, résolue côté serveur.
Le cache par en-tête HTTP, sans magie
Remix expose une fonction headers par route, qui définit les en-têtes de la réponse renvoyée au navigateur ou à un CDN placé devant l’application :
export function headers() {
return {
'Cache-Control': 'public, max-age=60, stale-while-revalidate=600',
};
}
Un contenu de catalogue de céramique change rarement dans la minute : un court max-age combiné à un stale-while-revalidate généreux suffit à absorber le pic de trafic d’une mise en avant sur les réseaux sociaux, sans jamais afficher une page cassée pendant la revalidation.

Les actions : des formulaires qui écrivent vers WordPress
Une fonction action, exportée dans le même fichier que le loader, reçoit les soumissions de formulaire côté serveur. Pour un formulaire de contact qui écrit dans WordPress via une mutation GraphQL personnalisée :
export async function action({ request }) {
const donnees = await request.formData();
const resultat = await requeteWPGraphQL(MUTATION_CONTACT, {
nom: donnees.get('nom'),
message: donnees.get('message'),
});
if (resultat.erreurs) {
return json({ erreur: 'Envoi impossible' }, { status: 422 });
}
return redirect('/contact/merci');
}
Le formulaire HTML lui-même n’a besoin d’aucun gestionnaire JavaScript pour fonctionner : un simple <Form method="post"> de Remix suffit, et l’expérience reste fonctionnelle même si le JavaScript n’a pas encore fini de se charger côté client.
Gérer les erreurs de validation proprement
- La fonction
actionrenvoie un objet d’erreurs typé, jamais une exception non gérée. - Le composant de route lit cet objet via
useActionDatapour afficher les messages au bon endroit du formulaire. - Aucune bibliothèque de gestion de formulaire supplémentaire n’a été nécessaire sur ce projet, la plateforme suffisait.
Un piège rencontré avec l’authentification des mutations
Les mutations WPGraphQL qui écrivent du contenu exigent une authentification, généralement via un jeton transmis en en-tête. Sur Remix, ce jeton doit rester strictement côté serveur, jamais transmis au client : la fonction action s’exécutant déjà côté serveur, il suffit de lire le secret depuis les variables d’environnement du serveur, sans jamais l’exposer dans le bundle client.
Un projet où chaque route sait exactement ce qu’elle charge et ce qu’elle écrit reste lisible bien après le lancement, contrairement à un état global qui grossit au fil des fonctionnalités ajoutées.
Ce qu’on retient
Remix impose une discipline qui sert particulièrement bien un projet WordPress headless : un loader qui charge, une action qui écrit, des en-têtes qui cachent. Sur un site à dominante éditoriale sans interactivité complexe, cette simplicité se traduit directement par moins de code à maintenir et par des temps de chargement perçus très courts, sans étape de chargement côté client à gérer.