summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.dir-locals.el2
-rw-r--r--.dockerignore4
-rw-r--r--.envrc1
-rw-r--r--.gitignore7
-rw-r--r--.gitmodules3
-rw-r--r--Caddyfile77
-rw-r--r--Dockerfile24
-rw-r--r--Makefile93
-rw-r--r--config.toml63
-rw-r--r--content/_index.md26
-rw-r--r--content/feed-styles/index.md4
-rw-r--r--content/post/_index.md6
-rw-r--r--content/post/a-new-site.md15
-rw-r--r--content/post/back-again.md15
-rw-r--r--content/post/cedit-and-paredit.md44
-rw-r--r--content/post/emacs-package-archive-statistics.md169
-rw-r--r--content/post/git-cloning-similar-repositories.md19
-rw-r--r--content/post/nixos-on-nanopi-r5s.md142
-rw-r--r--content/post/now-on-three-continents.md25
-rw-r--r--content/post/opening-projects-with-projectile.md80
-rw-r--r--content/post/postfix-as-null-client-with-external-catchall.md55
-rw-r--r--content/post/repository-management-with-ghq.md76
-rw-r--r--content/post/self-hosted-git.md147
-rw-r--r--default.nix10
-rw-r--r--flake.lock45
-rw-r--r--flake.nix61
-rw-r--r--fly.toml38
-rw-r--r--redis.Caddyfile2
-rw-r--r--static/.domains6
-rw-r--r--static/.well-known/keybase.txt56
-rw-r--r--static/public_key.asc16
-rw-r--r--static/robots.txt5
-rw-r--r--templates/atom.xml48
-rw-r--r--templates/favicon.html3
-rw-r--r--templates/feed-styles.html79
-rw-r--r--templates/footer.html6
-rw-r--r--templates/header.html8
-rw-r--r--templates/index.html38
-rw-r--r--templates/nav.html10
-rw-r--r--templates/section.html38
-rw-r--r--templates/style.css.html169
-rw-r--r--templates/taxonomy_single.html30
-rw-r--r--themes/bear/.envrc1
-rw-r--r--themes/bear/.gitignore1
-rw-r--r--themes/bear/LICENSE (renamed from LICENSE)0
-rw-r--r--themes/bear/README.md (renamed from README.md)0
-rw-r--r--themes/bear/config.toml35
-rw-r--r--themes/bear/content/_index.md23
-rw-r--r--themes/bear/content/bear.md (renamed from content/bear.md)0
-rw-r--r--themes/bear/content/blog/_index.md (renamed from content/blog/_index.md)0
-rw-r--r--themes/bear/content/blog/markdown-syntax.md (renamed from content/blog/markdown-syntax.md)0
-rw-r--r--themes/bear/content/zola.md (renamed from content/zola.md)0
-rw-r--r--themes/bear/flake.lock61
-rw-r--r--themes/bear/flake.nix20
-rw-r--r--themes/bear/netlify.toml (renamed from netlify.toml)0
-rw-r--r--themes/bear/screenshot.png (renamed from screenshot.png)bin262700 -> 262700 bytes
-rw-r--r--themes/bear/templates/404.html (renamed from templates/404.html)0
-rw-r--r--themes/bear/templates/base.html (renamed from templates/base.html)5
-rw-r--r--themes/bear/templates/footer.html5
-rw-r--r--themes/bear/templates/header.html11
-rw-r--r--themes/bear/templates/index.html7
-rw-r--r--themes/bear/templates/nav.html6
-rw-r--r--themes/bear/templates/page.html (renamed from templates/page.html)18
-rw-r--r--themes/bear/templates/section.html34
-rw-r--r--themes/bear/templates/security_tags.html (renamed from templates/security_tags.html)0
-rw-r--r--themes/bear/templates/seo_tags.html (renamed from templates/seo_tags.html)0
-rw-r--r--themes/bear/templates/style.css193
l---------themes/bear/templates/style.css.html1
-rw-r--r--themes/bear/templates/taxonomy_list.html (renamed from templates/taxonomy_list.html)2
-rw-r--r--themes/bear/templates/taxonomy_single.html34
-rw-r--r--themes/bear/theme.toml (renamed from theme.toml)0
71 files changed, 1878 insertions, 344 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..d5837e0
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,2 @@
+((nil . ((compile-command . "make -B")
+         (web-mode-engine . "go"))))
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..dbb2ff0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+*
+!Caddyfile
+!redis.Caddyfile
+!public
diff --git a/.envrc b/.envrc
index 3550a30..6a4286b 100644
--- a/.envrc
+++ b/.envrc
@@ -1 +1,2 @@
 use flake
