pub

Internationalisation de la V 2.0

authors

Introduction

Dans cet article, nous discuterons de l'implémentation de i18n et de ce que cela change en comparaison de la version V.2 originale. Pour une meilleure compréhension des fonctionnalités de base, il vous faudra consulter les autres articles, ou la documentation originelle sur le github de timlrx

Motivation

Je suis actuellement en train de faire la refonte mon site web, (pas encore publié) qui utilise le router page, et une partie du code pour le blog internationalisé de la V.1. Je souhaite migrer vers le router app, mais pour cela, il me fallait d'abord apprendre à internationaliser un site avec l'app router, du coup j'ai pris ce dépôt comme entraînement.

Je prépare également un template, beaucoup plus complet pour les artistes, les créateurs de contenu et les développeurs, qui me servira pour mon propre site et qui sera bientôt disponible.

Changements:

Librairies

Pour les traductions, la bibliothèque choisie n'est pas next-translate comme dans la V.1 de GautierArcin, mais les bibliothèques suivantes :

  • i18next
  • i18next-browser-languagedetector
  • i18next-resources-to-backend
  • React-i18next

En effet, avec la nouvelle version de next-js et l'app router, il m'a été plus facile de trouver des informations et des tutoriels pour que tout fonctionne comme prévu. (J'ai d'abord essayé avec next-translate, mais il y a trop de problèmes non résolus actuellement avec cette bibliothèque et les fonctionnalités liées au nouveau router)

Configuration

Au sein du dossier app, tout le contenu a été déplaçé vers un nouveau dossier [locale]" : ceci est la manière officielle recommandée par next.js. A également été ajouté un dossier i18n:

app
 [locale]
    ├── i18n
    │     │
    │     ├──locales
    │     │     │
    │     │     ├── en
    │     │     │   ├── about.json
    │     │     │   │   
    │     │     │   ├── home.json
    │     │     │   │  
    │     │     │   └── ...
    │     │     └── fr
    │     │         ├── about.json
    │     │         │   
    │     │         ├── home.json
    │     │         │ 
    │     │         └── ...
    │     │  
    │     │
    │     ├── client.ts
    │     ├── locales.js
    │     ├── server.ts
    │     └── settings.ts
    └── ...

C'est donc dans ce dossier i18n que se situe la logique principale pour l'internationalisation de l'application.

  • Fichiers json :

