Tags auf mehrsprachigen Jekyll-Sites
Der Static Site Generator Jekyll speichert Tags gesammelt für die ganze Site. In einem früheren Beitrag Mehrsprachige Websites mit Jekyll hatte ich bereits beschrieben, wie sich mit Jekyll eine mehrsprachige Website realisieren lässt. Eine Sache, die noch fehlte, war die Unterstützung von Tags, also Schlag- bzw. Schlüsselwörtern für jede Sprache getrennt.
.
Anforderungen
Rubrikenseiten - also Seiten, die alle Beiträge für eine bestimmte Rubrik auflisten - pflege ich manuell, weil ohnehin eine Beschreibung zugefügt werden muss. Bei Tag-Seiten ziehe ich es vor, dass sie automatisch gepflegt werden.
Das Plug-in "jekyll-tagging"
De-facto-Standard für die Erzeugung von Tag-Seiten scheint das Plug-in jekyll-tagging zu sein. Meine Konfiguration dafür in _config.yml
sieht so aus:
tag_page_layout: tag_page
tag_page_dir: tags
tag_feed_layout: tag_feed
tag_feed_dir: tags
tag_permalink_style: pretty
Bemerkung: Tatsächlich muss das Plug-In von Jekyll auch noch geladen werden, was weitere Konfiguration erfordert. Dies fehlt hier, weil die hier gezeigte Lösung ohnehin auf andere Art funktioniert.
Für die Generierung der Tag-Seiten wird das Layout-Template _layout/tag_page.html
verwendet (Zeile 1). Geschrieben werden sollen sie in das Verzeichnis /tags/
(Zeile 2). In Zeile 3 und 4 werden RSS-Feeds pro Tag auf die gleiche Weise aktiviert und konfiguriert.
Schließlich wird noch in Zeile 5 der Link-Stil der Site gesetzt.
Das Template _layout/tag_page.html
sieht folgendermaßen aus:
---
layout: default
---
{% assign posts=site.tags[page.tag] | where: "lang", page.lang | where: "type", "posts" %}
<div class="col-md-8">
<span>{{ site.t[page.lang].tag }}: <i class="fa fa-tag"></i>{{ page.tag }}</span>
{% for post in posts %}
<article class="blog-post">
{% if post.image %}
<div class="blog-post-image">
<a href="{{post.url | prepend: site.baseurl}}">
<img src="{{post.image}}" alt="{{post.image_alt}}">
</a>
</div>
{% endif %}
<div class="blog-post-body">
<h2>
<a href="{{post.url | prepend: site.baseurl}}">{{ post.title }}</a>
</h2>
<div class="post-meta">
<span><i class="fa fa-clock-o"></i>{% include {{ page.lang }}/long_date.html param=post.date %}</span> {% if post.comments %} / <span><i
class="fa fa-comment-o"></i> <a href="#">{{ post.comments }}</a></span>
{% endif %}
</div>
<p>{{post.excerpt}}</p>
<div class="read-more">
<a href="{{post.url}}">Continue Reading</a>
</div>
</div>
</article>
{% endfor %}
</div>
Die einzig interessante Zeile ist Zeile 4. Der Hash site.tags
wird von Jekyll gefüllt. Das Attribut tag
wird als Schlüssel für den Zugriff auf den Hash verwendet, und die so ermittelten Dokumente werden danach noch nach Sprache und Dokumententyp gefiltert. Leider funktioniert das aber nicht.
Das erste Problem ist, dass jekyll-tagging
nichts über ein Dokumentenattribut lang
weiß, und es dementsprechend auch nicht setzen kann. Außerdem erzeugt das Plug-in nur eine Seite pro Tag, aber wir wollen eine Seite pro Sprache und Tag.
Wrapper Um jekyll-tagging
Einzige Lösung war die Entwicklung eines Wrappers um das Plug-in. Leider hatte ich bislang noch nie eine Zeile Ruby-Code verfasst. Aber die Aufgabe sah so simpel aus, dass ich es unbelastet von jeglichem Ruby-Know-How dennoch versuchte.
Ich beschloss die Konfigurations-Option ignore_tags
von jekyll-tagging
für meine Zwecke zu missbrauchen. Statt das Plug-in nur einmal aufzurufen, sollte der Wrapper es einmal für jede Sprache aufrufen, und dabei jedesmal eine leicht angepasste Konfiguration zu übergeben. Insbesondere der Wert von ignore_tags
sollte jedesmal auf die Liste von Tags, die für die aktuelle Sprache nicht existieren, gesetzt werden.
Grundgerüst für das Plug-in
Zunächste einmal muss eine Datei _plugins/ml_tagging.rb
erzeugt werden:
require 'jekyll/tagging'
module Jekyll
class MultiLangTagger < Tagger
@types = [:page, :feed]
def generate(site)
# Generate some pages.
end
end
end
Mein Wrapper-Plug-in heißt MultiLangTagger
und ist eine Unterklasse von Tagger
(Zeile 4), also der Generatorklasse, die von jekyll-tagging
definiert wird.
Zeile 6 sieht dubios aus. Sie ist eins-zu-eins aus der Elternklasse in jekyll-tagging
kopiert. Offensichtlich wird diese Variable nicht von der Elternklasse geerbt. Jemand mit etwas mehr Ruby-Know-How kann das wahrscheinlich erklären, ich leider nicht.
Die einzige Methode, die Generator-Plug-ins für Jekyll implementieren müssen, ist generate
, siehe Zeile 8. Ihr einziges Argument ist eine Instanz von Jekyll::Site
, mit deren Hilfe die Konfiguration, Seiten, Posts, Tags und so weiter abgefragt werden können.
Gruppierung von Tags
Zunächst müssen die Tags nach Sprachen gruppiert werden. Dazu wurde die Methode generate
wie folgt geändert:
def generate(site)
# Iteriere über alle Posts, und gruppiere nach Sprache.
for post in site.posts.docs do
lang = post.data['lang']
site.config['t'][lang]['tagnames'] =
{} unless site.config['t'][lang]['tagnames']
tagnames = site.config['t'][lang]['tagnames']
tags = post.data['tags']
for tag in tags do
slug = jekyll_tagging_slug(tag)
if tagnames[slug]
if tagnames[slug] && tagnames[slug] != tag
raise "Tag '%{tag1}' and tag '%{tag2}' will create the same filename. Change one of them!" % { :tag1 => tagnames[slug], :tag2 => tag }
end
else
tagnames[slug] = tag
end
end
end
end
site.posts.docs
ist ein Hash, der alle Blog-Beiträge enthält. Für jeden Beitrag wird die Sprache über das Attribut lang
ermittelt. Bei meiner Site ist dieses Attribut immer gesetzt, weshalb keine Überprüfung stattfindet, ob das Attribut existiert oder nicht.
In meiner Site-Konfiguration gibt es bereits einen Schlüssel t
, der die Übersetzungen für Zeichenketten-Konstanten in allen unterstützten Sprachen enthält. Die Tags wurden in Analogie zum Schlüssel catnames
für Rubriken in den Zeilen 5 bis 6 unter dem Schlüssel tagnames
ebenfalls hier abgeleigt.
Ab Zeile 9 wird über alle Tags für das aktuelle Dokument iteriert. In der nächsten Zeile wird das Tag mit der Hilfsfunktion jekyll_taging_slug()
in einen sicheren Dateinamen transformiert. Diese Funktion wird auch von jekyll-tagging
verwendet, um den Namen der Ausgabedatei zu bestimmen.
Für meine Site ist es wichtig, dass es eine eineindeutige Beziehung zwischen Tags und der entsprechenden Tag-Seite gibt. Die Zeilen 11 bis 17 stellen dies sicher. Dieser Schritt ist nicht wirklich notwendig, sondern eher eine Qualitätssicherungsmaßnahme, um eine konsistente Schreibweise für Tags sicherzustellen.
Ergebnis ist eine Datenstruktur, die von Ruby nach YAML übersetzt ungefähr so in _config.yml
aussähe:
t:
en:
dns: DNS
system-administration: "System Administration"
jekyll: Jekyll
development: Development
de:
dns: DNS
systemadministration: "Systemadministration"
jekyll: Jekyll
entwicklung: Entwicklung
Aufruf des Elternklassengenerators
Nachdem die Tags gruppiert sind, muss die Methode generate
für jede Sprache aufgerufen werdern, und zwar jedesmal mit einer leicht modifizierten Konfiguration. Dazu wird die Methode generate
des Wrapper-Plug-ins weiter erweitert:
saved_tag_page_dir = site.config['tag_page_dir']
saved_tag_feed_dir = site.config['tag_feed_dir']
for lang in site.config['t'].keys
site.config['tag_page_dir'] = '/' + lang + '/' + saved_tag_page_dir
site.config['tag_feed_dir'] = '/' + lang + '/' + saved_tag_feed_dir
site.config['ignored_tags'] = site.tags.keys - site.config['t'][lang]['tagnames'].values
super
end
Die Ausgabeverzeichnisse müssen sich für alle Sprachen unterscheiden. Andernfalls würden Tag-Seiten für Tags, die in mehreren Sprachen verwendet werden, sich jeweils überschreiben. In den Zeilen 1 und 2 werden zunächst die Originalwerte für die Ausgabeverzeichnisse aus der Konfiguration abgefragt und in Variablen abgelegt.
Danach wird über alle verfügbaren Sprachen iteriert, also die Schlüssel für den Hash-Wert t
. Für jede Sprache werden die Konfigurationsvariablen tag_page_dir
und tag_feed_dir
mit dem sprachspezifischen Wert überschreiben. Um die Dinge einfach zu halten, wird hier einfach der Sprachbezeichner dem ursprünglichen Wert vorangestellt.
Zeile 7 ist wichtig. jekyll-tagging
verwendet die Konfigurationsvariable ignored_tags
, um einzelne Tags zu ignorieren. Hier wird dieses Feature missbraucht, indem die Variable temporär mit einem Array befüllt wird, das die Differenzmenge zwischen allen auf der Site verwendeten Tags und den für die aktuelle Sprache verwendeten Tags enthält.
In Zeile 9 schließlich wird die Methode generate
der Basisklasse aus jekyll-tagging
aufgerufen, die das Erzeugen der Tag-Seiten implementiert.
An dieser Stelle vermasselte ein Showstopper das Konzeipt. jekyll-tagging
Version 1.0.1 hat einen Bug, aufgrunddessen der Mechanismus zum Ignorieren von Tags nicht funktioniert, siehe diesen Bug-Report auf GitHub für Einzelheiten.
Leider ist es mir nicht gelungen, den Bug durch Monkey-Patching aus jekyll-tagging
zu entfernen, und ich habe schließlich die Quelldatei von Hand geändert. In der Datei tagging.rb
in jekyll-tagging
muss die Methode active_tags
folgendermaßen aussehen:
def active_tags
return site.tags unless site.config["ignored_tags"]
site.tags.reject { |t| site.config["ignored_tags"].include? t }
end
Alternativ muss man warten, bis der Bug upstream gefixt wurde.
Ein Teil der Lösung fehlt allerdings noch immer. Auch die Tag-Seiten müssen ein Attribut lang
mit dem Code der jeweiligen Sprache aufweisen. jekyll-tagging
erlaubt jedoch leider keine Injizierung zusätzlicher Daten, weshalb wir das selber erledigen müssen:
for page in site.pages
if page.data['tag']
dir = Pathname(page.url).each_filename.to_a
lang = page.data['lang'] = dir[0]
description = site.config['t'][lang]['taglist']['description']
page.data['description'] = description % { :tag => page.data['tag'] }
end
end
Noch immer in der Methode generate
, iterieren wir jetzt über alle Dokumente vom Typ "page". In Ermangelung einer besseren Methode, um Tag-Seiten zu identifizieren, wird überprüft, ob das Dokument ein Attribut tag
besitzt. Falls ja, wird das erste Element des Dokumenten-URLs extrahiert und als Sprachkürzel ins Dokument injiziert. Wichtig: Um Pathname()
verwenden zu können muss am Anfang der Datei ein "require 'pathname'
" eingefügt werden.
Bei dieser Gelegenheit wird der den Tag-Seiten auch eine Beschreibung zugefügt. Die Beschreibung stammt aus einem sprachspezifischen String in _config.yml
. Diese Zeichenkette kann einen Platzhalter %{tag}
für das jeweilige Tag enthalten, der in Zeile 6 ersetzt wird.
Schließlich müssen noch die Konfigurationsvariablen auf ihren Originalwert zurückgesetzt werden, weil sie beim nächsten Aufruf erneut verwendet werden:
site.config['tag_page_dir'] = saved_tag_page_dir
site.config['tag_feed_dir'] = saved_tag_feed_dir
An dieser Stelle funktioniert die Implementierung mehr oder weniger korrekt. Die Methode generate
ruft generate
in jekyll-tagging
für jede Sprache separat und mit modifizierter Konfiguration auf. Dabei werden ebenfalls die Attribute lang
und description
sprachspezifisch in die generierten Seiten injiziert.
Weitere Optimierungen
Jekyll stellt offensichtlich fest, dass ein weiteres Generator-Plug-in geladen wurde, und ruft es nach unserem eigenen auf. Bei meiner Site steigt die Liquid-Template-Engine aber leider aus, wenn bei einer Seite oder einem Blog-Beitrag das Attribut lang
fehlt. Es muss also verhindert werden, dass das Plug-in ohne von uns gepatchte Konfiguration Seiten generiert.
Ich konnte allerdings keinen Weg finden, um Jekyll davon abzuhalten den Generator der Basisklasse aufzurufen. Nachdem unser Wrapper-Plug-in die Basismethode für jede Sprache aufgerufen hat, setze ich ignore_tags
einfach auf die Liste aller Tags:
site.config['ignored_tags'] = site.tags.keys
Die Basismethode wird jetzt zwar noch immer aufgerufen, generiert aber keine Seiten mehr, weil alle Tags ignoriert werden.
Ein weiteres Problem bestand darin, dass ich die Anzahl der Dokumente in einer bestimmten Sprache für jedes Tag bestimmen wollte. Dies habe ich mit Hilfe eines Hooks gelöst, der diese Zahlen nicht nur für Tags sondern auch für Rubriken berechnet und in der Site-Konfiguration ablegt.
Ein Download-Link für diese Datei `precompute.rb findet sich weiter unten.
Fragen
Wie erzeuge ich Links auf eine Tag-Seite?
So:
<a href="/{{ page.lang }}{{ tag | tag_url }}">{{% tag %}}</a>
Der Filter tag_url
wird von jekyll-tagging
bereitgestellt und liefert den URL der Tag-Seite. Aufgrund unserer Modifikationen muss aber das Sprachkürzel noch vorangestellt werden.
Wie lässt sich die Anzahl der Dokumente für ein Tag in einer Sprache ermitteln?
So:
{{ site.tagcounts[page.lang][tag] }}
Diese Zahl wird in precompute.rb
berechnet.
Wie lässt sich die Anzahl der Dokumente für eine Rubrik in einer Sprache ermitteln?
Genauso:
{{ site.catcounts[page.lang][category] }}
Diese Zahl wird in precompute.rb
berechnet.
Wie lassen sich sprachspezifische Tag-Clouds erzeugen?
Tag-Clouds? Hallo? Wir haben 2016!
Downloads
Links für alle relevanten Dateien:
- _plugins/ml_tagging.rb
- Wrapper-Plug-in für `jekyll-tagging`
- _plugins/precompute.rb
- Hook, mit dem die Anzahl der Dokumente pro Sprache errechnet wird
- _layouts/tag_page.html
- Template für Tag-Seiten
- _layouts/tag_feed.xml
- Template für Tag-Feeds
- _includes/feed.xml
- Include für alle Feeds
- _includes/tag-feeds.xml
- Include um alle Tag-Feeds für eine Sprache zu listen
Leave a comment