Post

Cómo implementar soporte multilingüe en un blog Jekyll con Polyglot (2) - Solución de problemas de fallos en la compilación del tema Chirpy y errores en la función de búsqueda

Presentamos el proceso de implementación del soporte multilingüe en un blog Jekyll basado en 'jekyll-theme-chirpy' utilizando el plugin Polyglot. Esta entrada es la segunda parte de la serie, que aborda la identificación y resolución de errores que surgen al aplicar Polyglot al tema Chirpy.

Cómo implementar soporte multilingüe en un blog Jekyll con Polyglot (2) - Solución de problemas de fallos en la compilación del tema Chirpy y errores en la función de búsqueda

Introducción

Hace aproximadamente 4 meses, a principios de julio del calendario holóceno 12024, implementé soporte multilingüe en este blog basado en Jekyll y alojado a través de Github Pages utilizando el plugin Polyglot. Esta serie comparte los errores encontrados durante el proceso de aplicación del plugin Polyglot al tema Chirpy, sus soluciones, y cómo escribir encabezados html y sitemap.xml considerando el SEO. La serie consta de 2 artículos, y este es el segundo artículo de la serie.

Requisitos

  • El resultado de la compilación (página web) debe proporcionar contenido separado por rutas de idioma (ej. /posts/ko/, /posts/ja/).
  • Para minimizar el tiempo y esfuerzo adicional requerido para el soporte multilingüe, el sistema debe reconocer automáticamente el idioma según la ruta local donde se encuentra el archivo markdown original (ej. /_posts/ko/, /_posts/ja/), sin necesidad de especificar manualmente las etiquetas ‘lang’ y ‘permalink’ en el YAML front matter.
  • El encabezado de cada página del sitio debe incluir las etiquetas meta Content-Language y hreflang alternativas adecuadas para cumplir con las directrices de SEO para búsquedas multilingües de Google.
  • El sitemap.xml debe incluir enlaces a todas las páginas en todos los idiomas soportados sin omisiones, y debe existir un único archivo sitemap.xml en la ruta raíz sin duplicados.
  • Todas las funcionalidades proporcionadas por el tema Chirpy deben funcionar correctamente en las páginas de cada idioma, o deben modificarse para que funcionen correctamente.
    • Funcionamiento correcto de ‘Recently Updated’ y ‘Trending Tags’
    • Sin errores durante el proceso de compilación utilizando GitHub Actions
    • Funcionamiento correcto de la función de búsqueda en la esquina superior derecha del blog

Antes de empezar

Este artículo es una continuación de la Parte 1, por lo que se recomienda leer el artículo anterior primero si aún no lo has hecho.

Solución de problemas (‘relative_url_regex’: target of repeat operator is not specified)

Después de completar los pasos anteriores, al ejecutar el comando bundle exec jekyll serve para probar la compilación, apareció un error que decía 'relative_url_regex': target of repeat operator is not specified y la compilación falló.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...(omitido)
                    ------------------------------------------------
      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)

...(omitido)

Después de buscar si se había reportado un problema similar, encontré exactamente el mismo problema ya registrado en el repositorio de Polyglot, junto con una solución.

En el archivo _config.yml del tema Chirpy que estoy utilizando en este blog, existe la siguiente sección:

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

El problema está en las funciones de expresiones regulares en el archivo site.rb de Polyglot, que no pueden procesar correctamente los patrones de globbing como "*.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="/es/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

Hay dos formas de resolver este problema:

1. Hacer un fork de Polyglot y modificar las partes problemáticas

En el momento de escribir este artículo (noviembre de 12024), la documentación oficial de Jekyll indica que la configuración exclude admite patrones de globbing de Ruby’s File.fnmatch para hacer coincidir múltiples entradas a excluir.

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

Es decir, el problema no está en el tema Chirpy sino en las funciones relative_url_regex() y absolute_url_regex() de Polyglot, por lo que la solución fundamental es modificarlas para que no generen errores.

Como este error aún no ha sido resuelto en Polyglot, siguiendo este artículo de blog y la respuesta en el problema de GitHub mencionado, se puede hacer un fork del repositorio de Polyglot y modificar las partes problemáticas de la siguiente manera:

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. Reemplazar los patrones de globbing en el archivo ‘_config.yml’ del tema Chirpy por nombres de archivo exactos

La solución ideal sería que este parche se incorporara al código principal de Polyglot. Sin embargo, hasta que eso suceda, habría que usar una versión bifurcada, lo que resulta engorroso ya que habría que mantenerse al día con las actualizaciones de Polyglot. Por eso, opté por un enfoque diferente.

Al revisar los archivos en la ruta raíz del repositorio del tema Chirpy que coinciden con los patrones "*.gem", "*.gemspec", "*.config.js", solo hay 3 archivos:

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

