Como suportar múltiplos idiomas em um blog Jekyll com Polyglot (2) - Solução de problemas de falha de compilação e erros na função de busca do tema Chirpy
Apresento o processo de implementação de suporte multilíngue em um blog Jekyll baseado no 'jekyll-theme-chirpy' usando o plugin Polyglot. Este post é o segundo da série e aborda a identificação e resolução de erros que ocorrem ao aplicar o Polyglot ao tema Chirpy.
Visão geral
Há cerca de 4 meses, no início de julho do ano 12024 do calendário holoceno, implementei o suporte multilíngue neste blog baseado em Jekyll e hospedado no Github Pages, utilizando o plugin Polyglot. Esta série compartilha os bugs encontrados durante a aplicação do plugin Polyglot ao tema Chirpy, o processo de solução e como escrever cabeçalhos HTML e sitemap.xml considerando SEO. A série consiste em 2 posts, e este é o segundo da série.
- Parte 1: Aplicando o plugin Polyglot & implementando tags hreflang alt, sitemap e botão de seleção de idioma
- Parte 2: Solucionando problemas de falha na compilação do tema Chirpy e erros na função de busca (este post)
Requisitos
- O resultado da compilação (páginas web) deve ser fornecido em caminhos separados por idioma (ex.
/posts/ko/
,/posts/ja/
). - Para minimizar o tempo e esforço adicionais necessários para o suporte multilíngue, o sistema deve reconhecer automaticamente o idioma com base no caminho local onde o arquivo markdown original está localizado (ex.
/_posts/ko/
,/_posts/ja/
), sem a necessidade de especificar manualmente as tags ‘lang’ e ‘permalink’ no YAML front matter de cada arquivo. - O cabeçalho de cada página do site deve incluir meta tags Content-Language apropriadas e tags alternativas hreflang para atender às diretrizes de SEO do Google para pesquisa multilíngue.
- O
sitemap.xml
deve incluir links para todas as páginas em todos os idiomas suportados, sem omissões, e o própriositemap.xml
deve existir apenas uma vez no caminho raiz, sem duplicações. - Todas as funcionalidades fornecidas pelo tema Chirpy devem funcionar normalmente em páginas de cada idioma, ou devem ser modificadas para funcionar corretamente.
- Funcionamento normal das funcionalidades ‘Recently Updated’ e ‘Trending Tags’
- Sem erros durante o processo de compilação usando GitHub Actions
- Funcionamento normal da função de busca de posts no canto superior direito do blog
Antes de começar
Este post é uma continuação da Parte 1, então recomendo ler o post anterior primeiro, caso ainda não o tenha feito.
Solução de problemas (‘relative_url_regex’: target of repeat operator is not specified)
Após concluir as etapas anteriores, ao executar o comando bundle exec jekyll serve
para testar a compilação, ocorreu um erro 'relative_url_regex': target of repeat operator is not specified
e a compilação falhou.
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)
Após pesquisar se problemas semelhantes já haviam sido relatados, encontrei exatamente o mesmo problema registrado no repositório do Polyglot, junto com uma solução.
No arquivo _config.yml
do tema Chirpy que estou usando neste blog, existe a seguinte configuração:
1
2
3
4
5
6
7
8
9
exclude:
- "*.gem"
- "*.gemspec"
- docs
- tools
- README.md
- LICENSE
- "*.config.js"
- package*.json
A causa do problema está nas expressões regulares das seguintes funções no arquivo site.rb
do Polyglot, que não conseguem processar corretamente padrões de globbing como "*.gem"
, "*.gemspec"
e "*.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="/pt-BR/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
Existem duas maneiras de resolver este problema:
1. Fazer um fork do Polyglot e modificar as partes problemáticas
No momento da escrita deste post (11.12024), a documentação oficial do Jekyll indica que a configuração exclude
suporta padrões de globbing para correspondência de arquivos.
“This configuration option supports Ruby’s File.fnmatch filename globbing patterns to match multiple entries to exclude.”
Ou seja, a causa do problema não está no tema Chirpy, mas nas funções relative_url_regex()
e absolute_url_regex()
do Polyglot, então a solução fundamental é modificá-las para evitar o problema.
Como o bug ainda não foi corrigido no Polyglot, podemos fazer um fork do repositório e modificar as partes problemáticas conforme sugerido neste post de blog e na resposta ao problema no GitHub:
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. Substituir os padrões de globbing por nomes de arquivos exatos no arquivo ‘_config.yml’ do tema Chirpy
A solução ideal seria que o patch acima fosse incorporado ao mainstream do Polyglot. No entanto, até que isso aconteça, seria necessário usar a versão com fork, o que pode ser inconveniente para acompanhar as atualizações do Polyglot. Por isso, optei por uma abordagem diferente.
Verificando os arquivos na raiz do repositório do tema Chirpy que correspondem aos padrões "*.gem"
, "*.gemspec"
e "*.config.js"
, encontrei apenas estes 3:
jekyll-theme-chirpy.gemspec
purgecss.config.js
rollup.config.js
Portanto, podemos modificar a seção exclude
no arquivo _config.yml
, removendo os padrões de globbing e substituindo-os pelos nomes exatos dos arquivos:
1
2
3
4
5
6
7
8
9
exclude: # Modificado com base na 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
Modificação da função de busca
Após concluir as etapas anteriores, quase todas as funcionalidades do site funcionavam conforme o esperado. No entanto, descobri tardiamente que a barra de busca localizada no canto superior direito das páginas com o tema Chirpy não indexava páginas em idiomas diferentes do site.default_lang
(inglês, no caso deste blog) e, ao realizar buscas em outros idiomas, exibia apenas resultados em inglês.
Para entender a causa, vamos examinar quais arquivos estão envolvidos na funcionalidade de busca e onde o problema ocorre.
‘_layouts/default.html’
Verificando o arquivo _layouts/default.html
, que define a estrutura de todas as páginas do blog, podemos ver que ele carrega os conteúdos de search-results.html
e search-loader.html
dentro do elemento <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">
(...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’
O arquivo _includes/search-result.html
cria o container search-results
para armazenar os resultados da busca quando uma palavra-chave é inserida:
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’
O arquivo _includes/search-loader.html
é a parte central que implementa a busca baseada na biblioteca Simple-Jekyll-Search. Ele executa JavaScript no navegador do visitante para encontrar correspondências entre a palavra-chave inserida e o conteúdo do arquivo de índice search.json
, retornando links para os posts correspondentes 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 arquivo usa a sintaxe Liquid do Jekyll para definir um arquivo JSON que contém o título, URL, informações de categorias e tags, data de criação, um snippet dos primeiros 200 caracteres do conteúdo e o conteúdo completo de todos os posts do site.
Estrutura de funcionamento da busca e identificação do problema
Resumindo, a funcionalidade de busca no tema Chirpy hospedado no GitHub Pages funciona através do seguinte processo:
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 --> [*]
Verifiquei que o arquivo search.json
é gerado pelo Polyglot para cada idioma da seguinte forma:
/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
Portanto, a parte problemática é o “Search Loader”. O problema de não encontrar páginas em idiomas diferentes do inglês ocorre porque o arquivo _includes/search-loader.html
carrega estaticamente apenas o arquivo de índice em inglês (/assets/js/data/search.json
), independentemente do idioma da página que está sendo visitada.
- No entanto, diferentemente dos arquivos markdown ou html, para arquivos JSON, o wrapper Polyglot funciona para variáveis fornecidas pelo Jekyll como
post.title
,post.content
, etc., mas a funcionalidade Relativized Local Urls parece não funcionar.- Da mesma forma, dentro dos templates de arquivos JSON, não é possível acessar as tags liquid adicionais fornecidas pelo Polyglot
{{ site.default_lang }}
,{{ site.active_lang }}
além das variáveis padrão do Jekyll, como confirmei durante o processo de teste.Portanto, valores como
title
,snippet
econtent
no arquivo de índice são gerados diferentemente para cada idioma, mas o valorurl
retorna o caminho padrão sem considerar o idioma, e um tratamento adequado deve ser adicionado à parte “Search Loader”.
Solução do problema
Para resolver isso, modifique o conteúdo de _includes/search-loader.html
da seguinte forma:
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)
- Modifiquei a sintaxe liquid na parte
{% capture result_elem %}
para adicionar o prefixo"/{{ site.active_lang }}"
antes da URL do post carregada do arquivo JSON quandosite.active_lang
(idioma da página atual) esite.default_lang
(idioma padrão do site) são diferentes. - Da mesma forma, modifiquei a parte
<script>
para comparar o idioma da página atual com o idioma padrão do site durante o processo de compilação e definirsearch_path
como o caminho padrão (/assets/js/data/search.json
) se forem iguais, ou como o caminho correspondente ao idioma (por exemplo,/ko/assets/js/data/search.json
) se forem diferentes.
Após essas modificações e reconstrução do site, confirmei que os resultados de busca são exibidos corretamente para cada idioma.
Como
{url}
é apenas um espaço reservado para o valor de URL que será lido do arquivo JSON, e não uma URL em si, o Polyglot não o reconhece como alvo de localização, então precisamos tratá-lo diretamente de acordo com o idioma. O problema é que"/{{ site.active_lang }}{url}"
é reconhecido como uma URL, e embora a localização já esteja concluída, o Polyglot não sabe disso e tenta realizar a localização novamente (por exemplo,"/ko/ko/posts/example-post"
). Para evitar isso, especifiquei a tag{% static_href %}
.