Post

Comment prendre en charge plusieurs langues sur un blog Jekyll avec Polyglot (2) - Résolution des problèmes de compilation du thème Chirpy et des erreurs de recherche

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 billet est le deuxième de la série, traitant de l'identification et de la résolution des erreurs survenues lors de l'application de Polyglot au thème Chirpy.

Comment prendre en charge plusieurs langues sur un blog Jekyll avec Polyglot (2) - Résolution des problèmes de compilation du thème Chirpy et des erreurs de recherche

Aperçu

Il y a environ 4 mois, début juillet 12024 du calendrier holocène, j’ai ajouté le support multilingue à ce blog hébergé sur GitHub Pages avec Jekyll en appliquant le plugin Polyglot. Cette série partage les bugs rencontrés lors de l’application du plugin Polyglot au thème Chirpy, leur processus de résolution, ainsi que la méthode de rédaction des en-têtes HTML et du sitemap.xml en tenant compte du référencement. La série se compose de deux articles, et celui que vous lisez est le deuxième.

Exigences

  • Le résultat de la compilation (pages web) doit être fourni avec des chemins distincts par langue (ex. /posts/ko/, /posts/ja/).
  • Pour minimiser le temps et l’effort supplémentaires nécessaires au support multilingue, le système doit reconnaître automatiquement la langue en fonction du chemin local où se trouve le fichier markdown original (ex. /_posts/ko/, /_posts/ja/), sans avoir à spécifier manuellement les balises ‘lang’ et ‘permalink’ dans le YAML front matter.
  • L’en-tête de chaque page du site doit inclure les balises méta Content-Language appropriées et les balises alternatives hreflang pour répondre aux directives de référencement de Google pour la recherche multilingue.
  • Le sitemap.xml doit fournir tous les liens vers les pages prenant en charge chaque langue sans omission, et le sitemap.xml lui-même doit exister uniquement dans le chemin racine, sans duplication.
  • Toutes les fonctionnalités fournies par le thème Chirpy doivent fonctionner normalement sur chaque page de langue, sinon elles doivent être modifiées pour fonctionner correctement.
    • Fonctionnement normal des fonctionnalités ‘Recently Updated’, ‘Trending Tags’
    • Absence d’erreurs lors du processus de compilation avec GitHub Actions
    • Fonctionnement normal de la fonction de recherche de posts en haut à droite du blog

Avant de commencer

Cet article fait suite à la première partie, donc si vous ne l’avez pas encore lue, je vous recommande de la lire d’abord.

Résolution de problèmes (‘relative_url_regex’: target of repeat operator is not specified)

Après avoir terminé les étapes précédentes, j’ai exécuté la commande bundle exec jekyll serve pour tester la compilation, mais elle a échoué avec l’erreur 'relative_url_regex': target of repeat operator is not specified.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...(début omis)
                    ------------------------------------------------
      Jekyll 4.3.4   Please append `--trace` to the `serve` command 
                     for any additional information or backtrace. 
                    ------------------------------------------------
