Post

Cómo implementar soporte multilingüe en un blog Jekyll con Polyglot (1) - Aplicación del plugin Polyglot e implementación de etiquetas hreflang alt, sitemap y botón de selección de idioma

Se introduce el proceso de implementación de soporte multilingüe utilizando el plugin Polyglot en un blog Jekyll basado en "jekyll-theme-chirpy". Este post es el primero de la serie y cubre la aplicación del plugin Polyglot y la modificación del encabezado html y el sitemap.

Cómo implementar soporte multilingüe en un blog Jekyll con Polyglot (1) - Aplicación del plugin Polyglot e implementación de etiquetas hreflang alt, sitemap y botón de selección de idioma

Resumen

Hace unos 4 meses, a principios de julio de 2024, implementé soporte multilingüe en este blog basado en Jekyll y alojado a través de Github Pages aplicando el plugin Polyglot. Esta serie comparte los errores que ocurrieron en el proceso de aplicar el plugin Polyglot al tema Chirpy, el proceso de resolución, y cómo escribir el encabezado html y sitemap.xml considerando el SEO. La serie consta de 2 posts, y este que estás leyendo es el primero de la serie.

Requisitos

  • Debe ser posible proporcionar el resultado de la compilación (páginas web) separado por rutas de idioma (ej. /posts/ko/, /posts/ja/).
  • Para minimizar el tiempo y esfuerzo adicional requerido para el soporte multilingüe, debe ser capaz de reconocer automáticamente el idioma según la ruta local donde se encuentra el archivo (ej. /_posts/ko/, /_posts/ja/) durante la compilación, sin tener que especificar manualmente las etiquetas ‘lang’ y ‘permalink’ en el YAML front matter del archivo markdown original.
  • El encabezado de cada página del sitio debe incluir las etiquetas meta Content-Language y hreflang alternativas apropiadas para cumplir con las pautas de SEO para la búsqueda multilingüe de Google.
  • Debe ser posible proporcionar enlaces a todas las páginas que soportan cada idioma en el sitio sin omisiones en sitemap.xml, y el propio sitemap.xml debe existir solo uno en la ruta raíz sin duplicados.
  • Todas las funciones proporcionadas por el tema Chirpy deben funcionar normalmente en las páginas de cada idioma, y si no, deben modificarse para que funcionen correctamente.
    • Funcionamiento normal de ‘Recently Updated’, ‘Trending Tags’
    • No deben ocurrir errores durante el proceso de compilación usando GitHub Actions
    • Funcionamiento normal de la función de búsqueda de posts en la esquina superior derecha del blog

Aplicación del plugin Polyglot

Como Jekyll no admite blogs multilingües de forma nativa, se debe utilizar un plugin externo para implementar un blog multilingüe que cumpla con los requisitos anteriores. Después de buscar, encontré que Polyglot se usa ampliamente para implementar sitios web multilingües y puede satisfacer la mayoría de los requisitos anteriores, así que adopté este plugin.

Instalación del plugin

Como estoy usando Bundler, agregué lo siguiente al Gemfile:

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

Luego, ejecutar bundle update en la terminal completará automáticamente la instalación.

Si no estás usando Bundler, también puedes instalar la gema directamente con el comando gem install jekyll-polyglot en la terminal y luego agregar el plugin a _config.yml de la siguiente manera:

1
2
plugins:
  - jekyll-polyglot

Configuración

A continuación, abre el archivo _config.yml y agrega lo siguiente:

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"]
parallel_localization: false
lang_from_path: true
  • languages: Lista de idiomas que deseas soportar
  • default_lang: Idioma predeterminado de fallback
  • exclude_from_localization: Especifica expresiones regulares de cadenas de ruta de archivos/carpetas raíz a excluir de la localización
  • parallel_localization: Valor booleano que especifica si paralelizar el procesamiento multilingüe durante la compilación
  • lang_from_path: Valor booleano, si se establece en ‘true’, reconocerá y usará automáticamente el código de idioma si la cadena de ruta del archivo markdown contiene el código de idioma, incluso si no se especifica explícitamente el atributo ‘lang’ en el YAML front matter dentro del archivo markdown

