Post

Comment prendre en charge plusieurs langues sur un blog Jekyll avec Polyglot (1) - Application du plugin Polyglot & implémentation des balises alt hreflang, sitemap et bouton de sélection de langue

Présentation du processus d'implémentation du support multilingue en appliquant le plugin Polyglot à un blog Jekyll basé sur le thème 'jekyll-theme-chirpy'. Ce post est le premier article de cette série, couvrant l'application du plugin Polyglot et la modification des en-têtes HTML et du sitemap.

Comment prendre en charge plusieurs langues sur un blog Jekyll avec Polyglot (1) - Application du plugin Polyglot & implémentation des balises alt hreflang, sitemap et bouton de sélection de langue

Aperçu

Il y a environ 4 mois, début juillet 12024, j’ai ajouté l’implémentation du support multilingue en appliquant le plugin Polyglot à ce blog basé sur Jekyll et hébergé via GitHub Pages. Cette série partage les bugs rencontrés lors de l’application du plugin Polyglot au thème Chirpy et leur processus de résolution, ainsi que les méthodes de rédaction des en-têtes HTML et du sitemap.xml en tenant compte du SEO. La série se compose de 2 articles, et cet article que vous lisez est le premier de cette série.

Exigences

  • Le résultat du build (page web) doit pouvoir être fourni en séparant les chemins par langue (ex. /posts/ko/, /posts/ja/).
  • Pour minimiser autant que possible le temps et les efforts supplémentaires requis pour le support multilingue, il doit être possible de reconnaître automatiquement la langue selon le chemin local où se trouve le fichier (ex. /_posts/ko/, /_posts/ja/) lors du build, sans avoir à spécifier manuellement les balises ‘lang’ et ‘permalink’ dans le YAML front matter du fichier markdown original écrit.
  • La partie en-tête de chaque page du site doit inclure les balises méta Content-Language appropriées, les balises alternatives hreflang et les liens canoniques pour répondre aux directives SEO Google pour la recherche multilingue.
  • Il doit être possible de fournir tous les liens de pages par version linguistique du site sans omission via sitemap.xml, et sitemap.xml lui-même ne doit exister qu’une seule fois dans le chemin racine sans duplication.
  • Toutes les fonctionnalités fournies par le thème Chirpy doivent fonctionner normalement sur chaque page linguistique, sinon elles doivent être corrigées pour fonctionner normalement.
    • Fonctionnement normal des fonctionnalités ‘Recently Updated’ et ‘Trending Tags’
    • Aucune erreur ne doit se produire lors du processus de build utilisant GitHub Actions
    • Fonctionnement normal de la fonction de recherche de posts en haut à droite du blog

Application du plugin Polyglot

Jekyll ne prend pas en charge nativement les blogs multilingues, donc pour implémenter un blog multilingue satisfaisant les exigences ci-dessus, il faut utiliser un plugin externe. Après recherche, j’ai trouvé que Polyglot est largement utilisé pour l’implémentation de sites web multilingues et peut satisfaire la plupart des exigences ci-dessus, j’ai donc adopté ce plugin.

Installation du plugin

Comme j’utilise Bundler, j’ai ajouté le contenu suivant au Gemfile.

1
2
3
group :jekyll_plugins do
   gem "jekyll-polyglot"
end

Ensuite, exécuter bundle update dans le terminal complète automatiquement l’installation.

Si vous n’utilisez pas Bundler, vous pouvez également installer directement le gem avec la commande gem install jekyll-polyglot dans le terminal, puis ajouter le plugin dans _config.yml comme suit.

1
2
plugins:
  - jekyll-polyglot

Configuration des paramètres

Ensuite, ouvrez le fichier _config.yml et ajoutez le contenu ci-dessous.

