Post

Comment supporter le multilinguisme 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 'jekyll-theme-chirpy'. Ce billet est le deuxième de la série et traite de l'identification et de la résolution des erreurs survenues lors de l'application de Polyglot au thème Chirpy.

Comment supporter le multilinguisme 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 2024, j’ai ajouté le support multilingue à ce blog hébergé sur GitHub Pages en utilisant 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 pour écrire l’en-tête html et le sitemap.xml en tenant compte du SEO. La série se compose de deux articles, et celui que vous lisez est le deuxième de la série.

Exigences

  • Pouvoir fournir les résultats de la compilation (pages web) séparés par chemin linguistique (ex. /posts/ko/, /posts/ja/).
  • Pour minimiser le temps et l’effort supplémentaires nécessaires au support multilingue, pouvoir reconnaître automatiquement la langue en fonction du chemin local où se trouve le fichier markdown original (ex. /_posts/ko/, /_posts/ja/) lors de la compilation, sans avoir à spécifier manuellement les balises ‘lang’ et ‘permalink’ dans le YAML front matter de chaque fichier markdown écrit.
  • L’en-tête de chaque page du site doit inclure les balises meta Content-Language et hreflang alternatives appropriées pour répondre aux directives SEO de Google pour la recherche multilingue.
  • Pouvoir fournir tous les liens des pages supportant chaque langue sur le site sans omission dans 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 modifiées pour fonctionner correctement.
    • Fonctionnement normal des fonctionnalités ‘Recently Updated’, ‘Trending Tags’
    • Pas d’erreur lors du processus de compilation utilisant GitHub Actions
    • Fonctionnement normal de la fonction de recherche de posts en haut à droite du blog

Avant de commencer

Cet article fait suite à la partie 1, donc si vous ne l’avez pas encore lue, il est recommandé de la lire d’abord.

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

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

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 déjà été signalé, j’ai trouvé exactement le même problème enregistré dans le dépôt Polyglot, et une solution existait également.

Dans le fichier _config.yml du thème Chirpy appliqué à ce blog, il existe la clause suivante :

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

La cause du problème réside dans le fait que les expressions régulières des deux fonctions suivantes incluses dans le fichier site.rb de Polyglot ne traitent pas correctement les motifs de globbing incluant des jokers comme "*.gem", "*.gemspec", "*.config.js" ci-dessus.

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, modifier la partie problématique et l’utiliser

Au moment de la rédaction de cet article (novembre 2024), la documentation officielle de Jekyll indique que le paramètre exclude prend en charge l’utilisation de motifs de globbing de noms de fichiers.

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

En d’autres termes, la cause du problème ne réside pas dans le thème Chirpy mais dans les deux fonctions relative_url_regex() et absolute_url_regex() de Polyglot, donc la solution fondamentale consiste à les modifier pour qu’elles ne posent plus de problème.

Comme ce bug n’a pas encore été résolu dans Polyglot, vous pouvez forker le dépôt Polyglot en vous référant à cet article de blog et à la réponse donnée au problème GitHub précédent, puis modifier la partie problématique comme suit et l’utiliser à la place du 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 motifs de globbing par des noms de fichiers exacts dans le fichier de configuration ‘_config.yml’ du thème Chirpy

En réalité, la méthode orthodoxe et idéale serait que le correctif ci-dessus soit intégré au flux principal de Polyglot. Cependant, en attendant, il faudrait utiliser la version forkée à la place, ce qui serait fastidieux de suivre et refléter chaque mise à jour du flux amont de Polyglot sans les manquer. C’est pourquoi j’ai utilisé une autre méthode.

Si l’on vérifie les fichiers situés dans le chemin racine du projet dans le dépôt du thème Chirpy correspondant aux motifs "*.gem", "*.gemspec", "*.config.js", il n’y en a de toute façon que 3 :

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

Par conséquent, si l’on supprime les motifs de globbing dans la clause exclude du fichier _config.yml et qu’on les remplace comme suit, Polyglot pourra les traiter sans problème.

1
2
3
4
5
6
7
8
9
exclude: # Modifié en référence au problème 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 de manière satisfaisante comme prévu. Cependant, j’ai découvert tardivement qu’il y avait un problème : la barre de recherche située en haut à droite de la page appliquant 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 lors d’une recherche dans une langue autre que l’anglais, elle affichait également les pages en anglais dans les résultats de recherche.

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

‘_layouts/default.html’

En vérifiant le fichier _layouts/default.html qui constitue le cadre de toutes les pages du blog, on peut voir qu’il charge le contenu de search-results.html et search-loader.html à l’intérieur de 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">
        
        (...omission...)

        {% 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>

    (...omission...)

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

‘_includes/search-result.html’

_includes/search-result.html constitue le conteneur search-results pour stocker les résultats de recherche pour le mot-clé saisi dans la barre 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 centrale qui implémente la recherche basée sur la bibliothèque Simple-Jekyll-Search. On peut voir qu’elle fonctionne côté client en exécutant JavaScript dans le navigateur du visiteur pour trouver les parties correspondant au mot-clé saisi dans le contenu du fichier d’index search.json et renvoyer le lien du post correspondant sous forme d’élément <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 %}
]

Il définit un fichier JSON contenant le titre, l’URL, les informations de catégorie et de tags, la date de création, un extrait des 200 premiers caractères du contenu, et le contenu complet de tous les posts du site en utilisant la syntaxe Liquid de Jekyll.

Structure de fonctionnement de la fonction de recherche et identification de la partie problématique

En résumé, lors de l’hébergement du thème Chirpy sur GitHub Pages, la fonction de recherche fonctionne selon le processus suivant :

stateDiagram
  state "Modifications" as CH
  state "Début de la compilation" as BLD
  state "Création de search.json" as IDX
  state "Site Web statique" as DEP
  state "En test" as TST
  state "Chargeur de recherche" as SCH
  state "Résultats" as R
    
  [*] --> CH: Faire des modifications
  CH --> BLD: Commit & Push origin
  BLD --> IDX: jekyll build
  IDX --> TST: Compilation terminée
  TST --> CH: Erreur détectée
  TST --> DEP: Déploiement
  DEP --> SCH: Saisie de recherche
  SCH --> R: Retour des résultats
  R --> [*]

J’ai confirmé que search.json est créé par Polyglot 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

Par conséquent, la partie qui cause le problème est le “Chargeur de recherche”. Le problème de non-recherche des pages dans des langues autres que l’anglais se produit parce que _includes/search-loader.html charge statiquement uniquement le fichier d’index en anglais (/assets/js/data/search.json), indépendamment de la langue de la page actuellement visitée.

Par conséquent, bien que les valeurs telles que title, snippet, content dans le fichier d’index soient générées différemment pour chaque langue, la valeur url renvoie le chemin par défaut sans tenir compte de la langue, et un traitement approprié doit être ajouté dans la partie “Chargeur de recherche”.

Résolution du problème

Pour résoudre cela, 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 %}

(...omission...)

<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 omise)
  • J’ai modifié la partie liquid de {% 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) et site.default_lang (langue par défaut du site) sont différentes.
  • De la même manière, j’ai modifié la partie <script> pour désigner search_path comme le chemin par défaut (/assets/js/data/search.json) si la langue de la page actuelle et la langue par défaut du site sont identiques lors du processus de compilation, sinon le chemin correspondant à cette langue (par exemple, /ko/assets/js/data/search.json).

Après avoir effectué ces modifications et recompilé le site web, j’ai confirmé que les résultats de recherche s’affichent correctement pour chaque langue.

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

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