+export SITE_ROOT=public
diff --git a/.gitignore b/.gitignore
index 87174b6..7753449 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
-/public/
+/public
+/.deploystamp
+/.formatstamp
+/.compressstamp
+.direnv
+/result
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..b7aeba2
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "themes/bear"]
+	path = themes/bear
+	url = ssh://git.alanpearce.eu/zola-bearblog
diff --git a/Caddyfile b/Caddyfile
new file mode 100644
index 0000000..d234c87
--- /dev/null
+++ b/Caddyfile
@@ -0,0 +1,77 @@
+{
+	admin off
+	persist_config off
+	auto_https off
+	import globals/*
+	servers :80 {
+		metrics
+		protocols h1 h2c
+		trusted_proxies static private_ranges
+	}
+	servers :9091 {
+		protocols h1
+	}
+}
+
+:9091 {
+	metrics
+}
+
+http://,
+http://alanpearce.uk,
+http://www.alanpearce.uk,
+http://www.alanpearce.eu {
+	header {
+		Cache-Control max-age=31536000
+		X-Content-Type-Options nosniff
+		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+		Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'"
+	}
+	redir https://alanpearce.eu{uri} permanent
+}
+
+http://aln.pe {
+	header {
+		Cache-Control max-age=86400
+		X-Content-Type-Options nosniff
+		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+		Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'"
+	}
+
+	redir /pronouns https://en.pronouns.page/@alanpearce
+	redir /pronomen https://de.pronouns.page/@alanpearce
+	redir /git https://git.alanpearce.eu
+	redir /gpg https://alanpearce.eu/public_key.asc
+	redir /status https://stats.uptimerobot.com/GgzRkHBDr7
+	redir /* https://alanpearce.eu/{uri}
+}
+
+http://alanpearce.eu {
+	root * {$SITE_ROOT}
+	file_server {
+		precompressed br zstd gzip
+	}
+	header {
+		Cache-Control max-age=14400
+		X-Content-Type-Options nosniff
+		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+		Content-Security-Policy "default-src 'none'; img-src 'self'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'"
+	}
+	handle_errors {
+		@404 expression `{err.status_code} == 404`
+		handle @404 {
+			rewrite * /404.html
+		}
+		file_server {
+			precompressed br zstd gzip
+		}
+	}
+	header /feed-styles/ Content-Type text/xsl
+	error /feed-styles/index.html* 404
+	respond /favicon.ico 204
+	redir /index.xml /atom.xml
+	redir /post/index.xml /atom.xml
+
+	@http header X-Forwarded-Proto http
+	redir @http https://alanpearce.eu{uri}
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..86b26ea
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+ARG VERSION=2.7.4
+ARG VARIANT=alpine
+
+FROM docker.io/caddy:${VERSION}-builder-${VARIANT} AS builder
+
+RUN xcaddy build \
+    --with github.com/gamalan/caddy-tlsredis
+
+FROM docker.io/caddy:${VERSION}-${VARIANT}
+
+COPY --from=builder /usr/bin/caddy /usr/bin/caddy
+
+COPY Caddyfile /etc/caddy/
+COPY public /srv
+
+EXPOSE 9091/tcp
+
+ENV SITE_ROOT=/srv
+
+RUN mkdir /etc/caddy/globals/
+RUN touch /etc/caddy/globals/dummy
+RUN ["/usr/bin/caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
+
+COPY redis.Caddyfile /etc/caddy/globals/redis
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0d658d4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,93 @@
+.SHELLFLAGS := -eu -o pipefail -c
+.ONESHELL:
+.DELETE_ON_ERROR:
+
+PREFIX ?= public
+
+md_files := $(shell fd . content --type=file -e md)
+brotli := @brotli --force --no-copy-stat
+gzip := @gzip --best --keep --force
+zstd := @zstd --force --no-pass-through -19 --quiet --stdout
+pr   := @printf "%4s %s\n"
+
+build: $(PREFIX)/index.html
+
+$(PREFIX)/index.html: config.toml $(md_files)
+	zola build --force --output-dir $(PREFIX)
+
+format: .formatstamp
+
+.formatstamp: $(PREFIX)/index.html
+	@echo "Formatting HTML..."
+	@prettier --write --parser html --print-width 200 "$(PREFIX)/**/*.html" > /dev/null
+	@touch .formatstamp
+
+clean:
+	rm -rf ./$(PREFIX)
+	rm -rf ./.*stamp
+
+to_compress := $(shell fd . $(PREFIX) --type=file -e html -e xml -e txt -e asc)
+brotli_files := $(patsubst %, %.br, $(to_compress))
+gzip_files := $(patsubst %, %.gz, $(to_compress))
+zstd_files := $(patsubst %, %.zst, $(to_compress))
+
+%.html.br: %.html
+	@echo $@
+	$(brotli) $<
+
+%.xml.br: %.xml
+	@echo $@
+	$(brotli) $<
+
+%.txt.br: %.txt
+	@echo $@
+	$(brotli) $<
+
+%.asc.br: %.asc
+	@echo $@
+	$(brotli) $<
+
+%.html.gz: %.html
+	@echo $@
+	$(gzip) $<
+
+%.xml.gz: %.xml
+	$(gzip) $<
+
+%.txt.gz: %.txt
+	@echo $@
+	$(gzip) $<
+
+%.asc.gz: %.asc
+	@echo $@
+	$(gzip) $<
+
+%.html.zst: %.html
+	@echo $@
+	$(zstd) $< > $@
+
+%.xml.zst: %.xml
+	@echo $@
+	$(zstd) $< > $@
+
+%.txt.zst: %.txt
+	@echo $@
+	$(zstd) $< > $@
+
+%.asc.zst: %.asc
+	@echo $@
+	$(zstd) $< > $@
+
+compress: .compressstamp
+
+.compressstamp: .formatstamp $(brotli_files) $(gzip_files) $(zstd_files)
+	@echo "Compressing output files..."
+	@touch .compressstamp
+
+deploy: .deploystamp
+
+.deploystamp: .compressstamp Caddyfile redis.Caddyfile Dockerfile fly.toml
+	fly deploy
+	@touch .deploystamp
+
+.PHONY: all clean build format compress deploy
diff --git a/config.toml b/config.toml
index 78e9f9a..55e8931 100644
--- a/config.toml
+++ b/config.toml
@@ -1,35 +1,52 @@
-title = "Zola ʕ•ᴥ•ʔ Bear Blog"
-base_url = "https://zola-bearblog.netlify.app/"
-description = "A Zola-theme based on Bear Blog."
+default_language = "en-GB"
+base_url = "https://alanpearce.eu"
 
-# Whether to automatically compile all Sass files in the sass directory
-compile_sass = false
+title = "Alan Pearce"
+description = "Developer, Emacs User"
 
-# Whether to build a search index to be used later on by a JavaScript library
-build_search_index = false
+generate_feed = true
 
-taxonomies = [
-  {name = "categories", feed = true},
-  {name = "tags", feed = true},
-]
+theme = "bear"
 
 [markdown]
-# Whether to do syntax highlighting
-# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
 highlight_code = true
+highlight_theme = "idle"
+
+[[taxonomies]]
+name = "tags"
+feed = true
 
 [extra]
-date_format="%d %b, %Y"
-webserver_sends_csp_headers=true
+gpg_fingerprint = "48E6 576C 0707 388C B8BE FD0C CD4B EB92 A8D4 6583"
+gpg_url = "/public_key.asc"
+author_name = "Alan Pearce"
+author_image = "/img/me-thumb.jpg"
+hide_made_with_line = true
+date_format = "%F"
 
 [[extra.main_menu]]
-name = "Bear"
-url = "@/bear.md"
-
+    name = "Posts"
+    url = "/post/"
 [[extra.main_menu]]
-name = "Zola"
-url = "@/zola.md"
-
+    name = "Tags"
+    url = "/tags/"
 [[extra.main_menu]]
-name = "Blog"
-url = "@/blog/_index.md"
+    name = "Repositories"
+    url = "https://git.alanpearce.eu"
+
+[[extra.contact_menu]]
+    name = "alan@alanpearce.eu"
+    url = "mailto:alan@alanpearce.eu"
+    weight = 1
+[[extra.contact_menu]]
+    name = "Codeberg"
+    url = "https://codeberg.org/alanpearce"
+[[extra.contact_menu]]
+    name = "GitHub"
+    url = "https://github.com/alanpearce"
+[[extra.contact_menu]]
+    name = "Mastodon"
+    url = "https://ieji.de/@alanpearce"
+[[extra.contact_menu]]
+    name = "Bluesky"
+    url = "https://bsky.app/profile/alanpearce.eu"
diff --git a/content/_index.md b/content/_index.md
index 24b3925..2bc1185 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -1,23 +1,7 @@
 +++
+title = "Home"
 +++
-# A match made in heaven
-
-There is a website obesity crisis. Bloated websites full of scripts, ads, and trackers are slowing your readers down every time they try to read your well-crafted content.
-
-Zola Bear Blog is all you need to build a fantastic and optimized site or blog. It works perfectly on **any** viewing device. All you need to focus on is writing good content.
-
-[Go to the original bear blog](https://bearblog.dev/).
-
----
-
-What happens when you combine the worlds' fastest, most lightweight static site generator with a design theme built to provide you with free, no-nonsense, super-fast blogging capabilities?
-
-**Use this theme, and find out!**
-
-Made with 💚 by [Alan Pearce](https://alanpearce.eu).
-
----
-
-Simply publish content online, grow an audience, and keep your pages tiny, fast, and **optimized for search engines**.
-
-Each page is ~5kb, and you can **host your blog yourself**.
+<p class="p-note note">
+I work as a Full-stack Developer in Berlin.  I occasionally write about Emacs and
+development-related topics.
+</p>
diff --git a/content/feed-styles/index.md b/content/feed-styles/index.md
new file mode 100644
index 0000000..bac7916
--- /dev/null
+++ b/content/feed-styles/index.md
@@ -0,0 +1,4 @@
++++
+title = "feed-styles"
+template = "feed-styles.html"
++++
diff --git a/content/post/_index.md b/content/post/_index.md
new file mode 100644
index 0000000..e0d2523
--- /dev/null
+++ b/content/post/_index.md
@@ -0,0 +1,6 @@
++++
+title = "Posts"
+sort_by = "date"
+paginate_reversed = true
+transparent = true
++++
diff --git a/content/post/a-new-site.md b/content/post/a-new-site.md
new file mode 100644
index 0000000..af61143
--- /dev/null
+++ b/content/post/a-new-site.md
@@ -0,0 +1,15 @@
++++
+description = "I made a website."
+title = "A New Site"
+date = 2014-06-07T20:16:16Z
+[taxonomies]
+tags = ["website"]
++++
+
+I finally got around to making a website.  I decided to use [Hugo][] with a slightly-modified [Hyde theme][]
+
+Someday I'll make my own theme, probably using [Stylus][] for CSS processing. But for now, this will do.  The more important thing is just to create some content.
+
+[Hugo]: https://gohugo.io
+[Hyde theme]: https://github.com/spf13/hyde
+[Stylus]: http://learnboost.github.io/stylus/
diff --git a/content/post/back-again.md b/content/post/back-again.md
new file mode 100644
index 0000000..9529415
--- /dev/null
+++ b/content/post/back-again.md
@@ -0,0 +1,15 @@
++++
+description = "I'm back"
+date = 2017-05-06T16:55:57+02:00
+title = "Back again"
+[taxonomies]
+tags = ["website"]
++++
+
+I've not made any posts for quite some time.  My life has changed
+quite a bit, I've emigrated from the UK and it's only now that I'm
+starting to feel more settled.
+
+I hope to start posting a bit more often.  Hopefully this post,
+despite being light on content, will help me to get back into the
+sharing mindset.
diff --git a/content/post/cedit-and-paredit.md b/content/post/cedit-and-paredit.md
new file mode 100644
index 0000000..89f8cb3
--- /dev/null
+++ b/content/post/cedit-and-paredit.md
@@ -0,0 +1,44 @@
++++
+description = "Cedit and paredit for structural editing"
+title = "Cedit and Paredit"
+date = 2014-08-04T07:10:14Z
+[taxonomies]
+tags = ["development", "emacs"]
++++
+
+I recently discovered [cedit][], which provides some structural
+commands for editing c-like languages.  (See this
+[Emacs Rocks! episode][e14] if you're not familiar with the concept:
+it introduces [paredit][], a structural editing mode for lisps).
+
+So, it deals with curly braces and semicolons, keeping things balanced
+and correct as show in its [screencast][cedit-readme]. It mentions that it
+integrates with [paredit][] rather than duplicating *all* its
+functionality.  After setting up cedit, I decided to try enabling
+paredit alongside cedit and disabling autopair.  Once I did,
+however, I noticed an annoying formatting issue: If I were to type
+`foo` and then `(`, paredit would format this as `foo ()`, which makes
+sense, considering that paredit is written for lisps — s-expressions
+are usually separated by spaces — but not so much for c-like languages.
+
+I was thinking about disabling paredit and going back to autopair,
+when I decided to look through the configuration variables for
+paredit.  Turns out it provides
+`paredit-space-for-delimiter-predicates`, which is a list of functions
+that control whether a space should be inserted.  So, solving the
+formatting issue turned out to be pretty simple:
+
+```lisp
+(defun ap/cedit-space-delimiter-p (endp delimiter)
+"Don't insert a space before delimiters in c-style modes"
+(not cedit-mode))
+(add-to-list 'paredit-space-for-delimiter-predicates #'ap/cedit-space-delimiter-p)
+```
+
+Hopefully that saves someone some time if they try to use the two
+together.
+
+[cedit]: https://github.com/zk-phi/cedit
+[cedit-readme]: https://github.com/zk-phi/cedit#readme
+[e14]: http://emacsrocks.com/e14.html
+[paredit]: http://www.emacswiki.org/emacs/ParEdit
diff --git a/content/post/emacs-package-archive-statistics.md b/content/post/emacs-package-archive-statistics.md
new file mode 100644
index 0000000..6efa5bd
--- /dev/null
+++ b/content/post/emacs-package-archive-statistics.md
@@ -0,0 +1,169 @@
++++
+description = "Working out which package archives I'm using"
+title = "Emacs Package Archive Statistics"
+date = 2014-07-19T13:19:54Z
+[taxonomies]
+tags = ["emacs"]
++++
+
+I use [cask][] for managing the dependencies of my Emacs
+configuration.  Whenever I opened my `Cask` file, I wondered if I
+really was using all the sources I had defined:
+
+```lisp
+(source gnu)
+(source marmalade)
+(source melpa)
+(source melpa-stable)
+(source org)
+```
+
+It seemed quite strange that we have so many package repositories in
+the Emacs world and I'm not even using all of them.  I find this state
+less than ideal, much as
+[Jorgen Schäfer details][state of emacs package archives].  My ideal
+package repository would be once that works with VCS releases, mostly
+because it's a much simpler process to work with than having to sign
+up to yet another website just to upload a package, then ensure it's
+kept up-to-date on every release.
+
+As such, I prefer the concepts behing [MELPA][] and [MELPA Stable][] to
+those of [Marmalade][].  [GNU ELPA][] doesn't appear to allow any
+submissions and [org][org archive] is specific to [org-mode].  I've
+also noticed that many packages I find and use are on github and so
+work with the [MELPA][] system.  However, I don't like [MELPA's][MELPA]
+versioning: it just gets the latest code and puts the build date in
+the version, meaning that packages could break at any time.
+
+So, ideally I would use [MELPA Stable][] as much as possible and reduce my
+usage of [Marmalade][] and [MELPA][].  [GNU ELPA][] doesn't appear to have
+many packages, but I wasn't sure if I was using any.
+I couldn't see the information listed in the `*Packages*` buffer, so I
+decided to try to figure out how to generate some usage statistics.
+
+I found [how to get a list of installed packages][], but that just gives
+a list:
+
+```lisp
+(ace-jump-mode ag auto-compile auto-indent-mode autopair ...)
+```
+
+I needed to get more information about those packages.  I looked at
+where `list-packages` gets that information from.  It seems that
+`package-archive-contents` is a list of cons cells:
+
+```lisp
+(org-plus-contrib .
+				  [(20140714)
+				  nil "Outline-based notes management and organizer" tar "org"])
+```
+
+Then created a function to loop over the contents of
+`package-activated-list`, retrieving the corresponding contents of
+`package-archive-contents`:
+
+```lisp
+(defun package-list-installed ()
+  (loop for pkg in package-activated-list
+        collect (assq pkg package-archive-contents)))
+```
+
+This generates a list of arrays from `package-archive-contents`.
+There are some helper functions in package.el such as
+`package-desc-kind`.  `package-desc-archive` was exactly what I
+needed.  I happened to be using a pretest version of Emacs at the time
+and didn't know that it's not in 24.3, so I just made sure it was defined:
+
+```lisp
+(if (not (fboundp #'package-desc-archive))
+    (defsubst package-desc-archive (desc)
+      (aref desc (1- (length desc)))))
+```
+
+Weirdly, some of the arrays (seemingly the ones from the
+[org archive][]) had a different length, but the repository/archive was
+always the last element, which is why I used `(1- (length ))` and not
+a constant, like the other `package-desc-*` functions.
+
+To generate a list of statistics, I just needed to loop over the
+installed packages from `package-list-installed` and update a count
+for each archive:
+
+```lisp
+(defun package-archive-stats ()
+  (let ((archives (makehash))
+        (assoc '()))
+    (dolist (arc package-archives)
+      (puthash (car arc) 0 archives))
+    (maphash (lambda (k v)
+               (setq assoc (cons (cons k v) assoc)))
+             (dolist (pkg (-filter #'identity (package-list-installed)) archives)
+               (let ((pkg-arc (package-desc-archive (cdr pkg))))
+                 (incf (gethash pkg-arc archives)))))
+    assoc))
+```
+
+Running this gives a list of cons cells:
+
+```lisp
+(("gnu" . 0)
+ ("org" . 1)
+ ("melpa-stable" . 2)
+ ("melpa" . 106)
+ ("marmalade" . 1))
+```
+
+I wrapped it in an interactive function so that I could check the
+numbers quickly:
+
+```lisp
+(defun package-show-archive-stats ()
+  (interactive)
+  (message "%s" (package-archive-stats)))
+```
+
+With that, I removed `(source gnu)` from my `Cask` file.  Now I had
+another question.  What package was installed from [marmalade][]?  In
+the lisp fashion, I created yet another function:
+
+```lisp
+(defun package-show-installed-from-archive (archive)
+  (interactive (list (helm-comp-read "Archive: " (mapcar #'car package-archives)
+                                      :must-match t)))
+  (let ((from-arc (mapcar #'car
+                          (--filter (equalp (package-desc-archive (cdr it)) archive)
+                                    (package-list-installed)))))
+    (if (called-interactively-p)
+        (message "%s" from-arc)
+      from-arc)))
+```
+(Non-helm users can replace `helm-comp-read` with
+`ido-completing-read` or similar)
+
+Running this with the argument `"marmalade"` gives:
+
+```lisp
+(php-extras)
+```
+
+I checked on [MELPA Stable][] and [MELPA][], but it's not available
+there.  Given that I use [php-extras][] quite a bit at work, I can't remove
+[marmalade][] just yet.  However, as it's a git repository, it should be
+easy for me to create a recipe for MELPA.  Then I can remove marmalade
+from my [cask][] configuration.  Hooray for simplification!
+
+Hopefully, packaging in Emacs will become simpler in the future.
+There are some interesting things in 24.4 like pinning packages to a
+repository, which would allow [MELPA Stable][] to be used even when
+[MELPA][] defines the same package with a higher "version".
+
+[cask]: https://github.com/cask/cask/
+[state of emacs package archives]: http://blog.jorgenschaefer.de/2014/06/the-sorry-state-of-emacs-lisp-package.html
+[marmalade]: http://marmalade-repo.org/
+[GNU ELPA]: http://elpa.gnu.org/packages/
+[MELPA]: https://melpa.org/
+[MELPA Stable]: http://stable.melpa.org/
+[org archive]: http://orgmode.org/elpa.html
+[how to get a list of installed packages]: http://stackoverflow.com/questions/13866848/how-to-save-a-list-of-all-the-installed-packages-in-emacs-24
+[php-extras]: https://github.com/arnested/php-extras
+[org-mode]: http://orgmode.org/
diff --git a/content/post/git-cloning-similar-repositories.md b/content/post/git-cloning-similar-repositories.md
new file mode 100644
index 0000000..8f1eb43
--- /dev/null
+++ b/content/post/git-cloning-similar-repositories.md
@@ -0,0 +1,19 @@
++++
+description = "Speed up cloning of similar git repositories"
+title = "Cloning Similar Git Repositories"
+date = 2014-06-22T08:35:24Z
+[taxonomies]
+tags = ["git"]
++++
+With multiple similar git repositories, for example where a base repository contains a framework or base system installation and other repositories are created from that repository, it's possible to save some time when cloning down another repository by using the `reference` option to [git-clone][]:
+
+	git clone git@github.com/my/repo --reference another-repo
+(Where `another-repo` points to a local version of a repository.)
+
+The reference here doesn't have to be the base repository itself, it could just be another variant of it.  The speedup can be quite dramatic if the repositories have megabytes of shared history, from minutes to seconds.
+
+On a related note, I'm surprised that [GitHub][] doesn't allow for multiple renamed forks, which would be very useful in this scenario.  [BitBucket][] does support this, however.  It even has a 'sync' button for pulling updates from the base into the child repositories, which is very useful, especially for those who prefer GUIs over CLIs.
+
+[git-clone]:https://www.kernel.org/pub/software/scm/git/docs/git-clone.html
+[GitHub]:https://github.com
+[BitBucket]:https://bitbucket.org/
diff --git a/content/post/nixos-on-nanopi-r5s.md b/content/post/nixos-on-nanopi-r5s.md
new file mode 100644
index 0000000..825d6b4
--- /dev/null
+++ b/content/post/nixos-on-nanopi-r5s.md
@@ -0,0 +1,142 @@
++++
+title = "Running NixOS on a NanoPi R5S"
+date = 2023-07-30T08:51:46Z
+[taxonomies]
+tags = ["NixOS", "home-networking", "infrastructure"]
++++
+
+I managed to get [NixOS](https://nixos.org) running on my [NanoPi R5S](https://www.friendlyelec.com/index.php?route=product/product&product_id=287) ([FriendlyElec Wiki](https://wiki.friendlyelec.com/wiki/index.php/NanoPi_R5S)).
+
+Firstly, I flashed a pre-built stock Debian image from [inindev](https://github.com/inindev/nanopi-r5) to an SD card. This can be used as a rescue system later on. 
+
+From that SD card, I then flashed the same system onto the internal <abbr title="embedded MultiMediaCard">eMMC</abbr> Storage. I only really needed to this to ensure UBoot was correctly installed; I think there will be an easier way to do it.
+
+I had nix already installed on the <abbr title="Non-Volatile Memory Express">NVMe</abbr> <abbr title="Solid-State Drive">SSD</abbr> along with a home directory. I bind-mounted `/nix` and `/home` following the fstab I had previously set up:
+
+```conf
+UUID=replaceme  /mnt    ext4    relatime,lazytime   0 2
+/mnt/nix        /nix    none    defaults,bind       0 0
+/mnt/srv        /srv    none    defaults,bind       0 0
+/mnt/home       /home   none    defaults,bind       0 0
+```
+
+I then created a user for myself using that home directory, I had full access to nix in the new Debian environment. This meant I had access to `nixos-install`. 
+
+I wanted to use the [extlinux support in UBoot](https://u-boot.readthedocs.io/en/latest/develop/distro.html#boot-configuration-files), so I made `/mnt/boot` point to `/boot` on the <abbr>eMMC</abbr>:
+
+```sh
+mkdir /mnt/{emmc,boot}
+mount LABEL=rootfs /mnt/emmc
+mount --bind /mnt/emmc /mnt/boot
+```
+
+<aside>
+One could <em>probably</em> delete everything else on the <abbr>eMMC</abbr> and move the contents of <code>/mnt/emmc/boot</code> to <code>/mnt/emmc</code>, thus obviating the need to bind-mount <code>/boot</code>
+</aside>
+
+I ran `nixos-generate-config` as usual, which set up the mount points in `hardware-configuration.nix` correctly. `configuration.nix` needed a bit of tweaking. My first booting configuration was something like this, mostly borrowed from [Artem Boldariev's comment](https://github.com/inindev/nanopi-r5/issues/11#issue-1789308883):
+
+```nix
+{ config
+, pkgs
+, lib
+, ...
+}:
+let
+  fsTypes = [ "f2fs" "ext" "exfat" "vfat" ];
+in
+{
+  imports = [ ./hardware-configuration.nix ];
+  boot = {
+    kernelPackages = pkgs.linuxKernel.packages.linux_6_4;
+
+    # partial Rockchip related changes from Debian 12 kernel version 6.1
+    # Also, see here:
+    # https://discourse.nixos.org/t/how-to-provide-missing-headers-to-a-kernel-build/11422/3
+    kernelPatches = [
+      {
+        name = "rockchip-config.patch";
+        patch = null;
+        extraConfig = ''
+          PHY_ROCKCHIP_PCIE Y
+          PCIE_ROCKCHIP_EP y
+          PCIE_ROCKCHIP_DW_HOST y
+          ROCKCHIP_VOP2 y
+        '';
+      }
+      {
+        name = "status-leds.patch";
+        patch = null;
+        # old:
+        # LEDS_TRIGGER_NETDEV y
+        extraConfig = ''
+          LED_TRIGGER_PHY y
+          USB_LED_TRIG y
+          LEDS_BRIGHTNESS_HW_CHANGED y
+          LEDS_TRIGGER_MTD y
+        '';
+      }
+    ];
+    
+    supportedFilesystems = fsTypes;
+    initrd.supportedFilesystems = fsTypes;
+
+    initrd.availableKernelModules = [
+      ## Rockchip
+      ## Storage
+      "sdhci_of_dwcmshc"
+      "dw_mmc_rockchip"
+
+      "analogix_dp"
+      "io-domain"
+      "rockchip_saradc"
+      "rockchip_thermal"
+      "rockchipdrm"
+      "rockchip-rga"
+      "pcie_rockchip_host"
+      "phy-rockchip-pcie"
+      "phy_rockchip_snps_pcie3"
+      "phy_rockchip_naneng_combphy"
+      "phy_rockchip_inno_usb2"
+      "dwmac_rk"
+      "dw_wdt"
+      "dw_hdmi"
+      "dw_hdmi_cec"
+      "dw_hdmi_i2s_audio"
+      "dw_mipi_dsi"
+    ];
+    loader = {
+      timeout = 3;
+      grub.enable = false;
+      generic-extlinux-compatible = {
+        enable = true;
+        useGenerationDeviceTree = true;
+      };
+    };
+  };
+  # this file is from debian and should be in /boot/
+  hardware.deviceTree.name = "../../rk3568-nanopi-r5s.dtb";
+  # Most Rockchip CPUs (especially with hybrid cores) work best with "schedutil"
+  powerManagement.cpuFreqGovernor = "schedutil";
+  
+  boot.kernelParams = [
+    "console=tty1"
+    "console=ttyS2,1500000"
+    "earlycon=uart8250,mmio32,0xfe660000"
+  ];
+  # Let's blacklist the Rockchips RTC module so that the
+  # battery-powered HYM8563 (rtc_hym8563 kernel module) will be used
+  # by default
+  boot.blacklistedKernelModules = [ "rtc_rk808" ];
+
+  # ... typical config omitted for brevity
+}
+```
+
+Due to the custom kernel configuration, building takes a while. I set up a [distributed build](https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html) to speed things up, using a [Hetzner Cloud](https://www.hetzner.com/cloud) CAX21 ARM64 instance (although I could have used an x86_64 system with one of the methods mentioned on the [NixOS on ARM NixOS wiki page](https://nixos.wiki/wiki/NixOS_on_ARM#Build_your_own_image_natively)). This made for a very long `nixos-install` command line:
+
+```sh
+sudo env PATH=$PATH =nixos-install --root /mnt --no-channel-copy --channel https://nixos.org/channels/nixos-23.05 --option builders'ssh://my-host aarch64-linux /root/.ssh/id_pappel_nixpkgs 4 2 big-parallel' --option builders-use-substitutes true --max-jobs 0
+```
+
+I added `setenv bootmeths "extlinux"` to `/boot/boot.txt` and ran `/boot/mkscr.sh` as root to ensure that UBoot would search for the `extlinux.conf` file
diff --git a/content/post/now-on-three-continents.md b/content/post/now-on-three-continents.md
new file mode 100644
index 0000000..3e813aa
--- /dev/null
+++ b/content/post/now-on-three-continents.md
@@ -0,0 +1,25 @@
++++
+title = "Now on three continents"
+description = "This website is now hosted on three continents"
+date = 2023-07-02
+[taxonomies]
+tags = ["website", "infrastructure"]
++++
+
+This website is now hosted on three continents.
+
+I recently changed the hosting for this site to [fly](http://fly.io), since I was rather intrigued by the idea of being able to run three small <abbr>VMs</abbr> (<dfn id="VMs">Virtual Machines</dfn>) worldwide for free. I would gladly have paid a small amount for their services. If they didn't have a free allowance for <abbr>VMs</abbr> then it would only be around $6 a month, so I'm not worried about them removing the free allowance.
+
+Previously it was running on one [Hetzner](https://www.hetzner.com) <abbr title="Virtual Machine">VM</abbr> in Nuremberg, Germany that I set up and maintained myself. The maintenance wasn't a problem for me, but rather the idea of slow loading times for anyone reading this outside of Europe.
+
+American visitors should notice a definite speedup now, as there's a server on the west coast and for the few visitors in the Asia-Pacific region, there's also a server in Australia. I kept track of the response time before and after the change using the [Online or not](https://onlineornot.com/) [Do I need a CDN?](https://onlineornot.com/do-i-need-a-cdn) tool, which you can see in the table below (measured in <abbr title="milliseconds">ms</abbr>)
+
+| Region                  | Before | After |
+|-------------------------|--------|-------|
+| Europe (Frankfurt)      | 62     | 32    |
+| US East (N. Virginia)   | 348    | 185   |
+| US West (N. California) | 503    | 61    |
+| Asia Pacific (Tokyo)    | 732    | 251   |
+| Asia Pacific (Sydney)   | 1114   | 76    |
+
+I do find it rather amusing that I spend more time tinkering with the site than actually posting anything, but, for once, tinkering has actually led to me posting something (this post). I would like to think that this might encourage me to post more in the future, but only time will tell.
diff --git a/content/post/opening-projects-with-projectile.md b/content/post/opening-projects-with-projectile.md
new file mode 100644
index 0000000..b44c5e8
--- /dev/null
+++ b/content/post/opening-projects-with-projectile.md
@@ -0,0 +1,80 @@
++++
+description = ""
+title = "Opening Projects with Projectile"
+date = 2014-07-12T09:12:34Z
+[taxonomies]
+tags = ["emacs", "lisp"]
++++
+
+I use [Projectile][] for working with projects in Emacs.  It's really good at finding files in projects, working with source code indexes (I use [Global][]), and with its [perspective][] support, it's also great at separating projects into workspaces.  However, I've always felt it lacking in actually opening projects.  I tend to work on different projects all the time and `projectile-switch-project` only tracks projects once they've been opened initially (despite the name, it works across Emacs sessions).
+
+With this in mind, I decided to try to add support for opening projects under a given subdirectory, e.g. `~/projects`, regardless of whether or not I've visited them before.
+
+I saw that projectile uses [Dash.el][] in some places, and after reading about [anaphoric macros], I decided that I'd try to use them to aid me.
+
+```lisp
+(defun ap/subfolder-projects (dir)
+  (--map (file-relative-name it dir)
+         (-filter (lambda (subdir)
+                    (--reduce-from (or acc (funcall it subdir)) nil
+                                   projectile-project-root-files-functions))
+                  (-filter #'file-directory-p (directory-files dir t "\\<")))))
+```
+
+First, this filters the non-special files under `dir`, filtering non-directories.  Then it runs the list of `projectile-project-root-files-functions` on it to determine if it looks like a projectile project.  To make the list more readable, it makes the filenames relative to the passed-in directory.  It runs like this:
+                  
+```lisp
+(ap/subfolder-projects "~/projects") =>
+("dotfiles" "ggtags" …)
+```
+
+So, we've got ourselves a list, but now we need to be able to open the project that's there, even though the folders are relative.
+
+```lisp
+(defun ap/open-subfolder-project (from-dir &optional arg)
+  (let ((project-dir (projectile-completing-read "Open project: "
+                                     (ap/subfolder-projects from-dir))))
+    (projectile-switch-project-by-name (expand-file-name project-dir from-dir) arg)))
+```
+
+By wrapping the call to `ap/subfolder-projects` in another function that takes the same directory argument, we can re-use the project parent directory and expand the selected project name into an absolute path before passing it to `projectile-switch-project-by-name`.
+
+We get support for multiple completion systems for free, since projectile has a wrapper function that works with the default system, ido, [grizzl][] and recently, [helm][].
+
+Then I defined some helper functions to make it easy to open work and home projects.
+
+```lisp
+(defvar work-project-directory "~/work")
+(defvar home-project-directory "~/projects")
+
+(defun ap/open-work-project (&optional arg)
+  (interactive "P")
+  (ap/open-subfolder-project work-project-directory arg))
+
+(defun ap/open-home-project (&optional arg)
+  (interactive "P")
+  (ap/open-subfolder-project home-project-directory arg))
+```
+
+I could probably simplify this with a macro, but I'm not sure that there's much advantage in it.  I only have two project types right now, after all.
+
+With this all set up, whenever I want to start working on a project I just type `M-x home RET` to call up the list.
+
+I also considered trying to add all the projects under a directory to the projectile known project list.  I didn't find it quite as easy to use, but it's available below if anyone would prefer that style.
+
+```lisp
+(defun ap/-add-known-subfolder-projects (dir)
+  (-map #'projectile-add-known-project (--map (concat (file-name-as-directory dir) it) (ap/subfolder-projects dir))))
+
+(defun ap/add-known-subfolder-projects ()
+  (interactive)
+  (ap/-add-known-subfolder-projects (ido-read-directory-name "Add projects under: ")))
+```
+
+[Projectile]: https://github.com/bbatsov/projectile
+[Dash.el]: https://github.com/magnars/dash.el
+[Helm]: https://github.com/emacs-helm/helm
+[Global]: https://www.gnu.org/software/global/
+[Anaphoric macros]: https://en.wikipedia.org/wiki/Anaphoric_macro
+[Perspective]: https://github.com/nex3/perspective-el
+[Grizzl]: https://github.com/d11wtq/grizzl
diff --git a/content/post/postfix-as-null-client-with-external-catchall.md b/content/post/postfix-as-null-client-with-external-catchall.md
new file mode 100644
index 0000000..b3fd5a4
--- /dev/null
+++ b/content/post/postfix-as-null-client-with-external-catchall.md
@@ -0,0 +1,55 @@
++++
+title = 'Postfix on a NixOS null client with external catch-all'
+date = 2020-09-11T18:49:00+02:00
+
+[taxonomies]
+tags = ["nixos","infrastructure"]
++++
+I wanted to set up a server so that any local email (e.g. generated by cron jobs/systemd timers) would be forwarded to an external address, regardless of the user.  I also wanted the from address to keep the system hostname whilst not allowing any external use of the mailserver.
+
+It took me a while to figure out how to this, so I thought I'd share my method.
+
+Here's the config that can be used to do this on any NixOS host, after redefining the first two variables.
+
+```txt,linenos,hl_lines=2-3
+services.postfix = let
+  localUser = "example-user";
+  forwardingAddress = "user@external.domain";
+in
+{
+  enable = true;
+  destination = [];
+  domain = config.networking.domain;
+  virtual = ''
+    @${config.networking.hostName}.${config.networking.domain} ${localUser}
+    ${localUser} ${forwardingAddress}
+  '';
+  config = {
+    inet_interfaces = "loopback-only";
+  };
+};
+```
+
+Emails to any user without a domain part are all sent to the forwarding address with a clear *from* address (e.g. `System administrator <root@host.example.com>`).
+
+## Background 
+
+First, the basic setup for a null client can be found in the [postfix documentation][0]. The example config would be translated into NixOS like so:
+
+```txt
+services.postfix = {
+  enable = true;
+  destination = [];
+  domain = config.networking.domain;
+  origin = config.networking.domain;
+  relayHost = config.networking.domain;
+  lookupMX = true;
+  config = {
+    inet_interfaces = "loopback-only";
+  };
+};
+```
+
+However, this rewrites user\@hostname.example.com to user\@example.com (due to `origin` on line 5).  I wanted to be able to see which host a mail concerns. 
+
+[0]: http://www.postfix.org/STANDARD_CONFIGURATION_README.html#null_client
diff --git a/content/post/repository-management-with-ghq.md b/content/post/repository-management-with-ghq.md
new file mode 100644
index 0000000..dd21db9
--- /dev/null
+++ b/content/post/repository-management-with-ghq.md
@@ -0,0 +1,76 @@
++++
+date = 2017-05-06T23:31:51+02:00
+title = "Repository management with ghq"
+[taxonomies]
+tags = ["development","git"]
++++
+
+I recently encountered [ghq][], a tool for automatically organising VCS-backed
+projects automatically.  Give it a repository URL, it will clone a project to
+your projects dir (set by `$GHQ_ROOT`) like so:
+
+```sh
+$ ghq get https://github.com/motemen/ghq
+# Runs `git clone https://github.com/motemen/ghq ~/.ghq/github.com/motemen/ghq`
+```
+
+I don't like the idea of having projects hidden away, so I set
+`$GHQ_ROOT` to `$HOME/projects`.
+
+From there, the `list` and `look` subcommands allow listing
+repositories and visiting them in the shell (actually a subshell).
+
+I wanted a nicer way to visit project directories.  Since I'm
+using [fzf][] as a fuzzy-finder, I thought it would be nice to use it
+for this.  I created a simple function, `fp` (find project) to do that:
+
+```sh
+fp () {
+  ghq look $(ghq list | fzf +m)
+}
+```
+
+I ran into some issues with the subshell of `ghq look` and wondered
+whether it might be possible to create a zsh command to remove the
+need for a subshell.
+
+I found that `fzf` includes a [cd-widget function][fzf-cd-widget] and created
+something similar that uses `ghq` instead of `find`:
+
+```sh
+cd-project-widget () {
+  local cmd="ghq list"
+  setopt localoptions pipefail 2> /dev/null
+  local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" fzf +m)"
+  if [[ -z "$dir" ]]; then
+    zle redisplay
+    return 0
+  fi
+  cd $(ghq list --full-path | grep "$dir")
+  local ret=$?
+  zle reset-prompt
+  typeset -f zle-line-init >/dev/null && zle zle-line-init
+  return $ret
+}
+zle -N cd-project-widget
+```
+
+It should be quite simple to modify it to work with other
+fuzzy-finders.  The basic idea is to show the output of `ghq list` for
+selection, and use `ghq list --full-path` with the selected candidate
+to print the correct directory for `cd`.
+
+What's really nice about this, is that I can bind it to a key
+sequence:
+
+```sh
+bindkey '\es' cd-project-widget
+```
+
+Now I can press `M-s` in a shell, start typing "nixfiles" and press enter to `cd`
+to my [nixfiles][] project. Pretty neat!
+
+[ghq]:https://github.com/motemen/ghq
+[fzf]:https://github.com/junegunn/fzf
+[fzf-cd-widget]:https://github.com/junegunn/fzf/blob/337cdbb37c1efc49b09b4cacc6e9ee1369c7d76d/shell/key-bindings.zsh#L40-L54
+[nixfiles]:https://git.alanpearce.eu/dotfiles
diff --git a/content/post/self-hosted-git.md b/content/post/self-hosted-git.md
new file mode 100644
index 0000000..d0ac370
--- /dev/null
+++ b/content/post/self-hosted-git.md
@@ -0,0 +1,147 @@
++++
+description = "I describe my git server setup (using cgit and gitolite), and what it allows"
+date = 2017-06-04T12:33:02+02:00
+title = "A simple, powerful self-hosted git setup"
+[taxonomies]
+tags = ["development","git"]
++++
+
+I had been using [gogs][] for about a year.  It worked reasonably
+well, as it focuses on being a lightweight self-hosted GitHub
+replacement.  However, that wasn't really what I wanted.  I just
+wanted to host my own projects, I didn't need things like issues, pull
+requests or wikis.
+
+I recently switched to [gitolite][] and [cgit][], as they were even
+lighter on resources, don't require another login and work without
+an external database.  Gitolite is unusual in its configuration: it
+creates a git repository with its configuration file.  I will describe
+how I use them, rather than how to set them up, as they both have
+enough documentation on that.
+
+My gitolite configuration file looks like this:
+
+```
+repo gitolite-admin
+    RW+     =   alan
+
+repo dotfiles
+    C   =   alan
+    RW+ =   alan
+    R   =   READERS
+    option hook.post-update   =    github-mirror
+
+repo [a-z].*
+    C   =   alan
+    RW+ =   CREATOR
+    RW  =   WRITERS
+    R   =   READERS
+```
+
+The first block just allows me to work with the configuration
+repository, as the initial setup only enables one specific public SSH
+key, whereas I have three keys that I configure gitolite with.
+
+The second configures my dotfiles specifically.  Naturally, I should
+be the only person with read/write access.  The `R = READERS` line
+allows remote configuration of read permissions via `ssh $DOMAIN
+perms` (explained further below).  The last line runs a mirror script
+(just `git push --mirror…`) so that
+my [dotfiles repository on GitHub][dotfiles-github] is updated when I
+push to my private version.
+
+## Wild (or magic) repositories
+
+The third block is where things get interesting.  gitolite has a
+feature called [wildrepos][], which allows configuring a set of
+repositories at once, using a regular expression to match the
+repository name.
+
+The really nice thing here is that the repository need not exist
+before applying the configuration.  Therefore, the line `C = alan`
+means that I can create a remote repository automatically by cloning a
+repository URL that doesn't already exist.
+I can clone and create a new repo simultaneously like so:
+
+```bash
+cd ~/projects
+git clone alanpearce.eu:some-new-repository
+```
+
+But with [ghq][], which I [blogged about before][using-ghq], I don't
+have to concern myself with where to put the repository:
+
+```bash
+$ ghq get alanpearce.eu:some-new-repository
+     clone ssh://alanpearce.eu/some-new-repository -> /Volumes/Code/projects/alanpearce.eu/some-new-repository
+       git clone ssh://alanpearce.eu/some-new-repository /Volumes/Code/projects/alanpearce.eu/some-new-repository
+Cloning into '/Volumes/Code/projects/alanpearce.eu/some-new-repository'...
+Initialized empty Git repository in /var/lib/gitolite/repositories/some-new-repository.git/
+warning: You appear to have cloned an empty repository.
+```
+
+The nice URLs come from this piece of my SSH configuration:
+
+```
+Host alanpearce.eu
+  HostName git.alanpearce.eu
+  User gitolite
+```
+
+## Configuring wild repositories
+
+This repository would be private by default, but I can change that by an
+SSH command.  Here's how I would do it:
+
+```bash
+ssh alanpearce.eu perms some-new-repository + READERS gitweb
+ssh alanpearce.eu perms some-new-repository + READERS daemon
+```
+
+The first command makes it visible in cgit, whilst the second makes it
+clonable via `git://` url.  I can make a repository
+publically-clonable, but invisible on cgit by only allowing the `daemon`
+user and not `gitweb`, if I wanted.
+
+I can also add or change the description of a repository shown on cgit like
+so:
+
+```bash
+ssh alanpearce.eu desc some-new-repository 'A new repository'
+```
+
+All the remote commands exposed by gitolite are described in the
+`help` command e.g. `ssh alanpearce.eu help`
+
+```
+hello alan, this is gitolite@oak running gitolite3 (unknown) on git 2.12.2
+
+list of remote commands available:
+
+	D
+	desc
+	help
+	info
+	motd
+	perms
+	writable
+
+```
+
+## Conclusion
+
+I much prefer creating repositories in this way.  It's much simpler
+and allows me to get on with working on the repositories rather than
+going through a multi-step process in a web browser.
+
+With cgit and gitolite, I have a minimal setup, that does exactly what
+I want, without consuming many system resources with daemons.
+
+[gogs]:https://gogs.io/ "Go Git Service"
+[gitolite]:http://gitolite.com/gitolite/
+[cgit]:https://git.zx2c4.com/cgit/
+[NixOS]:http://nixos.org
+[dotfiles-github]:https://github.com/alanpearce/dotfiles
+[wildrepos]:http://gitolite.com/gitolite/wild/
+[ghq]:https://github.com/motemen/ghq
+[using-ghq]:@/post/repository-management-with-ghq.md "Repository management with ghq"
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..2cccff2
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,10 @@
+(import
+  (
+    let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
+    fetchTarball {
+      url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
+      sha256 = lock.nodes.flake-compat.locked.narHash;
+    }
+  )
+  { src = ./.; }
+).defaultNix
diff --git a/flake.lock b/flake.lock
index afbf271..55003d6 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,15 +1,31 @@
 {
   "nodes": {
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1673956053,
+        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
     "flake-utils": {
       "inputs": {
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1681202837,
-        "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
+        "lastModified": 1687709756,
+        "narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
+        "rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
         "type": "github"
       },
       "original": {
@@ -18,13 +34,28 @@
         "type": "github"
       }
     },
+    "flockenzeit": {
+      "locked": {
+        "lastModified": 1671185345,
+        "narHash": "sha256-+5IWi+iJAYcRxvLN15hKO2hVwNokfN3U+lvWf/zFtCg=",
+        "owner": "balsoft",
+        "repo": "Flockenzeit",
+        "rev": "90abba65671690d95b5d28ce6dd8de7959aa1339",
+        "type": "github"
+      },
+      "original": {
+        "owner": "balsoft",
+        "repo": "Flockenzeit",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1681693905,
-        "narHash": "sha256-XdXMvCt+i2ZcmAIPZvu3RUwcdaC9OX7d1WMAJJokzeA=",
+        "lastModified": 1687977148,
+        "narHash": "sha256-gUcXiU2GgjYIc65GOIemdBJZ+lkQxuyIh7OkR9j0gCo=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "db34d7561caa508ece0265a56f382c5d3b7a6c1b",
+        "rev": "60a783e00517fce85c42c8c53fe0ed05ded5b2a4",
         "type": "github"
       },
       "original": {
@@ -36,7 +67,9 @@
     },
     "root": {
       "inputs": {
+        "flake-compat": "flake-compat",
         "flake-utils": "flake-utils",
+        "flockenzeit": "flockenzeit",
         "nixpkgs": "nixpkgs"
       }
     },
diff --git a/flake.nix b/flake.nix
index c257b2a..d788db4 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,20 +1,55 @@
 {
-  description = "A bear blog theme for Zola";
+  description = "My website, alanpearce.eu";
   inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+  inputs.flockenzeit.url = "github:balsoft/Flockenzeit";
   inputs.flake-utils.url = "github:numtide/flake-utils";
+  inputs.flake-compat = {
+    url = "github:edolstra/flake-compat";
+    flake = false;
+  };
 
-  outputs = { self, nixpkgs, flake-utils }:
-    flake-utils.lib.eachDefaultSystem (system:
-      let
-        pkgs = nixpkgs.legacyPackages.${system};
-      in
-      {
-        devShells.default = pkgs.mkShell {
-          packages = with pkgs; [
-            git
-            gnugrep
+  outputs = { self, nixpkgs, flockenzeit, flake-utils, ... }:
+    flake-utils.lib.eachDefaultSystem
+      (system:
+        let
+          pkgs = nixpkgs.legacyPackages.${system};
+          nativeBuildInputs = with pkgs; [
             zola
+            nodePackages_latest.prettier
+            fd
+            brotli
+            gzip
+            zstd
+            git
           ];
-        };
-      });
+        in
+        rec {
+          packages = {
+            default = pkgs.stdenv.mkDerivation {
+              name = "alanpearce.eu";
+              src = self;
+
+              enableParallelBuilding = true;
+              makeFlags = [ "PREFIX=$(out)/public" ];
+
+              inherit nativeBuildInputs;
+
+              dontFixup = true;
+
+              postInstall = ''
+                cp Caddyfile $out/
+              '';
+            };
+            docker = import ./docker.nix {
+              inherit self pkgs flockenzeit;
+              website = packages.default;
+            };
+          };
+          devShell = pkgs.mkShell {
+            buildInputs = with pkgs; [
+              caddy
+              flyctl
+            ] ++ nativeBuildInputs;
+          };
+        });
 }
diff --git a/fly.toml b/fly.toml
new file mode 100644
index 0000000..4b06784
--- /dev/null
+++ b/fly.toml
@@ -0,0 +1,38 @@
+app = "alanpearce-eu"
+kill_signal = "SIGINT"
+kill_timeout = 5
+primary_region = "ams"
+
+[metrics]
+  port = 9091
+  path = "/metrics"
+
+[env]
+  CADDY_CLUSTERING_REDIS_HOST = "fly-caddy-storage.upstash.io"
+  SITE_ROOT = "/srv"
+
+[[services]]
+  internal_port = 80
+  protocol = "tcp"
+  [services.concurrency]
+    type = "connections"
+    hard_limit = 200
+    soft_limit = 100
+  [[services.ports]]
+    handlers = ["http"]
+    port = 80
+  [[services.ports]]
+    handlers = ["tls"]
+    port = "443"
+    tls_options = { "alpn" = ["h2"] }
+  [[services.http_checks]]
+    interval = 10000
+    grace_period = "5s"
+    method = "head"
+    path = "/"
+    protocol = "http"
+    restart_limit = 0
+    timeout = 2000
+    tls_skip_verify = false
+    [services.http_checks.headers]
+    Host = "alanpearce.eu"
diff --git a/redis.Caddyfile b/redis.Caddyfile
new file mode 100644
index 0000000..c0f4bfc
--- /dev/null
+++ b/redis.Caddyfile
@@ -0,0 +1,2 @@
+storage redis {
+}
diff --git a/static/.domains b/static/.domains
new file mode 100644
index 0000000..44c5614
--- /dev/null
+++ b/static/.domains
@@ -0,0 +1,6 @@
+alanpearce.eu
+www.alanpearce.eu
+alanpearce.uk
+www.alanpearce.uk
+website.alanpearce.codeberg.page
+pages.website.alanpearce.codeberg.page
diff --git a/static/.well-known/keybase.txt b/static/.well-known/keybase.txt
new file mode 100644
index 0000000..f027aa9
--- /dev/null
+++ b/static/.well-known/keybase.txt
@@ -0,0 +1,56 @@
+==================================================================
+https://keybase.io/alanpearce
+--------------------------------------------------------------------
+
+I hereby claim:
+
+  * I am an admin of https://www.alanpearce.eu
+  * I am alanpearce (https://keybase.io/alanpearce) on keybase.
+  * I have a public key ASAktAZWh67GLebI8PNw4QNlJ4zEiIogKiQQ8WsqVsQa8Qo
+
+To do so, I am signing this object:
+
+{
+  "body": {
+    "key": {
+      "eldest_kid": "01200d892fbb34517abd5120fa546cb65dad1172cd85405cfae4936dcdb8bf5ac1850a",
+      "host": "keybase.io",
+      "kid": "012024b4065687aec62de6c8f0f370e10365278cc4888a202a2410f16b2a56c41af10a",
+      "uid": "91ae6da6b67277c6eded2451d6925919",
+      "username": "alanpearce"
+    },
+    "merkle_root": {
+      "ctime": 1529691082,
+      "hash": "6588b60bdcbf5836c74db6647f69ed9f88e8d45b237f896e75d790534fcb3058a3c2e3e9b7f026469b0ca30fe58f20e47fbe074306e02eba912348f19ab1abd2",
+      "hash_meta": "68aec5954816532401f402af55121c0c7496f3aac93475db68ea50e38e7e45b4",
+      "seqno": 3127786
+    },
+    "service": {
+      "entropy": "pkn3peHHXkyLn2KYC2q0CKkC",
+      "hostname": "www.alanpearce.eu",
+      "protocol": "https:"
+    },
+    "type": "web_service_binding",
+    "version": 2
+  },
+  "client": {
+    "name": "keybase.io go client",
+    "version": "2.1.1"
+  },
+  "ctime": 1529691093,
+  "expire_in": 504576000,
+  "prev": "9d580c5bd9f3b4a01356f507d808de55add562ddf3fded05a7d74299c5766503",
+  "seqno": 25,
+  "tag": "signature"
+}
+
+which yields the signature:
+
+hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgJLQGVoeuxi3myPDzcOEDZSeMxIiKICokEPFrKlbEGvEKp3BheWxvYWTESpcCGcQgnVgMW9nztKATVvUH2AjeVa3VYt3z/e0Fp9dCmcV2ZQPEIIRjxssrSyS8RF3Xr7Br780Q1Y0vy58txz8S6XBBaYpCAgHCo3NpZ8RAeAwN++lz+C+csgxZXXLSv76w2WcYaH41EcagALVLrULinV0j+Ea1TOUhmBfI9KNKFkOSiuEm+kOVktdf4BrWA6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIFnViSN5iA8WlkQfuHAD3PEQ0gkZyjv59iuNx7EoBhrRo3RhZ80CAqd2ZXJzaW9uAQ==
+
+And finally, I am proving ownership of this host by posting or
+appending to this document.
+
+View my publicly-auditable identity here: https://keybase.io/alanpearce
+
+==================================================================
\ No newline at end of file
diff --git a/static/public_key.asc b/static/public_key.asc
new file mode 100644
index 0000000..f081429
--- /dev/null
+++ b/static/public_key.asc
@@ -0,0 +1,16 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEXOZxBhYJKwYBBAHaRw8BAQdApEDmvmbv0fkrkND5LsR32g9QX8KtXAybgcCv
+euU6N9O0IEFsYW4gUGVhcmNlIDxhbGFuQGFsYW5wZWFyY2UuZXU+iIAEExYIABwF
+AlzmcQYCCwkCGwMEFQgJCgQWAgMBAheAAh4BABYJEM1L65Ko1GWDCxpUUkVaT1It
+R1BHgDABAICw5varaVWeuVlzJ0/XpLDFSHfY1CvQbMHe1LJ/iwGJAP9m3XC0yTyX
+uEG7w3R32Md5urcGH3fTIKK0ea6M+QVtArQgQWxhbiBQZWFyY2UgPGFsYW5Ac2F0
+b3NoaXBheS5pbz6IkAQTFggAOBYhBEjmV2wHBziMuL79DM1L65Ko1GWDBQJc5p5N
+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEM1L65Ko1GWD2Z4A/jggQexr
+za4DXNK2jolKjIBL9S7pOGbXxldHo69HC+dLAP4lJlaExUbompFaXzV/FETH2pdQ
+azi51lmD8wN5YX4AA7g4BFzmcQYSCisGAQQBl1UBBQEBB0C5WCVOLRJpEHSWMVFH
+0xtWavMXh3QUaoNIrX0jcEtpIAMBCAeIbQQYFggACQUCXOZxBgIbDAAWCRDNS+uS
+qNRlgwsaVFJFWk9SLUdQR5PKAP93z83yYaOZMQKZYAD3h2LHdlKD2wl2LaLiFOll
+4N4ghgEA5iTNV6d5PHo8NV73T4xm97qY94LpF1cDWwBDYhb0ywI=
+=VSmX
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/static/robots.txt b/static/robots.txt
new file mode 100644
index 0000000..80dda18
--- /dev/null
+++ b/static/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow:
+Host: alanpearce.eu
+Sitemap: https://alanpearce.eu/sitemap.xml
+
diff --git a/templates/atom.xml b/templates/atom.xml
new file mode 100644
index 0000000..3fd5dee
--- /dev/null
+++ b/templates/atom.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="/feed-styles/" type="text/xsl"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
+    <title>{{ config.title }}
+    {%- if term %} - {{ term.name }}
+    {%- elif section.title %} - {{ section.title }}
+    {%- endif -%}
+    </title>
+    {%- if config.description %}
+    <subtitle>{{ config.description }}</subtitle>
+    {%- endif %}
+    <link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
+    <link href="
+      {%- if section -%}
+        {{ section.permalink | escape_xml | safe }}
+      {%- else -%}
+        {{ config.base_url | escape_xml | safe }}
+      {%- endif -%}
+    "/>
+    <generator uri="https://www.getzola.org/">Zola</generator>
+    <updated>{{ last_updated | date(format="%+") }}</updated>
+    <id>{{ feed_url | safe }}</id>
+    {%- for page in pages %}
+    <entry xml:lang="{{ page.lang }}">
+        <title>{{ page.title }}</title>
+        <published>{{ page.date | date(format="%+") }}</published>
+        <updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
+        <author>
+          <name>
+            {%- if page.authors -%}
+              {{ page.authors[0] }}
+            {%- elif config.author -%}
+              {{ config.author }}
+            {%- else -%}
+              Unknown
+            {%- endif -%}
+          </name>
+        </author>
+        <link rel="alternate" href="{{ page.permalink | safe }}" type="text/html"/>
+        <id>{{ page.permalink | safe }}</id>
+        {% if page.summary %}
+        <summary type="html">{{ page.summary }}</summary>
+        {% else %}
+        <content type="html">{{ page.content }}</content>
+        {% endif %}
+    </entry>
+    {%- endfor %}
+</feed>
diff --git a/templates/favicon.html b/templates/favicon.html
deleted file mode 100644
index 28b504d..0000000
--- a/templates/favicon.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{% if config.extra.favicon %}
-  <link rel="shortcut icon" href="{{ config.extra.favicon }}">
-{%- endif -%}
diff --git a/templates/feed-styles.html b/templates/feed-styles.html
new file mode 100644
index 0000000..66dac33
--- /dev/null
+++ b/templates/feed-styles.html
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:atom="http://www.w3.org/2005/Atom">
+  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
+  <xsl:template match="/">
+    <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+      <head>
+        <title>
+          RSS Feed |
+          <xsl:value-of select="/atom:feed/atom:title"/>
+        </title>
+        <meta charset="utf-8"/>
+        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+        <meta name="viewport" content="width=device-width, initial-scale=1"/>
+        <style>
+        {% include "style.html" ignore missing -%}
+        </style>
+      </head>
+      <body>
+        <main>
+          <div class="helptext">
+            <strong>This is an RSS feed</strong>. Subscribe by copying
+            the URL from the address bar into your newsreader. Visit
+            <a href="https://aboutfeeds.com">About Feeds</a>
+            to learn more and get started. It's free.
+          </div>
+          <div>
+            <h1>
+              <!-- https://commons.wikimedia.org/wiki/File:Feed-icon.svg -->
+              <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+                   style="width: 1.5ex; height: 1.5ex"
+                   viewBox="0 0 256 256">
+                <rect width="256" height="256" x="0" y="0" fill="#7F7F7F"/>
+                <rect width="246" height="246" x="5" y="5" fill="#A0A0A0"/>
+                <rect width="236" height="236" x="10" y="10" fill="#A6A6A6"/>
+                <circle cx="68" cy="189" r="24" fill="#FFF"/>
+                <path
+                  d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z"
+                  fill="#FFF"/>
+                <path
+                  d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z"
+                  fill="#FFF"/>
+              </svg>
+              RSS Feed Preview |
+              <span>
+                <xsl:value-of select="/atom:feed/atom:title"/>
+              </span>
+            </h1>
+            <nav>
+              <a>
+                <xsl:attribute name="href">
+                  <xsl:value-of select="/atom:feed/atom:link[2]/@href"/>
+                </xsl:attribute>
+                Visit Website
+              </a>
+            </nav>
+            <ul class="blog-posts">
+              <xsl:for-each select="/atom:feed/atom:entry">
+                <li>
+                  <span>
+                    <xsl:value-of select="substring(atom:updated, 0, 11)" />
+                  </span>
+                  <div>
+                    <a>
+                      <xsl:attribute name="href">
+                        <xsl:value-of select="atom:link/@href"/>
+                      </xsl:attribute>
+                      <xsl:value-of select="atom:title"/>
+                    </a>
+                  </div>
+                </li>
+              </xsl:for-each>
+            </ul>
+          </div>
+        </main>
+      </body>
+    </html>
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/templates/footer.html b/templates/footer.html
index c952a93..422c90c 100644
--- a/templates/footer.html
+++ b/templates/footer.html
@@ -1,5 +1 @@
-<footer>
-  {%- if not config.extra.hide_made_with_line %}
-    Made with <a href="https://codeberg.org/alanpearce/zola-bearblog">Zola ʕ•ᴥ•ʔ Bear</a>
-  {%- endif %}
-</footer>
+<footer>Licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.</footer>
diff --git a/templates/header.html b/templates/header.html
deleted file mode 100644
index c1d3c3e..0000000
--- a/templates/header.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<header>
-  <a href="{{ config.base_url }}" class="title">
-    <h2>{{ config.title }}</h2>
-  </a>
-  <nav>
-    {% include "nav.html" %}
-  </nav>
-</header>
diff --git a/templates/index.html b/templates/index.html
index 6caf1d5..a79b1c8 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,7 +1,39 @@
 {% extends "base.html" %}
 
-{% block main %}
-  <main>
-    {{ section.content | safe }}
+{% block body_attrs %} class="h-card vcard"{% endblock %}
+
+{% block title_class %} p-name fn{% endblock %}
+
+{% block main -%}
+  <main id="content">
+    <div>
+      {{ section.content | safe -}}
+    </div>
+    <section>
+      <h2>Latest Posts</h2>
+      <ul class="h-feed">
+        {%- set section = get_section(path="post/_index.md") %}
+        {%- for page in section.pages | slice(end=3) %}
+          <li class="h-entry">
+            <time class="dt-published" datetime="{{ page.date | date(format="%+") }}">{{ page.date | date(format=config.extra.date_format) }}</time>
+            <a class="u-url p-name" href="{{ page.path | safe }}">{{ page.title }}</a>
+          </li>
+        {%- endfor %}
+      </ul>
+    </section>
+    <section>
+      <h2>Elsewhere on the Internet</h2>
+      <ul>
+        {%- for item in config.extra.contact_menu %}
+          <li>
+            {%- if item.url is starting_with("mailto:") -%}
+              <a href="{{ item.url | safe }}" class="u-email email" rel="me">{{ item.name }}</a>
+            {%- else -%}
+              <a href="{{ item.url | safe }}" class="u-url url" rel="me">{{ item.name }}</a>
+            {%- endif -%}
+          </li>
+        {%- endfor %}
+      </ul>
+    </section>
   </main>
 {% endblock %}
diff --git a/templates/nav.html b/templates/nav.html
deleted file mode 100644
index e006ab1..0000000
--- a/templates/nav.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<a href="{{ config.base_url }}">Home</a>
-{%- if config.extra.main_menu %}
-  {%- for item in config.extra.main_menu %}
-    {%- if item.url is matching("https?://") %}
-      <a href="{{ item.url }}">{{ item.name }}</a>
-    {%- else %}
-      <a href="{{ get_url(path=item.url )}}">{{ item.name }}</a>
-    {%- endif %}
-  {%- endfor %}
-{%- endif -%}
diff --git a/templates/section.html b/templates/section.html
deleted file mode 100644
index e596ffb..0000000
--- a/templates/section.html
+++ /dev/null
@@ -1,38 +0,0 @@
-{% extends "base.html" %}
-
-{% block main %}
-  <main>
-    {%- if taxonomy.term %}
-      <h3 style="margin-bottom:0">Filtering for "{{ section.title }}"</h3>
-      <small>
-        <a href="{{ get_url(path="@/blog/_index.md") }}">Remove filter</a>
-      </small>
-    {%- endif %}
-    <ul class="blog-posts">
-      {% for page in section.pages %}
-        <li>
-          <span>
-            <i>
-              <time datetime='{{ page.date | date(format='%+') }}' pubdate>
-                {{ page.date | date(format=config.extra.date_format) }}
-              </time>
-            </i>
-          </span>
-          <a href="{{ page.permalink }}">{{ page.title }}</a>
-        </li>
-        {% else %}
-        <li>
-          No posts yet
-        </li>
-      {% endfor %}
-    </ul>
-    <small>
-      <div>
-        {% set tags = get_taxonomy(kind="tags") %}
-        {% for post in tags.items %}
-          <a href="{{ post.permalink }}">#{{ post.name }}</a>&nbsp;
-        {% endfor %}
-      </div>
-    </small>
-  </main>
-{% endblock %}
diff --git a/templates/style.css.html b/templates/style.css.html
deleted file mode 100644
index 00b4131..0000000
--- a/templates/style.css.html
+++ /dev/null
@@ -1,169 +0,0 @@
-  body {
-    font-family: Verdana, sans-serif;
-    margin: auto;
-    padding: 20px;
-    max-width: 720px;
-    text-align: left;
-    background-color: #fff;
-    word-wrap: break-word;
-    overflow-wrap: break-word;
-    line-height: 1.5;
-    color: #444;
-  }
-
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6,
-  strong,
-  b {
-    color: #222;
-  }
-
-  a {
-    color: #3273dc;
-  }
-
-  .title {
-    text-decoration: none;
-    border: 0;
-  }
-
-  .title span {
-    font-weight: 400;
-  }
-
-  nav a {
-    margin-right: 10px;
-  }
-
-  textarea {
-    width: 100%;
-    font-size: 1rem;
-  }
-
-  input {
-    font-size: 1rem;
-  }
-
-  main,article {
-    line-height: 1.6;
-  }
-
-  table {
-    width: 100%;
-  }
-
-  img {
-    max-width: 100%;
-  }
-
-  code {
-    padding: 2px 5px;
-    background-color: #f2f2f2;
-  }
-
-  pre code {
-    color: #222;
-    display: block;
-    padding: 20px;
-    white-space: pre-wrap;
-    font-size: 0.875rem;
-    overflow-x: auto;
-  }
-
-  div.highlight pre {
-    background-color: initial;
-    color: initial;
-  }
-
-  div.highlight code {
-    background-color: unset;
-    color: unset;
-  }
-
-  blockquote {
-    border-left: 1px solid #999;
-    color: #222;
-    padding-left: 20px;
-    font-style: italic;
-  }
-
-  footer {
-    padding: 25px;
-    text-align: center;
-  }
-
-  .helptext {
-    color: #777;
-    font-size: small;
-  }
-
-  .errorlist {
-    color: #eba613;
-    font-size: small;
-  }
-
-  /* blog posts */
-  ul.blog-posts {
-    list-style-type: none;
-    padding: unset;
-  }
-
-  ul.blog-posts li {
-    display: flex;
-  }
-
-  ul.blog-posts li span {
-    flex: 0 0 130px;
-  }
-
-  ul.blog-posts li a:visited {
-    color: #8b6fcb;
-  }
-
-  @media (prefers-color-scheme: dark) {
-    body {
-      background-color: #333;
-      color: #ddd;
-    }
-
-    h1,
-    h2,
-    h3,
-    h4,
-    h5,
-    h6,
-    strong,
-    b {
-      color: #eee;
-    }
-
-    a {
-      color: #8cc2dd;
-    }
-
-    code {
-      background-color: #777;
-    }
-
-    pre code {
-      color: #ddd;
-    }
-
-    blockquote {
-      color: #ccc;
-    }
-
-    textarea,
-    input {
-      background-color: #252525;
-      color: #ddd;
-    }
-
-    .helptext {
-      color: #aaa;
-    }
-  }
diff --git a/templates/taxonomy_single.html b/templates/taxonomy_single.html
deleted file mode 100644
index d5712b0..0000000
--- a/templates/taxonomy_single.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends "base.html" %}
-
-{% block main %}
-  <main>
-    {%- if taxonomy.term %}
-      <h3 style="margin-bottom:0">Filtering for "{{ term.name }}"</h3>
-      <small>
-        <a href="{{ get_url(path="@/blog/_index.md") }}">Remove filter</a>
-      </small>
-    {%- endif %}
-    <ul class="blog-posts">
-      {% for page in term.pages %}
-        <li>
-          <span>
-            <i>
-              <time datetime='{{ page.date | date(format='%+') }}' pubdate>
-                {{ page.date | date(format=config.extra.date_format) }}
-              </time>
-            </i>
-          </span>
-          <a href="{{ page.permalink }}">{{ page.title }}</a>
-        </li>
-      {% else %}
-        <li>
-          No posts yet
-        </li>
-      {% endfor %}
-    </ul>
-  </main>
-{% endblock %}
diff --git a/themes/bear/.envrc b/themes/bear/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/themes/bear/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/themes/bear/.gitignore b/themes/bear/.gitignore
new file mode 100644
index 0000000..87174b6
--- /dev/null
+++ b/themes/bear/.gitignore
@@ -0,0 +1 @@
+/public/
diff --git a/LICENSE b/themes/bear/LICENSE
index ddc924b..ddc924b 100644
--- a/LICENSE
+++ b/themes/bear/LICENSE
diff --git a/README.md b/themes/bear/README.md
index db77233..db77233 100644
--- a/README.md
+++ b/themes/bear/README.md
diff --git a/themes/bear/config.toml b/themes/bear/config.toml
new file mode 100644
index 0000000..78e9f9a
--- /dev/null
+++ b/themes/bear/config.toml
@@ -0,0 +1,35 @@
+title = "Zola ʕ•ᴥ•ʔ Bear Blog"
+base_url = "https://zola-bearblog.netlify.app/"
+description = "A Zola-theme based on Bear Blog."
+
+# Whether to automatically compile all Sass files in the sass directory
+compile_sass = false
+
+# Whether to build a search index to be used later on by a JavaScript library
+build_search_index = false
+
+taxonomies = [
+  {name = "categories", feed = true},
+  {name = "tags", feed = true},
+]
+
+[markdown]
+# Whether to do syntax highlighting
+# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
+highlight_code = true
+
+[extra]
+date_format="%d %b, %Y"
+webserver_sends_csp_headers=true
+
+[[extra.main_menu]]
+name = "Bear"
+url = "@/bear.md"
+
+[[extra.main_menu]]
+name = "Zola"
+url = "@/zola.md"
+
+[[extra.main_menu]]
+name = "Blog"
+url = "@/blog/_index.md"
diff --git a/themes/bear/content/_index.md b/themes/bear/content/_index.md
new file mode 100644
index 0000000..24b3925
--- /dev/null
+++ b/themes/bear/content/_index.md
@@ -0,0 +1,23 @@
++++
++++
+# A match made in heaven
+
+There is a website obesity crisis. Bloated websites full of scripts, ads, and trackers are slowing your readers down every time they try to read your well-crafted content.
+
+Zola Bear Blog is all you need to build a fantastic and optimized site or blog. It works perfectly on **any** viewing device. All you need to focus on is writing good content.
+
+[Go to the original bear blog](https://bearblog.dev/).
+
+---
+
+What happens when you combine the worlds' fastest, most lightweight static site generator with a design theme built to provide you with free, no-nonsense, super-fast blogging capabilities?
+
+**Use this theme, and find out!**
+
+Made with 💚 by [Alan Pearce](https://alanpearce.eu).
+
+---
+
+Simply publish content online, grow an audience, and keep your pages tiny, fast, and **optimized for search engines**.
+
+Each page is ~5kb, and you can **host your blog yourself**.
diff --git a/content/bear.md b/themes/bear/content/bear.md
index dd7da4b..dd7da4b 100644
--- a/content/bear.md
+++ b/themes/bear/content/bear.md
diff --git a/content/blog/_index.md b/themes/bear/content/blog/_index.md
index 34651ab..34651ab 100644
--- a/content/blog/_index.md
+++ b/themes/bear/content/blog/_index.md
diff --git a/content/blog/markdown-syntax.md b/themes/bear/content/blog/markdown-syntax.md
index 18f912a..18f912a 100644
--- a/content/blog/markdown-syntax.md
+++ b/themes/bear/content/blog/markdown-syntax.md
diff --git a/content/zola.md b/themes/bear/content/zola.md
index 5458750..5458750 100644
--- a/content/zola.md
+++ b/themes/bear/content/zola.md
diff --git a/themes/bear/flake.lock b/themes/bear/flake.lock
new file mode 100644
index 0000000..afbf271
--- /dev/null
+++ b/themes/bear/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1681202837,
+        "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1681693905,
+        "narHash": "sha256-XdXMvCt+i2ZcmAIPZvu3RUwcdaC9OX7d1WMAJJokzeA=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "db34d7561caa508ece0265a56f382c5d3b7a6c1b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/themes/bear/flake.nix b/themes/bear/flake.nix
new file mode 100644
index 0000000..c257b2a
--- /dev/null
+++ b/themes/bear/flake.nix
@@ -0,0 +1,20 @@
+{
+  description = "A bear blog theme for Zola";
+  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+  inputs.flake-utils.url = "github:numtide/flake-utils";
+
+  outputs = { self, nixpkgs, flake-utils }:
+    flake-utils.lib.eachDefaultSystem (system:
+      let
+        pkgs = nixpkgs.legacyPackages.${system};
+      in
+      {
+        devShells.default = pkgs.mkShell {
+          packages = with pkgs; [
+            git
+            gnugrep
+            zola
+          ];
+        };
+      });
+}
diff --git a/netlify.toml b/themes/bear/netlify.toml
index 6be8468..6be8468 100644
--- a/netlify.toml
+++ b/themes/bear/netlify.toml
diff --git a/screenshot.png b/themes/bear/screenshot.png
index 273266a..273266a 100644
--- a/screenshot.png
+++ b/themes/bear/screenshot.png
Binary files differdiff --git a/templates/404.html b/themes/bear/templates/404.html
index 15fd75c..15fd75c 100644
--- a/templates/404.html
+++ b/themes/bear/templates/404.html
diff --git a/templates/base.html b/themes/bear/templates/base.html
index ee206b3..3a2d565 100644
--- a/templates/base.html
+++ b/themes/bear/templates/base.html
@@ -6,13 +6,12 @@
   {%- endif %}
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  {% include "favicon.html" ignore missing -%}
+  <link rel="shortcut icon" href="{{ config.extra.favicon | default(value="data:,") }}">
   <title>{%- block title %}{{ config.title }}{%- endblock %}</title>
-  {% include "seo_tags.html" ignore missing %}
   <meta name="referrer" content="no-referrer-when-downgrade">
   {%- if config.generate_feed %}
   {% block rss -%}
-  <link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{{ config.title }}" href="{{ get_url(path=config.feed_filename) | safe }}">
+  <link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{{ config.title }}" href="/{{ config.feed_filename }}">
   {%- endblock -%}
   {%- endif %}
   <style>
diff --git a/themes/bear/templates/footer.html b/themes/bear/templates/footer.html
new file mode 100644
index 0000000..c952a93
--- /dev/null
+++ b/themes/bear/templates/footer.html
@@ -0,0 +1,5 @@
+<footer>
+  {%- if not config.extra.hide_made_with_line %}
+    Made with <a href="https://codeberg.org/alanpearce/zola-bearblog">Zola ʕ•ᴥ•ʔ Bear</a>
+  {%- endif %}
+</footer>
diff --git a/themes/bear/templates/header.html b/themes/bear/templates/header.html
new file mode 100644
index 0000000..55c1756
--- /dev/null
+++ b/themes/bear/templates/header.html
@@ -0,0 +1,11 @@
+<a class="skip" href="#content">Skip to main content</a>
+<header>
+  <h2>
+    <a href="/" class="title">
+      {{- config.title -}}
+    </a>
+  </h2>
+  <nav>
+    {% include "nav.html" %}
+  </nav>
+</header>{{ "" -}}
diff --git a/themes/bear/templates/index.html b/themes/bear/templates/index.html
new file mode 100644
index 0000000..6caf1d5
--- /dev/null
+++ b/themes/bear/templates/index.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block main %}
+  <main>
+    {{ section.content | safe }}
+  </main>
+{% endblock %}
diff --git a/themes/bear/templates/nav.html b/themes/bear/templates/nav.html
new file mode 100644
index 0000000..fe5fdd6
--- /dev/null
+++ b/themes/bear/templates/nav.html
@@ -0,0 +1,6 @@
+  <a href="/">Home</a>
+{%- if config.extra.main_menu %}
+  {%- for item in config.extra.main_menu %}
+  <a href="{{ item.url | safe }}">{{ item.name }}</a>
+  {%- endfor %}
+{%- endif -%}
diff --git a/templates/page.html b/themes/bear/templates/page.html
index 89de955..93611b5 100644
--- a/templates/page.html
+++ b/themes/bear/templates/page.html
@@ -7,25 +7,23 @@
     <h1>{{ page.title }}</h1>
     {%- if page.date %}
       <p>
-        <i>
-          <time datetime='{{ page.date | date(format='%+') }}' pubdate>
-            {{- page.date | date(format=config.extra.date_format) -}}
-          </time>
-        </i>
+        <time datetime='{{ page.date | date(format='%+') }}' pubdate>
+          {{- page.date | date(format=config.extra.date_format) -}}
+        </time>
       </p>
     {%- endif %}
   {%- endif %}
-  <main>
-    {{ page.content | safe }}
+  <main id="content">
+    {{ page.content | trim | safe }}
   </main>
-  <p>
+  <ul class="tags">
     {%- if page.taxonomies %}
       {%- for name, taxon in page.taxonomies %}
         {{ name | capitalize }}:
         {%- for item in taxon %}
-          <a href="{{ get_taxonomy_url(kind=name, name=item) }}">#{{ item }}</a>
+          <li><a href="{{ get_taxonomy_url(kind=name, name=item) }}">#{{ item }}</a></li>
         {%- endfor %}
       {%- endfor %}
     {%- endif %}
-  </p>
+  </ul>
 {% endblock %}
diff --git a/themes/bear/templates/section.html b/themes/bear/templates/section.html
new file mode 100644
index 0000000..f633036
--- /dev/null
+++ b/themes/bear/templates/section.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+
+{% block main %}
+  <main id="content">
+    {%- if taxonomy.term %}
+      <h3 style="margin-bottom:0">Filtering for "{{ section.title }}"</h3>
+      <small>
+        <a href="{{ get_url(path="@/blog/_index.md") }}">Remove filter</a>
+      </small>
+    {%- endif %}
+    <ul class="blog-posts">
+      {%- for page in section.pages %}
+        <li>
+          <span>
+            <time datetime='{{ page.date | date(format='%+') }}' pubdate>
+              {{ page.date | date(format=config.extra.date_format) }}
+            </time>
+          </span>
+          <a href="{{ page.path | urlencode | safe }}">{{ page.title }}</a>
+        </li>
+      {% else %}
+        <li>
+          No posts yet
+        </li>
+      {%- endfor %}
+    </ul>
+    <ul class="tags">
+      {%- set tags = get_taxonomy(kind="tags") %}
+      {%- for post in tags.items %}
+        <li><a href="{{ post.path | urlencode | safe }}">#{{ post.name }}</a></li>
+      {%- endfor %}
+    </ul>
+  </main>
+{% endblock %}
diff --git a/templates/security_tags.html b/themes/bear/templates/security_tags.html
index 0f922ea..0f922ea 100644
--- a/templates/security_tags.html
+++ b/themes/bear/templates/security_tags.html
diff --git a/templates/seo_tags.html b/themes/bear/templates/seo_tags.html
index 4eb2bc8..4eb2bc8 100644
--- a/templates/seo_tags.html
+++ b/themes/bear/templates/seo_tags.html
diff --git a/themes/bear/templates/style.css b/themes/bear/templates/style.css
new file mode 100644
index 0000000..e1e12aa
--- /dev/null
+++ b/themes/bear/templates/style.css
@@ -0,0 +1,193 @@
+body {
+  font-family: Verdana, sans-serif;
+  margin: auto;
+  padding: 20px;
+  max-width: 720px;
+  text-align: left;
+  background-color: #fff;
+  word-wrap: break-word;
+  overflow-wrap: break-word;
+  line-height: 1.5;
+  color: #444;
+}
+
+.skip {
+  position: absolute;
+  top: -3em;
+  background: #fff;
+}
+.skip:focus {
+  top: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+strong,
+b {
+  color: #222;
+}
+
+a {
+  color: #3273dc;
+}
+
+.title {
+  color: #222;
+  text-decoration: none;
+  border: 0;
+}
+
+time {
+  font-style: italic;
+}
+
+.title span {
+  font-weight: 400;
+}
+
+nav a {
+  margin-right: 10px;
+}
+
+.tags {
+  padding: unset;
+  font-size: small;
+}
+
+.tags > li {
+  list-style: none;
+  display: inline-block;
+}
+
+textarea {
+  width: 100%;
+  font-size: 1rem;
+}
+
+input {
+  font-size: 1rem;
+}
+
+main,article {
+  line-height: 1.6;
+}
+
+table {
+  width: 100%;
+}
+
+img {
+  max-width: 100%;
+}
+
+code {
+  padding: 2px 5px;
+  background-color: #f2f2f2;
+}
+
+pre code {
+  color: #222;
+  display: block;
+  padding: 20px;
+  white-space: pre-wrap;
+  font-size: 0.875rem;
+  overflow-x: auto;
+}
+
+div.highlight pre {
+  background-color: initial;
+  color: initial;
+}
+
+div.highlight code {
+  background-color: unset;
+  color: unset;
+}
+
+blockquote {
+  border-left: 1px solid #999;
+  color: #222;
+  padding-left: 20px;
+  font-style: italic;
+}
+
+footer {
+  padding: 25px;
+  text-align: center;
+}
+
+.helptext {
+  color: #777;
+  font-size: small;
+}
+
+.errorlist {
+  color: #eba613;
+  font-size: small;
+}
+
+/* blog posts */
+ul.blog-posts {
+  list-style-type: none;
+  padding: unset;
+}
+
+ul.blog-posts li {
+  display: flex;
+}
+
+ul.blog-posts li span {
+  flex: 0 0 130px;
+}
+
+ul.blog-posts li a:visited {
+  color: #8b6fcb;
+}
+
+@media (prefers-color-scheme: dark) {
+  body {
+    background-color: #333;
+    color: #ddd;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  strong,
+  b {
+    color: #eee;
+  }
+
+  a {
+    color: #8cc2dd;
+  }
+
+  code {
+    background-color: #777;
+  }
+
+  pre code {
+    color: #ddd;
+  }
+
+  blockquote {
+    color: #ccc;
+  }
+
+  textarea,
+  input {
+    background-color: #252525;
+    color: #ddd;
+  }
+
+  .helptext {
+    color: #aaa;
+  }
+}
diff --git a/themes/bear/templates/style.css.html b/themes/bear/templates/style.css.html
new file mode 120000
index 0000000..f6b71cc
--- /dev/null
+++ b/themes/bear/templates/style.css.html
@@ -0,0 +1 @@
+style.css
\ No newline at end of file
diff --git a/templates/taxonomy_list.html b/themes/bear/templates/taxonomy_list.html
index 69d9fa2..abf4294 100644
--- a/templates/taxonomy_list.html
+++ b/themes/bear/templates/taxonomy_list.html
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 
 {% block main %}
-  <main>
+  <main id="content">
     <small>
       <div>
         {% set tags = get_taxonomy(kind="tags") %}
diff --git a/themes/bear/templates/taxonomy_single.html b/themes/bear/templates/taxonomy_single.html
new file mode 100644
index 0000000..a96139c
--- /dev/null
+++ b/themes/bear/templates/taxonomy_single.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+
+{% block rss -%}
+  <link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{{ config.title }}" href="/{{ config.feed_filename }}">
+  {%- set rss_path = "/tags/" ~ term.name ~ "/" ~ config.feed_filename %}
+  <link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{% if term %}{{ term.name | title }}{% else %}{{ section.title | title }}{% endif %}" href="{{ rss_path }}">
+{%- endblock -%}
+
+{% block main -%}
+  <main id="content">
+    {%- if taxonomy.term %}
+      <h3 style="margin-bottom:0">Filtering for "{{ term.name }}"</h3>
+      <small>
+        <a href="{{ get_url(path="@/blog/_index.md") }}">Remove filter</a>
+      </small>
+    {%- endif %}
+    <ul class="blog-posts">
+      {%- for page in term.pages %}
+        <li>
+          <span>
+            <time datetime='{{ page.date | date(format='%+') }}' pubdate>
+              {{ page.date | date(format=config.extra.date_format) }}
+            </time>
+          </span>
+          <a href="{{ page.path | urlencode | safe }}">{{ page.title }}</a>
+        </li>
+      {% else %}
+        <li>
+          No posts yet
+        </li>
+      {%- endfor %}
+    </ul>
+  </main>
+{% endblock %}
diff --git a/theme.toml b/themes/bear/theme.toml
index 902c8ec..902c8ec 100644
--- a/theme.toml
+++ b/themes/bear/theme.toml