1
2
3
4
5
6
# Polyglot Settings
languages: ["en", "ko", "ja", "zh-TW", "es", "pt-BR", "fr", "de"]
default_lang: "en"
exclude_from_localization: ["javascript", "images", "css", "public", "assets", "sitemap.xml"]
parallel_localization: false
lang_from_path: true
  • languages : Liste des langues à prendre en charge
  • default_lang : Langue de fallback par défaut
  • exclude_from_localization : Spécification des expressions régulières de chaînes de chemins de fichiers/dossiers racine à exclure de la localisation linguistique
  • parallel_localization : Valeur booléenne spécifiant s’il faut paralléliser le traitement multilingue lors du processus de build
  • lang_from_path : Valeur booléenne, si définie sur ‘true’, même sans spécifier séparément l’attribut ‘lang’ comme YAML front matter dans le fichier markdown du post, si la chaîne de chemin du fichier markdown contient un code de langue, elle sera automatiquement reconnue et utilisée

La documentation officielle du protocole Sitemap stipule ce qui suit :

“The location of a Sitemap file determines the set of URLs that can be included in that Sitemap. A Sitemap file located at http://example.com/catalog/sitemap.xml can include any URLs starting with http://example.com/catalog/ but can not include URLs starting with http://example.com/images/.”

“It is strongly recommended that you place your Sitemap at the root directory of your web server.”

Pour respecter cela, il faut s’assurer qu’un seul fichier sitemap.xml avec le même contenu existe dans le répertoire racine au lieu d’être créé séparément par langue, en l’ajoutant à la liste ‘exclude_from_localization’, pour éviter l’exemple incorrect suivant.

Exemple incorrect (le contenu de chaque fichier n’est pas différent par langue mais identique) :

  • /sitemap.xml
  • /ko/sitemap.xml
  • /es/sitemap.xml
  • /pt-BR/sitemap.xml
  • /ja/sitemap.xml
  • /fr/sitemap.xml
  • /de/sitemap.xml

(Mise à jour du 14.01.12025) Suite à l’acceptation de la Pull Request que j’ai soumise pour renforcer le contenu mentionné ci-dessus dans le README, les mêmes conseils peuvent maintenant être consultés dans la documentation officielle de Polyglot.

Bien que spécifier ‘parallel_localization’ sur ‘true’ ait l’avantage de réduire considérablement le temps de build, au moment de juillet 12024, lorsque cette fonctionnalité était activée pour ce blog, il y avait un bug où les titres des liens dans les parties ‘Recently Updated’ et ‘Trending Tags’ de la barre latérale droite de la page n’étaient pas traités correctement et se mélangeaient avec d’autres langues. Comme cela semble encore instable, il faut tester si cela fonctionne normalement avant de l’appliquer au site. De plus, cette fonctionnalité n’est pas prise en charge sur Windows et doit être désactivée.

De plus, dans Jekyll 4.0, il faut désactiver la génération de sourcemaps CSS comme suit.

1
2
sass:
  sourcemap: never # In Jekyll 4.0 , SCSS source maps will generate improperly due to how Polyglot operates

Points à noter lors de la rédaction de posts

Les points à noter lors de la rédaction de posts multilingues sont les suivants :

  • Spécification appropriée du code de langue : Il faut spécifier le code de langue ISO approprié en utilisant le chemin du fichier (ex. /_posts/ko/example-post.md) ou l’attribut ‘lang’ du YAML front matter (ex. lang: ko). Référez-vous aux exemples de la documentation Chrome Developer.

Cependant, bien que la documentation Chrome Developer indique les codes régionaux au format ‘pt_BR’, en réalité il faut utiliser - au lieu de _ comme ‘pt-BR’ pour que cela fonctionne normalement lors de l’ajout ultérieur de balises alternatives hreflang dans l’en-tête HTML.

  • Les chemins et noms de fichiers doivent être cohérents.

Pour plus de détails, veuillez consulter le README du dépôt GitHub untra/polyglot.

Modification de l’en-tête HTML et du sitemap

Maintenant, pour le SEO, il faut insérer les balises méta Content-Language et les balises alternatives hreflang dans l’en-tête HTML de chaque page du blog, et spécifier appropriément l’URL canonique.

En-tête HTML

Basé sur la version 1.8.1 qui est la dernière version au moment de novembre 12024, Polyglot a une fonctionnalité qui effectue automatiquement le travail ci-dessus lors de l’appel de la balise Liquid {% I18n_Headers %} dans la partie en-tête de la page. Cependant, cela suppose que l’attribut ‘permalink’ a été spécifié pour cette page, et ne fonctionne pas normalement sinon.