La documentación oficial del protocolo Sitemap establece lo siguiente:

“La ubicación de un archivo Sitemap determina el conjunto de URLs que se pueden incluir en ese Sitemap. Un archivo Sitemap ubicado en http://example.com/catalog/sitemap.xml puede incluir cualquier URL que comience con http://example.com/catalog/ pero no puede incluir URLs que comiencen con http://example.com/images/.”

“Se recomienda encarecidamente que coloque su Sitemap en el directorio raíz de su servidor web.”

Para cumplir con esto, debes agregar ‘sitemap.xml’ a la lista ‘exclude_from_localization’ para asegurarte de que exista un solo archivo sitemap.xml en el directorio raíz y no se creen archivos sitemap.xml con el mismo contenido para cada idioma, como en el siguiente ejemplo incorrecto.

Ejemplo incorrecto (el contenido de cada archivo es idéntico, no diferente por idioma):

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

(Actualizado el 14.01.2025) Después de que se aceptara la Pull Request que presenté reforzando el contenido mencionado anteriormente en el README, ahora se puede encontrar la misma guía en la documentación oficial de Polyglot.

Establecer ‘parallel_localization’ en ‘true’ tiene la ventaja de reducir significativamente el tiempo de compilación, pero a partir de julio de 2024, cuando activé esta función para este blog, había un error donde los títulos de los enlaces en las secciones ‘Recently Updated’ y ‘Trending Tags’ de la barra lateral derecha de la página no se procesaban correctamente y se mezclaban con otros idiomas. Parece que aún no está completamente estabilizado, así que si planeas aplicarlo a tu sitio, es necesario probarlo previamente para asegurarse de que funcione correctamente. Además, esta función no es compatible con Windows, por lo que debe desactivarse si se usa Windows.

Además, en Jekyll 4.0, debes desactivar la generación de sourcemaps CSS de la siguiente manera.

1
2
sass:
  sourcemap: never # En Jekyll 4.0, los sourcemaps SCSS se generarán incorrectamente debido a cómo opera Polyglot

Precauciones al escribir posts

Los puntos a tener en cuenta al escribir posts multilingües son los siguientes:

  • Especificación adecuada del código de idioma: Debes especificar el código de idioma ISO apropiado utilizando la ruta del archivo (ej. /_posts/ko/example-post.md) o el atributo ‘lang’ en el YAML front matter (ej. lang: ko). Consulta los ejemplos en la documentación para desarrolladores de Chrome.

Sin embargo, aunque la documentación para desarrolladores de Chrome muestra los códigos de región en el formato ‘pt_BR’, en realidad debes usar ‘-‘ en lugar de ‘_’, como ‘pt-BR’, para que funcione correctamente al agregar etiquetas hreflang alternativas al encabezado html más adelante.

  • Las rutas y nombres de los archivos deben ser consistentes.

Para más detalles, consulta el README del repositorio GitHub untra/polyglot.

Modificación del encabezado html y sitemap

Ahora debemos insertar etiquetas meta Content-Language y etiquetas hreflang alternativas en el encabezado html de cada página del blog para SEO.

Encabezado html

A partir de la versión 1.8.1, la última versión a noviembre de 2024, Polyglot tiene una función que realiza automáticamente esta tarea cuando se llama a la etiqueta Liquid {% I18n_Headers %} en la sección del encabezado de la página. Sin embargo, esto asume que se ha especificado explícitamente el atributo ‘permalink’ en esa página, y no funcionará correctamente si no es así.

Por lo tanto, tomé el head.html del tema Chirpy y agregué directamente el siguiente contenido. Me basé en la página SEO Recipes del blog oficial de Polyglot, pero lo modifiqué para usar el atributo page.url en lugar de page.permalink cuando este último no está disponible.