Le sous-dossier "locales" contient les fichiers .json où vous définirez vos traductions, la convention étant de définir un fichier par page de votre site, avec le nom de la page concernée pour le nom du fichier json. Il y a également un fichier "common" : si vous ne spécifiez pas de "namespace" ou ns (le nom du fichier sans l'extension json) dans vos pages ou composants, les traductions seront piochées dans ce fichier par défaut.

*Important : pour chaque langue, il doit y avoir un fichier correspondant avec le même nom, par exemple un fichier "about" pour "fr" et pour "en", etc. Ainsi que des clefs de traduction avec le même nom au sein de chaque fichier.

Exemple :

En anglais dans le dossier "en":

projects.json
{
  "title": "Projects",
  "description": "Showcase your projects with a hero image (16 x 9)",
  "learn": "Learn more",
  "subtitle": "Here you will find information about my current projects",
  "linkto": "Link to"
}

En français dans le dossier "fr":

projects.json
{
  "title": "Projets",
  "description": "Présentez vos projets avec une image (16 x 9)",
  "learn": "En savoir plus",
  "subtitle": "Ici vous trouverez des informations sur mes projets actuels.",
  "linkto": "Lien vers"
}
  • locales.js :

Il s'agit du fichier où vous définirez les langues que vous souhaitez utiliser, ainsi que la langue par défaut:

locales.js
const fallbackLng = 'en' // langue par défaut
const secondLng = 'fr' 

module.exports = { fallbackLng, secondLng }

Vous pouvez ajouter autant de langues que souhaité:

locales.js
/* Exemple d'ajout d'une 3ème langue :*/
const fallbackLng = 'en'
const secondLng = 'fr'
const thirdLng = 'es'

module.exports = { fallbackLng, secondLng, thirdLng }

Toutefois, cela nécessitera quelques étapes de configuration supplémentaires au sein d'autres fichiers (principalement des fichiers qui sont discutés dans cet article)

Vous pouvez également intervertir la langue par défaut et la seconde langue :

locales.js
/* Exemple de modification de langue par défaut:*/
const fallbackLng = 'fr'
const secondLng = 'en'

module.exports = { fallbackLng, secondLng}
  • settings.ts

Il s'agit d'un fichier de configuration, qui permet de définir un objet locales ainsi que les options correspondantes :

settings.ts
import type { InitOptions } from 'i18next'
import { fallbackLng, secondLng } from './locales'

/* Objet locales, qui définit toutes les langues qui seront utilisées dans l'application: */
export const locales = [fallbackLng, secondLng] as const
/* Définition typescript de type pour nos locales :*/
export type LocaleTypes = (typeof locales)[number]
/* "Namespace" (ou ns) par défaut : les traductions seront piochées dans le fichier 
common.json si aucun ns n'est spécifié dans vos composants ou pages: */
export const defaultNS = 'common' 
/* Fonction qui sera réutilisée dans les fichiers client.ts et server.ts: */
export function getOptions(locale = fallbackLng, ns = defaultNS): InitOptions {
  return {
    debug: true,
    supportedLngs: locales,
    fallbackLng,
    lng: locale,
    fallbackNS: defaultNS,
    defaultNS,
    ns,
  }
}
  • client.ts et server.ts :

Sans rentrer dans des détails complexes, ces deux fichiers exportent chacun une fonction pour la traduction (useTranslation côté client, createTranslation côté serveur), réutilisables dans vos pages et composants :

client.ts
export function useTranslation(lng: LocaleTypes, ns: string) {
  const translator = useTransAlias(ns)
  const { i18n } = translator

  /* Exécuté lorsque le contenu est rendu côté serveur: */
  if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
    i18n.changeLanguage(lng)
  } else {
    /* Utiliser notre implémentation personnalisée lors de l'exécution côté client: */
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useCustomTranslationImplem(i18n, lng)
  }
  return translator
}
server.ts
export async function createTranslation(lang: LocaleTypes, ns: string) {
  const i18nextInstance = await initI18next(lang, ns)

  return {
    /* La fonction de traduction "t" que nous utiliserons dans nos composants: */
    // e.g. t('greeting')
    t: i18nextInstance.getFixedT(lang, Array.isArray(ns) ? ns[0] : ns),
  }
}

Exemple de composant côté client, avec traduction de l'aria-label du bouton :

ThemeSwitch.tsx
'use client'

import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
/*Importation du hook fourni par next.js pour récupérer la langue
définie par l'utilisateur, et de la fonction de traduction côté client: */
import { useParams } from 'next/navigation'
import { LocaleTypes } from 'app/[locale]/i18n/settings'
import { useTranslation } from 'app/[locale]/i18n/client'

const ThemeSwitch = () => {
  /* Utilisation du hook fourni par next.js pour récupérer la langue actuellement définie: */ 
  const locale = useParams()?.locale as LocaleTypes
  /* Utilisation de la fonction de traduction côté client:
   pas de namespace (ns) défini (crochets vide), par conséquent la traduction sera piochée 
   dans le fichier common.json */
  const { t } = useTranslation(locale, '')
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme, resolvedTheme } = useTheme()

  useEffect(() => setMounted(true), [])

  if (!mounted) {
    return null
  }

  return (
    <button
    /* Traduction de l'aria-label */
      aria-label={t('darkmode')}
      onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 20 20"
        fill="currentColor"
        className="h-6 w-6 text-gray-900 dark:text-gray-100"
      >
        {mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
          <path
            fillRule="evenodd"
            d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
            clipRule="evenodd"
          />
        ) : (
          <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
        )}
      </svg>
    </button>
  )
}

export default ThemeSwitch

Exemple de composant côté serveur :