J’ai donc récupéré le head.html du thème Chirpy puis ajouté directement le contenu comme suit. J’ai travaillé en me référant à la page SEO Recipes du blog officiel Polyglot, mais j’ai modifié pour utiliser l’attribut page.url au lieu de page.permalink pour correspondre à mon environnement d’utilisation et mes exigences.

1
2
3
4
5
6
7
8
9
10
11
  <meta http-equiv="Content-Language" content="{{site.active_lang}}">
  
  {% if site.default_lang -%}
  <link rel="alternate" hreflang="{{site.default_lang}}" href="{{site.url}}{{page.url}}" />
  {%- endif -%}
  {% for lang in site.languages -%}
    {% if lang == site.default_lang -%}
      {%- continue -%}
    {%- endif %}
  <link rel="alternate" hreflang="{{lang}}" href="{{site.url}}/{{lang}}{{page.url}}" />
  {%- endfor %}

(Ajout du 29.07.12025) De plus, le thème Chirpy intègre par défaut le plugin Jekyll SEO Tag, mais j’ai confirmé que les propriétés de métadonnées Open Graph og:locale, og:url générées automatiquement par Jekyll SEO Tag et l’URL canonique (élément link rel="canonical") sont basées sur la langue par défaut du site (site.lang, site.default_lang) et nécessitent un traitement supplémentaire.
J’ai donc ajouté la déclaration suivante avant {{ seo_tags }}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(précédent)...

  {% capture seo_tags -%}
    {% seo title=false %}
  {%- endcapture %}

  ...(milieu)...

  {%- capture old_og_locale -%}
    <meta property="og:locale" content="{{site.lang}}" />
  {%- endcapture -%}
  {%- capture new_og_locale -%}
    <meta property="og:locale" content="{{site.active_lang}}" />
    {% for lang in site.languages -%}
      {%- if lang == site.active_lang -%}
        {%- continue -%}
      {%- endif %}
    <meta property="og:locale:alternate" content="{{lang}}" />
    {%- endfor %}
  {%- endcapture -%}
  {% assign seo_tags = seo_tags | replace: old_og_locale, new_og_locale %}
  
  {% unless site.active_lang == site.default_lang -%}
    {%- capture old_canonical_link -%}
      <link rel="canonical" href="{{site.url}}{{page.url}}" />
    {%- endcapture -%}
    {%- capture old_og_url -%}
      <meta property="og:url" content="{{site.url}}{{page.url}}" />
    {%- endcapture -%}
    {%- capture new_canonical_link -%}
      <link rel="canonical" href="{{site.url}}/{{site.active_lang}}{{page.url}}" />
    {%- endcapture -%}
    {%- capture new_og_url -%}
      <meta property="og:url" content="{{site.url}}/{{site.active_lang}}{{page.url}}" />
    {%- endcapture -%}
    {% assign seo_tags = seo_tags | replace: old_canonical_link, new_canonical_link %}
    {% assign seo_tags = seo_tags | replace: old_og_url, new_og_url %}
  {%- endunless %}

  {{ seo_tags }}

  ...(suite)

Selon la documentation Google Developer, lorsqu’une page a plusieurs versions linguistiques, elles ne sont considérées comme des doublons que si la langue du contenu principal est la même, c’est-à-dire lorsque seuls les en-têtes, pieds de page et autres textes non importants sont traduits et que le corps du texte est identique. Par conséquent, dans le cas où le texte du corps est fourni en plusieurs langues comme ce blog actuellement, toutes les versions linguistiques sont considérées comme des pages indépendantes et non des doublons, il faut donc spécifier des URL canoniques différentes selon la langue.
Par exemple, pour la version coréenne de cette page, l’URL canonique n’est pas “https://www.yunseo.kim/posts/how-to-support-multi-language-on-jekyll-blog-with-polyglot-1/” mais “https://www.yunseo.kim/ko/posts/how-to-support-multi-language-on-jekyll-blog-with-polyglot-1/”.

