diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4e8e071
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+
+[*.py]
+indent_style = space
+indent_size = 4
+
+[*.yaml]
+indent_style = space
+indent_size = 2
+
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..431cfd9
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @cccb/web
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..174728a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,28 @@
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    target-branch: "staging"
+    schedule:
+      interval: "weekly"
+    commit-message:
+      prefix: "gh-action"
+    labels:
+      - "gh-action"
+      - "dependencies"
+    reviewers:
+      - "cccb/web"
+  - package-ecosystem: "pip"
+    directory: "/"
+    target-branch: "dev"
+    schedule:
+      interval: "weekly"
+    commit-message:
+      prefix: "python"
+    labels:
+      - "python"
+      - "dependencies"
+    reviewers:
+      - "cccb/web"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..04f8e97
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,126 @@
+name: Release website
+
+on:
+  push:
+    branches:
+      - staging
+      - production
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  pages:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          submodules: recursive
+          fetch-depth: 0
+      - name: Setup Hugo
+        uses: peaceiris/actions-hugo@v2
+        with:
+          hugo-version: 'latest'
+      - name: Build pages
+        run: hugo $(cat .hugo-params)
+      - uses: actions/upload-artifact@v3
+        name: Upload pages
+        with:
+          name: pages
+          path: public
+
+  calendar:
+    needs: [ pages ]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Add de_DE.UTF-8 locale
+        run: |
+          sudo apt-get update
+          sudo apt-get -y install locales
+          sudo locale-gen de_DE.UTF-8
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          submodules: recursive
+          fetch-depth: 0
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          cache: 'pip' # caching pip dependencies
+      - name: Install dependencies
+        run: |
+          pip install --upgrade pip setuptools wheel
+          pip install -r requirements.txt
+      - name: Download pages
+        uses: actions/download-artifact@v3
+        with:
+          name: pages
+          path: public/
+      - name: Generate calendars
+        run: python tools/merge_cals.py
+      - name: Copy calendar to output dir
+        run: cp static/all.ics public/all.ics
+      - name: Update homepage with latest event
+        run: upcoming="$(python tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')" && sed -i "s#CALENDAR#$upcoming#g" public/index.html
+      - uses: actions/upload-artifact@v3
+        name: Upload pages
+        with:
+          name: enhanced_pages
+          path: public
+
+  staging:
+    needs: [ calendar ]
+    runs-on: ubuntu-latest
+    environment: staging
+    if: github.ref == 'refs/heads/staging' && github.event_name == 'push'
+    steps:
+      - name: Download pages
+        uses: actions/download-artifact@v3
+        with:
+          name: enhanced_pages
+          path: public
+      - name: Generate timestamp
+        run: echo "timestamp=$(date -u +'%Y-%m-%dT%H%M%SZ')" >> $GITHUB_ENV
+      - name: Create Release Archive
+        uses: thedoctor0/zip-release@0.7.1
+        with:
+          type: zip
+          filename: ../release-staging-${{ env.timestamp }}.zip
+          directory: public
+      - name: Create Release
+        uses: ncipollo/release-action@v1.12.0
+        with:
+          tag: staging-${{ env.timestamp }}
+          name: Website staging version ${{ env.timestamp }}
+          body: Website staging version ${{ env.timestamp }}
+          artifacts: release-staging-${{ env.timestamp }}.zip
+          token: ${{ secrets.GITHUB_TOKEN }}
+  
+  production:
+    needs: [ calendar ]
+    runs-on: ubuntu-latest
+    environment: production
+    if: github.ref == 'refs/heads/production' && github.event_name == 'push'
+    steps:
+      - name: Download pages
+        uses: actions/download-artifact@v3
+        with:
+          name: enhanced_pages
+          path: public
+      - name: Generate timestamp
+        run: echo "timestamp=$(date -u +'%Y-%m-%dT%H%M%SZ')" >> $GITHUB_ENV
+      - name: Create Release Archive
+        uses: thedoctor0/zip-release@0.7.1
+        with:
+          type: zip
+          filename: ../release-production-${{ env.timestamp }}.zip
+          directory: public
+      - name: Create Release
+        uses: ncipollo/release-action@v1.12.0
+        with:
+          makeLatest: true
+          tag: production-${{ env.timestamp }}
+          name: Website production version ${{ env.timestamp }}
+          body: Website production version ${{ env.timestamp }}
+          artifacts: release-production-${{ env.timestamp }}.zip
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 5806f78..c7503fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,94 @@
-/public
 static/all.ics