footer.tsx
import Link from './Link'
import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'
/*Importation de la fonction de traduction côté serveur: */
import { createTranslation } from 'app/[locale]/i18n/server'
import { LocaleTypes } from 'app/[locale]/i18n/settings'

type Props = {
  params: { locale: LocaleTypes }
}
/* la langue actuelle est passée en tant que prop des paramètres de la page: */
export default async function Footer({ params: { locale } }: Props) {
/* Utilisation de la fonction de traduction côté serveur, avec le namespace "footer" */
  const { t } = await createTranslation(locale, 'footer')
  return (
    <footer>
      <div className="mt-16 flex flex-col items-center">
        <div className="mb-3 flex space-x-4">
          <SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
          <SocialIcon kind="github" href={siteMetadata.github} size={6} />
          <SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
          <SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
          <SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
          <SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
        </div>
        <div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
          <div>{siteMetadata.author}</div>
          <div>{``}</div>
          <div>{`© ${new Date().getFullYear()}`}</div>
          <div>{``}</div>
          <Link href="/">{siteMetadata.title}</Link>
        </div>
        <div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
          <Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">      
       {t('theme')}
          </Link>
        </div>
      </div>
    </footer>
  )
}

Pour la création de nouveaux composants ou pages, vous devrez donc vous appuyer sur ces deux fonctions concernant vos traductions, selon que le composant soit rendu côté client, ou côté serveur.

  • Middleware.ts :

I18n n'étant pas supporté nativement au sein du nouveau router, il s'agit d'un fichier essentiel au bon fonctionnement de l'ensemble. Il est également essentiel d'utiliser le "matcher" (ici avec des valeurs inversées qui permettent d'exclure les dossiers et fichiers qui ne doivent pas être pris en charge par le middleware)

middleware.ts
import { NextResponse, NextRequest } from 'next/server'
import { locales } from 'app/[locale]/i18n/settings'
import { fallbackLng } from 'app/[locale]/i18n/locales'

export function middleware(request: NextRequest) {
  /* Vérifier si une langue est prise en charge dans le nom de chemin: */
  const pathname = request.nextUrl.pathname

  /* Vérifier si la langue par défaut est dans le nom de chemin: */
  if (pathname.startsWith(`/${fallbackLng}/`) || pathname === `/${fallbackLng}`) {
    /* ex: la requête entrante est: /en/about
    Le nouveau nom de chemin est maintenant: /about */
    return NextResponse.redirect(
      new URL(
        pathname.replace(`/${fallbackLng}`, pathname === `/${fallbackLng}` ? '/' : ''),
        request.url
      )
    )
  }

  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  )

  if (pathnameIsMissingLocale) {
    /* Nous sommes sur la langue par défaut
    Réecriture pour que next.js comprenne */

    // ex: la requête entrante est: /about 
    // Informer Next.js qu'il devrait se comporter comme si c'était: /en/about
    return NextResponse.rewrite(new URL(`/${fallbackLng}${pathname}`, request.url))
  }
}

export const config = {
  /* Ne pas executer le middleware sur les chemins suivants: */
  // prettier-ignore
  matcher:
  '/((?!api|static|data|css|scripts|.*\\..*|_next).*|robots.txt|sitemap.xml|favicon.ico)',
}

Articles

Tous les articles sont regroupés au sein du dossier "data/blog"

Ils sont organisés en sous-dossiers : "data/blog/posts" pour l'anglais, et "data/blog/articles" pour le français. Si vous ajoutez/modifiez une langue, je vous recommande simplement de créer un dossier avec comme nom la traduction de "posts" pour la langue choisie.

  • En-têtes de vos articles :
article.mdx
---
title: titre de l'article
date: date de création
lastmod: dernière date de modification
language: langue de l'article
localeid: identifiant pour faire correspondre les articles dans d'autres langues
tags: balises
authors: auteurs
images: images
draft: en construction ou non
summary: résumé
---
  • contentlayer.config.ts :

Au sein du fichier "contentlayer.config.ts" il y a donc des changements mineurs dûs à l'internationalisation :

