Kanały RSS/Atom w Hugo

tech • 1921 słów • 10 minut czytania

Ta notatka została oznaczona jako wymagająca dopracowania: pulapki.
Zawartość wpisu może ulec zmianie, zatem zapraszam do ponownych odwiedzin w niedalekiej przyszłości :)

Mój statyczny blog już działa, choć wymaga jeszcze trochę poprawek. Jednym z ważniejszych elementów wymagających dopracowania są kanały RSS/Atom. To przecież wciąż jedyna słuszna metoda syndykacji i notyfikacji o zmianach na stronie. Hugo w standardzie wspiera i automatycznie generuje kanały RSS. Niestety domyślne ustawienia i założenia nie spełniają moich specyficznych potrzeb, więc nadeszła pora na dostosowanie… i generowanie własnego kanału ;)

RSS 2.0 i domyślny szablon

Hugo posiada wbudowany wewnętrznie szablon dla kanałów RSS 2.0 i podobnie jak domyślne ustawienia jest on wystarczający w większości przypadków użycia. Szablon ten można znaleźć w repo - rss.xml. Jest to dobra baza do ewentualnych modyfikacji i budowy własnego szablonu. Dopasowanie szablonu dla różnych rodzajów stron bazuje na standardowych regułach wyszukiwania szablonów w Hugo, więcej w dokumentacji - RSS Templates.

Dla niektórych jednym z głównych mankamentów domyślnego szablonu jest dołączanie podsumowań zamiast pełnej zawartości strony/wpisu w kanale. To łatwo można “naprawić” zamieniając .Summary na .Content w tej linijce:

<description>{{ .Summary | html }}</description>

Nie wiem, która opcja jest lepsza. Do tej pory używałem tylko streszczenia notek w kanałach, argumentując sobie to tym, że to tylko kanał do powiadomień o nowych wpisach zachęcający do odwiedzin docelowej strony.

W standardowym szablonie nie odpowiada mi przedstawianie autora wpisu za pomocą adresu email i jego nazwy, coś w stylu ja/na/malcom.pl (Malcom). Zdecydowanie wolałbym tylko nazwę/nick autora, jak w większości kanałów.

Innym mankamentem jest to, że główny kanał, ten podchodzący pod stronę główną (index.html), będzie “agregował” zmiany z całej witryny, ze wszytskich podstron. To może być problemem przy budowaniu bloga, gdzie główny kanał powinien ograniczać się do zawierania wpisów, ignorując inne “statyczne” elementy, sekcje i podstrony…

To wymaga kolejnych zmian w szablonie, zamiast iterowania po .RegularPages dla strony głównej (.IsHome) trzeba dodać filtrację - where .RegularPages "Section" "post".

Dodatkowe, inne kosmetyczne zmiany musiałbym wprowadzić w nazwie i opisie kanału, co by zależnie od kategorii zawierało odpowiednie informacje, lepiej odzwierciedlające pochodzenie kanału i moje widzimisię w kategoryzacji ;)

Generowanie wybranych kanałów

Hugo zgodnie z dokumentacją (Default Output Formats) domyślnie generuje kanały RSS dla kilku rodzajów stron - głównej (home) i listujących (section, taxonomy, …).

Ja chcę mieć tylko 3 kanały ściśle odwozorowujące moją kategoryzację notatek, czyli strony głównej ze wszystkimi wpisami oraz tych pochodzących z kategorii tech i life.

To można łatwo osiągnąć nadpisując domyślne ustawienia generowanych formatów w konfiguracji:

outputs:
  home: [ HTML, RSS ]
  page: HTML
  section: HTML
  taxonomy: HTML
  term: HTML

Co skutkuje wyłączeniem RSS-a wszędzie prócz strony głównej.

Włączenie kanałów dla wybranych kategorii mogę zrobić wprost z “Branch Bundle” tych kanałów, przykład dla tech:

