Ch-ch-ch-ch-changes
Tim Wienk —
Mijn (nieuwe) website is ondertussen een paar maanden online, ik ben redelijk tevreden met de uitkomst en wilde daarom uitleggen hoe deze website tot stand is gekomen, met aandacht voor de redenen en technische details van een aantal beslissingen.
Curriculum vitae
Zo nu en dan wordt je weer eens herinnerda an je cv, soms door jezelf en soms omdat andere mensen om je heen op zoek zijn naar een baan en/of zelf hun cv bijwerken. In de meeste gevallen besluit ik dat ik mijn cv ook moet bijwerken maar doe dat uiteindelijk toch niet. Echter, deze keer, misschien omdat er veel dingen om mijn heen (gaan) veranderen de komende tijd, is het me toch eindelijk gelukt.
Mijn verouderde CV was achteraf niet eens zo slecht als ik had verwacht. Wat ik vond was een document met een half afgeronde LaTeX-lay-out, met het standaard (niet erg mooie) Latin Modern-lettertype, en werkervaring totaan een aantal jaren gelden. Na het herschrijven van een aantal delen van de cv, heb ik de lay-out afgerond en de inhoud ook verder bijgewerkt.
Als volgende stap wilde ik mijn CV op mijn persoonlijke website beschikbaar maken om er ook snel bij te kunnen. Goed idee, behalve dat mijn website er nogal... "oud" uit zag. Het bevatte nauwelijks nuttige informatie en zag er nog niet eens fatsoenlijk uit.
Mijn vorige website was oorspronkelijk gebouwd met Mark "keeto" Obcena's Raccoon framework, wat erg interessant was op dat moment, maar na een aantal serververhuizingen, is mijn website uitgekleed en werd het aangeboden via een oude testinstallatie van het platform dat we ontwikkelen bij het bedrijf waar ik werk: niet ideaal.
Gezien ik nu bij was met mijn "nieuwe" cv, besloot ik om dat als basisconcept te gebruiken voor een nieuwe website. De website hoefde er niet geweldig uit te zien, maar het moest er wel redelijk uitzien, simpel zijn en als belangrijkste zorgenvrij.
Zorgenvrij
Vanwege mijn historie met het beheren van mijn eigen website, wilde ik dat mijn nieuwe website simpel zou zijn en niet meer onderhoud nodig zou hebben dan het toevoegen van inhoud (voor het geval ik dat wel zou doen). Echter wilde ik het wel nog steeds zelf hosten, zodat ik anderen niet hoef te vertrouwen er goed mee om te gaan.
Dit heeft me even aan het denken gezet.
Hiermee had ik bij voorbaat het gebruik van diensten van derde partijen al uitgesloten, en ook het gebruik van iets experimenteels, iets dat geregeld veiligheidsupdates nodig heeft of iets dat op een andere manier onstabiel zou kunnen zijn. Het betekende feitelijk dat ik de meeste bestaande website-software niet kon gebruiken.
Ik trok hieruit de conclusie dat wat ik nodig had een statische website is. Gelukkig betekent een statische website niet dat het niet gegenereerd kan worden door iets minder statisch, en er is tegenwoordig een behoorlijke hoeveelheid software om uit te kiezen.
Een statische website zou zorgen voor een gemakkelijke en efficiënte website, zonder dat er iets speciaals nodig is dat kapot kan gaan. Zo lang de webserver werkt, werkt de website, wat erg handig is gezien ik toch al andere redenen heb om mijn servers te onderhouden, waardoor het feitelijk geen extra werk is.
Het enige dat ik nu moest doen was software vinden om de statische website mee te genereren dat aan mijn wensen voor flexibiliteit voldoet.
Pelican
Na wat zoeken en vergelijken, ben ik uitgekomen op een Python-gebaseerde statische website generator genaamd Pelican. Het lijkt vrij stabiel en goed georganiseerd, het is erg uitbreidbaar en is vrij gemakkelijk te gebruiken en configureren.
De manier waarop het meerdere talen voor pagina's en artikelen implementeert, werkt goed en is flexibel genoeg om in templates gebruikt te worden op de manier waarop ik het wil, wat belangrijk is omdat ik de website-inhoud in zowel Nederlands als Engels wil hebben.
Installatie
Mijn webserver heeft Python 2.7 geïnstalleerd met PIP 1.5.6, de versies
beschikbaar in Debian Jessie. Het python-pip
-pakket installeren is
genoeg als dit nog niet is geïnstalleerd.
Om met Pelican te beginnen heb ik een virtualenv opgezet en heb vervolgens daarin de benodigde Python-pakketten geïnstalleerd:
python -m virtualenv ~/website/env . ~/website/env/bin/activate pip install pelican Markdown
De in Debian Jessie beschikbare versie van PIP geeft twee compilatiefouten bij het installeren, terwijl deze fouten in nieuwere versies van PIP niet getoond worden. De fouten zijn niet belangrijk en je hebt er geen last van.
De situatie hierbij is dat Jinja2 een aantal optionele functies heeft
die syntaxis gebruike die alleen geldig is in Python 3.5+, omdat alle
bestanden worden gecompileerd tijdens de installatie, worden er fouten
gegenereerd bij de bestanden met deze functies (asyncsupport.py
en
asyncfilters.py
).
De website genereren
Ik heb de website opgezet in ~/website/srv
(meer over de configuratie
hieronder). Wanneer de configuratie is uitgevoerd en er inhoud
beschikbaar is, is het genereren van de website niets meer dan pelican
aanroepen binnen de virtualenv.
. ~/website/env/bin/activate
cd ~/website/srv
pelican
Echter, gezien ik er niet elke keer aan wil moeten denken om de
virtualenv te activeren en gezien ik de website ook wil kunnen genereren
vanuit een andere directory waar ik toevallig op dat moment bezig ben,
heb ik een simpel run
-script opgezet.
#!/home/tim/website/env/bin/python # -*- coding: utf-8 -*- import os import sys import pelican if __name__ == '__main__': directory = os.path.dirname(os.path.realpath(__file__)) if directory != os.getcwd(): os.chdir(directory) sys.argv[0] = 'pelican' sys.exit(pelican.main())
Om de website te genereren, roep ik nu gewoon aan:
~/website/srv/run
Configuratie
Na het installeren, is de initiële configuratie erg gemakkelijk op te
zetten met pelican-quickstart
:
mkdir ~/website/srv
cd ~/website/srv
pelican-quickstart
Ik heb "nee" geantwoord op vragen over extra scripts, en had daarna de volgende bestanden:
content
(directory)output
(directory)pelicanconf.py
publishconf.py
Ik zag geen reden voor een losse publicatie-configuratie in mijn geval,
dus ik heb de instellingen van publishconf.py
toevoegd aan
pelicanconf.py
en daarna de eerste verwijderd.
De documentatie over de configuratie is goed en uitgebreid en de configuratie is op zichzelf ook vrij logisch. Je kunt mijn configuratie op Github vinden, maar ik zal er ook hier verder op in gaan (vooral voor het geval ik zelf toch nog een herinnering nodig heb - maar misschien helpt het iemand anders ook).
Aan het begin heb ik een plugins
-directory gemaakt als toevoeging aan
de bestaande directories en heb de volgende basis-instellingen gedaan:
AUTHOR = 'Tim Wienk' SITENAME = 'Tim.Wienk.name' SITEURL = 'https://tim.wienk.name' RELATIVE_URLS = False PATH = 'content' PLUGIN_PATHS = ['plugins'] OUTPUT_PATH = 'output/' DELETE_OUTPUT_DIRECTORY = True TIMEZONE = 'Europe/Amsterdam' DEFAULT_LANG = 'en' DEFAULT_DATE = 'fs' DEFAULT_DATE_FORMAT = '%Y-%m-%d'
Ik wilde de website in zowel Nederlands als Engels en ik besloot dat ik veel van Pelican's standaardpagina's niet nodig had.
Het leek mij het meest logische om de website gewoon op te delen in een
nl
- en een en
-sectie, om alle Markdown-bestanden in een directory
per sectie te hebben, en ik wilde een "afgeschermde" directory om
informatie in te bewaren voor eventuele plugins.
Verder leek het me gemakkelijk om alle pagina's (en artikelen) op te
laten slaan als index.html
-bestanden in hun eigen directories. Als ik
dan wilde, kan ik gemakkelijk paden herschrijven in de
webserver-configuratie om de /
- en /index.html
-achtervoegsels er af
te halen.
Om deze dingen te bereiken, heb ik de volgende configuratie toegevoegd:
PAGE_PATHS = ['en', 'nl'] ARTICLE_PATHS = ['en/articles', 'nl/articles'] STATIC_PATHS = [''] STATIC_EXCLUDES = ['data'] ARTICLE_URL = '{lang}/articles/{slug}' ARTICLE_SAVE_AS = '{lang}/articles/{slug}/index.html' ARTICLE_LANG_URL = ARTICLE_URL ARTICLE_LANG_SAVE_AS = ARTICLE_SAVE_AS DRAFT_URL = '{lang}/articles/{slug}/draft.html' DRAFT_SAVE_AS = '{lang}/articles/{slug}/draft.html' DRAFT_LANG_URL = DRAFT_URL DRAFT_LANG_SAVE_AS = DRAFT_SAVE_AS PAGE_URL = '{lang}/{slug}' PAGE_SAVE_AS = '{lang}/{slug}/index.html' PAGE_LANG_URL = PAGE_URL PAGE_LANG_SAVE_AS = PAGE_SAVE_AS ARCHIVES_SAVE_AS = '' AUTHOR_SAVE_AS = '' INDEX_SAVE_AS = '' AUTHORS_SAVE_AS = '' CATEGORY_SAVE_AS = '' CATEGORIES_SAVE_AS = '' TAG_SAVE_AS = '' TAGS_SAVE_AS = ''
Als volgende punt wilde ik er voor zorgen dat ik niet meer informatie per pagina hoef aan te geven dan nodig, maar daarbij wilde ik er wel nog volledige controle over.
Ik heb een aantal opties voor categorieën uitgeschakeld en hoopte om
alle relevante informatie uit het pad te kunnen halen met de
PATH_METADATA
-instelling. Zoals verwacht bleek niet alles op die
manier te doen, dus is er ook nog extra metadata geconfigureerd.
USE_FOLDER_AS_CATEGORY = False DISPLAY_CATEGORIES_ON_MENU = False DEFAULT_PAGINATION = False DEFAULT_CATEGORY = 'article' SLUG_SUBSTITUTIONS = ( ('&', 'and'), ) PATH_METADATA = '(?P<lang>[a-z]{2})/(?:articles/(?P<date>\d{4}\d{2}\d{2})\.)?(?P<slug>.*)\.[a-z]{1,4}' EXTRA_PATH_METADATA = { 'en/index.md': {'save_as': 'en/index.html', 'url': 'en'}, 'nl/index.md': {'save_as': 'nl/index.html', 'url': 'nl'}, 'en/error/400.md': {'save_as': 'error/400.html', 'status': 'hidden'}, 'en/error/401.md': {'save_as': 'error/401.html', 'status': 'hidden'}, 'en/error/403.md': {'save_as': 'error/403.html', 'status': 'hidden'}, 'en/error/404.md': {'save_as': 'error/404.html', 'status': 'hidden'}, 'en/error/410.md': {'save_as': 'error/410.html', 'status': 'hidden'}, 'en/error/500.md': {'save_as': 'error/500.html', 'status': 'hidden'}, }
Natuurlijk was het allemaal te makkelijk geweest als dit allemaal zomaar zou werken. Ik liep tegen een probleem op waarbij Pelican fouten teruggaf bij het genereren van pagina's zonder een datum in het pad, ookal was de datum als optioneel onderdeel aangegeven in de reguliere expressie.
Om hier omheen te werken, heb ik de relevante functie lokaal aangepast
("monkey patching") in het run
-script dat ik heb gemaakt om de
website-generatie heen. (Aanpassing - Ondertussen heb ik deze patch
ingediend bij het Pelican-project.)
import os import re import pelican def parse_path_metadata(source_path, settings=None, process=None): metadata = {} dirname, basename = os.path.split(source_path) base, ext = os.path.splitext(basename) subdir = os.path.basename(dirname) if settings: checks = [] for key, data in [('FILENAME_METADATA', base), ('PATH_METADATA', source_path)]: checks.append((settings.get(key, None), data)) if settings.get('USE_FOLDER_AS_CATEGORY', None): checks.append(('(?P<category>.*)', subdir)) for regexp, data in checks: if regexp and data: match = re.match(regexp, data) if match: # .items() for py3k compat. for k, v in match.groupdict().items(): k = k.lower() # metadata must be lowercase if k not in metadata and v is not None: if process: v = process(k, v) metadata[k] = v return metadata pelican.readers.parse_path_metadata = parse_path_metadata
Nu er een werkende basis was, ben ik door de (vrij uitgebreide) verzameling van plugins gegaan. Ik heb daarvan "neighbors", voor volgende/vorige artikel-functionaliteit, en "touch", zodat bestanden een relevante wijzigingsdatum hebben (nuttig voor browser caches) toegevoegd.
Voor het genereren van een sitemap, de projectenpagina en ondersteuning voor HTML-secties heb ik nog wat specifieker werk gedaan:
Sitemap
Voor het generern van een sitemap heb ik de "sitemap"-plugin genomen en gedeeltelijk aangepast om meer differentiatie toe te voegen voor de gebruikte veranderingsfrequenties en prioriteiten gebaseerd op het type pagina of bestand.
Github-bijdragen
Een aanvullende functie die ik wilde, om de projectenpagina er wat interessanter uit te laten zien, was een lijst met daadwerkelijke Github-project-bijdragen, in plaats van alleen een lijst met projecten of de activiteit van projecten. Hiervoor heb ik een simpele "githubcontributions"-plugin gemaakt, welke de relevante informatie ophaalt (en in een cache bewaard).
De reden voor de cache is dat ik tegen een situatie opliep bij inactieve repositories, waarbij de eerste keer aanroepen geen informatie teruggeeft en de volgende keer aanroepen een paar seconden moest wachten, waardoor het ophalen van deze informatie voor alle projecten erg lang kon duren, wat er weer voor zorgde dat het genereren van de website zelf erg lang kon duren.
HTML-secties
Ik heb een simpele "customsection"-extensie voor Markdown gemaakt, deels gebaseerd op een bestaande "sections"-extensie, om delen van de inhoud in secties te kunnen indelen en om titelniveaus te hernummeren.
PLUGINS = ['neighbors', 'githubcontributions', 'touch', 'sitemap'] THEME = 'theme' THEME_STATIC_DIR = '' MARKDOWN = { 'extensions': ['extra', 'headerid', 'meta', 'plugins.markdown_customsection'], 'extension_configs': { 'codehilite': { 'linenums': False, 'guess_lang': False, 'css_class': 'code', }, }, 'output_format': 'xhtml5', } JINJA_ENVIRONMENT = { 'extensions': ['jinja2.ext.i18n'], } FEED_ALL_ATOM = 'atom.xml' CATEGORY_FEED_ATOM = None AUTHOR_FEED_ATOM = None AUTHOR_FEED_RSS = None TRANSLATION_FEED_ATOM = '%s/atom.xml' SITEMAP = { 'format': 'xml', 'extra_files': ['media/cv/timwienk-cv-nederlands.pdf', 'media/cv/timwienk-cv-english.pdf'] }
Het enige wat nog overblijft is wat andere configuratie die wordt gebruikt door plugins of in thema-templates. Voor de volledigheid:
from datetime import date SOCIAL = ( ('GitHub', 'https://github.com/timwienk', '/timwienk'), ('LinkedIn', 'https://www.linkedin.com/in/timwienk', '/in/timwienk'), ('Twitter', 'https://twitter.com/timwienk', '@timwienk'), ('Facebook', 'https://www.facebook.com/timwienk', '/timwienk'), ('Google+', 'https://plus.google.com/+TimWienk', '+timwienk'), ) GITHUB_USER = 'timwienk' FACEBOOK_USER = 'timwienk' TWITTER_USER = 'timwienk' FIRST_YEAR = 1989 LAST_YEAR = date.today().year GOOGLE_ANALYTICS = 'UA-7267499-1' GOOGLE_ANALYTICS_ID_SCRIPT = '/scripts/id'
Thema
Pelican-thema's bestaan uit een verzameling van statische bestanden en
Jinja2-templates. Ik vond het erg gemakkelijk op te zetten, en omdat je
specifieke templates kunt instellen voor specifieke pagina's, is het
makkelijk om verschillen aan te brengen voor "speciale" pagina's met de
extends
- en block
-templatefuncties.
Ik heb besloten om geen dingen als gettext toe te voegen om vertalingen netjes te organiseren, omdat mijn website vrij klein is en ik niet verwacht meer talen of vertaalbare vaste tekst toe te voegen. Misschien verander ik dit in de toekomst nog, maar voor nu vond ik het goed genoeg hiervoor taalafhankelijke dingen in de template te doen.
Ik denk niet dat het helpt om verder de diepte in te gaan over het thema, je kunt het thema op Github vinden indien je geïnteresseerd bent in de details.
Apache HTTP Server
Om mijn website beschikbaar te maken, ben ik bij de Apache HTTP Server gebleven. Ookal vind ik Nginx's manier van werken erg goed en heeft het zich bewezen als erg snel en efficiënt, vond ik deze dingen toch niet erg belangrijk in mijn geval, en gezien op mijn webserver de Apache HTTP Server al draaide voor andere projecten, was de keuze niet zo moeilijk.
Het grootste deel van de configuratie was alleen het opzetten van een VirtualHost met de juiste instellingen:
<VirtualHost *:443> ServerName tim.wienk.name ServerAdmin webmaster@localhost DocumentRoot /srv/www/tim.wienk.name/http ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined SSLEngine on SSLHonorCipherOrder on SSLUseStapling on SSLCertificateFile /srv/acme/certs/tim.wienk.name.crt SSLCertificateKeyFile /srv/acme/private/tim.wienk.name.key BrowserMatch "MSIE [2-6]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0 BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
Over het algemeen probeer ik de gegevens te bewaren op de in FHS
bepaalde locaties, dat houdt alles duidelijk en interoperabel. Dat
gezegd hebbende, /srv/www/tim.wienk.name/http
is in dit geval een
symbolische link naar /home/tim/website/srv/output
om de dingen in
mijn home-directory te houden.
Voor SSL heb ik gekozen voor het de Let's Encrypt ACME-opzet, hoewel ik het certificaatbeheer wel zo veel mogelijk heb losgekoppeld van de webserver.
Als deel van de Pelican-opzet heb ik een aantal simpele foutdocumenten gegenereerd, deze zijn ook hier ingesteld:
ErrorDocument 400 /error/400.html ErrorDocument 401 /error/401.html ErrorDocument 403 /error/403.html ErrorDocument 404 /error/404.html ErrorDocument 410 /error/410.html ErrorDocument 500 /error/500.html
Als volgende wilde ik een aantal extra headers toevoegen voor de goede
orde: X-Content-Type-Options en X-Frame-Options voor alle
pagina's, en een Link
-header voor de "homepage" om aan
niet-menselijke bezoekers uit te leggen wat er aan het gebeuren is:
<IfModule mod_headers.c> Header always set Link "<https://tim.wienk.name/>; rel=canonical; hreflang=x-default" "expr=%{REQUEST_URI}=='/'" Header always append Link "<https://tim.wienk.name/>; rel=alternate; hreflang=x-default" "expr=%{REQUEST_URI}=='/'" Header always append Link "<https://tim.wienk.name/en>; rel=alternate; hreflang=en" "expr=%{REQUEST_URI}=='/'" Header always append Link "<https://tim.wienk.name/nl>; rel=alternate; hreflang=nl" "expr=%{REQUEST_URI}=='/'" Header always set X-XSS-Protection "1; mode=block" Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "DENY" </IfModule>
En als laatste heb ik regels toegevoegd voor het herschrijven van paden,
zodat alle pagina's beschikbaar zijn zonder /
- of
/index.html
-achtervoegsel, met een speciale conditie op basis van de
Accept Language
-header voor de homepage en een aantal extra
doorverwijzingen voor het geval mensen inhoud proberen te bereiken
zonder taal-voorvoegsel:
RewriteEngine on RewriteCond %{HTTP:Accept-Language} ^nl [NC] RewriteRule ^/?$ /nl [R=302,QSA,L] RewriteRule ^/?$ /en [R=302,QSA,L] RewriteRule ^/about/?$ /en/about [R=301,QSA,L] RewriteRule ^/articles/?$ /en/articles [R=301,QSA,L] RewriteRule ^/contact/?$ /en/contact [R=301,QSA,L] RewriteRule ^/cv/?$ /en/cv [R=301,QSA,L] RewriteRule ^/projects/?$ /en/projects [R=301,QSA,L] RewriteRule ^/(.*)/(index.html)?$ /$1 [R=301,QSA,L] RewriteCond /srv/www/tim.wienk.name/http/%{REQUEST_URI}/index.html -f RewriteRule ^/(.*)$ /$1/index.html [QSA,L] </VirtualHost>
Het eindresultaat is wat je nu ziet, een werkende, simpele website. Er is nog steeds niet erg veel inhoud, maar dat komt misschien nog in de toekomst, in ieder geval heb ik de optie nu zonder me over de website zelf zorgen te hoeven maken.