+
+# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
+# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo
+
+### Hugo ###
+# Generated files by hugo
+/public/
+/resources/_gen/
+/assets/jsconfig.json
+hugo_stats.json
+
+# Executable may be added to repository
+hugo.exe
+hugo.darwin
+hugo.linux
+
+# Temporary lock file while building
+/.hugo_build.lock
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon

+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index b8f4f6e..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-stages:
-  - pages
-  - calendar
-  - deploy
-
-cache:
-  paths:
-    - ~/.cache/pip/
-    - public/
-
-variables:
-  GIT_SUBMODULE_STRATEGY: recursive
-
-build_pages:
-  image: "golang:1.10-alpine3.8"
-  stage: pages
-  variables:
-    SHELL: "/bin/sh"
-  artifacts:
-    expire_in: "1 week"
-    untracked: true
-  script:
-    - rm -rf public/*
-    - apk add --no-cache --upgrade hugo
-    - hugo $(cat .hugo-params)
-
-build_calendar:
-  image: "python:3.11.1-alpine3.17"
-  stage: calendar
-  dependencies: 
-    - build_pages
-  artifacts:
-    expire_in: "1 week"
-    untracked: true
-  variables:
-    SHELL: "/bin/sh"
-  script:
-    - apk --no-cache update
-    - pip install -r requirements.txt
-    - python tools/merge_cals.py
-    - cp static/all.ics public/all.ics
-    - upcoming="$(python tools/gen_upcoming.py static/all.ics 20 5|tr '\n' ' ')" && sed -i "s#CALENDAR#$upcoming#g" public/index.html
-
-deploy_staging:
-  image: "alpine:3.17"
-  stage: deploy
-  dependencies: 
-    - build_calendar
-  variables:
-    SHELL: "/bin/sh"
-  script:
-    - apk --no-cache --upgrade add openssh-client rsync
-    - mkdir -p ~/.ssh
-    - eval $(ssh-agent -s)
-    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
-    - echo "$PRIVATE_KEY" | ssh-add -
-    - rsync -e "ssh -l deploy -p 31337" -av --delete public/ 195.160.173.9:staging
-  when: on_success
-  environment:
-    name: staging
-    url: https://staging.berlin.ccc.de/
-  artifacts:
-    expire_in: "1 week"
-    paths:
-      - public/
-  only:
-     - staging
-
-deploy_production:
-  image: "alpine:3.17"
-  stage: deploy
-  dependencies: 
-    - build_calendar
-  variables:
-    SHELL: "/bin/sh"
-  script:
-    - apk --no-cache --upgrade add openssh-client rsync
-    - mkdir -p ~/.ssh
-    - eval $(ssh-agent -s)
-    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
-    - echo "$PRIVATE_KEY" | ssh-add -
-    - rsync -e "ssh -l deploy -p 31337" -av --delete public/ 195.160.173.9:production
-  when: on_success
-  environment:
-    name: production
-    url: https://berlin.ccc.de/
-  artifacts:
-    expire_in: "1 week"
-    paths:
-      - public/
-  only:
-     - production
-
diff --git a/.hugo-params b/.hugo-params
index ac16630..e3abdb3 100644
--- a/.hugo-params
+++ b/.hugo-params
@@ -1 +1 @@
--b https://berlin.ccc.de/
+--baseURL=https://berlin.ccc.de/
\ No newline at end of file
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..2c07333
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/README.md b/README.md
index 3525e34..a544e7a 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,51 @@
-[![pipeline status](https://gitlab.berlin.ccc.de/cccb/www/badges/master/pipeline.svg)](https://gitlab.berlin.ccc.de/cccb/www/commits/master)
+![CCCB logo](static/img/logo.png)
 
 # CCCB Website
 
-This is the website of CCCB.
+This is the website of the CCCB.
 
 ## Getting started
 
-Get Hugo: <https://gohugo.io/getting-started/installing>
-
-Clone this repo
-```
-git clone https://github.com/cccb/www
-```
-
-Switch directory
-```
-cd www
-```
-
-Fetch Submodules
-```
-git submodule update --recursive --remote --init
-```
+1. Get Hugo: <https://gohugo.io/getting-started/installing>
+2. Clone this repo
+   ```shell
+   git clone https://github.com/cccb/www
+   ```
+3. Switch directory
+   ```shell
+   cd www
+   ```
+3. Fetch Submodules
+   ```shell
+   git submodule update --recursive --remote --init
+   ```
 
 ### Run site locally
 
-Run hugo webserver
-```
+Run hugo webserver:
+
+```shell
 hugo serve
 ```
-Point your browser to http://localhost:1313/
 
-To ready your site for upload, run "./build.sh", which also generates all.ics and adds the calendar table to index.html
+Point your browser to: http://localhost:1313/
+
+To ready your site for upload, run `./build.sh`, which also generates `all.ics` and adds the calendar table to `index.html`.
 Every change you make on the project will be reflected in your browser as long as `hugo serve` is running.
 
 ## Making a change
 
-* Use your local dev setup (see Getting started) or via GitLab editor. 
-* Make your change in `staging` branch.
-* Commit (and push) your change.
-* Gitlab CI is running pipeline. If successfull, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
-* Create merge request to merge changes from `staging` to `production`. Ask somebody to check merge request or if small change, merge yourself.
-* Gitlab CI is running pipeline. If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
+1. Use your local dev setup (see Getting started) or via GitHub editor.
+2. Make your change in `staging` branch.
+3. Commit (and push) your change.
+4. GitHub Actions is running the release workflow.
+   - If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
+5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself.
+6. GitHub Actions is running the release workflow.
+   - If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
+7. Profit!
+
+---
+
+Made with ❤️ and [Hugo](https://gohugo.io).
 
diff --git a/TODO.md b/TODO.md
index f2e7640..7401796 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,15 +1,15 @@
-Todo
-----
+# Todo
+
 - DSGVO-compliant Datenschutzerklärung reinbasteln
 - Entscheiden, welche Seiten sonst noch konvertiert werden sollen und welche in die ewigen Datengründe gehen können
+- add nix config to repo
 
-Done
-----
+# Done
 
 - Template aussuchen
 - Rausfinden, wie man eine Seite konvertiert
-  - hugo new page/mypage.md
-  - pandoc -f mediawiki page.mw -t markdown >> page/mypage.md
+  - `hugo new page/mypage.md`
+  - `pandoc -f mediawiki page.mw -t markdown >> page/mypage.md`
 - Wichtigste Seiten konvertieren
 - Membership-Seite auf den aktuellen Stand der Wahrheit bringen
 - Ical template bauen
@@ -17,3 +17,4 @@ Done
 - Bestehende Datengarten-Termine konvertieren
   - ggf. template mit frontmatter
 - Theme forken, alle assets sollten lokal gehosted sein und nicht von irgendwelchen CDNs bezogen werden (HTTP/2 ftw!)
+
diff --git a/build.sh b/build.sh
index dac88b4..d60aa87 100755
--- a/build.sh
+++ b/build.sh
@@ -1,7 +1,8 @@
-#!/usr/bin/env sh
+#!/bin/sh
 
 hugo $(cat .hugo-params)
-tools/merge_cals.py
-upcoming="$(tools/gen_upcoming.py static/all.ics 20 5|tr '\n' ' ')"
+./tools/merge_cals.py
+upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
 cp static/all.ics public/all.ics
 sed -i "s#CALENDAR#$upcoming#g" public/index.html
+
diff --git a/config.yaml b/config.yaml
index 96e2637..161bfe0 100644
--- a/config.yaml
+++ b/config.yaml
@@ -16,6 +16,8 @@ Params:
   readingTime: true
   useHLJS: true
   DateForm: "30.12.2006"
+  # for GDPR / EU-DSGVO compliance
+  selfHosted: true
 
 taxonomies:
   category: "categories"
@@ -63,7 +65,6 @@ mediaTypes:
     suffixes:
     - "xml"
 
-
 outputFormats:
   RSS:
     mediaType: "application/rss"
@@ -72,11 +73,12 @@ outputFormats:
     mediaType: "application/xml"
 
 outputs:
-  section: 
-   - "HTML"
-   - "Calendar"
-   - "RSS"
-   - "XML"
+  section:
+  - "HTML"
+  - "Calendar"
+  - "RSS"
+  - "XML"
   page:
-   - "HTML"
-   - "Calendar"
+  - "HTML"
+  - "Calendar"
+
diff --git a/layouts/datengarten/section.xml b/layouts/datengarten/section.xml
index ecd0932..47eaf0c 100644
--- a/layouts/datengarten/section.xml
+++ b/layouts/datengarten/section.xml
@@ -30,6 +30,6 @@
         <links/>
       </event>
     </room>
-  </day> 
+  </day>
   {{end -}}
 </schedule>
diff --git a/layouts/datengarten/single.html b/layouts/datengarten/single.html
index 448ab34..337e465 100644
--- a/layouts/datengarten/single.html
+++ b/layouts/datengarten/single.html
@@ -3,7 +3,7 @@
   <div class="row">
     <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
       <article role="main" class="blog-post">
-	{{ partial "talk-infobox" . }}
+        {{ partial "talk-infobox" . }}
         {{ .Content }}
 
         {{ if .Params.tags }}
@@ -11,7 +11,7 @@
             {{ range .Params.tags }}
               <a href="{{ $.Site.LanguagePrefix | absURL }}/tags/{{ . | urlize }}/">{{ . }}</a>&nbsp;
             {{ end }}
-          </div>
+          </div><!--/.blog-tags-->
         {{ end }}
 
         {{ if .Site.Params.socialShare }}
@@ -19,24 +19,24 @@
             <section id="social-share">
               <ul class="list-inline footer-links">
                 {{ partial "share-links" . }}
-              </ul>
-            </section>
+              </ul><!--/.social-share-->
+            </section><!--/.list-inline .footer-links-->
         {{ end }}
-      </article>
+      </article><!--/.blog-post-->
 
       {{ if ne .Type "page" }}
         <ul class="pager blog-pager">
           {{ if .PrevInSection }}
             <li class="previous">
               <a href="{{ .PrevInSection.Permalink }}" data-toggle="tooltip" data-placement="top" title="{{ .PrevInSection.Title }}">&larr; {{ i18n "previousPost" }}</a>
-            </li>
+            </li><!--/.previous-->
           {{ end }}
           {{ if .NextInSection }}
             <li class="next">
               <a href="{{ .NextInSection.Permalink }}" data-toggle="tooltip" data-placement="top" title="{{ .NextInSection.Title }}">{{ i18n "nextPost" }} &rarr;</a>
-            </li>
+            </li><!--/.next-->
           {{ end }}
-        </ul>
+        </ul><!--/.pager .blog-pager-->
       {{ end }}
 
 
@@ -44,16 +44,16 @@
         {{ if .Site.DisqusShortname }}
           <div class="disqus-comments">
             {{ template "_internal/disqus.html" . }}
-          </div>
+          </div><!--/.disqus-comments-->
         {{ end }}
         {{ if .Site.Params.staticman }}
           <div class="staticman-comments">
             {{ partial "staticman-comments.html" . }}
-          </div>
+          </div><!--/.staticman-comments-->
         {{ end }}
       {{ end }}
 
-    </div>
-  </div>
-</div>
+    </div><!--/.col-lg-8 .col-lg-offset-2 .col-md-10 .col-md-offset-1-->
+  </div><!--/.row-->
+</div><!--/.container-->
 {{ end }}
diff --git a/layouts/page/single.ics b/layouts/page/single.ics
index 28437c8..50a1de3 100644
--- a/layouts/page/single.ics
+++ b/layouts/page/single.ics
@@ -31,7 +31,7 @@ DTEND;TZID=Europe/Berlin:{{.Params.dtend}}
 RRULE:{{.Params.rrule}}
 {{if isset .Params "rrule_excludes" }}
 {{range .Params.rrule_excludes }}
-EXDATE;TZID=Europe/Berlin:{{.}} 
+EXDATE;TZID=Europe/Berlin:{{.}}
 {{end -}}
 {{end -}}
 LOCATION:{{with .Params.location}}{{.}}{{else}}CCCB{{end}}
diff --git a/layouts/shortcodes/series.html b/layouts/shortcodes/series.html
index a428093..8f97228 100644
--- a/layouts/shortcodes/series.html
+++ b/layouts/shortcodes/series.html
@@ -1,29 +1,29 @@
 {{ $series := or (.Get 0) $.Page.Params.series }}
 <table>
-<tr>
-<th>No.</th>
-<th>Date</th>
-<th>Speaker</th>
-<th>Topic</th>
-<th>Video</th>
-</tr>
-    {{ range $ind,$art := $.Site.Pages.ByDate.Reverse }}
-      {{ if eq $art.Params.series $series }}
-      <tr>
+  <tr>
+    <th>No.</th>
+    <th>Date</th>
+    <th>Speaker</th>
+    <th>Topic</th>
+    <th>Video</th>
+  </tr>
+  {{ range $ind,$art := $.Site.Pages.ByDate.Reverse }}
+    {{ if eq $art.Params.series $series }}
+  <tr>
 	<td>{{ $art.Params.no }}</td>
 	<td>{{ dateFormat "02.01.2006" $art.Params.event.start }}</td>
 	{{ if isset $art.Params "speaker_url" }}
 	<td><a href="{{ $art.Params.speaker_url }}">{{ $art.Params.speaker }}</a></td>
-        {{ else }}
+    {{ else }}
 	<td>{{ $art.Params.speaker }}</td>
-        {{ end }}
-	<td><a href="{{ $art.Permalink }}">{{ $art.Params.subtitle }}</a></td>
-        {{ if $art.Params.recording }}
-	<td><a href="{{ $art.Params.recording }}"><i class="fa fa-video fa-fw"></i></a></td>
-        {{ else }}
-	<td><i class="fa fa-video-slash fa-fw"></i></a></td>
-        {{ end }}
-      </tr>
-      {{ end }}
     {{ end }}
+	<td><a href="{{ $art.Permalink }}">{{ $art.Params.subtitle }}</a></td>
+    {{ if $art.Params.recording }}
+	<td><a href="{{ $art.Params.recording }}"><i class="fa fa-video fa-fw"></i></a></td>
+    {{ else }}
+	<td><i class="fa fa-video-slash fa-fw"></i></a></td>
+    {{ end }}
+  </tr>
+    {{ end }}
+  {{ end }}
 </table>
diff --git a/requirements.txt b/requirements.txt
index 744b7a9..134909f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-icalendar
+icalendar==5.0.7
diff --git a/static/img/chaoswelle.png b/static/img/chaoswelle.png
index b3a450b..707955d 100644
Binary files a/static/img/chaoswelle.png and b/static/img/chaoswelle.png differ
diff --git a/static/img/logo.png b/static/img/logo.png
index 27eec38..2c2fc26 100644
Binary files a/static/img/logo.png and b/static/img/logo.png differ
diff --git a/themes/beautifulhugo b/themes/beautifulhugo
index 0da13d2..1e66e4a 160000
--- a/themes/beautifulhugo
+++ b/themes/beautifulhugo
@@ -1 +1 @@
-Subproject commit 0da13d20d0bf3c9002651f06a54d21608d292958
+Subproject commit 1e66e4ae945f12da3f8e6fb603c396156c1b04e9
diff --git a/tools/convert_page b/tools/convert_page
deleted file mode 100755
index 5c9916e..0000000
--- a/tools/convert_page
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-
-if [ $# -lt 2 ]; then
-  echo "Usage: $0 Mediawiki_Page_Name [page|post]"
-  exit
-fi
-
-echo Converting $1...
-
-page=$(echo $1 | sed 's,/,_,g'|tr '[:upper:]' '[:lower:]')
-
-hugo new $2/$page.md
-curl -s  https://berlin.ccc.de/api.php\?action\=query\&prop\=revisions\&rvprop\=content\&format\=json\&titles\=$1 | \
-	 jq -r '.query.pages |..| objects|.["*"]'| sed '/null/d' | pandoc -f mediawiki -t markdown  \
-	>> content/$2/$page.md
diff --git a/tools/convert_page.sh b/tools/convert_page.sh
new file mode 100755
index 0000000..f34bc5a
--- /dev/null
+++ b/tools/convert_page.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+if [ $# -lt 2 ]; then
+  echo "Usage: $0 Mediawiki_Page_Name [page|post]"
+  exit
+fi
+
+echo Converting $1...
+
+page=$(echo $1 | sed 's,/,_,g' | tr '[:upper:]' '[:lower:]')
+
+hugo new $2/$page.md
+curl -s "https://berlin.ccc.de/api.php?action=query&prop=revisions&rvprop=content&format=json&titles=$1" \
+    | jq -r '.query.pages |..| objects|.["*"]' \
+    | sed '/null/d' \
+    | pandoc -f mediawiki -t markdown \
+    >> content/$2/$page.md
diff --git a/tools/gen_upcoming.py b/tools/gen_upcoming.py
index ff05fe7..03494a3 100755
--- a/tools/gen_upcoming.py
+++ b/tools/gen_upcoming.py
@@ -3,7 +3,6 @@
 import sys
 import logging
 import locale
-from pprint import pprint
 from dateutil.parser import parse
 from datetime import datetime, timedelta
 from dateutil.rrule import rruleset, rrulestr
@@ -12,79 +11,83 @@ import icalendar
 
 
 def vevent_to_event(event, rrstart=None):
-	if rrstart == None:
-		begin = parse(event['DTSTART'].to_ical())
-	else:
-		begin = rrstart
-	return { "name": event['SUMMARY'].to_ical(), "url": event['URL'].to_ical(), "begin": begin }
+    if rrstart == None:
+        begin = parse(event["DTSTART"].to_ical())
+    else:
+        begin = rrstart
+
+    return {
+        "name": event["SUMMARY"].to_ical(),
+        "url": event["URL"].to_ical(),
+        "begin": begin
+    }
 
 
 def parse_single_event(event, start, end):
-	logging.info("Processing single event %s" % event['SUMMARY'].to_ical().decode('utf-8'))
-	dtstart = parse(event['DTSTART'].to_ical())
-	if dtstart >= start and dtstart < end:
-		return vevent_to_event(event)
-	else:
-		return None
+    logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
+    dtstart = parse(event["DTSTART"].to_ical())
+    if dtstart >= start and dtstart < end:
+        return vevent_to_event(event)
 
 
 def parse_recurring_event(event, start, end):
-	logging.info("Processing recurring event %s" % event['SUMMARY'].to_ical().decode('utf-8'))
-	dtstart = parse(event['DTSTART'].to_ical())
-	rs = rruleset()
-	rs.rrule(rrulestr(event['RRULE'].to_ical().decode('utf-8'), dtstart=dtstart))
-	if 'EXDATE' in event.keys():
-		exdates = event['EXDATE']
-		for exdate in exdates:
-			rs.exdate(parse(exdate.to_ical()))
+    logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
+    dtstart = parse(event["DTSTART"].to_ical())
+    rs = rruleset()
+    rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart))
+    if "EXDATE" in event.keys():
+        for exdate in event["EXDATE"]:
+            rs.exdate(parse(exdate.to_ical()))
 
-	dates = list(rs)
-	events = []
-	for date in dates:
-		if date >= start and date < end:
-			events.append(vevent_to_event(event, date))
-	return events
+    events = []
+    for date in list(rs):
+        if date >= start and date < end:
+            events.append(vevent_to_event(event, date))
+
+    return events
 
 
 def find_events(icsfilestr, start, end, num):
-	with open(icsfilestr, 'r') as icsfile:
-		cal=icalendar.Calendar.from_ical(icsfile.read())
+    with open(icsfilestr, "r") as icsfile:
+        cal = icalendar.Calendar.from_ical(icsfile.read())
 
-	events=[]
-	for event in cal.subcomponents:
-		if event.name == 'VEVENT':
-			if 'RRULE' in event.keys():
-				events = events + parse_recurring_event(event, start, end)
-			else:
-				ev = parse_single_event(event, start, end)
-				if ev != None:
-					events.append(ev)
+    events = []
+    for event in cal.subcomponents:
+        if event.name == "VEVENT":
+            if "RRULE" in event.keys():
+                events.extend(parse_recurring_event(event, start, end))
+            elif ev := parse_single_event(event, start, end) != None:
+                events.append(ev)
 
-	events = sorted(events, key=lambda k: k['begin'])
-	events = events[0:num]
-	return events
+    events = sorted(events, key=lambda k: k["begin"])
+    events = events[0:num]
+
+    return events
 
 
 def format_events(events):
-	print('<table class="table table-condensed">')
-	for event in events:
-		dateStr = event['begin'].strftime("%A, %d.%m um %H:%M Uhr")
-		#print("<li><a href=\"%s\">%s: %s</a></li>" % (event['url'].decode('utf-8'), dateStr, event['name'].decode('utf-8')))
-		print("<tr><td>%s</td><td><a href=\"%s\">%s</a></td></tr>"
-			% (dateStr, event['url'].decode('utf-8'), event['name'].decode('utf-8')))
-	print('</table>')
+    print("<table class=\"table table-condensed\">")
+    for event in events:
+        print(
+            "<tr>"
+            f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>"
+            f"<td><a href=\"{event['url'].decode('utf-8')}\">{event['name'].decode('utf-8')}</a></td>"
+            "</tr>"
+        )
+    print("</table><!--/.table .table-condensed-->")
 
 
 if __name__ == "__main__":
-	if len(sys.argv) < 3:
-		print("Usage: %s calendar max_days max_items" % sys.argv[0])
-		sys.exit(-1)
+    if len(sys.argv) < 3:
+        print(f"Usage: {sys.argv[0]} calendar max_days max_items")
+        sys.exit(-1)
 
-	locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
-	calendar=sys.argv[1]
-	max_days=int(sys.argv[2])
-	max_items=int(sys.argv[3])
+    locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
+    calendar = sys.argv[1]
+    max_days = int(sys.argv[2])
+    max_items = int(sys.argv[3])
 
-	events=find_events(calendar, datetime.now(), datetime.now() + timedelta(days=max_days), max_items)
-	format_events(events)
+    now = datetime.now()
+    events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
+    format_events(events)
 
diff --git a/tools/merge_cals.py b/tools/merge_cals.py
index 0df4949..c406c93 100755
--- a/tools/merge_cals.py
+++ b/tools/merge_cals.py
@@ -6,22 +6,22 @@ import pytz
 import icalendar
 
 
-cals = []
+calendars = []
 merged = icalendar.Calendar()
-merged.add('prodid', '-//CCCB Calendar Generator//berlin.ccc.de//')
-merged.add('version', '2.0')
+merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//")
+merged.add("version", "2.0")
 
-for icsfilestr in glob('public/*/**/*.ics', recursive=True):
-	with open(icsfilestr, 'r') as icsfile:
-		print('Importing', icsfilestr)
-		cals.append(icalendar.Calendar.from_ical(icsfile.read()))
+for icsfilestr in glob("public/*/**/*.ics", recursive=True):
+	with open(icsfilestr, "r") as icsfile:
+		print(f"Importing {icsfilestr}")
+		calendars.append(icalendar.Calendar.from_ical(icsfile.read()))
 
-for cal in cals:
-	for e in cal.subcomponents:
-		merged.add_component(e)
+for calendar in calendars:
+	for event in calendar.subcomponents:
+		merged.add_component(event)
 
-outfile = 'static/all.ics'
-with open(outfile, 'wb') as f:
-	print(f'writing to {outfile}...')
+outfile = "static/all.ics"
+with open(outfile, "wb") as f:
+	print(f"writing to {outfile}...")
 	f.write(merged.to_ical())