---
title: technikalia
linkTitle: tech
description: technika w czystej postaci, wpisy nie tylko o hackowaniu, programowaniu i elektronice...
url: /tech/
outputs: [ html, rss ]
---

Ma to swoje zalety - tylko wybrane kategorie będą posiadały syndykację. Inaczej musiałbym włączyć dla wszystkich “taxonomy”, a potem usuwać z niepotrzebnych kategorii, tagów i pozostałych zdefiniowanych terms-ów…

Listowanie wszystkich kanałów

Fajnie też jakby wszystkie kanały były “listowane” na każdej stronie bloga, co przy automatycznym wykrywaniu (autodiscovery) kanałów wyłapie wszystkie dostępne niezalenie od miejsca przebywania odwiedzającego na stronie.

Normalnie w wielu szablonach dodawane są ręczne “linki” w nagłówku strony z głównym kanałem:

<link rel="alternate" type="application/rss+xml"
  href="{{ `/index.xml` | relURL }}" title="{{ .Site.Title }}" />

Czasami też za pomocą pętli po liście dostępnych alternatywnych formatów strony (.AlternativeOutputFormats) lub bezpośrednio tylko format RSS-a (.OutputFormats.Get "rss"). To jednak wylistuje kanały danej podstrony…

Co prawda mógłbym ręcznie dodać 3 “wpisy” z moimi kanałami na sztywno w <head/> i zapomnieć.

{{- $feeds := slice }}
{{- $feeds = $feeds | append (.GetPage "/") }}
{{- $feeds = $feeds | append (.GetPage "/categories/tech") }}
{{- $feeds = $feeds | append (.GetPage "/categories/life") }}

{{- range $feeds }}
	{{- $rss := .OutputFormats.Get "rss" -}}
	{{- if $rss }}
	<link rel="alternate" type="application/rss+xml" href="{{ $rss.RelPermalink }}"
		title="{{ .Site.Title }}{{ if not .IsHome }} - {{ .LinkTitle }}{{ end }}" />
	{{- else -}}
		{{- errorf `RSS feed for page "%s" is not available!` .File -}}
	{{- end }}
{{- end }}

Wolę jednak dodać trochę kodu jak wyżej, który pozwoli mi wyłapać ewentualne błędy, gdybym kiedyś zaczął coś zmieniać w ustawieniach i konfiguracji swojego bloga. Gdy kiedyś dorobię się testów, to pewnie kod ten się uprości.

Niestandardowe nazwy plików

Nazwą pliku z kanałem, niezależnie od opcji uglyURLs, zawsze jest index.xml, a ja preferowałbym feed.xml, rss.xml.

Do zmiany nazwy pliku należy przekonfigurować ustawienia formatu, w tym przypadku RSS:

outputFormats:
  RSS:
    mediatype: application/rss
    baseName: feed

A może by tak jeszcze zmienić rozszerzenie?

Zdecydowanie feed.rss byłby bardziej odpowiedni, gdybym chciał dorzucić dodatkowo Atom-a pod nazwą feed.atom lub coś innego. Do zmiany rozszerzenia (sufiksu) trzeba poprawić konfigurację media-type dla application/rss. O tym jak to zrobić będzie dalej przy definiowaniu własnego kanału na przykładzie Atom-a.

Kanał Atom

Gdy już okiełznałem generowanie kanałów RSS według moich preferencji, przyszła pora na kanały w formacie Atom.

Dodanie kanału Atom w Hugo wymaga stworzenia od zera nowego formatu wyjściowego, ale patrząc wyżej na wymagane zmiany w moim dostosowaniu RSS-a, wcale dużo więcej tej roboty z Atom-em nie będzie.

Na początek zdefiniowałem sobie w config.yaml nowy format i media-type:

# define a new ATOM output format
outputFormats:
  ATOM:
    name: ATOM
    baseName: feed
    mediaType: application/atom+xml

# define a new ATOM media type
mediaTypes:
  application/atom+xml:
    suffixes: [ atom ]

