Jak dodać obsługę wielu języków w blogu Jekyll za pomocą Polyglot (3) – troubleshooting: błąd budowania motywu Chirpy i usterki wyszukiwania
Opisuję wdrożenie wielojęzyczności w blogu Jekyll opartym o jekyll-theme-chirpy z użyciem wtyczki Polyglot. To 3. część serii: identyfikacja przyczyn błędów przy integracji Polyglot z Chirpy oraz naprawa problemów z buildem i wyszukiwaniem.
Przegląd
Na początku lipca 12024, na tym blogu opartym o Jekyll i hostowanym przez GitHub Pages zastosowałem wtyczkę Polyglot, aby dodać obsługę wielu języków. Ta seria dzieli się bugami, które wystąpiły podczas stosowania Polyglot w motywie Chirpy, procesem ich naprawy oraz sposobem przygotowania nagłówka HTML i sitemap.xml z uwzględnieniem SEO. Seria składa się z 3 wpisów; czytany teraz tekst to trzecia część.
- Część 1: Zastosowanie wtyczki Polyglot & modyfikacje nagłówka html oraz sitemap
- Część 2: Implementacja przycisku wyboru języka & lokalizacja języka layoutu
- Część 3: Troubleshooting błędu budowania motywu Chirpy oraz błędów funkcji wyszukiwania (ten wpis)
Pierwotnie seria miała składać się z 2 części, jednak po późniejszych uzupełnieniach objętość znacznie wzrosła, więc przebudowałem ją do 3 części.
Wymagania
- Zbudowany wynik (strony WWW) musi dać się serwować z rozdzieleniem na ścieżki per język (np.
/posts/ko/,/posts/ja/). - Aby możliwie zminimalizować dodatkowy czas i wysiłek związany z wielojęzycznością, podczas budowania język powinien być rozpoznawany automatycznie na podstawie lokalnej ścieżki pliku (np.
/_posts/ko/,/_posts/ja/), bez ręcznego ustawiania tagówlangipermalinkw YAML front matter każdego pliku źródłowego. - Nagłówek każdej strony powinien zawierać właściwy meta tag Content-Language, alternatywne tagi
hreflangoraz link canonical, spełniając wytyczne Google SEO dla wyszukiwania wielojęzycznego. - Linki do stron dla każdej wersji językowej muszą być bez braków dostarczane przez
sitemap.xml, a samsitemap.xmlma istnieć tylko raz, w katalogu głównym (bez duplikatów). - Wszystkie funkcje dostarczane przez motyw Chirpy muszą działać poprawnie na stronach w każdym języku; a jeśli nie, trzeba je poprawić, aby działały.
- Poprawne działanie funkcji „Recently Updated” i „Trending Tags”
- Brak błędów podczas procesu build w GitHub Actions
- Poprawne działanie wyszukiwarki postów w prawym górnym rogu bloga
Zanim zaczniesz
Ten wpis jest kontynuacją części 1 i części 2, więc jeśli jeszcze ich nie czytałeś(-aś), polecam najpierw nadrobić poprzednie.
Troubleshooting (‘relative_url_regex’: target of repeat operator is not specified)
(+ aktualizacja 12025.10.08.) Ten błąd został naprawiony w Polyglot 1.11.
Po przejściu poprzednich kroków uruchomiłem testowy build poleceniem bundle exec jekyll serve, ale build zakończył się niepowodzeniem z błędem: '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
...(pominięto)
------------------------------------------------
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)
...(pominięto)
Sprawdziłem, czy ktoś zgłaszał podobny problem — i okazało się, że w repozytorium Polyglot istnieje już dokładnie to samo zgłoszenie oraz jest też rozwiązanie.
W pliku _config.yml używanego na tym blogu motywu Chirpy znajduje się m.in. taki fragment:
1
2
3
4
5
6
7
8
9
exclude:
- "*.gem"
- "*.gemspec"
- docs
- tools
- README.md
- LICENSE
- "*.config.js"
- package*.json
Źródło problemu polega na tym, że wyrażenia regularne w dwóch funkcjach w pliku Polyglot site.rb nie potrafią poprawnie przetworzyć wzorców globbing zawierających wildcardy, takich jak "*.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="/pl/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
Są dwa sposoby rozwiązania tego problemu.
1. Sfrokować Polyglot i poprawić problematyczny fragment
W momencie pisania tego tekstu (12024.11.) oficjalna dokumentacja Jekyll jasno mówi, że konfiguracja exclude wspiera wzorce globbing.
“This configuration option supports Ruby’s File.fnmatch filename globbing patterns to match multiple entries to exclude.”
To znaczy, że winny nie jest motyw Chirpy, tylko funkcje relative_url_regex() oraz absolute_url_regex() w Polyglot — więc fundamentalnym rozwiązaniem jest poprawienie ich tak, by nie generowały błędów.
W Polyglot ten bug nie był jeszcze naprawiony, ale jak wspomniałem wyżej, od Polyglot 1.11 problem jest rozwiązany. Gdy problem występował, rozwiązanie polegało na skorzystaniu z tego wpisu(strona już nie istnieje) oraz odpowiedzi w powyższym zgłoszeniu GitHub: należało sforkować repozytorium Polyglot i zmienić problematyczny fragment tak, jak poniżej, a następnie używać tej wersji zamiast upstreamowego Polyglot.
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. W pliku konfiguracyjnym ‘_config.yml’ motywu Chirpy zastąpić wzorce globbing konkretnymi nazwami plików
W praktyce najbardziej eleganckie i „kanoniczne” podejście to włączenie powyższej poprawki do głównego Polyglot. Jednak zanim to nastąpi, trzeba używać wersji sforkowanej — a wtedy za każdym razem, gdy upstream Polyglot podnosi wersję, robi się kłopotliwe śledzenie i wciąganie zmian, żeby niczego nie przegapić. Dlatego zastosowałem inną metodę.
Jeśli sprawdzić pliki w katalogu głównym repozytorium motywu Chirpy, to wzorcom "*.gem", "*.gemspec", "*.config.js" odpowiadają i tak tylko te 3 pliki:
jekyll-theme-chirpy.gemspecpurgecss.config.jsrollup.config.js
Wystarczy więc usunąć wzorce globbing z sekcji exclude w _config.yml i przepisać to tak jak poniżej — wtedy Polyglot jest w stanie to przetworzyć bez błędów.
1
2
3
4
5
6
7
8
9
exclude: # Zmodyfikowane w oparciu o 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
Poprawka funkcji wyszukiwania
Po wykonaniu powyższych kroków niemal wszystkie funkcje witryny działały satysfakcjonująco i zgodnie z zamierzeniem. Jednak później odkryłem problem: pasek wyszukiwania w prawym górnym rogu stron z motywem Chirpy nie indeksował stron w językach innych niż site.default_lang (w moim przypadku angielski). Co więcej, nawet jeśli wyszukiwać na stronach w innym języku, wyniki zwracały linki do stron angielskich.
Aby zrozumieć przyczynę, zobaczmy, które pliki biorą udział w wyszukiwaniu i gdzie powstaje problem.
‘_layouts/default.html’
Gdy zajrzeć do pliku _layouts/default.html, który buduje szkielet wszystkich stron bloga, widać, że wewnątrz elementu <body> ładowane są treści search-results.html oraz 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">
(...pominięto...)
{% 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>
(...pominięto...)
{% include_cached search-loader.html lang=lang %}
</body>
‘_includes/search-result.html’
Plik _includes/search-result.html konfiguruje kontener search-results, w którym przechowywane są wyniki wyszukiwania dla wpisanego słowa kluczowego.
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’
Plik _includes/search-loader.html to kluczowa część implementacji wyszukiwania opartej o bibliotekę Simple-Jekyll-Search. Widać tu, że JavaScript po stronie przeglądarki (Client-Side) przeszukuje plik indeksu search.json (poniżej w sekcji /assets/js/data/search.json) i zwraca dopasowane posty jako elementy <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 %}
]
To plik JSON tworzony z użyciem składni Liquid Jekyll, definiujący: tytuł, URL, kategorie i tagi, datę, pierwsze 200 znaków jako snippet oraz pełną treść dla wszystkich postów w serwisie.
Jak działa wyszukiwanie i gdzie powstaje problem
Podsumowując: przy hostowaniu motywu Chirpy na GitHub Pages wyszukiwanie działa w takim procesie.
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 --> [*]
W tym miejscu potwierdziłem, że search.json jest generowany przez Polyglot osobno dla każdego języka, np.:
/assets/js/data/search.json/ko/assets/js/data/search.json/ja/assets/js/data/search.json/zh-TW/assets/js/data/search.json/es/assets/js/data/search.json/pt-BR/assets/js/data/search.json/fr/assets/js/data/search.json/de/assets/js/data/search.json
Zatem winny jest element „Search Loader”. Problem polega na tym, że _includes/search-loader.html statycznie ładuje wyłącznie angielski plik indeksu (/assets/js/data/search.json), niezależnie od języka aktualnie oglądanej strony — przez co strony w innych językach nie są przeszukiwane.
- Natomiast w przypadku plików JSON (w odróżnieniu od Markdown/HTML) wrapper Polyglot dla zmiennych Jekyll takich jak
post.title,post.contentdziała, ale wygląda na to, że funkcja Relativized Local Urls nie działa.- Podobnie, w testach potwierdziłem, że w szablonie JSON nie da się odwołać do tagów Liquid dostarczanych przez Polyglot ponad standardowe zmienne Jekyll:
{{ site.default_lang }},{{ site.active_lang }}.W efekcie wartości
title,snippet,contentsą generowane inaczej per język, aleurlzwraca bazową ścieżkę bez uwzględnienia języka — i trzeba to odpowiednio obsłużyć w „Search Loader”.
Rozwiązanie
Aby to naprawić, wystarczy zmienić _includes/search-loader.html tak jak poniżej.
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 %}
(...pominięto...)
<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 }}',
(...pominięto)
- Jeśli
site.active_lang(język bieżącej strony) różni się odsite.default_lang(domyślny język witryny), zmodyfikowałem część Liquid w{% capture result_elem %}, aby do URL posta wczytanego z JSON doklejać prefix"/{{ site.active_lang }}". - Analogicznie, w części
<script>build porównuje język bieżącej strony z domyślnym językiem witryny: jeśli są równe, używany jest domyślny indeks (/assets/js/data/search.json); jeśli nie — ustawiany jestsearch_pathna ścieżkę dla danego języka (np./ko/assets/js/data/search.json).
Po tej zmianie i ponownym zbudowaniu witryny potwierdziłem, że wyniki wyszukiwania wyświetlają się poprawnie dla każdego języka.
{url}nie jest poprawnym URL na etapie builda — to miejsce, w które JS podczas wyszukiwania wstawi URL odczytany z pliku JSON. Polyglot nie traktuje tego więc jako obiektu do lokalizacji, dlatego trzeba to obsłużyć ręcznie. Problem w tym, że szablon"/{{ site.active_lang }}{url}"na etapie builda jest rozpoznawany jako względny URL; mimo że lokalizacja została już wykonana, Polyglot o tym „nie wie” i próbuje wykonać lokalizację ponownie (np."/ko/ko/posts/example-post"). Żeby temu zapobiec, jawnie użyłem taga{% static_href %}.