Sitemap

Si aucun modèle n’est spécifié séparément, le sitemap généré automatiquement par Jekyll lors du build ne prend pas en charge normalement les pages multilingues, donc créez un fichier sitemap.xml dans le répertoire racine et saisissez le contenu comme suit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
---
layout: content
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
{% for lang in site.languages -%}

  {% for node in site.pages %}
    {%- comment -%}<!-- very lazy check to see if page is in the exclude list - this means excluded pages are not gonna be in the sitemap at all, write exceptions as necessary -->{%- endcomment -%}
    {%- comment -%}<!-- Exclude redirects from sitemap -->{%- endcomment -%}
    {%- if node.redirect.to -%}
      {%- continue -%}
    {%- endif -%}
    {%- unless site.exclude_from_localization contains node.path -%}
      {%- comment -%}<!-- assuming if there's not layout assigned, then not include the page in the sitemap, you may want to change this -->{%- endcomment -%}
      {% if node.layout %}
        <url>
          <loc>
            {%- if lang == site.default_lang -%}
              {{ node.url | absolute_url }}
            {%- else -%}
              {{ node.url | prepend: lang | prepend: '/' | absolute_url }}
            {%- endif -%}
          </loc>
          {% if node.last_modified_at and node.last_modified_at != node.date -%}
          <lastmod>{{ node.last_modified_at | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
          {%- elsif node.date -%}
          <lastmod>{{ node.date | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
          {% endif -%}
          {% if site.default_lang -%}
          <xhtml:link rel="alternate" hreflang="{{site.default_lang}}" href="{{site.url}}{{node.url}}" />
          {%- endif -%}
          {% for lang in site.languages -%}
            {% if lang == site.default_lang -%}
              {%- continue -%}
            {%- endif %}
          <xhtml:link rel="alternate" hreflang="{{lang}}" href="{{site.url}}/{{lang}}{{node.url}}" />
          {%- endfor %}
        </url>
      {% endif %}
    {%- elsif site.default_lang -%}
        <url>
          <loc>{{ node.url | absolute_url }}</loc>
      {% if node.last_modified_at and node.last_modified_at != node.date -%}
          <lastmod>{{ node.last_modified_at | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
      {%- elsif node.date -%}
          <lastmod>{{ node.date | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
      {% endif -%}
        </url>
    {%- endunless -%}
  {% endfor %}

  {%- comment -%}<!-- This loops through all site collections including posts -->{%- endcomment -%}
  {% for collection in site.collections %}
    {% for node in site[collection.label] %}
      <url>
        <loc>
          {%- if lang == site.default_lang -%}
            {{ node.url | absolute_url }}
          {%- else -%}
            {{ node.url | prepend: lang | prepend: '/' | absolute_url }}
          {%- endif -%}
        </loc>
        {% if node.last_modified_at and node.last_modified_at != node.date -%}
        <lastmod>{{ node.last_modified_at | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
        {%- elsif node.date -%}
        <lastmod>{{ node.date | date: '%Y-%m-%dT%H:%M:%S%:z' }}</lastmod>
        {%- endif %}
        {% if site.default_lang -%}
        <xhtml:link rel="alternate" hreflang="{{site.default_lang}}" href="{{site.url}}{{node.url}}" />
        {%- endif -%}
        {% for lang in site.languages -%}
          {% if lang == site.default_lang -%}
            {%- continue -%}
          {%- endif %}
        <xhtml:link rel="alternate" hreflang="{{lang}}" href="{{site.url}}/{{lang}}{{node.url}}" />
        {%- endfor %}
      </url>
    {% endfor %}
  {% endfor %}

{%- endfor %}
</urlset>

Ajout d’un bouton de sélection de langue dans la barre latérale

(Mise à jour du 05.02.12025) J’ai amélioré le bouton de sélection de langue sous forme de liste déroulante.
J’ai créé le fichier _includes/lang-selector.html et saisi le contenu comme suit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<link rel="stylesheet" href="{{ '/assets/css/lang-selector.css' | relative_url }}">

<div class="lang-dropdown">
    <select class="lang-select" onchange="changeLang(this.value)" aria-label="Select Language">
    {%- for lang in site.languages -%}
        <option value="{% if lang == site.default_lang %}{{ page.url }}{% else %}/{{ lang }}{{ page.url }}{% endif %}"
                {% if lang == site.active_lang %}selected{% endif %}>
            {% case lang %}
            {% when 'ko' %}🇰🇷 한국어
            {% when 'en' %}🇺🇸 English
            {% when 'ja' %}🇯🇵 日本語
            {% when 'zh-TW' %}🇹🇼 正體中文
            {% when 'es' %}🇪🇸 Español
            {% when 'pt-BR' %}🇧🇷 Português
            {% when 'fr' %}🇫🇷 Français
            {% when 'de' %}🇩🇪 Deutsch
            {% else %}{{ lang }}
            {% endcase %}
        </option>
    {%- endfor -%}
    </select>
</div>

<script>
function changeLang(url) {
    window.location.href = url;
}
</script>

J’ai également créé le fichier assets/css/lang-selector.css et saisi le contenu comme suit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
 * Styles du sélecteur de langue
 * 
 * Définit les styles du menu déroulant de sélection de langue situé dans la barre latérale.
 * Prend en charge le mode sombre du thème et est optimisé pour les environnements mobiles.
 */

/* Conteneur du sélecteur de langue */
.lang-selector-wrapper {
    padding: 0.35rem;
    margin: 0.15rem 0;
    text-align: center;
}

/* Conteneur du menu déroulant */
.lang-dropdown {
    position: relative;
    display: inline-block;
    width: auto;
    min-width: 120px;
    max-width: 80%;
}

/* Élément d'entrée de sélection */
.lang-select {
    /* Styles de base */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    width: 100%;
    padding: 0.5rem 2rem 0.5rem 1rem;
    
    /* Police et couleurs */
    font-family: Lato, "Pretendard JP Variable", "Pretendard Variable", sans-serif;
    font-size: 0.95rem;
    color: var(--sidebar-muted);
    background-color: var(--sidebar-bg);
    
    /* Forme et interaction */
    border-radius: var(--bs-border-radius, 0.375rem);
    cursor: pointer;
    transition: all 0.2s ease;
    
    /* Ajout d'icône de flèche */
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 1rem;
}

/* Styles des emojis de drapeaux */
.lang-select option {
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
    padding: 0.35rem;
    font-size: 1rem;
}

.lang-flag {
    display: inline-block;
    margin-right: 0.5rem;
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
}

/* État de survol */
.lang-select:hover {
    color: var(--sidebar-active);
    background-color: var(--sidebar-hover);
}

/* État de focus */
.lang-select:focus {
    outline: 2px solid var(--sidebar-active);
    outline-offset: 2px;
    color: var(--sidebar-active);
}

/* Prise en charge du navigateur Firefox */
.lang-select:-moz-focusring {
    color: transparent;
    text-shadow: 0 0 0 var(--sidebar-muted);
}

/* Prise en charge du navigateur IE */
.lang-select::-ms-expand {
    display: none;
}

/* Prise en charge du mode sombre */
[data-mode="dark"] .lang-select {
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
}

/* Optimisation pour environnement mobile */
@media (max-width: 768px) {
    .lang-select {
        padding: 0.75rem 2rem 0.75rem 1rem;  /* Zone tactile plus grande */
    }
    
    .lang-dropdown {
        min-width: 140px;  /* Zone de sélection plus large sur mobile */
    }
}

Ensuite, dans le _includes/sidebar.html du thème Chirpy, j’ai ajouté trois lignes juste avant la classe “sidebar-bottom” comme suit pour que Jekyll charge le contenu du _includes/lang-selector.html précédemment écrit lors du build de la page.

1
2
3
4
5
6
7
  (précédent)...
  <div class="lang-selector-wrapper w-100">
    {%- include lang-selector.html -%}
  </div>

  <div class="sidebar-bottom d-flex flex-wrap align-items-center w-100">
    ...(suite)

Lecture complémentaire

Suite dans la Partie 2

Cet article est sous licence CC BY-NC 4.0 par l'auteur.