Pliki z kanałem w formacie Atom będą dostępnej pod nazwą feed.atom.

Włączyłem generowanie formatu ATOM dla wybranych typów stron. Dla głównej (home) w config.yaml:

outputs:
  home: [ HTML, RSS, ATOM ]

I wybranych kategoriach w dedykowanych plikach _index.md, dokładnie tak, jak to miało miejsce przy RSS-ach.

Dla szybkiego sprawdzenia, czy wszystko póki co śmiga, dodałem sobie linkowanie wszystkich alternatywnych formatów:

{{- range .AlternativeOutputFormats }}
	{{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` 
		.Rel .MediaType.Type .RelPermalink $.Title | safeHTML }}
{{- end }}

W wygenerowanym kodzie strony głównej pojawiły się poprawne wpisy:

<link rel="alternate" type="application/rss+xml" href="/feed.rss" title="MalLog" />
<link rel="alternate" type="application/atom+xml" href="/feed.atom" title="MalLog" />

W konsoli pojawiły się stosowne ostrzeżenia, bo nie zdefiniowano szablonów dla tych kanałów.

WARN 2021/02/23 15:57:36 found no layout file for "ATOM" for kind "home":
  You should create a template file which matches Hugo Layouts Lookup Rules for this combination.

Szablon podlega standardowym regułom dopasowania Hugo opisanym w dokumentacji. Ja chcę mieć jeden szablon dla wszystkich kanałów, zatem zgodnie z regułami, plik ten musi się nazywać list.atom, aby był wykorzystany przy renderowaniu głównego kanału (index) i kategorii (list).

Format Atoma oparty jest na XML-u i został opisany w dokumencie RFC 4287. Standard ten, w porównaniu do swoich poprzedników RSS-owych, zawiera wiele nowych elementów i funkcji. Niemniej mi bardzo zależy na prostocie, więc ograniczyłem się tylko do minimalnej implementacji najważniejszych i potrzebnych mi elementów.

Mój szablon z nagłówkiem kanału wygląda mniej więcej tak:

{{ printf `<?xml version="1.0" encoding="utf-8"?>` | safeHTML }}
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ .Site.LanguageCode }}">
{{- if .IsHome }}
	<title>{{ .Site.Title }}</title>
	<subtitle>{{ .Site.Params.SubTitle }}</subtitle>
{{- else }}
	<title>{{ .Site.Title }} - {{ .LinkTitle }}</title>
	<subtitle>{{ .Description }}</subtitle>
{{- end }}
	<generator uri="http://gohugo.io" version="{{ hugo.Version }}">Hugo</generator>
{{- with .OutputFormats.Get "ATOM" }}
	{{ printf `<link rel="self" type="%s" href="%s" />` .MediaType.Type .Permalink | safeHTML }}
{{- end }}
{{- range .AlternativeOutputFormats }}
	{{ printf `<link rel="alternate" type="%s" href="%s" />` .MediaType.Type .Permalink | safeHTML }}
{{- end }}
	<id>tag:{{ $tan }}:/{{ if not .IsHome }}{{ trim .RelPermalink "/" }}{{ end }}</id>
	<updated>{{ now.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
	<author>
		<name>{{ .Site.Author.name }}</name>
		<email>{{ .Site.Author.email }}</email>
		<uri>{{ .Site.Author.web }}</uri>
	</author>
</feed>

Opis chyba raczej nie jest potrzebny. Wszystkie wpisy w kanale Atom reprezentowane są poprzez elementy entry leżące bezpośrednio w feed. Je generuje w pętli po dostępnych dla danego kanału podstronach (postach) w taki sposób:

{{- $pages := where .Site.RegularPages "Section" "post" -}}
{{- if not .IsHome -}}
	{{- $pages = .RegularPages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
	{{- $pages = $pages | first $limit -}}
{{- end -}}

{{- range $pages }}
	<entry>
		<title>{{ .Title }}</title>
		<link rel="alternate" type="text/html" href="{{ .Permalink }}{{ $utm }}" />
		<id>tag:{{ $tan }}:{{ .File.ContentBaseName }}</id>
		<updated>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
		<summary type="html">{{ .Summary | htmlEscape | safeHTML }}</summary>
		<content type="html">{{ .Content | htmlEscape | safeHTML }}</content>
	</entry>
{{- end }}

Wpisy mogą zawierać samo streszczenie, pełną zawartość lub oba naraz (jak w przykładzie wyżej). Ja póki co pozostaję przy “zajawkach” i ograniczyłem się tylko do <summary/>, może kiedyś się to zmieni.

Większość tekstu, zawartości elementów w Atomie może zawierać czysty tekst, HTML lub XHTML, co specyfikowane jest przez atrybut type. Dla prostoty i czystości przekazu, wszędzie walę czysty tekst (tytuły, nazwy) a tylko kontent pakuję w oryginalny HTML. Chociaż u mnie .Summary generowane jest przez Hugo, a tam chyba też w standardzie jest czysty tekst z wyprutymi tagami HTML-a.

Zastanawiam się czy do linków nie doklejać utm, co by umożliwiało śledzenie wejść z kanałów w statystykach:

{{- $utm := "?utm_source=atom_feed" -}}

Ale na razie i być może docelowo, wrzuciłem pusty string do $utm.

Kolejna ciekawostką może być sposób generowania id. Według standardu jest on wymagany i powinien być unikalny dla kanału i globalnie, można tu wykorzystać jakiś rodzaj URI, często używany jest URL, tag lub URN.

Ja wybrałem tag, ale nie do końca podążam za RFC 4151, bo olałem wymaganą przez standard datę w taggingEntity, pozostawiając tylko authorityName, które jest domeną bloga:

{{- $tan := .Site.BaseURL |
	strings.TrimPrefix "http://" | strings.TrimPrefix "https://" -}}

Nie chciałem używać adresu bo wiadomo, że on tak jak tytuł (i slug) może się z czasem zmienić. Z drugiej strony mój identyfikator dokumentu - data-slug, używany do rozróżniania wpisów i referencji też może ulec zmianie, bo nie wyobrażam sobie, aby się rozłaził z tytułem lub kontentem po jakiś poprawkach, więc mam tutaj mały paradoks ;)

No i na tym etapie Hugo powinień już móc poprawnie wygenerować pliki z kanałem w formacie Atom. Jego poprawność można zawsze sprawdzić dostępnymi w sieci walidatorami (W3C Feed Validation Service).

Nie było tak ciężko!

Poprawny typ MIME

Niezalenie od wybranych kanałów, użytych nazw i rozszerzeń plików, warto się upewnić że będą one poprawnie serwowane przez serwer z odpowiednim dla nich typem MIME, by żyć w zgodzie z webowymi standardami.

Na serwerach z Apachem wystarczy dodać dyrektywą AddType z modułu mod_mime w pliku .htaccess:

AddType application/rss+xml  .rss
AddType application/atom+xml .atom

To jest proste, gdy kanały mają dedykowane rozszerzenia. Co jednak począć, gdy pozostawiono domyślne XML-e i pliki lecą jako standardowe application/xml, zamiast tych dla nich przeznaczonych?

Na szczęście da się to też bardzo łatwo zrobić wymuszając typ dla wybranych plików przez FilesMatch:

<FilesMatch rss.xml>
	ForceType application/rss+xml
</FilesMatch>

<FilesMatch atom.xml>
	ForceType application/atom+xml
</FilesMatch>

A także może jeszcze prościej za pomocą mod_rewrite-a:

RewriteEngine on
RewriteRule  rss.xml - [T=application/rss+xml]
RewriteRule atom.xml - [T=application/atom+xml]

W obu przypadkach można specyfikować nazwy plików bądź też bardziej rozbudowane wyrażenia regularne dla dopasowania specyficznych plików i ścieżek…

FeedBurner

Kanały w WordPressie, a przynajmniej te główne, miałem podpięte pod FeedBurner-a. Serwis ten oferuje łatwe zarzadzanie kanałem RSS i oczywiście co najważniejsze statystyki. Sam od dawana z niego korzystałem głównie tylko do sprawdzania ilości subskrypcji. Nawet kiedyś napisałem sobie jakąś wtyczkę korzystająca z API serwisu.

Najprostszą integracją jest podmiana linków do kanałów w szablonie na te wygenerowane przez FeedBurner-a.

Ja jednak chcę, aby oficjalnym punktem wejścia do kanałów były moje “naturalne” urle, a przy tym nie chcę tracić możliwości śledzenia potencjalnych nowych subskrybentów. Dlatego potrzebne jest przekierowanie… Ma to także swoją zaletę, że jak kiedyś oleję FB to dla subskrybentów będzie to niewidoczne.

Na serwerach z Apachem można wykorzystać mod_alias do prostego przekierowania w .htaccess:

Redirect /feed.rss http://feeds.feedburner.com/malcom/MalLog

Tylko, że takie proste przekierowanie nie spełni swojej roli. Wynika to z mechanizmu działania FeedBurner-a, który jest takim jakby proxy serwerem pobierającym kanały i udostepniającym je dalej. Bot FeedBurner-a musi pobrać źródłowy kanał, więc uda się pod podany adres na blogu, a tam zostanie odesłany pod adres swojego serwera… i tak w kółko…

Potrzebne jest przekierowanie warunkowe, które żądania pochodzące z serwisu FeedBurner będzie przepuszczać do źródłowego pliku na serwerze, a pozostałe przekierowywać. Identyfikacji bota można dokonać po nazwie:

RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} !FeedBurner
RewriteRule /feed.rss http://feeds.feedburner.com/malcom/MalLog [R,L]

Pułapki…

Todo ;)

W ramach podsumowania…

Ostatecznie zdecydowałem się tylko na syndykację w standardzie Atom. Nie ma sensu udostępniać dwóch formatów, a skoro Atom, jako następca pozbawiony wielu wad poczciwego RSS-a, to wybór wydaje się prosty ;)

Jeśli jednak dla kogoś nie jest to takie oczywiste, to artykuł Atom vs. RSS może jakoś pomoże podjąć decyzję ;)

Kanały mojego bloga dostępne są pod tymi adresami:

http://blog.malcom.pl/feed.atom
http://blog.malcom.pl/tech/feed.atom
http://blog.malcom.pl/life/feed.atom

Adresy te także powinny być automatycznie wykrywane przez czytniki na dowolnej stronie bloga.

Nadal “wypalam” kanały FeedBurnerem, więc oczywistością jest, że są one “podłączone” pod ten serwis. Usilnie zalecam jednak używanie tylko tych adresów podanych wyżej, które zawsze będą prowadzić do aktualnych lokalizacji kanałów.

Dla ciekawskich konfiguracji i tego jak to u mnie na MalLogu działa można będzie można znaleźć w repo bloga

Jest to najprostsza implementacja Atoma, która spełnia moje wymagania i jest ściśle dostosowany do mojej strony. Gdyby jednak ktoś szukał czegoś bardziej ogólnego, bardziej elastycznego rozwiązania out-of-the-box to może zerknąć do repo hugo-atom-feed, gdzie znajdzie szablonowy komponent do Hugo wspierający Atoma.

Jak już wspomniałem, Atom posiada duże możliwości wzbogacających kanał i przenoszone w nim treści, które zależnie od potrzeb można zaimplementować. W moim przypadku liczyła się prostota i wygoda, niezbędna do zachowania poprawnego działania syndykacji. Co nie zmienia faktu, że jeśli ktoś przekona mnie do jakiś kanałowych dodatków i funkcjonalności, które uznam za przydatne, to nie widzę przeszkód w ich implementacji…

Komentarze (0)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/