export const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: 'blog/**/*.mdx',
  contentType: 'mdx',
  fields: {
    title: { type: 'string', required: true },
    date: { type: 'date', required: true },
    language: { type: 'string', required: true }, // Nouveau champs obligatoire
    localeid: { type: 'string', required: true }, // Nouveau champs obligatoire
    tags: { type: 'list', of: { type: 'string' }, default: [] },
    lastmod: { type: 'date' },
    draft: { type: 'boolean' },
    summary: { type: 'string' },
    images: { type: 'json' },
    authors: { type: 'list', of: { type: 'string' }, required: true },
    layout: { type: 'string' },
    bibliography: { type: 'string' },
    canonicalUrl: { type: 'string' },
  },
  ...
)})

Pour le champs "auteurs" :

export const Authors = defineDocumentType(() => ({
  name: 'Authors',
  filePathPattern: 'authors/**/*.mdx',
  contentType: 'mdx',
  fields: {
    name: { type: 'string', required: true },
    language: { type: 'string', required: true }, // Nouveau champs obligatoire
    avatar: { type: 'string' },
    occupation: { type: 'string' },
    company: { type: 'string' },
    email: { type: 'string' },
    twitter: { type: 'string' },
    linkedin: { type: 'string' },
    github: { type: 'string' },
    layout: { type: 'string' },
  },

-Génération des tags :

Là aussi, il a été nécessaire de faire des modifications, afin de générer un objet .json avec des tags pour chaque langue.

function createTagCount(allBlogs) {
  const tagCount = {
    [fallbackLng]: {},
    [secondLng]: {},
  }

  allBlogs.forEach((file) => {
    if (file.tags && (!isProduction || file.draft !== true)) {
      file.tags.forEach((tag: string) => {
        const formattedTag = GithubSlugger.slug(tag)
        if (file.language === fallbackLng) { // tags pour la langue par défaut
          tagCount[fallbackLng][formattedTag] = (tagCount[fallbackLng][formattedTag] || 0) + 1
        } else if (file.language === secondLng) {  // tags pour la seconde langue
          tagCount[secondLng][formattedTag] = (tagCount[secondLng][formattedTag] || 0) + 1
        }
      })
    }
  })

  writeFileSync('./app/[locale]/tag-data.json', JSON.stringify(tagCount))
}

Note : si vous souhaitez ajouter d'autres langues (3, 4 ou même 5 langues), il vous faudra modifier la logique pour prendre en charge ces nouvelles langues.

-Fonction generateSlugMap :

async function generateSlugMap(allBlogs) {
  const slugMap = {}

  // Traitement de chaque article de blog
  allBlogs.forEach((blog) => {
    const { localeid, language, slug } = blog
    const formattedLng = language === fallbackLng ? fallbackLng : secondLng

    if (!slugMap[localeid]) {
      slugMap[localeid] = {}
    }

    // Ajout du slug au mapping pour la langue spécifique
    slugMap[localeid][formattedLng] = slug
  })

  writeFileSync('./app/[locale]/localeid-map.json', JSON.stringify(slugMap, null, 2))
}

Cette fonction est celle qui permet de faire correspondre les articles entre eux :

isons que vous lisez un article, mais que vous préférez le lire dans une autre langue, au lieu de vous rediriger vers une page d'erreur 404, cela affichera l'article dans la langue correspondante !

Mais pour cela, vous devez attribuer un identifiant unique (LocaleID dans vos fichiers MDX) à votre article, et à l'article correspondant dans la langue traduite. Si aucun article correspondant n'est trouvé, le routeur vous redirigera simplement vers la page de présentation du blog.

  • Important: si vous êtes un utilisateur windows, un script adapté à l'internationalisation est prévu (dossier /scripts) afin de contourner un beug lié à la librairie contentlayer.

Auteurs

Les dossiers contenant les auteurs sont organisés par langue, et les informations sur les auteurs peuvent être traduites.

L'implémentation est assez simple et directe : si vous voulez modifier ou ajouter une langue, modifiez ou ajoutez simplement les dossiers avec vos traductions correspondantes pour les nouvelles langues.

Fichier siteMetadata et nouveau fichier localeMetadata

Le fichier siteMetadata.js présent dans le dossier "/data" ne nécessite pas de modifications liées à l'internationalisation.

Par contre, afin de gérer au mieux les métadonnées, il a fallu créer un nouveau fichier, pour le titre et la description :

localeMetadata.ts
type Metadata = {
  [locale: string]: string
}
/* Ajoutez ou modifiez le titre ici selon les langues choisies: */
export const maintitle: Metadata = {
  en: 'Next.js i18n Starter Blog',
  fr: 'Starter Blog Next.js i18n',
}
/* Ajoutez ou modifiez la description ici selon les langues choisies: */
export const maindescription: Metadata = {
  en: 'A blog created with Next.js, i18n and Tailwind.css',
  fr: 'Un blog crée avec tailwind, i18n et next.js',
}

Onglet "Projets"

La logique nécessaire à l'onglet "projets" réside dans le fichier suivant, également présent dans le dossier"/data" :

projectsData.ts
type Project = {
  title: string
  description: string
  imgSrc: string
  href: string
}

type ProjectsData = {
  [locale: string]: Project[]
}

const projectsData: ProjectsData = {
  en: [
    {
      title: 'A Search Engine',
      description: `What if you could look up any information in the world? Webpages, images, videos
        and more. Google has many features to help you find exactly what you're looking
        for.`,
      imgSrc: '/static/images/google.png',
      href: 'https://www.google.com',
    },
    {
      title: 'The Time Machine',
      description: `Imagine being able to travel back in time or to the future. Simple turn the knob
        to the desired date and press "Go". No more worrying about lost keys or
        forgotten headphones with this simple yet affordable solution.`,
      imgSrc: '/static/images/time-machine.jpg',
      href: '/blog/posts/the-time-machine',
    },
  ],

  fr: [
    {
      title: 'Un moteur de recherche',
      description: `Et si vous pouviez rechercher n'importe quelle information dans le monde ? Pages Web, images, vidéos
        et plus. Google propose de nombreuses fonctionnalités pour vous aider à trouver exactement ce que vous cherchez.`,
      imgSrc: '/static/images/google.png',
      href: 'https://www.google.com',
    },
    {
      title: 'La Machine à remonter le temps',
      description: `Imaginez pouvoir voyager dans le temps ou vers le futur. Tournez simplement le bouton
        à la date souhaitée et appuyez sur "Go". Ne vous inquiétez plus des clés perdues ou
        écouteurs oubliés avec cette solution simple mais abordable.`,
      imgSrc: '/static/images/time-machine.jpg',
      href: '/blog/articles/la-machine-a-remonter-le-temps',
    },
  ],
}

export default projectsData

Encore une fois, modifiez simplement la logique en conservant la même structure générale, et selon vos langues choisies/et/ou nombre de langues.

Logique "sitemap"

Là aussi, il a eu des modifications necessaires. Ce fichier se trouve dans le dossier "app", et permets aux robots google de comprendre la manière dont votre site est construit, il est donc essentiel pour l'indexation et le SEO :

sitemap.ts
import { MetadataRoute } from 'next'
import { allBlogs } from 'contentlayer/generated'
import siteMetadata from '@/data/siteMetadata'
import { fallbackLng, secondLng } from './i18n/locales'

export default function sitemap(): MetadataRoute.Sitemap {
  const siteUrl = siteMetadata.siteUrl
  // routes pour les articles de blog en anglais
  const blogRoutes = allBlogs
    .filter((p) => p.language === fallbackLng)
    .map((post) => ({
      url: `${siteUrl}/${fallbackLng}/${post.path}`,
      lastModified: post.lastmod || post.date,
    }))
  // routes pour les articles de blog en français (ou votre propre seconde langue)
  const secondBlogRoutes = allBlogs
    .filter((p) => p.language === secondLng)
    .map((post) => ({
      url: `${siteUrl}/${secondLng}/${post.path}`,
      lastModified: post.lastmod || post.date,
    }))
  // toutes les autres routes pour l'anglais
  const routes = ['', 'blog', 'projects', 'tags', 'about'].map((route) => ({
    url: `${siteUrl}/${fallbackLng}/${route}`,
    lastModified: new Date().toISOString().split('T')[0],
  }))
  // toutes les routes pour le français (ou votre propre seconde langue)
  const secondRoutes = ['', 'blog', 'projects', 'tags', 'about'].map((route) => ({
    url: `${siteUrl}/${secondLng}/${route}`,
    lastModified: new Date().toISOString().split('T')[0],
  }))

  return [...routes, ...secondRoutes, ...blogRoutes, ...secondBlogRoutes]
}

Note : modifiez ce fichier en conséquence si vous ajoutez d'autres langues.

Barre de recherche :

Le dépôt original permets le support de kbar et d'algolia.

Ici, la barre de recherche repose sur la librairie kbar, et le support d'Algolia n'est pas prévu. Si vous préferez utiliser Algolia, ce sera donc à vous de l'implémenter sur votre site, à la place de kbar.

Choses à faire :

  • Correction de la traduction dans la page 404. Ceci est lié au fonctionnement actuel de la fonction not-found, nous devons donc attendre un correctif du côté de next-js voir ici: i18n for not-found page

  • J'ai essayé de mettre à jour le script rss.mjs à des fins de premier déploiement, mais je ne sais pas s'il est correct pour la configuration de la langue (mais j'ai fait de mon mieux). Si vous êtes un développeur expérimenté, faites-moi savoir ce que vous en pensez.

Tout le reste fonctionne actuellement comme prévu.

J'ai donc dû faire des changements importants concernant le SEO et les métadonnées au sein des pages, mais j'ai fini par trouver une solution qui a fonctionné, avec un score SEO parfait !

Voici une autre solution possible pour l'intégration d'i18n concernant le SEO, et même l'url traduite:

Toute aide pour des améliorations et/ou rapports de bug est bienvenue!

Notes importantes :

  • J'utilise un composant Link personnalisé pour la sélection de la langue : je préfère cela à l'élément de sélection HTML (plus facile à personnaliser). Le petit inconvénient est qu'il nécessite plus de code. Si vous préférez, vous êtes libre d'adapter et d'utiliser l'élément select à la place, mais je le garderai tel quel pour le template.

  • Ne mettez pas à jour les dépendances : cela casserait votre application puisque certaines choses doivent être corrigées du côté de ces bibliothèques.

Quoi de plus?

Ce dépôt sera parfois mis à jour avec de nouvelles fonctionnalités (non présentes dans le dépôt d'origine)

Pour l'instant :

  • Transitions de page fluides grâce à Framer Motion (voir le fichier template.tsx dans le dossier app et jetez un œil à la documentation de next.js suivante pour les fonctionnalités des fichiers template)

Remarque : il s'agit d'une implémentation basique mais efficace. Je vous encourage fortement à expérimenter avec framer-motion et son usage au sein du nouveau routeur.

  • Nouveau composant: excellent lecteur audio pour les fichiers mdx (au cas où vous feriez des podcasts, ou même de la musique), grâce à react-h5-audio-player
--:--
--:--
  • Indicateur de taille d'écran Tailwind : petite aide pour le mode développement et le responsive design (voir TwSizeIndicator.tsx dans /components/helper)

  • Formspree prise en charge de l'icône de courrier, avec une belle boîte de dialogue modale. Formspree permet à vos utilisateurs de vous contacter et de vous envoyer des messages directement depuis votre site, avec protection anti-spam. Créez simplement un compte de base gratuit, récupérez la clé sur votre compte Formspree et remplacez la clef dans le fichier suivant, dans components/formspree/index.tsx :

formspree/index.tsx
/* Ligne 19*/
 const [state, handleSubmit, reset] = useForm('xdojkndq')

Si vous ne souhaitez pas utiliser Formspree, accédez au fichier siteMetadata.js et définissez Formspree sur "false".

Auteur : pxlsyl