Aller au contenu

Nuxt Content v3 en pratique — des collections typées pour un blog multilingue

Comment ce blog est construit avec Nuxt Content v3 — deux collections typées, un schéma partagé et une convention de traduction élégante.

Yann LerouxYann LerouxFrontend Engineer

Ce blog tourne avec Nuxt Content v3, et son modèle de contenu multilingue tient en une quarantaine de lignes de configuration. Cet article parcourt l'ensemble du dispositif : les collections typées, le schéma partagé, la convention de traduction et les requêtes qui rendent les deux locales naturelles.

Ceci est la moitié française d'une paire traduite. La version anglaise vit au même slug, sans préfixe — ce couplage est la convention expliquée ci-dessous.

La forme du contenu

Les articles vivent à côté du code, dans deux répertoires — un par langue :

  • content/
    • blog/
      • en/
        • nuxt-content-multilingual-blog.md
        • go-context-patterns.md
      • fr/
        • nuxt-content-multilingual-blog.md
        • tailwind-v4-design-system.md
    • authors/
      • ava-martin.md
      • yann-leroux.md

Chaque fichier commence par un frontmatter. Le schéma ci-dessous est le seul contrat entre un·e auteur·rice et le site :

content.config.ts (extrait)
const blogSchema = z.object({
  date: z.coerce.date(),
  updated: z.coerce.date().optional(),
  category: z.enum(['engineering', 'nuxt', 'go', 'css', 'devops', 'tooling']),
  tags: z.array(z.string()).default([]),
  author: z.string(),
  cover: z.string().optional(),
  draft: z.boolean().default(false),
  featured: z.boolean().default(false),
  series: z.string().optional(),
  seriesOrder: z.number().optional(),
  canonical: z.string().optional()
})

Un schéma, deux collections. blog_en et blog_fr sont définies avec le même objet zod. Si un article français oublie sa category, c'est le build qui échoue — pas la mise en page.

La convention de traduction

Même nom de fichier, les deux répertoires : les deux fichiers sont des traductions l'un de l'autre. Tout le reste découle de cette règle unique :

  • la page d'article cherche son jumeau et n'affiche le sélecteur de locale que lorsqu'un jumeau existe ;
  • les hreflang alternates ne sont émis qu'entre traductions existantes ;
  • le sitemap regroupe la paire sous une entrée unique, avec ses alternates de langue.

Ce que cela remplace

Approche CMS typiqueConvention du même slug
Lien de traductionChamp manuel « contenu lié »Un nom de fichier
Traduction cassée404 depuis des liens obsolètesImpossible — le lien ne se rend que si le fichier existe
Précision du hreflangDiscipline de l'éditeurMécanique

Interroger par locale

Le composable cache le choix de la collection derrière la locale active :

app/composables/useBlog.ts (extrait)
export function blogCollectionName(locale: string): 'blog_en' | 'blog_fr' {
  return locale === 'fr' ? 'blog_fr' : 'blog_en'
}

export async function useBlogPosts(locale: string): Promise<BlogPost[]> {
  const items = await queryCollection(blogCollectionName(locale)).all()
  return items.map(toPost).filter(p => !p.draft).sort(sortByDateDesc)
}

Ne faites pas confiance au système de fichiers à l'exécution. Les brouillons sont filtrés en code (!p.draft), et l'accès direct à un brouillon ne se rend qu'en développement. Oublier le filtre dans une seule route de requête fait fuiter les brouillons dans les sitemaps et le RSS.

Ajouter un article, de bout en bout

  1. Écrire le fichier

  2. Créez content/blog/en/my-idea.md avec un frontmatter complet. Si le projet est neuf, installez d'abord les dépendances :

    npm install
    

    Lancez ensuite le serveur de dev — les types de collection se régénèrent automatiquement.

  3. Vérifier la carte

  4. La page d'accueil, la page de catégorie et la page auteur reprennent l'article immédiatement ; le temps de lecture est calculé depuis le corps parsé.

  5. Traduire (optionnel)

  6. Créez content/blog/fr/my-idea.md avec les mêmes clés de frontmatter. Le sélecteur de locale et le hreflang apparaissent sans aucun code supplémentaire.

L'expérience d'écriture

Les auteur·ices écrivent en Markdown avec un petit jeu de composants — callouts, étapes, blocs de code à onglets, arbres de fichiers, comparaisons. Ils sont documentés dans la vitrine MDC qui accompagne le blog lui-même :

nuxt nuxt-content i18n typescript
Yann Leroux

Écrit par

Yann Leroux

Frontend Engineer

Yann covers the Vue and Nuxt ecosystem, design systems and CSS. He maintains a few open-source UI utilities and cares deeply about typographic detail.