Por lo tanto, se puede modificar la sección exclude en el archivo _config.yml eliminando los patrones de globbing y reemplazándolos por los nombres exactos de los archivos:

1
2
3
4
5
6
7
8
9
exclude: # Modificado según 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

Modificación de la función de búsqueda

Después de completar los pasos anteriores, casi todas las funciones del sitio funcionaban según lo previsto. Sin embargo, descubrí tardíamente que la barra de búsqueda ubicada en la esquina superior derecha de las páginas con el tema Chirpy no indexaba las páginas en idiomas distintos al site.default_lang (inglés en el caso de este blog), y al realizar búsquedas en otros idiomas, mostraba resultados de páginas en inglés.

Para entender la causa, examinemos qué archivos están involucrados en la función de búsqueda y dónde se produce el problema.

‘_layouts/default.html’

Al revisar el archivo _layouts/default.html que forma la estructura de todas las páginas del blog, se puede ver que dentro del elemento <body> se cargan los contenidos de search-results.html y search-loader.html.

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">
        
        (...omitido...)

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

    (...omitido...)

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

‘_includes/search-result.html’

_includes/search-result.html configura el contenedor search-results para almacenar los resultados de búsqueda cuando se introduce una palabra clave en el campo de búsqueda.

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 es la parte central que implementa la búsqueda basada en la biblioteca Simple-Jekyll-Search. Este archivo contiene JavaScript que se ejecuta en el navegador del visitante para encontrar coincidencias con las palabras clave de entrada en el archivo de índice search.json y devolver enlaces a las publicaciones correspondientes como elementos <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 %}
]

Este archivo utiliza la sintaxis Liquid de Jekyll para definir un archivo JSON que contiene el título, URL, información de categorías y etiquetas, fecha de creación, un fragmento de las primeras 200 caracteres del contenido y el contenido completo de todas las publicaciones del sitio.

Estructura de funcionamiento de la búsqueda e identificación del problema

En resumen, la función de búsqueda en un sitio con tema Chirpy alojado en GitHub Pages funciona según el siguiente proceso:

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 --> [*]

Confirmé que Polyglot genera search.json para cada idioma de la siguiente manera:

  • /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

Por lo tanto, la parte que causa el problema es el “Search Loader”. El problema de que las páginas en idiomas distintos al inglés no se buscan se debe a que _includes/search-loader.html carga estáticamente solo el archivo de índice en inglés (/assets/js/data/search.json), independientemente del idioma de la página que se está visitando.

  • Sin embargo, a diferencia de los archivos en formato markdown o html, para los archivos JSON, el wrapper de Polyglot funciona para variables proporcionadas por Jekyll como post.title, post.content, etc., pero la función Relativized Local Urls parece no funcionar.
  • De manera similar, dentro de las plantillas de archivos JSON, no es posible acceder a las etiquetas liquid proporcionadas adicionalmente por Polyglot {{ site.default_lang }}, {{ site.active_lang }} más allá de las variables básicas proporcionadas por Jekyll, como confirmé durante las pruebas.

Por lo tanto, aunque valores como title, snippet, content en el archivo de índice se generan de manera diferente para cada idioma, el valor url devuelve la ruta básica sin considerar el idioma, y se debe agregar un procesamiento adecuado para esto en la parte “Search Loader”.

Solución del problema

Para resolver esto, se debe modificar el contenido de _includes/search-loader.html de la siguiente manera:

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

(...omitido...)

<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 }}',

(...omitido)
  • Modifiqué la parte {% capture result_elem %} para añadir el prefijo "/{{ site.active_lang }}" delante de la URL del post cargada desde el archivo JSON cuando site.active_lang (idioma de la página actual) y site.default_lang (idioma predeterminado del sitio) son diferentes.
  • De manera similar, modifiqué la parte <script> para comparar el idioma de la página actual con el idioma predeterminado del sitio durante el proceso de compilación, y asignar la ruta predeterminada (/assets/js/data/search.json) si son iguales, o la ruta correspondiente al idioma (por ejemplo, /ko/assets/js/data/search.json) si son diferentes, como search_path.

Después de hacer estas modificaciones y volver a compilar el sitio web, confirmé que los resultados de búsqueda se muestran correctamente para cada idioma.

{url} es un marcador de posición para el valor URL que se leerá del archivo JSON, no una URL en sí misma, por lo que Polyglot no lo reconoce como objetivo de localización y debe procesarse directamente según el idioma. El problema es que "/{{ site.active_lang }}{url}" sí se reconoce como URL, y aunque ya está localizado, Polyglot no lo sabe e intenta localizarlo nuevamente (por ejemplo, "/ko/ko/posts/example-post"). Para evitar esto, se especifica la etiqueta {% static_href %}.

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