/Users/yunseo/.gem/ruby/3.2.2/gems/jekyll-polyglot-1.8.1/lib/jekyll/polyglot/
patches/jekyll/site.rb:234:in `relative_url_regex': target of repeat operator 
is not specified: /href="?\/((?:(?!*.gem)(?!*.gemspec)(?!tools)(?!README.md)(
?!LICENSE)(?!*.config.js)(?!rollup.config.js)(?!package*.json)(?!.sass-cache)
(?!.jekyll-cache)(?!gemfiles)(?!Gemfile)(?!Gemfile.lock)(?!node_modules)(?!ve
ndor\/bundle\/)(?!vendor\/cache\/)(?!vendor\/gems\/)(?!vendor\/ruby\/)(?!en\/
)(?!ko\/)(?!es\/)(?!pt-BR\/)(?!ja\/)(?!fr\/)(?!de\/)[^,'"\s\/?.]+\.?)*(?:\/[^
\]\[)("'\s]*)?)"/ (RegexpError)

...(fin omise)

Après avoir recherché si un problème similaire avait été signalé, j’ai trouvé exactement le même problème déjà enregistré dans le dépôt Polyglot, avec une solution.

Le fichier _config.yml du thème Chirpy que j’utilise contient la syntaxe suivante :

1
2
3
4
5
6
7
8
9
exclude:
  - "*.gem"
  - "*.gemspec"
  - docs
  - tools
  - README.md
  - LICENSE
  - "*.config.js"
  - package*.json

Le problème vient des expressions régulières dans les deux fonctions du fichier site.rb de Polyglot qui ne traitent pas correctement les modèles de globbing contenant des caractères génériques comme "*.gem", "*.gemspec", "*.config.js".

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
    # a regex that matches relative urls in a html document
    # matches href="baseurl/foo/bar-baz" href="/fr/foo/bar-baz" and others like it
    # avoids matching excluded files.  prepare makes sure
    # that all @exclude dirs have a trailing slash.
    def relative_url_regex(disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          regex += "(?!#{x})"
        end
        @languages.each do |x|
          regex += "(?!#{x}\/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      %r{#{start}="?#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

    # a regex that matches absolute urls in a html document
    # matches href="http://baseurl/foo/bar-baz" and others like it
    # avoids matching excluded files.  prepare makes sure
    # that all @exclude dirs have a trailing slash.
    def absolute_url_regex(url, disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          regex += "(?!#{x})"
        end
        @languages.each do |x|
          regex += "(?!#{x}\/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      %r{(?<!hreflang="#{@default_lang}" )#{start}="?#{url}#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

Il existe deux façons de résoudre ce problème.

1. Forker Polyglot et modifier les parties problématiques

Au moment de la rédaction de cet article (11.12024), la documentation officielle de Jekyll indique que le paramètre exclude prend en charge les modèles de globbing pour les noms de fichiers.

“This configuration option supports Ruby’s File.fnmatch filename globbing patterns to match multiple entries to exclude.”

Le problème ne vient donc pas du thème Chirpy mais des fonctions relative_url_regex() et absolute_url_regex() de Polyglot, donc la solution fondamentale est de les modifier pour éviter ce problème.

Comme ce bug n’est pas encore résolu dans Polyglot, vous pouvez forker le dépôt Polyglot en vous référant à ce billet de blog et à la réponse dans l’issue GitHub mentionnée, puis modifier les parties problématiques comme suit et utiliser cette version au lieu de Polyglot original.

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
    def relative_url_regex(disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x})"
        end
        @languages.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x}\/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      %r{#{start}="?#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

    def absolute_url_regex(url, disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x})"
        end
        @languages.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x}\/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      %r{(?<!hreflang="#{@default_lang}" )#{start}="?#{url}#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

2. Remplacer les modèles de globbing par des noms de fichiers exacts dans le fichier ‘_config.yml’ du thème Chirpy

La méthode idéale serait que ce correctif soit intégré au flux principal de Polyglot. Mais en attendant, il faudrait utiliser une version forkée, ce qui peut être fastidieux car il faut suivre les mises à jour de Polyglot. J’ai donc opté pour une autre approche.

En examinant les fichiers à la racine du dépôt du thème Chirpy, on constate que les modèles "*.gem", "*.gemspec", "*.config.js" ne correspondent qu’à 3 fichiers :

  • jekyll-theme-chirpy.gemspec
  • purgecss.config.js
  • rollup.config.js

On peut donc supprimer les modèles de globbing dans la section exclude du fichier _config.yml et les remplacer comme suit pour que Polyglot puisse les traiter sans problème.

1
2
3
4
5
6
7
8
9
exclude: # Modifié en référence à l'issue https://github.com/untra/polyglot/issues/204
  # - "*.gem"
  - jekyll-theme-chirpy.gemspec # - "*.gemspec"
  - tools
  - README.md
  - LICENSE
  - purgecss.config.js # - "*.config.js"
  - rollup.config.js
  - package*.json

Modification de la fonction de recherche

Après avoir terminé les étapes précédentes, presque toutes les fonctionnalités du site fonctionnaient comme prévu. Cependant, j’ai découvert que la barre de recherche située en haut à droite de la page avec le thème Chirpy n’indexait pas les pages dans des langues autres que site.default_lang (l’anglais dans le cas de ce blog), et affichait des pages en anglais même lors de recherches dans d’autres langues.

Pour comprendre la cause, examinons les fichiers impliqués dans la fonction de recherche et où le problème se produit.

‘_layouts/default.html’

En examinant le fichier _layouts/default.html qui structure toutes les pages du blog, on constate qu’il charge le contenu de search-results.html et search-loader.html dans l’élément <body>.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  <body>
    {% include sidebar.html lang=lang %}

    <div id="main-wrapper" class="d-flex justify-content-center">
      <div class="container d-flex flex-column px-xxl-5">
        
        (...omis...)

        {% include_cached search-results.html lang=lang %}
      </div>

      <aside aria-label="Scroll to Top">
        <button id="back-to-top" type="button" class="btn btn-lg btn-box-shadow">
          <i class="fas fa-angle-up"></i>
        </button>
      </aside>
    </div>

    (...omis...)

    {% include_cached search-loader.html lang=lang %}
  </body>

‘_includes/search-result.html’

_includes/search-result.html crée le conteneur search-results pour stocker les résultats de recherche.

1
2
3
4
5
6
7
8
9
10
<!-- The Search results -->

<div id="search-result-wrapper" class="d-flex justify-content-center d-none">
  <div class="col-11 content">
    <div id="search-hints">
      {% include_cached trending-tags.html %}
    </div>
    <div id="search-results" class="d-flex flex-wrap justify-content-center text-muted mt-3"></div>
  </div>
</div>

‘_includes/search-loader.html’

_includes/search-loader.html est la partie essentielle qui implémente la recherche basée sur la bibliothèque Simple-Jekyll-Search. Elle exécute un JavaScript côté client qui trouve les correspondances avec les mots-clés saisis dans le fichier d’index search.json et renvoie les liens des articles correspondants sous forme d’éléments <article>.

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
{% capture result_elem %}
  <article class="px-1 px-sm-2 px-lg-4 px-xl-0">
    <header>
      <h2><a href="{url}">{title}</a></h2>
      <div class="post-meta d-flex flex-column flex-sm-row text-muted mt-1 mb-1">
        {categories}
        {tags}
      </div>
    </header>
    <p>{snippet}</p>
  </article>
{% endcapture %}

{% capture not_found %}<p class="mt-5">{{ site.data.locales[include.lang].search.no_results }}</p>{% endcapture %}

<script>
  {% comment %} Note: dependent library will be loaded in `js-selector.html` {% endcomment %}
  document.addEventListener('DOMContentLoaded', () => {
    SimpleJekyllSearch({
      searchInput: document.getElementById('search-input'),
      resultsContainer: document.getElementById('search-results'),
      json: '{{ '/assets/js/data/search.json' | relative_url }}',
      searchResultTemplate: '{{ result_elem | strip_newlines }}',
      noResultsText: '{{ not_found }}',
      templateMiddleware: function(prop, value, template) {
        if (prop === 'categories') {
          if (value === '') {
            return `${value}`;
          } else {
            return `<div class="me-sm-4"><i class="far fa-folder fa-fw"></i>${value}</div>`;
          }
        }

        if (prop === 'tags') {
          if (value === '') {
            return `${value}`;
          } else {
            return `<div><i class="fa fa-tag fa-fw"></i>${value}</div>`;
          }
        }
      }
    });
  });
</script>

‘/assets/js/data/search.json’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
layout: compress
swcache: true
---

[
  {% for post in site.posts %}
  {
    "title": {{ post.title | jsonify }},
    "url": {{ post.url | relative_url | jsonify }},
    "categories": {{ post.categories | join: ', ' | jsonify }},
    "tags": {{ post.tags | join: ', ' | jsonify }},
    "date": "{{ post.date }}",
    {% include no-linenos.html content=post.content %}
    {% assign _content = content | strip_html | strip_newlines %}
    "snippet": {{ _content | truncate: 200 | jsonify }},
    "content": {{ _content | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

Ce fichier utilise la syntaxe Liquid de Jekyll pour définir un fichier JSON contenant le titre, l’URL, les catégories et tags, la date de création, un extrait des 200 premiers caractères du contenu, et le contenu complet de tous les articles du site.

Structure de fonctionnement de la recherche et identification du problème

En résumé, la fonction de recherche sur GitHub Pages avec le thème Chirpy fonctionne selon le processus suivant :

stateDiagram
  state "Changes" as CH
  state "Build start" as BLD
  state "Create search.json" as IDX
  state "Static Website" as DEP
  state "In Test" as TST
  state "Search Loader" as SCH
  state "Results" as R
    
  [*] --> CH: Make Changes
  CH --> BLD: Commit & Push origin
  BLD --> IDX: jekyll build
  IDX --> TST: Build Complete
  TST --> CH: Error Detected
  TST --> DEP: Deploy
  DEP --> SCH: Search Input
  SCH --> R: Return Results
  R --> [*]

J’ai constaté que Polyglot génère search.json pour chaque langue comme suit :

  • /assets/js/data/search.json
  • /ko/assets/js/data/search.json
  • /es/assets/js/data/search.json
  • /pt-BR/assets/js/data/search.json
  • /ja/assets/js/data/search.json
  • /fr/assets/js/data/search.json
  • /de/assets/js/data/search.json

Le problème se situe donc dans le “Search Loader”. Les pages dans des langues autres que l’anglais ne sont pas trouvées car _includes/search-loader.html charge statiquement uniquement le fichier d’index anglais (/assets/js/data/search.json), quelle que soit la langue de la page visitée.

Par conséquent, les valeurs comme title, snippet, content dans le fichier d’index sont générées différemment selon la langue, mais la valeur url renvoie le chemin de base sans tenir compte de la langue, et un traitement approprié doit être ajouté dans la partie “Search Loader”.

Résolution du problème

Pour résoudre ce problème, il faut modifier le contenu de _includes/search-loader.html 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
{% capture result_elem %}
  <article class="px-1 px-sm-2 px-lg-4 px-xl-0">
    <header>
      {% if site.active_lang != site.default_lang %}
      <h2><a {% static_href %}href="/{{ site.active_lang }}{url}"{% endstatic_href %}>{title}</a></h2>
      {% else %}
      <h2><a href="{url}">{title}</a></h2>
      {% endif %}

(...omis...)

<script>
  {% comment %} Note: dependent library will be loaded in `js-selector.html` {% endcomment %}
  document.addEventListener('DOMContentLoaded', () => {
    {% assign search_path = '/assets/js/data/search.json' %}
    {% if site.active_lang != site.default_lang %}
      {% assign search_path = '/' | append: site.active_lang | append: search_path %}
    {% endif %}
    
    SimpleJekyllSearch({
      searchInput: document.getElementById('search-input'),
      resultsContainer: document.getElementById('search-results'),
      json: '{{ search_path | relative_url }}',
      searchResultTemplate: '{{ result_elem | strip_newlines }}',

(...suite)
  • J’ai modifié la syntaxe liquid dans la partie {% capture result_elem %} pour ajouter le préfixe "/{{ site.active_lang }}" devant l’URL du post chargée depuis le fichier JSON lorsque site.active_lang (langue de la page actuelle) est différente de site.default_lang (langue par défaut du site).
  • De même, j’ai modifié la partie <script> pour définir search_path comme le chemin par défaut (/assets/js/data/search.json) si la langue de la page actuelle est identique à la langue par défaut du site, ou comme le chemin correspondant à cette langue (par exemple, /ko/assets/js/data/search.json) si elles sont différentes.

Après ces modifications et une nouvelle compilation du site web, j’ai confirmé que les résultats de recherche s’affichent correctement pour chaque langue.

{url} est un emplacement où sera insérée la valeur URL lue depuis le fichier JSON, et non une URL en soi, donc Polyglot ne le reconnaît pas comme cible de localisation et doit être traité directement selon la langue. Le problème est que "/{{ site.active_lang }}{url}" est reconnu comme une URL, et bien que la localisation soit déjà terminée, Polyglot ne le sait pas et tente de la localiser à nouveau (par exemple, "/ko/ko/posts/example-post"). Pour éviter cela, j’ai spécifié la balise {% static_href %}.

This post is licensed under CC BY-NC 4.0 by the author.