Tags auf mehrsprachigen Jekyll-Sites
Jekyll speichert Tags
gesammelt für die ganze Site. In einem früheren Beitrag
<a href=
/multilang-mit-jekyll/>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.
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
eingefügt werden.require 'pathname'
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
blog comments powered by Disqus