Direct naar inhoud van de pagina

Beschikbare talen:

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 een 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. Om te beginnen heb ik een virtualenv opgezet en de benodigde pakketten geïnstalleerd:

python -m virtualenv ~/website
. ~/website/bin/activate
pip install pelican Markdown

De 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 is zo 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. Aangenomen dat alle configuratie is gedaan en dat er inhoud is toegevoegd, is het genereren van de website niets meer dan pelican aanroepen binnen de virualenv.

. ~/website/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 gemaakt.

#!/home/tim/website/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

De initiële configuratie is erg gemakkelijk op te zetten met 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 meet 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

Uit dit deel heb je misschien al opgemaakt dat ik probeer om alle informatie te bewaren in FHS-bepaalde locaties. Dat gezegd hebbende, /srv/www/tim.wienk.name/http is eigenlijk een symbolische link naar /home/tim/website/srv/output om de dingen simpel 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.