Bernardo Knoblauch

beknologie

Blog headless multilingue pour les rédacteurs et les éditeurs, sur Next.js 16 et Strapi 5.

Projet personnel · Blog headless · Node.js

Stack
Node.js, TypeScript, Next.js, Strapi, PostgreSQL, Redis, Firebase Auth, Cloudinary, Tailwind, Turborepo

Images

Contexte

Chaque article, catégorie et tag a une version par locale dans Strapi, avec pt-BR comme langue d’origine. Le front et le CMS forment un monorepo Turborepo, tous deux sur Node.js, avec des types partagés dans @blog/types. Le front utilise l’App Router et Tailwind.

Problème

La personne qui écrit a besoin d’un seul flux pour publier, voir ce qui a été lu et décider qui accède au texte entier. Déploiement, cache et compte visiteur restent hors de ce flux. Brouillon, SEO et médias y entrent.

Ce que j’ai fait et pourquoi

Architecture et contenu

  • Strapi 5 sur PostgreSQL : Post, Author, Category et Tag avec brouillon/publication. L’i18n est par champ : titre, slug, chapô, texte et SEO changent par locale ; image, auteur, catégorie et tags restent les mêmes dans toutes les traductions d’un article. Le temps de lecture est calculé dans un middleware Strapi à partir du contenu.
  • Theme Settings, single type sans i18n, stocke deux palettes en hex. Next les injecte en custom properties, sombre par défaut. Sans enregistrement, la palette de ce portfolio s’applique.
  • SSG et ISR. Chaque chemin porte la locale, y compris celle par défaut : / répond 307 vers /pt-BR. À la publication, un webhook invalide la page par tag, tout de suite (revalidateTag avec expire 0). Les TTL (300 s sur l’API, 1 h sur l’ISR) ne servent que de filet de sécurité. Les routes d’article, de catégorie, de tag et d’auteur n’utilisent pas generateStaticParams. Le fichier de middleware s’appelle proxy.ts.
  • Pages : accueil paginé, article, catégorie, tag, auteur, recherche, 404, compte, connexion, inscription et zone d’admin. Pagination, pas de défilement infini. Uploads sur Cloudinary. Le droit de lecture public est créé au bootstrap de Strapi, pas à la main dans le panneau.

Cache Redis de bout en bout

  • Un cache handler Next.js maison sur Redis, sans le paquet @neshca/cache-handler. L’index par tag est un SET, pour qu’une revalidation n’efface pas une clé écrite en même temps. Le cache ISR survit aux déploiements et redémarrages. Je l’ai testé en tuant le processus Next.js et en suspendant Strapi : un nouveau processus a servi la page depuis le cache, origine hors ligne.
  • En rejouant réellement le scénario de panne, j’ai trouvé deux bugs : une promise de connexion rejetée restait mémorisée et coupait le cache jusqu’à la fin du processus, et la reconnexion par défaut du client Redis bloquait tout rendu quand Redis tombait. Corrigé en réinitialisant la promise et avec reconnectStrategy: false et un timeout de 2 s : Redis hors ligne, la page répond en 0,5 s environ sans cache, puis se remet à écrire seule quand Redis revient.
  • Cache de l’API REST de Strapi sur Redis avec le plugin communautaire compatible Strapi 5, le plugin prévu par la spécification ne gérant que la v4.

Authentification et SEO

  • Firebase Auth (e-mail/mot de passe et Google) avec les rôles cumulatifs lecteur, abonné, éditeur et admin en Custom Claims, définis uniquement côté serveur. Le rôle par défaut est posé au moment de la session. Session cookies, routes protégées sur le runtime Node.js et Firebase Admin SDK initialisé à la demande. La connexion au panneau Strapi reste séparée.
  • Article premium : le texte complet est réservé à l’abonné et au-dessus ; le visiteur voit un extrait. Draft Mode limité aux éditeurs et admins.
  • SEO par langue : slug réel de chaque locale dans le hreflang, le sitemap et le sélecteur ; JSON-LD (Article, BreadcrumbList, WebSite), Open Graph généré à la volée, RSS, robots et images AVIF/WebP.
  • GA4 seulement après consentement (LGPD), avec événements de lecture et Web Vitals.

CI

  • CI sur GitHub Actions avec install, lint, type-check et build sur chaque PR et push sur main. Les builds Next et Strapi passent sans Postgres, Redis ni CMS en ligne.

Résultat

  • La page publique suit la publication, sans rebuild de Next.
  • Smoke test en production. 15 ADR consignent les compromis.

Prochaines étapes

  • Tests automatisés.
  • Domaine personnalisé et CDN Cloudflare, reportés en attendant le domaine.

Contact