about summary refs log tree commit diff
diff options
context:
space:
mode:
-rwxr-xr-x.envrc14
-rw-r--r--.gitignore2
-rw-r--r--README.md18
-rw-r--r--config.toml41
-rw-r--r--content/_index.md4
-rw-r--r--content/pages/_index.md3
-rw-r--r--content/pages/about.md54
-rw-r--r--content/pages/projects.md9
-rw-r--r--content/posts/2023-01-23-hello-world.md20
-rw-r--r--content/posts/2023-01-31-nixos-zfs-mirrored-boot.md361
-rw-r--r--content/posts/_index.md5
-rw-r--r--flake.lock43
-rwxr-xr-xflake.nix31
-rw-r--r--sass/style.scss169
-rw-r--r--static/.well-known/openpgpkey/hu/dj3498u4hyyarh35rkjfnghbjxug6b19bin0 -> 779 bytes
-rw-r--r--static/.well-known/openpgpkey/policy1
-rw-r--r--static/font/sarasa-gothic-sc-bold.woffbin0 -> 10532 bytes
-rw-r--r--static/font/sarasa-gothic-sc-bold.woff2bin0 -> 8672 bytes
-rw-r--r--static/normalize.css349
-rw-r--r--static/sarasa-gothic.css8
-rw-r--r--templates/404.html33
-rw-r--r--templates/base.html25
-rw-r--r--templates/index.html14
-rw-r--r--templates/macros/posts.html61
-rw-r--r--templates/page.html44
-rw-r--r--templates/parts/head.html11
-rw-r--r--templates/parts/nav.html8
-rw-r--r--templates/posts.html15
-rw-r--r--templates/taxonomy_list.html23
-rw-r--r--templates/taxonomy_single.html11
30 files changed, 1377 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100755
index 0000000..29b20cd
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# This will be supported in the future
+export NIX_USER_CONF_FILES=$PWD/etc/nix.conf
+
+if nix flake info &>/dev/null; then
+  # Flake!
+  watch_file flake.lock
+  watch_file flake.nix
+  eval "$(nix print-dev-env)"
+else
+  use_nix
+fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..face841
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+public/
+result
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e51e84a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+sefidel-web
+===========
+
+> Source code for sefidel.net. Powered by [Zola](getzola.org)
+
+To-do
+- Add Feed link for taxonomies too
+- Consider supporting Gemini
+    - Needs a new static page generator,
+      possibly create one with Rust/Haskell
+- Header & Navbar on left, not center?
+
+Attribution
+-----------
+
+The code used for generating this website has codes either copied or inspired from those repositories:
+- [emersion.fr (MIT)](https://git.sr.ht/~emersion/emersion.fr)
+- [nullbuffer.com (Unlicense)](https://github.com/spodernet/spodernet.github.io)
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..2b39a4c
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,41 @@
+# The URL the site will be built for
+base_url = "https://sefidel.net"
+
+# The site title and description; used in feeds by default.
+title = "セピー"
+description = ""
+
+# Whether to automatically compile all Sass files in the sass directory
+compile_sass = true
+
+# Whether to build a search index to be used later on by a JavaScript library
+build_search_index = true
+
+# Whether to generate a feed for the site
+generate_feed = true
+feed_filename = "atom.xml"
+
+# The taxonomies to be rendered for the site and their configuration of the default languages
+# Example:
+#     taxonomies = [
+#       {name = "tags", feed = true}, # each tag will have its own feed
+#       {name = "tags"}, # you can have taxonomies with the same name in multiple languages
+#       {name = "categories", paginate_by = 5},  # 5 items per page for a term
+#       {name = "authors"}, # Basic definition: no feed or pagination
+#     ]
+#
+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
+
+# The theme to use for code highlighting.
+highlight_theme = "monokai"
+
+[extra]
+# Put all your custom variables here
diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 0000000..87f0dca
--- /dev/null
+++ b/content/_index.md
@@ -0,0 +1,4 @@
++++
+title = "Home"
+sort_by = "date"
++++
diff --git a/content/pages/_index.md b/content/pages/_index.md
new file mode 100644
index 0000000..800a244
--- /dev/null
+++ b/content/pages/_index.md
@@ -0,0 +1,3 @@
++++
+render = false
++++
diff --git a/content/pages/about.md b/content/pages/about.md
new file mode 100644
index 0000000..863ff92
--- /dev/null
+++ b/content/pages/about.md
@@ -0,0 +1,54 @@
++++
+title = "About"
+path = "about"
+
+[extra]
+raw = true
++++
+## About me
+Hi, I'm **sefidel** (sef).
+
+- PGP: <a href="/.well-known/openpgpkey/hu/dj3498u4hyyarh35rkjfnghbjxug6b19" download="sefidel.pgp">
+8BDF DFB5 6842 2393 82A0 &nbsp; 441B 9238 BC70 9E05 516A
+    </a>
+- Email: [contact@sefidel.net][email] [sef@exotic.sh][email-secondary]
+- Fediverse: [@sefidel@stella.place][fedi-stella] (ko-KR)
+- Matrix: [@sef:exotic.sh][matrix] [@sefidel:nixos.dev][matrix-secondary] (for
+  FOSS use only)
+- Timezone: UTC+0900
+- Languages: English (en-GB), Korean (ko-KR), Japanese (ja-JP)
+
+***REMOVED***
+
+My main interests are low-level systems, compilers, distributed computing, microservices and security.
+
+Feel free to contact me on Matrix (preferred) or email!
+I'm also on IRC as `sefidel` (Libera, OFTC).
+
+Most of my works can be found on [exotic.sh git][git-exotic],
+[GitHub][git-github], and on [SourceHut][git-srht].
+
+## System
+I mainly use MacBook Air M1 (2020) for development, as my NixOS workstation's CPU cooler is currently defunct.
+
+I use Nix for my system configuration, and my nixrc can be found [here][nixrc].
+You should be able to find configurations for most of the software I use there.
+
+My favourite choice of text editor is Neovim, and the configuration for it can
+be found [here][nvimrc]. It's not managed with Nix, since Nix doesn't have
+decent Lua config support (yet).
+Plus, I sometimes have to use this configuration on non-nix systems.
+If you're going to use this configuration, keep in mind that it looks best with
+a bitmap font like [Dina].
+
+[email]: mailto:contact@sefidel.net
+[email-secondary]: mailto:sef@exotic.sh
+[matrix]: https://matrix.to/#/@sef:exotic.sh
+[matrix-secondary]: https://matrix.to/#/@sefidel:nixos.dev
+[fedi-stella]: https://stella.place/@sefidel
+[git-exotic]: https://git.exotic.sh/pub/sefidel
+[git-github]: https://github.com/sefidel
+[git-srht]: https://sr.ht/~sefidel
+[nixrc]: https://git.exotic.sh/pub/sefidel/nixrc
+[nvimrc]: https://git.exotic.sh/pub/sefidel/nvimrc
+[Dina]: https://www.dcmembers.com/jibsen/download/61
diff --git a/content/pages/projects.md b/content/pages/projects.md
new file mode 100644
index 0000000..98d370f
--- /dev/null
+++ b/content/pages/projects.md
@@ -0,0 +1,9 @@
++++
+title = "Projects"
+path = "projects"
+
+[extra]
+raw = true
++++
+
+// TODO: Open source projects goes here
diff --git a/content/posts/2023-01-23-hello-world.md b/content/posts/2023-01-23-hello-world.md
new file mode 100644
index 0000000..8024895
--- /dev/null
+++ b/content/posts/2023-01-23-hello-world.md
@@ -0,0 +1,20 @@
++++
+title = "Hello, world!"
+date = "2023-01-23"
+
+[taxonomies]
+categories = ["meta"]
++++
+
+**Hello, world!**
+
+This is my first post on my personal website.
+
+Here's a quick overview of what will be posted here:
+
+- Status Updates
+- Development notes
+- Generally anything I find interesting (they'll be tagged accordingly)
+
+I have a broad interest in technology, so if you have something to share, please
+don't hesitate to [reach out](@/pages/about.md) to me!
diff --git a/content/posts/2023-01-31-nixos-zfs-mirrored-boot.md b/content/posts/2023-01-31-nixos-zfs-mirrored-boot.md
new file mode 100644
index 0000000..99b4004
--- /dev/null
+++ b/content/posts/2023-01-31-nixos-zfs-mirrored-boot.md
@@ -0,0 +1,361 @@
++++
+title = "Installing NixOS with ZFS mirrored boot"
+date = "2023-01-31"
+
+[taxonomies]
+categories = ["system"]
+tags = ["linux", "nixos"]
++++
+
+// TODO: add PlantUML diagrams
+
+## Overview
+
+In this post, we're going to set up a ZFS mirrored boot system with full-disk encryption that is unlockable remotely.
+
+## Preparing the installation medium
+
+This step may vary depending on what system you're going to install NixOS into.
+
+This post assumes that you're installing this on a normal server, with a
+minimal NixOS image.
+
+The community-maintained [NixOS wiki][nixos-wiki] contains guides to install
+NixOS to devices in other conditions, such as a server with only remote access.
+
+You will need a USB stick before proceeding to the next step.
+
+First, download the latest NixOS image, and flash it:
+
+```sh
+$ curl -L https:#channels.nixos.org/nixos-unstable/latest-nixos-minimal-x86_64-linux.iso -O nixos.iso
+$ dd if=./nixos.iso of=/dev/sdX bs=1M status=progress
+```
+
+If your target machine architecture is not `x86_64`, replace it with your
+desired architecture (e.g. `i686`, `aarch64`).
+
+After the image has been successfully flashed into your installation medium,
+unplug it and boot using the medium on the target machine.
+
+## Preparing Disks
+
+We'll start by defining variables pointing to each disk by ID.
+
+According to the [Archlinux.org Wiki][arch-wiki], If you create zpools using device names
+(e.g. `/dev/sda`), ZFS might not be able to detect zpools intermittently on
+boot.
+
+You can grab the ID via `ls -lh /dev/disk/by-id/`.
+
+```sh
+DISK1=/dev/disk/by-id/ata-VENDOR-ID-OF-THE-FIRST-DRIVE
+DISK2=/dev/disk/by-id/ata-VENDOR-ID-OF-THE-SECOND-DRIVE
+```
+
+### Partitioning
+
+Then we'll partition our disks. Since this is a mirrored setup, we'll have to do
+the exactly same operation twice. Fortunately, bash function come into rescue.
+
+The partition structure is the following:
+```
+1GiB Boot | ~Remaining ZFS
+```
+
+
+```sh
+partition() {
+    sgdisk --zap-all "$1"
+    sgdisk -n 1:0:+1GiB -t 1:EF00 -c 1:boot "$1"
+    # Swap is omitted.
+    sgdisk -n 2:0:0 -t 2:BF01 -c 2:zfs "$1"
+    sgdisk --print "$1"
+}
+
+partition $DISK1
+partition $DISK2
+```
+
+### Creating vfat filesystem for boot
+
+Boot partitions should be formatted with 'vfat', in order for it to mount and
+function without issues.
+
+```sh
+mkfs.vfat $DISK1-part1
+mkfs.vfat $DISK2-part1
+```
+
+### Configuring ZFS pool
+
+This dataset structure is based on [Erase your darlings][erase-your-darlings].
+
+Now that we're done partitioning our disks, we'll create a ZFS pool named
+'rpool', which is mirrored. This will prompt you to enter a passphrase for your
+new ZFS pool.
+```sh
+zpool create \
+    -o ashift=12 \
+    -O mountpoint=none -O atime=off -O acltype=posixacl -O xattr=sa \
+    -O compression=lz4 -O encryption=aes-256-gcm -O keyformat=passphrase \
+    rpool mirror \
+    $DISK1-part2 $DISK2-part2
+```
+
+Then, we create a 'root dataset' which is `/ (root)` for the target machine,
+then snapshot the empty state as 'blank'.
+```sh
+zfs create -p -o mountpoint=legacy rpool/local/root
+zfs snapshot rpool/local/root@blank
+```
+
+Note the 'local' after rpool. In this setup, 'local' is treated as unimportant
+data, i.e. packages, root, etc., Whereas 'safe' is treated as important data,
+which needs to be backed up.
+
+And mount it:
+```sh
+mount -t zfs rpool/local/root /mnt
+```
+
+Then we mount the multiple boot partitions we created:
+```sh
+mkdir /mnt/boot
+mkdir /mnt/boot-fallback
+
+mount $DISK1-part1 /mnt/boot
+mount $DISK2-part1 /mnt/boot-fallback
+```
+
+Create and mount a dataset for `/nix`:
+```sh
+zfs create -p -o mountpoint=legacy rpool/local/nix
+mkdir /mnt/nix
+mount -t zfs rpool/local/nix /mnt/nix
+```
+
+And a dataset for `/home`:
+```sh
+zfs create -p -o mountpoint=legacy rpool/safe/home
+mkdir /mnt/home
+mount -t zfs rpool/safe/home /mnt/home
+```
+
+And a dataset for states that needs to be persisted between boots:
+```sh
+zfs create -p -o mountpoint=legacy rpool/safe/persist
+mkdir /mnt/persist
+mount -t zfs rpool/safe/persist /mnt/persist
+```
+
+Note: All states will be wiped each boot after setting up
+[these](#erasing-your-darlings).
+Make sure to put states that need to persist on `/persist`.
+
+
+## Configuring NixOS
+
+Now that we're done with partitions and ZFS, it's time to declaratively
+configure the machine. This step may vary depending on your machine,
+please consult the docs when in doubt.
+
+### Getting the base configuration
+
+In this post, we're going to use plain `nixos-generate-config` to get a base
+configuration files for the machine.
+
+```sh
+nixos-generate-config --root /mnt
+```
+
+### Erasing your darlings
+
+In the [previous step](#configuring-zfs-pool), we've made a snapshot of blank
+root to roll back to it each boot, to keep the system stateless.
+
+Add this to the `configuration.nix` to wipe the root dataset on each boot by
+rolling back to the blank snapshot after the devices are made available:
+```nix
+{
+  boot.initrd.postDeviceCommands = lib.mkAfter ''
+    zfs rollback -r rpool/local/root@blank
+  '';
+}
+```
+
+### Configuring Bootloader
+
+In order to get ZFS to work, we need the following options to be set:
+```nix
+{
+  boot.supportedFilesystems = [ "zfs" ];
+  networking.hostId = "<8 random chars>";
+}
+```
+
+You can grab your machine ID at `/etc/machine-id` for the `hostId`.
+
+Then we'll configure grub:
+```nix
+{
+  # Whether installer can modify the EFI variables.
+  # If you encounter errors, set this to `false`.
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  boot.loader.grub.enable = true;
+  boot.loader.grub.efiSupport = true;
+  boot.loader.grub.device = "nodev";
+
+  # This should be done automatically, but explicitly declare it just in case.
+  boot.loader.grub.copyKernels = true;
+  # Make sure that you've listed all of the boot partitions here.
+  boot.loader.grub.mirroredBoots = [
+    { path = "/boot"; devices = ["/dev/disk/by-uuid/<ID-HERE>"]; }
+    { path = "/boot-fallback"; devices = ["/dev/disk/by-uuid/<ID-HERE>"]; }
+    # ...
+  ];
+}
+```
+
+### Handling boot partitions gracefully
+
+By default, NixOS will throw an error and complain about it when there is a
+missing partition/disk. Since we want the server to boot smoothly even if there
+is a missing boot partition, so we need to set the 'nofail' option to those
+partitions:
+
+```nix
+{
+  fileSystems."/boot".options = [ "nofail" ];
+  fileSystems."/boot-fallback".options = [ "nofail" ];
+}
+```
+
+
+### Enabling Remote ZFS Unlock
+
+On each boot, ZFS will ask for a passphrase to unlock the ZFS pool.
+To work around this issue, we can start an SSH server in `initrd`, that is going
+to live until the pool is unlocked.
+
+Note: If you rename the keys after, you may have some trouble rolling back to
+previous generations: See [here](caveat-remote-unlock) for details.
+
+To achieve that, we'll first have to generate an SSH host key for the initrd:
+```sh
+ssh-keygen -t ed25519 -N "" -f /mnt/boot/initrd-ssh-key
+
+# Each boot partition should have the same key
+cp /mnt/boot/initrd-ssh-key /mnt/boot-fallback/initrd-ssh-key
+```
+
+Then configure `initrd`:
+```nix
+{
+  boot.kernelModules = [ "<YOUR-NETWORK-CARD>" ];
+  boot.initrd.kernelModules = [ "<YOUR-NETWORK-CARD>" ];
+
+  # DHCP Configuration, comment on Static IP
+  networking.networkmanager.enable = false;
+  networking.useDHCP = true;
+
+  # Uncomment on Static IP
+  # boot.kernelParams = [
+  #   # See <https:#www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt> for documentation.
+  #   # ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>:<dns0-ip>:<dns1-ip>:<ntp0-ip>
+  #   # The server ip refers to the NFS server -- not needed in this case.
+  #   "ip=<YOUR-IPV4-ADDR>::<YOUR-IPV4-GATEWAY>:<YOUR-IPV4-NETMASK>:<YOUR-HOSTNAME>-initrd:<YOUR-NETWORK-INTERFACE>:off:<DNS-IP>"
+  # ];
+
+  boot.initrd.network.enable = true;
+  boot.initrd.network.ssh = {
+    enable = true;
+
+    # Using the same port as the actual SSH will cause clients to throw errors
+    # related to host key mismatch.
+    port = 2222;
+
+    # This takes 'path's, not 'string's.
+    hostKeys = [
+      /boot/initrd-ssh-key
+      /boot-fallback/initrd-ssh-key
+      # ...
+    ];
+
+    # Public ssh key to log into the initrd ssh
+    authorizedKeys = [ "<YOUR-SSH-PUBKEY>" ];
+  };
+  boot.initrd.network.postCommands = ''
+    cat <<EOF > /root/.profile
+    if pgrep -x "zfs" > /dev/null
+    then
+      zfs load-key -a
+      killall zfs
+    else
+      echo "ZFS is not running -- this could be a sign of failure."
+    fi
+    EOF
+  '';
+}
+```
+
+## Installing NixOS
+
+Run `nixos-install`, then reboot your machine.
+
+Note: Make sure that you've configured SSH and network for your machine,
+failure to do so may result in an inaccessible system.
+
+That's it! Enjoy your fresh NixOS machine!
+
+## Troubleshooting
+
+### Failed to import pool - more than one matching pool
+
+This error might occur when
+
+- one of your disks were previously used in another ZFS pool, and its metadata
+weren't properly removed
+- you messed up during install, and you repartitioning the disk without removing
+  its ZFS metadata.
+
+This is because the ZFS metadata doesn't live on a partition, but on a disk.
+
+Note: the following operations will irrevocably delete ANY data on your disk!
+
+To remove those left behind:
+
+```sh
+sgdisk --zap-all $DISK
+# Overwrite first 256M of the disk, removing metadata
+# In some cases just `wipefs -a` works, but I found this to be the most
+# reliable way to wipe them no matter what operations were performed on the disk
+# before.
+dd if=/dev/urandom bs=1M count=256 of=$DISK
+```
+
+And then you can try the installation again.
+
+## Conclusion
+
+## Acknowledgements
+
+I wrote this article because I've noticed that I always forget some steps
+during NixOS installation to a newly acquired server.
+
+I've compiled resources listed below to make a step-by-step guide for a setup I
+find 'optimal'. Please do check out those resources!
+
+- [NixOS Discourse Thread][discourse-thread]
+- [Erase your darlings][erase-your-darlings]
+- [Remote, encrypted ZFS storage server with NixOS][hetzner-zfs]
+- [Encrypted ZFS mirror with mirrored boot on NixOS][nixos-zfs-mirrored-boot]
+
+[erase-your-darlings]: https://grahamc.com/blog/erase-your-darlings
+[nixos-wiki]: https://nixos.wiki
+[arch-wiki]: https://wiki.archlinux.org
+[caveat-remote-unlock]: https://github.com/NixOS/nixpkgs/issues/101462#issuecomment-1172926129
+[discourse-thread]: https://discourse.nixos.org/t/nixos-on-mirrored-ssd-boot-swap-native-encrypted-zfs/9215
+[hetzner-zfs]: https://mazzo.li/posts/hetzner-zfs.html
+[nixos-zfs-mirrored-boot]: https://elis.nu/blog/2019/08/encrypted-zfs-mirror-with-mirrored-boot-on-nixos
diff --git a/content/posts/_index.md b/content/posts/_index.md
new file mode 100644
index 0000000..60885e1
--- /dev/null
+++ b/content/posts/_index.md
@@ -0,0 +1,5 @@
++++
+title = "Posts"
+sort_by = "date"
+template = "posts.html"
++++
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..f8b3ed5
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,43 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "locked": {
+        "lastModified": 1667395993,
+        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1673606088,
+        "narHash": "sha256-wdYD41UwNwPhTdMaG0AIe7fE1bAdyHe6bB4HLUqUvck=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "37b97ae3dd714de9a17923d004a2c5b5543dfa6d",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100755
index 0000000..c568975
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,31 @@
+{
+  description = "sefidel-web devshell";
+  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 = import nixpkgs { inherit system; };
+      in
+      {
+        packages.sefidel-web = pkgs.stdenv.mkDerivation rec {
+          pname = "sefidel-web";
+          version = self.shortRev or "dirty";
+
+          src = ./.;
+          nativeBuildInputs = [ pkgs.zola ];
+          buildPhase = "zola build";
+          installPhase = "cp -r public $out";
+        };
+
+        defaultPackage = self.packages.${system}.sefidel-web;
+
+        devShell = pkgs.mkShell {
+          nativeBuildInputs = with pkgs; [
+            zola
+          ];
+          buildInputs = [ ];
+        };
+      });
+}
diff --git a/sass/style.scss b/sass/style.scss
new file mode 100644
index 0000000..8373bc0
--- /dev/null
+++ b/sass/style.scss
@@ -0,0 +1,169 @@
+@charset "utf-8";
+
+$black: black;
+$white: #dfdfdf;
+$muted: grey;
+// more readable colour?
+$accent: #d39fa3;
+
+html {
+  font-size: calc(min(max(1rem, 4vw), 16px));
+}
+
+body {
+  font-family: 'Sarasa Gothic SC', monospace;
+  display: flex;
+  flex-flow: row;
+}
+
+header.top-header {
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+
+  a {
+    text-decoration: none;
+    color: $accent;
+  }
+  .header-title {
+    font-size: 30px;
+  }
+  .header-description {
+    margin: 5px auto;
+    font-size: 15px;
+    padding-bottom: 10px;
+  }
+}
+
+#main {
+  display: flex;
+  flex: auto;
+  align-items: center;
+  flex-flow: column;
+  min-height: 100vh;
+  padding: 1em;
+  overflow: hidden;
+}
+
+#main > nav {
+  text-align: center;
+}
+
+#main > main, #main > footer {
+  width: 90vw;
+  max-width: 70em;
+}
+
+#main > footer {
+  padding-top: 50px;
+}
+
+#intro {
+  width: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  box-sizing: border-box;
+}
+
+a, a:visited {
+  color: inherit;
+}
+
+a:hover {
+  text-decoration: dotted;
+}
+
+pre {
+  padding: 1em;
+  // border-radius: 0.5em;
+  border: 1px solid darken($white, 30%);
+  overflow-x: auto;
+  white-space: pre;
+  word-wrap: normal;
+}
+
+:not(pre) > code {
+  display: inline-block;
+  padding: 2px;
+  border: 1px solid darken($white, 30%);
+  background-color: $white;
+}
+
+hr {
+  border: none;
+  border-top: 1px solid $muted;
+  max-width: 25px;
+  margin: 35px auto;
+}
+
+.title a {
+  text-decoration: none;
+  color: inherit;
+}
+
+.post-meta {
+  font-size: smaller;
+  color: $muted;
+}
+
+.alt-links {
+  float: right;
+  color: $muted;
+  margin: 30px 0;
+}
+
+.muted {
+  color: $muted;
+}
+
+.split-horizontal {
+  border-bottom: 1px solid $muted;
+  padding: 5px 20px;
+}
+
+.article-list article h3 {
+  display: inline-block;
+  margin: 0;
+}
+
+.article-list article .post-meta {
+  font-size: smaller;
+  margin: 0;
+  padding-bottom: 10px;
+}
+
+article > header time {
+  color: $muted;
+  white-space: nowrap;
+}
+
+article > footer {
+  text-align: center;
+}
+
+@media (prefers-color-scheme: dark) {
+  body {
+    background-color: $black;
+    color: $white;
+  }
+  pre {
+    border: 1px solid lighten($black, 30%);
+  }
+  :not(pre) > code {
+    border: 1px solid lighten($black, 30%);
+    background-color: lighten($black, 15%);
+  }
+}
+
+@media print {
+//   header.top-header {
+//     display: none;
+//   }
+  #main nav {
+    display: none;
+  }
+}
diff --git a/static/.well-known/openpgpkey/hu/dj3498u4hyyarh35rkjfnghbjxug6b19 b/static/.well-known/openpgpkey/hu/dj3498u4hyyarh35rkjfnghbjxug6b19
new file mode 100644
index 0000000..65edc9b
--- /dev/null
+++ b/static/.well-known/openpgpkey/hu/dj3498u4hyyarh35rkjfnghbjxug6b19
Binary files differdiff --git a/static/.well-known/openpgpkey/policy b/static/.well-known/openpgpkey/policy
new file mode 100644
index 0000000..a26f9a0
--- /dev/null
+++ b/static/.well-known/openpgpkey/policy
@@ -0,0 +1 @@
+protocol-version: 6
diff --git a/static/font/sarasa-gothic-sc-bold.woff b/static/font/sarasa-gothic-sc-bold.woff
new file mode 100644
index 0000000..231b8a2
--- /dev/null
+++ b/static/font/sarasa-gothic-sc-bold.woff
Binary files differdiff --git a/static/font/sarasa-gothic-sc-bold.woff2 b/static/font/sarasa-gothic-sc-bold.woff2
new file mode 100644
index 0000000..c189871
--- /dev/null
+++ b/static/font/sarasa-gothic-sc-bold.woff2
Binary files differdiff --git a/static/normalize.css b/static/normalize.css
new file mode 100644
index 0000000..192eb9c
--- /dev/null
+++ b/static/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}
diff --git a/static/sarasa-gothic.css b/static/sarasa-gothic.css
new file mode 100644
index 0000000..2b24868
--- /dev/null
+++ b/static/sarasa-gothic.css
@@ -0,0 +1,8 @@
+@font-face {
+  font-family: 'Sarasa Gothic SC';
+  font-style: normal;
+  font-weight: bold;
+  unicode-range: U+30BB, U+30D4, U+30FC;
+  src: url(./font/sarasa-gothic-sc-bold.woff2) format('woff2'),
+       url(./font/sarasa-gothic-sc-bold.woff) format('woff');
+}
diff --git a/templates/404.html b/templates/404.html
new file mode 100644
index 0000000..9ea1de3
--- /dev/null
+++ b/templates/404.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+
+{% block html_title %} 404 Not Found | {{ config.title }}{% endblock html_title %}
+
+{% block content %}
+  <article lang="en">
+    <header>
+      <h1 class="title">404 Not Found</h1>
+      <p class="post-meta">Published on 1970-01-01</p>
+  </header>
+  <p>In a world where technology reigns supreme, there was a digital kingdom called the World Wide Web. It was a vast expanse of information, a realm where people could connect, communicate, and collaborate with one another in ways that were previously unimaginable.</p>
+
+  <p>In the midst of this virtual kingdom, there was a curious creature called the 404 error. It was a mysterious entity, shrouded in darkness and enigma. Its origins were unknown, and its motives were unclear. Some whispered that it was a curse, others believed it to be a supernatural force that was beyond human comprehension.</p>
+
+  <p>Regardless of its true nature, the 404 error was feared by all who roamed the World Wide Web. It would often appear when a user clicked on a broken link or attempted to access a page that no longer existed. In such instances, the user would be greeted with an ominous message that read "404 Error: Page Not Found."</p>
+
+  <p>The message was chilling, and it was accompanied by an image of a dark abyss, which seemed to swallow all who dared to venture too close. It was a warning, a reminder that the digital realm was not without its dangers, and that the 404 error was one of its most potent threats.</p>
+
+  <p>Despite its fearsome reputation, the 404 error was not invincible. There were those who had managed to vanquish it, to restore broken links, and resurrect lost pages. These brave souls were known as web developers, and they possessed a unique set of skills that allowed them to navigate the complex landscape of the World Wide Web.</p>
+
+  <p>To the web developer, the 404 error was a challenge, a puzzle that needed to be solved. They would delve deep into the code of the website, analyzing every line, every character, and every symbol. They would search for clues, follow breadcrumbs, and piece together the fragments of information that lay scattered across the digital landscape.</p>
+
+  <p>It was a difficult task, one that required patience, persistence, and an unrelenting desire to succeed. But for the web developer, the rewards were great. They would emerge from the depths of the digital abyss victorious, having defeated the 404 error and restored order to the World Wide Web.</p>
+
+  <p>And so, the legend of the 404 error lived on, a reminder of the dangers that lurked in the digital realm, and the courage and skill of those who dared to face them. It was a tale that would be told for generations, a testament to the power of technology, and the human spirit that drove it forward.</p>
+
+  <p>Also, this page isn't real.</p>
+
+  <p class="muted">Generated using ChatGPT</p>
+  <a href="/">Go to site root</a><br>
+  <a href="javascript:history.back()">Go back to previous page [JS]</a>
+</article>
+{% endblock content %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..33f4fd5
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,25 @@
+{% import "macros/posts.html" as posts %}
+
+<!DOCTYPE html>
+<html lang="{{ config.default_language }}">
+  <head>
+    {% include "parts/head.html" %}
+    <title>{% block html_title %}{% endblock html_title %}</title>
+  </head>
+  <body>
+    <div id="main">
+      <header class="top-header">
+        <b><a class="header-title" href="/">{{ config.title }}</a></b>
+        <p class="header-description"><span class="split-horizontal">Fixing nonexistent problems</span></p>
+      </header>
+      {% include "parts/nav.html" %}
+      <main>
+        {% block content %}{% endblock %}
+      </main>
+      <footer>
+        © 2023 sefidel
+        <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons Licence" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /></a><br />This work is licensed under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
+      </footer>
+    </div>
+  </body>
+</html>
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..7e3b531
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    {% include "parts/head.html" %}
+    <title>{{ section.title }} | {{ config.title }}</title>
+  </head>
+  <body>
+    <main id="intro">
+      <h1>{{ config.title }}</h1>
+      {% set isHome = true %}
+      {% include "parts/nav.html" %}
+    </main>
+  </body>
+</html>
diff --git a/templates/macros/posts.html b/templates/macros/posts.html
new file mode 100644
index 0000000..506adff
--- /dev/null
+++ b/templates/macros/posts.html
@@ -0,0 +1,61 @@
+{% macro display(pages) %}
+  <div class="article-list">
+    <!-- If you are using pagination, section.pages will be empty. You need to use the paginator object -->
+    {% for page in pages %}
+      <article>
+        <header lang="{{ page.lang }}">
+          <time datetime="{{ page.date }}">{{ page.date }}</time>
+          <h3>{{ posts::link(page=page) }}</h3>
+          <p class="post-meta">{{ posts::taxonomies(taxonomies=page.taxonomies) }}</p>
+        </header>
+      </article>
+    {% endfor %}
+</div>
+{% endmacro display %}
+
+{% macro link(page) %}
+  <a href="{{ page.permalink | safe }}">{{ page.title }}</a>
+{% endmacro link %}
+
+{% macro meta(page) %}
+  {% if page.date %}
+    Published on {{ page.date }}
+  {% endif %}
+  {% if page.updated %}
+    | Edited on {{ page.updated }}
+  {% endif %}
+  {% if page.taxonomies %}
+    :: {{ posts::taxonomies(taxonomies=page.taxonomies) }}
+  {% endif %}
+{% endmacro meta %}
+
+{% macro taxonomies(taxonomies) %}
+  {% if taxonomies.categories %}
+    {{ posts::categories(categories=taxonomies.categories) }}
+  {% endif %}
+  {% if taxonomies.tags %}
+    {{ posts::tags(tags=taxonomies.tags) }}
+  {% endif %}
+{% endmacro taxonomies %}
+
+{% macro categories(categories) %}
+  [
+  {% for category in categories %}
+    {% if loop.last %}
+      <a href="{{ get_taxonomy_url(kind="categories", name=category) }}">{{ category }}</a>
+    {% else %}
+      <a href="{{ get_taxonomy_url(kind="categories", name=category) }}">{{ category }}</a>,
+    {% endif %}
+  {% endfor %}
+  ]
+{% endmacro categories %}
+
+{% macro tags(tags) %}
+  {% for tag in tags %}
+    {% if loop.last %}
+      #<a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">{{ tag }}</a>
+    {% else %}
+      #<a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">{{ tag }}</a>,
+    {% endif %}
+  {% endfor %}
+{% endmacro tags %}
diff --git a/templates/page.html b/templates/page.html
new file mode 100644
index 0000000..cef35d9
--- /dev/null
+++ b/templates/page.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+
+{% block html_title %}{{ page.title }} | {{ config.title }}{% endblock html_title %}
+
+{% block content %}
+  <article lang="{{ lang }}">
+    <header>
+      <h1 class="title">
+        <a href="{{ page.permalink }}">{{ page.title }}</a>
+      </h1>
+      {% if not page.extra.raw %}
+        <p class="post-meta">{{ posts::meta(page=page) }}</p>
+      {% endif %}
+    </header>
+    {% if page.toc and not page.extra.no_toc %}
+      <h3>Table of Contents</h3>
+      <ul>
+      {% for h1 in page.toc %}
+        <li>
+          <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
+            {% if h1.children %}
+              <ul>
+                {% for h2 in h1.children %}
+                  <li>
+                    <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+                  </li>
+                {% endfor %}
+              </ul>
+            {% endif %}
+        </li>
+      {% endfor %}
+      </ul>
+    {% endif %}
+    {{ page.content | safe }}
+    {% if not page.extra.raw %}
+      <footer>
+        <hr>
+        <p class="muted">
+        Should you have any questions or comments, please reach out to me by sending <a href="mailto:contact@sefidel.net">an email</a> or <a href="https://matrix.to/#/@sef:exotic.sh">via Matrix</a>.
+        </p>
+      </footer>
+    {% endif %}
+    </article>
+{% endblock content %}
diff --git a/templates/parts/head.html b/templates/parts/head.html
new file mode 100644
index 0000000..1336ec0
--- /dev/null
+++ b/templates/parts/head.html
@@ -0,0 +1,11 @@
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+{% block rss %}
+<link rel="alternate" type="application/atom+xml" title="RSS" href="{{ get_url(path="atom.xml", trailing_slash=false) }}">
+{% endblock %}
+{% if current_url %}
+<link rel="canonical" href="{{ current_url | safe }}">
+{% endif %}
+<link rel="stylesheet" href="{{ get_url(path="normalize.css") }}">
+<link rel="stylesheet" href="{{ get_url(path="style.css") }}">
+<link rel="stylesheet" href="{{ get_url(path="sarasa-gothic.css") }}">
diff --git a/templates/parts/nav.html b/templates/parts/nav.html
new file mode 100644
index 0000000..4d47ce0
--- /dev/null
+++ b/templates/parts/nav.html
@@ -0,0 +1,8 @@
+<nav>
+  {% if not isHome %}
+  <a href="/">Home</a> &middot;
+  {% endif %}
+  <a href="/posts">Posts</a> &middot;
+  <a href="/projects">Projects</a> &middot;
+  <a rel="me" href="/about">About</a>
+</nav>
diff --git a/templates/posts.html b/templates/posts.html
new file mode 100644
index 0000000..f2ccc74
--- /dev/null
+++ b/templates/posts.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block html_title %}{{ section.title }} | {{ config.title }}{% endblock html_title %}
+
+{% block content %}
+  <div class="alt-links">
+    [<a href="{{ get_url(path="atom.xml", trailing_slash=false) }}">feed</a>]
+    [<a href="{{ get_url(path="categories") }}">cat</a>]
+    [<a href="{{ get_url(path="tags") }}">tag</a>]
+  </div>
+  <h1 class="title">
+    Posts
+  </h1>
+  {{ posts::display(pages=section.pages) }}
+  {% endblock content %}
diff --git a/templates/taxonomy_list.html b/templates/taxonomy_list.html
new file mode 100644
index 0000000..0c4a579
--- /dev/null
+++ b/templates/taxonomy_list.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block html_title %}{{ taxonomy.name | capitalize }} | {{ config.title }}{% endblock html_title %}
+
+{% block content %}
+  <h1 class="title">
+    {{ taxonomy.name | capitalize }}
+  </h1>
+  <h3>{{ terms | length }} term{{ terms | length | pluralize }}</h3>
+  <table class="terms">
+    <tbody>
+      {% for term in terms %}
+      <tr>
+        <td>
+          <b><span class="prefix">|</span></b>
+          <a href="{{ term.permalink }}">{{ term.name }}</a>
+          ({{ term.pages | length }} post{{ term.pages | length | pluralize }})
+        </td>
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+{% endblock content %}
diff --git a/templates/taxonomy_single.html b/templates/taxonomy_single.html
new file mode 100644
index 0000000..e13f439
--- /dev/null
+++ b/templates/taxonomy_single.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block html_title %}{{ term.name | capitalize}} | {{ config.title }}{% endblock html_title %}
+
+{% block content %}
+  <h1 class="title">
+    {{ taxonomy.name | capitalize }} -> {{ term.name | capitalize }}
+  </h1>
+  <h3>{{ term.pages | length }} post{{ term.pages | length | pluralize }}</h3>
+  {{ posts::display(pages=term.pages) }}
+{% endblock content %}