1
2
3
4
5
6
  <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 %}

Sitemap

Como el sitemap generado automáticamente por Jekyll durante la compilación no admite correctamente las páginas multilingües, crea un archivo sitemap.xml en el directorio raíz e ingresa el siguiente contenido:

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
---
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 %}<!-- verificación muy perezosa para ver si la página está en la lista de exclusión - esto significa que las páginas excluidas no estarán en el sitemap en absoluto, escriba excepciones según sea necesario -->{% endcomment %}
        {% unless site.exclude_from_localization contains node.path %}
            {% comment %}<!-- asumiendo que si no hay diseño asignado, entonces no incluya la página en el sitemap, es posible que desee cambiar esto -->{% 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 %}
                </url>
            {% endif %}
        {% endunless %}
    {% endfor %}

    {% comment %}<!-- Esto recorre todas las colecciones del sitio, incluidos los 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 %}
            </url>
        {% endfor %}
    {% endfor %}

{% endfor %}
</urlset>

Agregar botón de selección de idioma a la barra lateral

(Actualizado el 05.02.2025) He mejorado el botón de selección de idioma a un formato de lista desplegable.
Crea el archivo _includes/lang-selector.html e ingresa el siguiente contenido:

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="Seleccionar idioma">
    {%- 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>

Además, crea el archivo assets/css/lang-selector.css e ingresa el siguiente contenido:

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
/**
 * Estilos del selector de idioma
 * 
 * Define los estilos para el desplegable de selección de idioma ubicado en la barra lateral.
 * Soporta el modo oscuro del tema y está optimizado para entornos móviles.
 */

/* Contenedor del selector de idioma */
.lang-selector-wrapper {
    padding: 0.35rem;
    margin: 0.15rem 0;
    text-align: center;
}

/* Contenedor del desplegable */
.lang-dropdown {
    position: relative;
    display: inline-block;
    width: auto;
    min-width: 120px;
    max-width: 80%;
}

/* Elemento de entrada de selección */
.lang-select {
    /* Estilos básicos */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    width: 100%;
    padding: 0.5rem 2rem 0.5rem 1rem;
    
    /* Fuente y color */
    font-family: Lato, "Pretendard JP Variable", "Pretendard Variable", sans-serif;
    font-size: 0.95rem;
    color: var(--sidebar-muted);
    background-color: var(--sidebar-bg);
    
    /* Forma e interacción */
    border-radius: var(--bs-border-radius, 0.375rem);
    cursor: pointer;
    transition: all 0.2s ease;
    
    /* Agregar icono de flecha */
    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;
}

/* Estilos de emojis de banderas */
.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;
}

/* Estado hover */
.lang-select:hover {
    color: var(--sidebar-active);
    background-color: var(--sidebar-hover);
}

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

/* Soporte para Firefox */
.lang-select:-moz-focusring {
    color: transparent;
    text-shadow: 0 0 0 var(--sidebar-muted);
}

/* Soporte para IE */
.lang-select::-ms-expand {
    display: none;
}

/* Soporte para modo oscuro */
[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");
}

/* Optimización para entornos móviles */
@media (max-width: 768px) {
    .lang-select {
        padding: 0.75rem 2rem 0.75rem 1rem;  /* Área táctil más grande */
    }
    
    .lang-dropdown {
        min-width: 140px;  /* Área de selección más amplia en móviles */
    }
}

Luego, agrega las siguientes tres líneas justo antes de la clase “sidebar-bottom” en el _includes/sidebar.html del tema Chirpy para que Jekyll cargue el contenido de _includes/lang-selector.html que escribimos anteriormente durante la compilación de la página:

1
2
3
4
5
6
7
  (estrategia previa)...
  <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">
    ...(estrategia posterior)

Lectura adicional

Continúa en la Parte 2

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