about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-06 15:45:11 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-06 15:45:11 +0200
commita6baea06697f6c76c695dc4198099deb8ba916e0 (patch)
tree476a3865f6b4bef04751ba20534813a58892811b
parentchore: Initial commit (diff)
downloadback-a6baea06697f6c76c695dc4198099deb8ba916e0.zip
feat(treewide): Prepare for first release
This commit contains many changes, as they were developed alongside
`git-bug-rs` and unfortunately not separately committed.

A toplevel summary would include:
- Appropriate redirects,
- The templating moved to `vy` (as this works with rustfmt formatting),
- Search support (via `git-bug-rs`),
- And better layout in the link section.
-rw-r--r--Cargo.lock325
-rw-r--r--Cargo.toml7
-rw-r--r--assets/search.pngbin0 -> 711 bytes
-rw-r--r--assets/style.css3
-rw-r--r--contrib/projects.list3
-rw-r--r--flake.lock54
-rw-r--r--flake.nix91
-rw-r--r--rustfmt_config.toml (renamed from rustfmt.toml)2
-rw-r--r--src/cli.rs2
-rw-r--r--src/config/mod.rs66
-rw-r--r--src/error/mod.rs165
-rw-r--r--src/main.rs9
-rw-r--r--src/web/generate/mod.rs522
-rw-r--r--src/web/generate/templates.rs44
-rw-r--r--src/web/mod.rs125
-rw-r--r--src/web/responses.rs135
-rw-r--r--templates/issue.html74
-rw-r--r--templates/issues.html77
-rw-r--r--templates/repos.html62
-rw-r--r--treefmt.nix21
20 files changed, 1070 insertions, 717 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 61a5656..59d9fb3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -100,12 +100,12 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
 dependencies = [
  "anstyle",
- "once_cell",
+ "once_cell_polyfill",
  "windows-sys 0.59.0",
 ]
 
@@ -151,8 +151,8 @@ name = "back"
 version = "0.1.0"
 dependencies = [
  "bytes",
- "chrono",
  "clap",
+ "git-bug",
  "gix",
  "http",
  "http-body-util",
@@ -160,15 +160,14 @@ dependencies = [
  "hyper-util",
  "log",
  "markdown",
- "rinja",
  "rss",
  "serde",
  "serde_json",
- "sha2",
  "stderrlog",
  "thiserror",
  "tokio",
  "url",
+ "vy",
 ]
 
 [[package]]
@@ -187,10 +186,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
 name = "bitflags"
-version = "2.9.0"
+version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "block-buffer"
@@ -238,9 +246,9 @@ checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
 
 [[package]]
 name = "cc"
-version = "1.2.22"
+version = "1.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
+checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
 dependencies = [
  "shlex",
 ]
@@ -267,9 +275,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.38"
+version = "4.5.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
+checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -277,9 +285,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.38"
+version = "4.5.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
+checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
 dependencies = [
  "anstream",
  "anstyle",
@@ -312,6 +320,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"
 
 [[package]]
+name = "cobs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+
+[[package]]
 name = "colorchoice"
 version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -483,6 +497,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
 
 [[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
 name = "encoding_rs"
 version = "0.8.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -499,9 +525,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
-version = "0.3.11"
+version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -547,6 +573,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
 name = "fnv"
 version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -623,9 +658,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi",
  "wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -635,6 +672,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
 [[package]]
+name = "git-bug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adbfe9a5393ead84bdd88d054a0e2bb71296490bf90846b07d90eba8ec86fa6d"
+dependencies = [
+ "base64",
+ "chrono",
+ "faster-hex",
+ "gix",
+ "log",
+ "postcard",
+ "redb",
+ "serde",
+ "sha2",
+ "simd-json",
+ "thiserror",
+ "url",
+]
+
+[[package]]
 name = "gix"
 version = "0.72.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,6 +736,7 @@ dependencies = [
  "gix-submodule",
  "gix-tempfile",
  "gix-trace",
+ "gix-transport",
  "gix-traverse",
  "gix-url",
  "gix-utils",
@@ -689,6 +747,7 @@ dependencies = [
  "once_cell",
  "parking_lot",
  "regex",
+ "serde",
  "signal-hook",
  "smallvec",
  "thiserror",
@@ -704,15 +763,16 @@ dependencies = [
  "gix-date",
  "gix-utils",
  "itoa",
+ "serde",
  "thiserror",
  "winnow",
 ]
 
 [[package]]
 name = "gix-archive"
-version = "0.21.1"
+version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85e59cc5867c40065122324265d99416ca0ddeb51d007870868b5f835423fb6c"
+checksum = "8826f20d84822a9abd9e81d001c61763a25f4ec96caa073fb0b5457fe766af21"
 dependencies = [
  "bstr",
  "gix-date",
@@ -724,9 +784,9 @@ dependencies = [
 
 [[package]]
 name = "gix-attributes"
-version = "0.26.0"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7e26b3ac280ddb25bb6980d34f4a82ee326f78bf2c6d4ea45eef2d940048b8e"
+checksum = "6f50d813d5c2ce9463ba0c29eea90060df08e38ad8f34b8a192259f8bce5c078"
 dependencies = [
  "bstr",
  "gix-glob",
@@ -734,6 +794,7 @@ dependencies = [
  "gix-quote",
  "gix-trace",
  "kstring",
+ "serde",
  "smallvec",
  "thiserror",
  "unicode-bom",
@@ -759,9 +820,9 @@ dependencies = [
 
 [[package]]
 name = "gix-command"
-version = "0.6.0"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f47f3fb4ba33644061e8e0e1030ef2a937d42dc969553118c320a205a9fb28"
+checksum = "d05dd813ef6bb798570308aa7f1245cefa350ec9f30dc53308335eb22b9d0f8b"
 dependencies = [
  "bstr",
  "gix-path",
@@ -780,6 +841,7 @@ dependencies = [
  "gix-chunk",
  "gix-hash",
  "memmap2",
+ "serde",
  "thiserror",
 ]
 
@@ -831,6 +893,7 @@ dependencies = [
  "gix-sec",
  "gix-trace",
  "gix-url",
+ "serde",
  "thiserror",
 ]
 
@@ -843,6 +906,7 @@ dependencies = [
  "bstr",
  "itoa",
  "jiff",
+ "serde",
  "smallvec",
  "thiserror",
 ]
@@ -931,9 +995,9 @@ dependencies = [
 
 [[package]]
 name = "gix-filter"
-version = "0.19.1"
+version = "0.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90c21f0d61778f518bbb7c431b00247bf4534b2153c3e85bcf383876c55ca6c"
+checksum = "ecf004912949bbcf308d71aac4458321748ecb59f4d046830d25214208c471f1"
 dependencies = [
  "bstr",
  "encoding_rs",
@@ -966,14 +1030,15 @@ dependencies = [
 
 [[package]]
 name = "gix-glob"
-version = "0.20.0"
+version = "0.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2926b03666e83b8d01c10cf06e5733521aacbd2d97179a4c9b1fdddabb9e937d"
+checksum = "90181472925b587f6079698f79065ff64786e6d6c14089517a1972bca99fb6e9"
 dependencies = [
  "bitflags",
  "bstr",
  "gix-features",
  "gix-path",
+ "serde",
 ]
 
 [[package]]
@@ -984,6 +1049,7 @@ checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8"
 dependencies = [
  "faster-hex",
  "gix-features",
+ "serde",
  "sha1-checked",
  "thiserror",
 ]
@@ -1009,6 +1075,7 @@ dependencies = [
  "gix-glob",
  "gix-path",
  "gix-trace",
+ "serde",
  "unicode-bom",
 ]
 
@@ -1036,6 +1103,7 @@ dependencies = [
  "libc",
  "memmap2",
  "rustix",
+ "serde",
  "smallvec",
  "thiserror",
 ]
@@ -1060,6 +1128,7 @@ dependencies = [
  "bstr",
  "gix-actor",
  "gix-date",
+ "serde",
  "thiserror",
 ]
 
@@ -1095,6 +1164,7 @@ dependencies = [
  "gix-utils",
  "gix-validate",
  "itoa",
+ "serde",
  "smallvec",
  "thiserror",
  "winnow",
@@ -1117,6 +1187,7 @@ dependencies = [
  "gix-path",
  "gix-quote",
  "parking_lot",
+ "serde",
  "tempfile",
  "thiserror",
 ]
@@ -1135,6 +1206,7 @@ dependencies = [
  "gix-object",
  "gix-path",
  "memmap2",
+ "serde",
  "smallvec",
  "thiserror",
  "uluru",
@@ -1221,6 +1293,7 @@ dependencies = [
  "gix-transport",
  "gix-utils",
  "maybe-async",
+ "serde",
  "thiserror",
  "winnow",
 ]
@@ -1253,6 +1326,7 @@ dependencies = [
  "gix-utils",
  "gix-validate",
  "memmap2",
+ "serde",
  "thiserror",
  "winnow",
 ]
@@ -1286,6 +1360,7 @@ dependencies = [
  "gix-object",
  "gix-revwalk",
  "gix-trace",
+ "serde",
  "thiserror",
 ]
 
@@ -1313,6 +1388,7 @@ dependencies = [
  "bitflags",
  "gix-path",
  "libc",
+ "serde",
  "windows-sys 0.59.0",
 ]
 
@@ -1325,6 +1401,7 @@ dependencies = [
  "bstr",
  "gix-hash",
  "gix-lock",
+ "serde",
  "thiserror",
 ]
 
@@ -1401,6 +1478,7 @@ dependencies = [
  "gix-quote",
  "gix-sec",
  "gix-url",
+ "serde",
  "thiserror",
 ]
 
@@ -1431,6 +1509,7 @@ dependencies = [
  "gix-features",
  "gix-path",
  "percent-encoding",
+ "serde",
  "thiserror",
  "url",
 ]
@@ -1473,6 +1552,7 @@ dependencies = [
  "gix-object",
  "gix-path",
  "gix-validate",
+ "serde",
 ]
 
 [[package]]
@@ -1497,9 +1577,9 @@ dependencies = [
 
 [[package]]
 name = "gix-worktree-stream"
-version = "0.21.1"
+version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c7731f9e7ffc45f1f79d6601a37169be0697d4e2bd015495b4f53dffd80b42e"
+checksum = "5acc0f942392e0cae6607bfd5fe39e56e751591271bdc8795c6ec34847d17948"
 dependencies = [
  "gix-attributes",
  "gix-features",
@@ -1533,6 +1613,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "halfbrown"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2c385c6df70fd180bbb673d93039dbd2cd34e41d782600bdf6e1ca7bce39aa"
+dependencies = [
+ "hashbrown 0.15.3",
+]
+
+[[package]]
 name = "hash32"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1557,6 +1646,8 @@ version = "0.15.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
 dependencies = [
+ "allocator-api2",
+ "equivalent",
  "foldhash",
 ]
 
@@ -1665,12 +1756,12 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.11"
+version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
+checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
 dependencies = [
  "bytes",
- "futures-util",
+ "futures-core",
  "http",
  "http-body",
  "hyper",
@@ -1751,9 +1842,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
 
 [[package]]
 name = "icu_properties"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
 dependencies = [
  "displaydoc",
  "icu_collections",
@@ -1767,9 +1858,9 @@ dependencies = [
 
 [[package]]
 name = "icu_properties_data"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
 
 [[package]]
 name = "icu_provider"
@@ -1868,10 +1959,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
+name = "itoap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
+
+[[package]]
 name = "jiff"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
+checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
 dependencies = [
  "jiff-static",
  "jiff-tzdb-platform",
@@ -1884,9 +1981,9 @@ dependencies = [
 
 [[package]]
 name = "jiff-static"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
+checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1924,6 +2021,7 @@ version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
 dependencies = [
+ "serde",
  "static_assertions",
 ]
 
@@ -2027,13 +2125,13 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 dependencies = [
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2067,6 +2165,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
 [[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
 name = "parking_lot"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2123,6 +2227,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "postcard"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
 name = "potential_utf"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2178,6 +2294,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
 
 [[package]]
+name = "redb"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cef6a6d3a65ea334d6cdfb31fa2525c20184b7aa7bd1ad1e2e37502610d4609f"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "redox_syscall"
 version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2187,6 +2312,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "regex"
 version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2216,12 +2361,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
-name = "rinja"
-version = "0.4.0+deprecated"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67047e6d76ddf18132ef41c8a3465dceb48cf05edd3eb38189456ebe83875ea8"
-
-[[package]]
 name = "rss"
 version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2254,9 +2393,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.20"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
 
 [[package]]
 name = "ryu"
@@ -2375,6 +2514,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "simd-json"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c962f626b54771990066e5435ec8331d1462576cd2d1e62f24076ae014f92112"
+dependencies = [
+ "getrandom",
+ "halfbrown",
+ "ref-cast",
+ "simdutf8",
+ "value-trait",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
 name = "slab"
 version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2388,12 +2546,15 @@ name = "smallvec"
 version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "socket2"
-version = "0.5.9"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -2531,9 +2692,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.45.0"
+version = "1.45.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
 dependencies = [
  "backtrace",
  "bytes",
@@ -2655,12 +2816,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
+name = "value-trait"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0508fce11ad19e0aab49ce20b6bec7f8f82902ded31df1c9fc61b90f0eb396b8"
+dependencies = [
+ "float-cmp",
+ "halfbrown",
+ "itoa",
+ "ryu",
+]
+
+[[package]]
 name = "version_check"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
+name = "vy"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca9415823937514bbba8aeddee84871f1b137f107878a0fdebf6c8ec2846b3a6"
+dependencies = [
+ "vy-core",
+ "vy-macros",
+]
+
+[[package]]
+name = "vy-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95c86e70f659b92a9bb20120384e1af5a29eac7f926c484b51fad034e45c8acf"
+dependencies = [
+ "itoap",
+ "ryu",
+]
+
+[[package]]
+name = "vy-macros"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567c0fb4cea81da221859fbcffa37f614c0460f8804f058c0973b76b571997db"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "vy-core",
+]
+
+[[package]]
 name = "walkdir"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2776,9 +2981,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-core"
-version = "0.61.0"
+version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
 dependencies = [
  "windows-implement",
  "windows-interface",
@@ -2817,18 +3022,18 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
 
 [[package]]
 name = "windows-result"
-version = "0.3.2"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
 dependencies = [
  "windows-link",
 ]
 
 [[package]]
 name = "windows-strings"
-version = "0.4.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
 dependencies = [
  "windows-link",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index a898572..ac8b303 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@
 name = "back"
 description = "An extremely simple git bug visualization system. Inspired by TVL's panettone."
 version = "0.1.0"
-edition = "2021"
+edition = "2024"
 license = "AGPL-3.0-or-later"
 homepage = ""
 repository = "https://git.foss-syndicate.org/vhack.eu/git_bug/back"
@@ -23,8 +23,8 @@ repository = "https://git.foss-syndicate.org/vhack.eu/git_bug/back"
 
 
 [dependencies]
+git-bug = { version = "0.2.3" }
 bytes = "1.10.1"
-chrono = "0.4.41"
 clap = { version = "4.5.38", features = ["derive"] }
 gix = "0.72.1"
 http = "1.3.1"
@@ -33,15 +33,14 @@ hyper = { version = "1.6.0", features = ["http1", "http2", "server"] }
 hyper-util = { version = "0.1.11", features = ["tokio"] }
 log = "0.4.27"
 markdown = "1.0.0"
-rinja = "0.4.0"
 rss = "2.0.12"
 serde = "1.0.219"
 serde_json = "1.0.140"
-sha2 = "0.10.9"
 stderrlog = "0.6.0"
 thiserror = "2.0.12"
 tokio = { version = "1.45.0", features = ["macros", "net", "rt-multi-thread"] }
 url = { version = "2.5.4", features = ["serde"] }
+vy = "0.2.0"
 
 [profile.release]
 lto = true
diff --git a/assets/search.png b/assets/search.png
new file mode 100644
index 0000000..0fd78c6
--- /dev/null
+++ b/assets/search.png
Binary files differdiff --git a/assets/style.css b/assets/style.css
index e7ce376..2f7414f 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -141,7 +141,7 @@ label.checkbox {
 
 .issue-search input[type="search"] {
   padding: 0.5rem;
-  background-image: url("static/search.png");
+  background-image: url("/search.png");
   background-position: 10px 10px;
   background-repeat: no-repeat;
   background-size: 1rem;
@@ -309,6 +309,7 @@ nav {
   display: flex;
   color: var(--gray);
   justify-content: space-between;
+  align-items: center;
 }
 
 nav .nav-group {
diff --git a/contrib/projects.list b/contrib/projects.list
new file mode 100644
index 0000000..dd4a060
--- /dev/null
+++ b/contrib/projects.list
@@ -0,0 +1,3 @@
+owner1/project_one.git
+owner2/project_two
+all_the_issues_cache
diff --git a/flake.lock b/flake.lock
index 78982af..d984446 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,36 +1,5 @@
 {
   "nodes": {
-    "crane": {
-      "locked": {
-        "lastModified": 1746291859,
-        "narHash": "sha256-DdWJLA+D5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q=",
-        "owner": "ipetkov",
-        "repo": "crane",
-        "rev": "dfd9a8dfd09db9aad544c4d3b6c47b12562544a5",
-        "type": "github"
-      },
-      "original": {
-        "owner": "ipetkov",
-        "repo": "crane",
-        "type": "github"
-      }
-    },
-    "flake-compat": {
-      "flake": false,
-      "locked": {
-        "lastModified": 1733328505,
-        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
-        "type": "github"
-      },
-      "original": {
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "type": "github"
-      }
-    },
     "flake-utils": {
       "inputs": {
         "systems": [
@@ -69,35 +38,12 @@
     },
     "root": {
       "inputs": {
-        "crane": "crane",
-        "flake-compat": "flake-compat",
         "flake-utils": "flake-utils",
         "nixpkgs": "nixpkgs",
-        "rust-overlay": "rust-overlay",
         "systems": "systems",
         "treefmt-nix": "treefmt-nix"
       }
     },
-    "rust-overlay": {
-      "inputs": {
-        "nixpkgs": [
-          "nixpkgs"
-        ]
-      },
-      "locked": {
-        "lastModified": 1747017456,
-        "narHash": "sha256-C/U12fcO+HEF071b5mK65lt4XtAIZyJSSJAg9hdlvTk=",
-        "owner": "oxalica",
-        "repo": "rust-overlay",
-        "rev": "5b07506ae89b025b14de91f697eba23b48654c52",
-        "type": "github"
-      },
-      "original": {
-        "owner": "oxalica",
-        "repo": "rust-overlay",
-        "type": "github"
-      }
-    },
     "systems": {
       "locked": {
         "lastModified": 1680978846,
diff --git a/flake.nix b/flake.nix
index 7389429..b6df3c8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,25 +21,9 @@
       };
     };
 
-    crane = {
-      url = "github:ipetkov/crane";
-      inputs = {};
-    };
-    rust-overlay = {
-      url = "github:oxalica/rust-overlay";
-      inputs = {
-        nixpkgs.follows = "nixpkgs";
-      };
-    };
-
-    # inputs for following
     systems = {
       url = "github:nix-systems/x86_64-linux"; # only evaluate for this system
     };
-    flake-compat = {
-      url = "github:edolstra/flake-compat";
-      flake = false;
-    };
     flake-utils = {
       url = "github:numtide/flake-utils";
       inputs = {
@@ -53,81 +37,42 @@
     nixpkgs,
     flake-utils,
     treefmt-nix,
-    crane,
-    rust-overlay,
     ...
   }:
     flake-utils.lib.eachDefaultSystem (system: let
-      pkgs = import nixpkgs {
-        inherit system;
-        overlays = [(import rust-overlay)];
-      };
-
-      nightly = true;
-      rust_minimal =
-        if nightly
-        then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal)
-        else pkgs.rust-bin.stable.latest.minimal;
-      rust_default =
-        if nightly
-        then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
-        else pkgs.rust-bin.stable.latest.default;
-
-      cargo_toml = craneLib.cleanCargoToml {cargoToml = ./Cargo.toml;};
-      pname = cargo_toml.package.name;
-
-      craneLib = (crane.mkLib pkgs).overrideToolchain rust_minimal;
-      craneBuild = craneLib.buildPackage {
-        src = craneLib.cleanCargoSource ./.;
-
-        doCheck = true;
-      };
-
-      manual = pkgs.stdenv.mkDerivation {
-        name = "${pname}-manual";
-        inherit (cargo_toml.package) version;
-
-        src = ./docs;
-        nativeBuildInputs = with pkgs; [pandoc];
-
-        buildPhase = ''
-          mkdir --parents $out/docs;
-
-          pandoc "./${pname}.1.md" -s -t man > $out/docs/${pname}.1
-        '';
-
-        installPhase = ''
-          install -D $out/docs/${pname}.1  $out/share/man/man1/${pname};
-        '';
-      };
+      pkgs = nixpkgs.legacyPackages."x86_64-linux";
 
       treefmtEval = import ./treefmt.nix {
         inherit treefmt-nix pkgs;
-        rustfmt = rust_default;
       };
-    in {
-      packages.default = pkgs.symlinkJoin {
-        inherit (cargo_toml.package) name;
 
-        paths = [manual craneBuild];
-      };
+      rustfmt = pkgs.writeShellScriptBin "rustfmt" ''
+        # Avoid the duplicated edition flag, that rust-analyzer passes.
+        if [ "$1" = "--edition" ] && [ "$2" == "2024" ]; then
+            shift 2
+        fi
 
+        ${pkgs.lib.getExe pkgs.rustfmt} ${builtins.concatStringsSep " " treefmtEval.config.settings.formatter.rustfmt.options} "$@"
+      '';
+    in {
       checks = {
-        inherit craneBuild;
         formatting = treefmtEval.config.build.check self;
       };
 
       formatter = treefmtEval.config.build.wrapper;
 
       devShells.default = pkgs.mkShell {
-        packages = with pkgs; [
-          cocogitto
-          git-bug
+        packages = [
+          pkgs.cocogitto
+          pkgs.git-bug
 
-          rust_default
-          cargo-edit
+          pkgs.rustc
+          pkgs.cargo
+          pkgs.clippy
+          rustfmt
+          pkgs.cargo-edit
 
-          reuse
+          pkgs.reuse
         ];
       };
     });
diff --git a/rustfmt.toml b/rustfmt_config.toml
index 5c3f6ba..044f5fb 100644
--- a/rustfmt.toml
+++ b/rustfmt_config.toml
@@ -21,7 +21,6 @@ normalize_doc_attributes = true
 format_strings = true
 format_macro_matchers = true
 format_macro_bodies = true
-skip_macro_invocations = []
 hex_literal_case = "Upper"
 empty_item_single_line = true
 struct_lit_single_line = true
@@ -74,6 +73,5 @@ skip_children = false
 show_parse_errors = true
 error_on_line_overflow = true
 error_on_unformatted = true
-ignore = []
 emit_mode = "Files"
 make_backup = false
diff --git a/src/cli.rs b/src/cli.rs
index eeeed2b..f217b00 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -19,7 +19,7 @@ use clap::Parser;
 #[allow(clippy::module_name_repetitions)]
 /// An extremely simple git issue tracking system.
 /// Inspired by tvix's panettone
-pub struct Cli {
+pub(crate) struct Cli {
     /// The path to the configuration file. The file should be written in JSON.
     pub config_file: PathBuf,
 }
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 6c90fce..07c6c29 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -15,16 +15,13 @@ use std::{
     path::{Path, PathBuf},
 };
 
-use gix::ThreadSafeRepository;
+use git_bug::{entities::issue::Issue, replica::Replica};
 use serde::Deserialize;
 use url::Url;
 
-use crate::{
-    error::{self, Error},
-    git_bug::dag::is_git_bug,
-};
+use crate::error::{self, Error};
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 pub struct BackConfig {
     /// The url to the source code of back. This is needed, because back is licensed under the
     /// AGPL.
@@ -35,17 +32,24 @@ pub struct BackConfig {
     ///     `issues.foss-syndicate.org`
     pub root_url: Url,
 
-    project_list: PathBuf,
+    /// The file that list all the projects.
+    ///
+    /// This is `cgit`'s `project-list` setting.
+    pub project_list: PathBuf,
 
     /// The path that is the common parent of all the repositories.
+    ///
+    /// This is `cgit`'s `scan-path` setting.
     pub scan_path: PathBuf,
 }
 
+#[derive(Debug)]
 pub struct BackRepositories {
     repositories: Vec<BackRepository>,
 }
 
 impl BackRepositories {
+    #[must_use]
     pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
         self.into_iter()
     }
@@ -62,23 +66,31 @@ impl<'a> IntoIterator for &'a BackRepositories {
 
 impl BackRepositories {
     /// Try to get the repository at path `path`.
-    /// If no repository was registered/found at `path`, returns an error.
-    pub fn get(&self, path: &Path) -> Result<&BackRepository, error::Error> {
+    ///
+    /// # Errors
+    /// - If no repository was registered/found at `path`
+    pub fn get(&self, path: &Path) -> Result<&BackRepository, Error> {
         self.repositories
             .iter()
             .find(|p| p.repo_path == path)
-            .ok_or(error::Error::RepoFind {
+            .ok_or(Error::RepoFind {
                 repository_path: path.to_owned(),
             })
     }
 }
 
+#[derive(Debug)]
 pub struct BackRepository {
     repo_path: PathBuf,
 }
 
 impl BackRepository {
-    pub fn open(&self, scan_path: &Path) -> Result<ThreadSafeRepository, error::Error> {
+    /// Open this repository.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the repository could not be opened.
+    pub fn open(&self, scan_path: &Path) -> Result<Replica, Error> {
         let path = {
             let base = scan_path.join(&self.repo_path);
             if base.is_dir() {
@@ -87,30 +99,43 @@ impl BackRepository {
                 PathBuf::from(base.display().to_string() + ".git")
             }
         };
-        let repo = ThreadSafeRepository::open(path).map_err(|err| Error::RepoOpen {
-            repository_path: self.repo_path.to_owned(),
+        let repo = Replica::from_path(path).map_err(|err| Error::RepoOpen {
+            repository_path: self.repo_path.clone(),
             error: Box::new(err),
         })?;
-        if is_git_bug(&repo.to_thread_local())? {
+
+        // We could also check that Identity data is in the Replica,
+        // but Issue existent is paramount.
+        if repo.contains::<Issue>()? {
             Ok(repo)
         } else {
-            Err(error::Error::NotGitBug {
+            Err(Error::NotGitBug {
                 path: self.repo_path.clone(),
             })
         }
     }
 
+    #[must_use]
     pub fn path(&self) -> &Path {
         &self.repo_path
     }
 }
 
 impl BackConfig {
+    /// Returns the repositories of this [`BackConfig`].
+    ///
+    /// # Note
+    /// This will always re-read the `projects.list` file, to pick up repositories that were added
+    /// in the mean time.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the associated IO operations fail.
     pub fn repositories(&self) -> error::Result<BackRepositories> {
         let repositories = fs::read_to_string(&self.project_list)
-            .map_err(|err| error::Error::ProjectListRead {
+            .map_err(|err| Error::ProjectListRead {
                 error: err,
-                file: self.project_list.to_owned(),
+                file: self.project_list.clone(),
             })?
             .lines()
             .try_fold(vec![], |mut acc, path| {
@@ -118,11 +143,16 @@ impl BackConfig {
                     repo_path: PathBuf::from(path),
                 });
 
-                Ok::<_, error::Error>(acc)
+                Ok::<_, Error>(acc)
             })?;
         Ok(BackRepositories { repositories })
     }
 
+    /// Construct this [`BackConfig`] from a config file.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the associated IO operations fail.
     pub fn from_config_file(path: &Path) -> error::Result<Self> {
         let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead {
             file: path.to_owned(),
diff --git a/src/error/mod.rs b/src/error/mod.rs
index 026cc58..f109e11 100644
--- a/src/error/mod.rs
+++ b/src/error/mod.rs
@@ -10,126 +10,87 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
-use std::{fmt::Display, io, net::SocketAddr, path::PathBuf};
+use std::{io, net::SocketAddr, path::PathBuf};
 
-use gix::hash::Prefix;
+use git_bug::{
+    entities::{identity::Identity, issue::Issue},
+    replica::{
+        self,
+        entity::{
+            id::prefix::{self, IdPrefix},
+            snapshot::Snapshot,
+        },
+    },
+};
 use thiserror::Error;
 
-pub type Result<T> = std::result::Result<T, Error>;
+pub(crate) type Result<T> = std::result::Result<T, Error>;
 
 #[derive(Error, Debug)]
 pub enum Error {
+    #[error("while trying to parse the config file ({file}): {error}")]
     ConfigParse {
         file: PathBuf,
         error: serde_json::Error,
     },
 
-    ProjectListRead {
-        file: PathBuf,
-        error: io::Error,
-    },
-    ConfigRead {
-        file: PathBuf,
-        error: io::Error,
-    },
-    NotGitBug {
-        path: PathBuf,
-    },
+    #[error("while trying to read the project.list file ({file}): {error}")]
+    ProjectListRead { file: PathBuf, error: io::Error },
+
+    #[error("while trying to read the config file ({file}): {error}")]
+    ConfigRead { file: PathBuf, error: io::Error },
+
+    #[error("Repository ('{path}') has no initialized git-bug data")]
+    NotGitBug { path: PathBuf },
+
+    #[error("while trying to open the repository ({repository_path}): {error}")]
     RepoOpen {
         repository_path: PathBuf,
-        error: Box<gix::open::Error>,
-    },
-    RepoFind {
-        repository_path: PathBuf,
+        error: Box<replica::open::Error>,
     },
+
+    #[error("while trying to get an entity reference from a replica: {0}")]
+    RepoGetReferences(#[from] replica::get::Error),
+
+    #[error("while trying to read an issues's data from a replica: {0}")]
+    RepoIssueRead(#[from] replica::entity::read::Error<Issue>),
+
+    #[error("while trying to read an identity's data from a replica: {0}")]
+    RepoIdentityRead(#[from] replica::entity::read::Error<Identity>),
+
+    #[error("failed to find the repository at path: '{repository_path}'")]
+    RepoFind { repository_path: PathBuf },
+
+    #[error("while iteration over the refs in a repository: {0}")]
     RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error),
-    RepoRefsPrefixed {
-        error: io::Error,
-    },
 
-    TcpBind {
-        addr: SocketAddr,
-        err: io::Error,
-    },
-    TcpAccept {
-        err: io::Error,
-    },
+    #[error("while prefixing the refs with a path: {error}")]
+    RepoRefsPrefixed { error: io::Error },
+
+    #[error("while trying to open tcp {addr} for listening: {err}.")]
+    TcpBind { addr: SocketAddr, err: io::Error },
 
+    #[error("while trying to accept a tcp connection: {err}.")]
+    TcpAccept { err: io::Error },
+
+    #[error("There is no 'issue' associated with the prefix '{prefix}': {err}")]
     IssuesPrefixMissing {
-        prefix: Prefix,
+        prefix: Box<IdPrefix>,
+        err: prefix::resolve::Error,
     },
-    IssuesPrefixParse(#[from] gix::hash::prefix::from_hex::Error),
-}
 
-impl Display for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Error::ConfigParse { file, error } => {
-                write!(
-                    f,
-                    "while trying to parse the config file ({}): {error}",
-                    file.display()
-                )
-            }
-            Error::ProjectListRead { file, error } => {
-                write!(
-                    f,
-                    "while trying to read the project.list file ({}): {error}",
-                    file.display()
-                )
-            }
-            Error::ConfigRead { file, error } => {
-                write!(
-                    f,
-                    "while trying to read the config file ({}): {error}",
-                    file.display()
-                )
-            }
-            Error::RepoOpen {
-                repository_path,
-                error,
-            } => {
-                write!(
-                    f,
-                    "while trying to open the repository ({}): {error}",
-                    repository_path.display()
-                )
-            }
-            Error::NotGitBug { path } => {
-                write!(
-                    f,
-                    "Repository ('{}') has no initialized git-bug data",
-                    path.display()
-                )
-            }
-            Error::RepoFind { repository_path } => {
-                write!(
-                    f,
-                    "failed to find the repository at path: '{}'",
-                    repository_path.display()
-                )
-            }
-            Error::RepoRefsIter(error) => {
-                write!(f, "while iteration over the refs in a repository: {error}",)
-            }
-            Error::RepoRefsPrefixed { error, .. } => {
-                write!(f, "while prefixing the refs with a path: {error}")
-            }
-            Error::IssuesPrefixMissing { prefix } => {
-                write!(
-                    f,
-                    "There is no 'issue' associated with the prefix: {prefix}"
-                )
-            }
-            Error::IssuesPrefixParse(error) => {
-                write!(f, "The given prefix can not be parsed as prefix: {error}")
-            }
-            Error::TcpBind { addr, err } => {
-                write!(f, "while trying to open tcp {addr} for listening: {err}.")
-            }
-            Error::TcpAccept { err } => {
-                write!(f, "while trying to accept a tcp connection: {err}.")
-            }
-        }
-    }
+    #[error("The given prefix ('{prefix}') can not be parsed as prefix: {error}")]
+    IssuesPrefixParse {
+        prefix: String,
+        error: prefix::decode::Error,
+    },
+
+    #[error("Route '{0}' was unknown.")]
+    NotFound(PathBuf),
+
+    #[error("Query '{query}' was not valid: {err}")]
+    InvalidQuery {
+        err: git_bug::query::parse::parser::Error<Snapshot<Issue>>,
+        query: String,
+    },
 }
diff --git a/src/main.rs b/src/main.rs
index 49ffe5c..28ca543 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,15 +19,12 @@ use crate::config::BackConfig;
 mod cli;
 pub mod config;
 mod error;
-pub mod git_bug;
 mod web;
 
-fn main() -> Result<(), String> {
+fn main() {
     if let Err(err) = server_main() {
         eprintln!("Error {err}");
         process::exit(1);
-    } else {
-        Ok(())
     }
 }
 
@@ -37,9 +34,9 @@ async fn server_main() -> Result<(), error::Error> {
 
     stderrlog::new()
         .module(module_path!())
-        .modules(["hyper", "http"])
+        .modules(["hyper", "http", "git_bug"])
         .quiet(false)
-        .show_module_names(false)
+        .show_module_names(true)
         .color(stderrlog::ColorChoice::Auto)
         .verbosity(2)
         .timestamp(stderrlog::Timestamp::Off)
diff --git a/src/web/generate/mod.rs b/src/web/generate/mod.rs
index 06bab17..e68bddf 100644
--- a/src/web/generate/mod.rs
+++ b/src/web/generate/mod.rs
@@ -1,144 +1,353 @@
-// Back - An extremely simple git bug visualization system. Inspired by TVL's
-// panettone.
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This file is part of Back.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/agpl.txt>.
-
 use std::{fs, path::Path};
 
-use gix::hash::Prefix;
-use log::info;
-use rinja::Template;
-use url::Url;
-
-use crate::{
-    config::BackConfig,
-    error,
-    git_bug::{
-        dag::issues_from_repository,
-        issue::{CollapsedIssue, Status},
+use git_bug::{
+    entities::{
+        identity::Identity,
+        issue::{
+            Issue, data::status::Status, query::MatchKeyValue, snapshot::timeline::IssueTimeline,
+        },
+    },
+    query::{Matcher, Query},
+    replica::{
+        Replica,
+        entity::{
+            Entity,
+            id::prefix::IdPrefix,
+            snapshot::{
+                Snapshot,
+                timeline::{Timeline, history_step::HistoryStep},
+            },
+        },
     },
 };
+use log::info;
+use templates::to_markdown;
+use vy::{IntoHtml, a, div, footer, form, h1, header, input, li, main, nav, ol, p, span};
 
-#[derive(Template)]
-#[template(path = "./issues.html")]
-struct IssuesTemplate {
-    wanted_status: Status,
-    counter_status: Status,
-    issues: Vec<CollapsedIssue>,
+use crate::{config::BackConfig, error, web::generate::templates::make_page};
 
-    /// The path to the repository
-    repo_path: String,
+pub(crate) mod templates;
 
-    /// The URL to `back`'s source code
-    source_code_repository_url: Url,
+fn get_other_status(current: Status) -> Status {
+    match current {
+        Status::Open => Status::Closed,
+        Status::Closed => Status::Open,
+    }
 }
-pub fn issues(
+
+fn set_query_status(
+    mut root_matcher: Matcher<Snapshot<Issue>>,
+    status: Status,
+) -> Matcher<Snapshot<Issue>> {
+    fn change_status(matcher: &mut Matcher<Snapshot<Issue>>, status: Status) {
+        match matcher {
+            Matcher::Or { lhs, rhs } | Matcher::And { lhs, rhs } => {
+                change_status(lhs, status);
+                change_status(rhs, status);
+            }
+            Matcher::Match { key_value } => {
+                if let MatchKeyValue::Status(found_status) = key_value {
+                    *found_status = status;
+                }
+            }
+        }
+    }
+
+    change_status(&mut root_matcher, status);
+    root_matcher
+}
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn issues(
     config: &BackConfig,
-    wanted_status: Status,
-    counter_status: Status,
     repo_path: &Path,
+    replica: &Replica,
+    query: &Query<Snapshot<Issue>>,
 ) -> error::Result<String> {
-    let repository = config
-        .repositories()?
-        .get(repo_path)?
-        .open(&config.scan_path)?;
-
-    let mut issue_list = issues_from_repository(&repository.to_thread_local())?
+    let mut issue_list = replica
+        .get_all_with_query::<Issue>(query)?
+        .collect::<Result<Result<Vec<Issue>, _>, _>>()??
         .into_iter()
-        .map(|issue| issue.collapse())
-        .filter(|issue| issue.status == wanted_status)
-        .collect::<Vec<CollapsedIssue>>();
+        .map(|i| i.snapshot())
+        .map(|i| -> error::Result<_> {
+            let author = i.author();
+            Ok((i, author.resolve(replica)?.snapshot()))
+        })
+        .collect::<error::Result<Vec<_>>>()?;
 
     // Sort by date descending.
     // SAFETY:
     // The time stamp is only used for sorting, so a malicious attacker could only affect the issue
     // sorting.
-    issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() });
-    issue_list.reverse();
-
-    Ok(IssuesTemplate {
-        wanted_status,
-        counter_status,
-        source_code_repository_url: config.source_code_repository_url.clone(),
-        issues: issue_list,
-        repo_path: repo_path.display().to_string(),
-    }
-    .render()
-    .expect("This should always work"))
-}
+    issue_list.sort_by_key(|(issue, _)| unsafe {
+        issue
+            .timeline()
+            .history()
+            .first()
+            .expect("Is some")
+            .at()
+            .to_unsafe()
+    });
+
+    let root_matcher = query.as_matcher().unwrap_or(&Matcher::Match {
+        key_value: MatchKeyValue::Status(Status::Open),
+    });
+
+    let status = {
+        fn find_status(matcher: &Matcher<Snapshot<Issue>>) -> Option<Status> {
+            fn merge(status1: Option<Status>, status2: Option<Status>) -> Option<Status> {
+                match (status1, status2) {
+                    (None, None) => None,
+                    (None, Some(b)) => Some(b),
+                    #[allow(clippy::match_same_arms)]
+                    (Some(a), None) => Some(a),
+                    // TODO(@bpeetz): Should we warn the user somehow?  <2025-05-28>
+                    (Some(a), Some(_)) => Some(a),
+                }
+            }
 
-use crate::git_bug::format::HtmlString;
-#[derive(Template)]
-#[template(path = "./issue.html")]
-struct IssueTemplate {
-    issue: CollapsedIssue,
+            match matcher {
+                Matcher::Or { lhs, rhs } | Matcher::And { lhs, rhs } => {
+                    merge(find_status(lhs), find_status(rhs))
+                }
+                Matcher::Match { key_value } => match key_value {
+                    MatchKeyValue::Status(status) => Some(*status),
+                    _ => None,
+                },
+            }
+        }
 
-    /// The path to the repository
-    repo_path: String,
+        find_status(root_matcher)
+    };
 
-    /// The URL to `back`'s source code
-    source_code_repository_url: Url,
+    // TODO(@bpeetz): Be less verbose. <2025-06-06>
+    let query_string = query.to_string();
+
+    Ok(make_page(
+        (
+            header!(h1!((
+                status.map_or(String::new(), |a| a.to_string()),
+                " Issues"
+            ))),
+            main!(
+                div!(
+                    class = "issue-links",
+                    if let Some(status) = status {
+                        a!(
+                            href = format!(
+                                "/{}/issues/?query={}",
+                                repo_path.display(),
+                                Query::from_matcher(set_query_status(
+                                    root_matcher.clone(),
+                                    get_other_status(status)
+                                ))
+                                .to_string()
+                            ),
+                            "View ",
+                            get_other_status(status).to_string(),
+                            " issues"
+                        )
+                    },
+                    a!(href = "/", "View repos",),
+                    form!(
+                        class = "issue-search",
+                        method = "get",
+                        input!(
+                            name = "query",
+                            value = &query_string,
+                            title = "Issue search query",
+                            "type" = "search",
+                            placeholder = "status:open title:\"Test\"",
+                            size = query_string.chars().count().clamp(20, 40),
+                        ),
+                        input!(
+                            class = "sr-only",
+                            "type" = "submit",
+                            value = "Search Issues"
+                        )
+                    )
+                ),
+                ol!(
+                    class = "issue-list",
+                    issue_list.iter().map(|(issue, identity)| {
+                        li!(a!(
+                            href = format!("/{}/issue/{}", repo_path.display(), issue.id()),
+                            p!(span!(
+                                class = "issue-subject",
+                                to_markdown(issue.title(), true)
+                            )),
+                            span!(
+                                class = "issue-number",
+                                issue.id().as_id().shorten().to_string()
+                            ),
+                            " ",
+                            display_issue_open(identity, issue.timeline()),
+                            if !issue.comments().is_empty() {
+                                span!(
+                                    class = "comment-count",
+                                    " - ",
+                                    issue.comments().len(),
+                                    " ",
+                                    {
+                                        if issue.comments().len() > 1 {
+                                            "comments"
+                                        } else {
+                                            "comment"
+                                        }
+                                    }
+                                )
+                            }
+                        ))
+                    })
+                )
+            ),
+            footer!(nav!(a!(
+                href = config.source_code_repository_url.to_string(),
+                "Source Code"
+            ))),
+        ),
+        "issue list page",
+        None,
+    )
+    .into_string())
 }
-pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result<String> {
-    let repository = config
+
+pub(crate) fn issue(
+    config: &BackConfig,
+    repo_path: &Path,
+    prefix: IdPrefix,
+) -> error::Result<String> {
+    let replica = config
         .repositories()?
         .get(repo_path)?
-        .open(&config.scan_path)?
-        .to_thread_local();
+        .open(&config.scan_path)?;
 
-    let maybe_issue = issues_from_repository(&repository)?
-        .into_iter()
-        .map(|val| val.collapse())
-        .find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
-
-    match maybe_issue {
-        Some(issue) => Ok(IssueTemplate {
-            issue,
-            repo_path: repo_path.display().to_string(),
-            source_code_repository_url: config.source_code_repository_url.clone(),
-        }
-        .render()
-        .expect("This should always work")),
-        None => Err(error::Error::IssuesPrefixMissing { prefix }),
-    }
-}
+    let issue = replica
+        .get(prefix.resolve::<Issue>(replica.repo()).map_err(|err| {
+            error::Error::IssuesPrefixMissing {
+                prefix: Box::new(prefix),
+                err,
+            }
+        })?)?
+        .snapshot();
+
+    let identity = issue.author().resolve(&replica)?.snapshot();
 
-#[derive(Template)]
-#[template(path = "./repos.html")]
-struct ReposTemplate {
-    repos: Vec<RepoValue>,
+    Ok(make_page(
+        div!(
+            class = "content",
+            nav!(
+                a!(
+                    href = format!("/{}/issues/?query=status:open", repo_path.display()),
+                    "Open Issues"
+                ),
+                a!(
+                    href = format!("/{}/issues/?query=status:closed", repo_path.display()),
+                    "Closed Issues"
+                ),
+            ),
+            header!(
+                h1!(to_markdown(issue.title(), true)),
+                div!(
+                    class = "issue-number",
+                    issue.id().as_id().shorten().to_string()
+                )
+            ),
+            main!(
+                div!(
+                    class = "issue-info",
+                    display_issue_open(&identity, issue.timeline())
+                ),
+                to_markdown(issue.body(), false),
+                if !issue.comments().is_empty() {
+                    ol!(
+                        class = "issue-history",
+                        issue
+                            .comments()
+                            .into_iter()
+                            .map(|comment| {
+                                Ok(li!(
+                                    class = "comment",
+                                    id = comment.combined_id.shorten().to_string(),
+                                    to_markdown(&comment.message, false),
+                                    p!(
+                                        class = "comment-info",
+                                        span!(
+                                            class = "user-name",
+                                            comment
+                                                .author
+                                                .resolve(&replica)?
+                                                .snapshot()
+                                                .name()
+                                                .to_owned(),
+                                            " at ",
+                                            comment.timestamp.to_string()
+                                        )
+                                    )
+                                ))
+                            })
+                            .collect::<error::Result<Vec<_>>>()?
+                    )
+                }
+            ),
+            footer!(nav!(a!(
+                href = config.source_code_repository_url.to_string(),
+                "Source code"
+            ),))
+        ),
+        "issue detail page",
+        Some(format!("{} | Back", issue.title()).as_str()),
+    )
+    .into_string())
+}
 
-    /// The URL to `back`'s source code
-    source_code_repository_url: Url,
+fn display_issue_open(
+    identity: &Snapshot<Identity>,
+    issue_timeline: &IssueTimeline,
+) -> impl IntoHtml {
+    span!(
+        class = "created-by-at",
+        "Opened by ",
+        span!(class = "user-name", identity.name()),
+        " ",
+        identity
+            .email()
+            .map(|email| span!(class = "user-email", "<", email, "> ",)),
+        "at ",
+        span!(class = "timestamp", {
+            {
+                issue_timeline
+                    .history()
+                    .last()
+                    .expect("Exists")
+                    .at()
+                    .to_string()
+            }
+        })
+    )
 }
+
 struct RepoValue {
     description: String,
     owner: String,
     path: String,
 }
-pub fn repos(config: &BackConfig) -> error::Result<String> {
+
+pub(crate) fn repos(config: &BackConfig) -> error::Result<String> {
     let repos: Vec<RepoValue> = config
         .repositories()?
         .iter()
         .filter_map(|raw_repo| match raw_repo.open(&config.scan_path) {
-            Ok(repo) => {
-                let repo = repo.to_thread_local();
-                let git_config = repo.config_snapshot();
+            Ok(replica) => {
+                let git_config = replica.repo().config_snapshot();
 
                 let path = raw_repo.path().to_string_lossy().to_string();
 
                 let owner = git_config
                     .string("cgit.owner")
-                    .map(|v| v.to_string())
-                    .unwrap_or("<No owner>".to_owned());
+                    .map_or("<No owner>".to_owned(), |v| v.to_string());
 
-                let description = fs::read_to_string(repo.git_dir().join("description"))
+                let description = fs::read_to_string(replica.repo().git_dir().join("description"))
                     .unwrap_or("<No description>".to_owned());
 
                 Some(RepoValue {
@@ -157,73 +366,124 @@ pub fn repos(config: &BackConfig) -> error::Result<String> {
         })
         .collect();
 
-    Ok(ReposTemplate {
-        repos,
-        source_code_repository_url: config.source_code_repository_url.clone(),
-    }
-    .render()
-    .expect("this should work"))
+    Ok(make_page(
+        div!(
+            class = "content",
+            header!(h1!("Repositiories")),
+            main!(
+                div!(
+                    class = "issue-links",
+                    // TODO(@bpeetz): Add a search. <2025-05-21>
+                ),
+                if !repos.is_empty() {
+                    ol!(
+                        class = "issue-list",
+                        repos.into_iter().map(|repo| {
+                            li!(a!(
+                                href = format!("/{}/issues/?query=status:open", repo.path),
+                                p!(span!(class = "issue-subject", repo.path)),
+                                span!(
+                                    class = "created-by-at",
+                                    span!(class = "timestamp", repo.description),
+                                    " - ",
+                                    span!(class = "user-name", repo.owner)
+                                )
+                            ))
+                        })
+                    )
+                }
+            ),
+            footer!(nav!(a!(
+                href = config.source_code_repository_url.to_string(),
+                "Source Code"
+            ))),
+        ),
+        "repository listing page",
+        Some("Repos | Back"),
+    )
+    .into_string())
 }
 
-pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> {
+pub(crate) fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> {
     use rss::{ChannelBuilder, Item, ItemBuilder};
 
-    let repository = config
+    let replica = config
         .repositories()?
         .get(repo_path)?
-        .open(&config.scan_path)?
-        .to_thread_local();
+        .open(&config.scan_path)?;
 
-    let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
+    let issues: Vec<Snapshot<Issue>> = replica
+        .get_all::<Issue>()?
+        .collect::<Result<Result<Vec<Issue>, _>, _>>()??
         .into_iter()
-        .map(|issue| issue.collapse())
-        .collect();
+        .map(|i| i.snapshot())
+        .collect::<Vec<_>>();
 
     // Collect all Items as rss items
     let mut items: Vec<Item> = issues
         .iter()
-        .map(|issue| {
-            ItemBuilder::default()
-                .title(issue.title.to_string())
-                .author(issue.author.to_string())
-                .description(issue.message.to_string())
-                .pub_date(issue.timestamp.to_string())
+        .map(|issue| -> error::Result<_> {
+            Ok(ItemBuilder::default()
+                .title(issue.title().to_string())
+                .author(
+                    replica
+                        .get(issue.author().id())?
+                        .snapshot()
+                        .name()
+                        .to_owned(),
+                )
+                .description(issue.body().to_string())
+                .pub_date(
+                    issue
+                        .timeline()
+                        .history()
+                        .last()
+                        .expect("Exists")
+                        .at()
+                        .to_string(),
+                )
                 .link(format!(
                     "/{}/{}/issue/{}",
                     repo_path.display(),
                     &config.root_url,
-                    issue.id
+                    issue.id()
                 ))
-                .build()
+                .build())
         })
-        .collect();
+        .collect::<error::Result<Vec<Item>>>()?;
 
     // Append all comments after converting them to rss items
     items.extend(
         issues
             .iter()
-            .filter(|issue| !issue.comments.is_empty())
+            .filter(|issue| !issue.comments().is_empty())
             .flat_map(|issue| {
                 issue
-                    .comments
+                    .comments()
                     .iter()
-                    .map(|comment| {
-                        ItemBuilder::default()
-                            .title(issue.title.to_string())
-                            .author(comment.author.to_string())
+                    .map(|comment| -> error::Result<_> {
+                        Ok(ItemBuilder::default()
+                            .title(issue.title().to_string())
+                            .author(
+                                replica
+                                    .get(comment.author.id())?
+                                    .snapshot()
+                                    .name()
+                                    .to_owned(),
+                            )
                             .description(comment.message.to_string())
                             .pub_date(comment.timestamp.to_string())
                             .link(format!(
                                 "/{}/{}/issue/{}",
                                 repo_path.display(),
                                 &config.root_url,
-                                issue.id
+                                comment.combined_id.shorten()
                             ))
-                            .build()
+                            .build())
                     })
-                    .collect::<Vec<Item>>()
+                    .collect::<Vec<_>>()
             })
-            .collect::<Vec<Item>>(),
+            .collect::<error::Result<Vec<Item>>>()?,
     );
 
     let channel = ChannelBuilder::default()
diff --git a/src/web/generate/templates.rs b/src/web/generate/templates.rs
new file mode 100644
index 0000000..693b862
--- /dev/null
+++ b/src/web/generate/templates.rs
@@ -0,0 +1,44 @@
+use vy::{DOCTYPE, IntoHtml, PreEscaped, body, div, head, html, link, meta, title};
+
+pub(crate) fn make_page(
+    content: impl IntoHtml,
+    description: &str,
+    title: Option<&str>,
+) -> impl IntoHtml {
+    (
+        DOCTYPE,
+        html!(
+            lang = "en",
+            head!(
+                title!(title.unwrap_or("Back")),
+                link!(rel = "icon", href = "/favicon.ico"),
+                link!(rel = "stylesheet", "type" = "text/css", href = "/style.css"),
+                meta!(charset = "UTF-8"),
+                meta!(
+                    name = "viewport",
+                    content = "width=device-width,initial-scale=1"
+                ),
+                meta!(name = "description", content = description),
+            ),
+            body!(div!(class = "content", content))
+        ),
+    )
+}
+
+pub(super) fn to_markdown(input: &str, is_title: bool) -> PreEscaped<String> {
+    let markdown = markdown::to_html(input.trim());
+
+    // If the markdown contains only one line line, assuming that it is a title is okay.
+    if input.lines().count() == 1 && markdown.starts_with("<p>") && is_title {
+        PreEscaped(
+            markdown
+                .strip_prefix("<p>")
+                .expect("We checked")
+                .strip_suffix("</p>")
+                .expect("markdown crate produces no invalid html")
+                .to_owned(),
+        )
+    } else {
+        PreEscaped(markdown)
+    }
+}
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 8e2e9b0..0f9835a 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -9,21 +9,28 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
-use std::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc};
+use std::{cell::OnceCell, convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc};
 
 use bytes::Bytes;
+use git_bug::{
+    entities::issue::{Issue, data::status::Status, query::MatchKeyValue},
+    query::{Matcher, ParseMode, Query},
+    replica::entity::{id::prefix::IdPrefix, snapshot::Snapshot},
+};
 use http_body_util::combinators::BoxBody;
 use hyper::{Method, Request, Response, StatusCode, server::conn::http1, service::service_fn};
 use hyper_util::rt::TokioIo;
-use log::{error, info};
-use responses::{html_response, html_response_status, html_response_status_content_type};
+use log::{error, info, warn};
+use responses::{html_response, html_response_status, html_response_status_content_type, redirect};
 use tokio::net::TcpListener;
+use url::Url;
 
-use crate::{config::BackConfig, error, git_bug::issue::Status};
+use crate::{config::BackConfig, error};
 
 mod generate;
 mod responses;
 
+#[allow(clippy::unused_async, clippy::too_many_lines)]
 async fn match_uri(
     config: Arc<BackConfig>,
     req: Request<hyper::body::Incoming>,
@@ -36,36 +43,69 @@ async fn match_uri(
     }
 
     let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> {
-        match req.uri().path().trim_end_matches("/") {
+        match req.uri().path().trim_matches('/') {
             "" => Ok(html_response(generate::repos(&config)?)),
 
-            "/style.css" => Ok(responses::html_response_status_content_type(
+            "style.css" => Ok(html_response_status_content_type(
                 include_str!("../../assets/style.css"),
                 StatusCode::OK,
                 "text/css",
             )),
+            "search.png" => Ok(html_response_status_content_type(
+                &include_bytes!("../../assets/search.png")[..],
+                StatusCode::OK,
+                "image/png",
+            )),
 
-            path if path.ends_with("/issues/open") => {
-                let repo_path = PathBuf::from(
-                    path.strip_suffix("/issues/open")
-                        .expect("This suffix exists")
-                        .strip_prefix("/")
-                        .expect("This also exists"),
-                );
-
-                let issues = generate::issues(&config, Status::Open, Status::Closed, &repo_path)?;
-                Ok(html_response(issues))
-            }
-            path if path.ends_with("/issues/closed") => {
-                let repo_path = PathBuf::from(
-                    path.strip_suffix("/issues/closed")
-                        .expect("This suffix exists")
-                        .strip_prefix("/")
-                        .expect("This also exists"),
-                );
+            path if path.ends_with("/issues") => {
+                let repo_path =
+                    PathBuf::from(path.strip_suffix("/issues").expect("This suffix exists"));
+
+                let replica = config
+                    .repositories()?
+                    .get(&repo_path)?
+                    .open(&config.scan_path)?;
+
+                let query = {
+                    // HACK(@bpeetz): We cannot use the uri directly, because that would require us
+                    // to parse the query string. As such, we need to turn into into a url::Url,
+                    // which provides this. Unfortunately, this re-parsing is the “officially”
+                    // sanctioned way of doing this. <2025-05-28>
+                    let url: Url = format!("https://{}{}", config.root_url, req.uri())
+                        .parse()
+                        .expect("Was a url before");
+
+                    let mut query = OnceCell::new();
+                    for (name, value) in url.query_pairs() {
+                        if name == "query" && query.get().is_none() {
+                            let val =
+                                Query::from_continuous_str(&replica, &value, ParseMode::Relaxed)
+                                    .map_err(|err| error::Error::InvalidQuery {
+                                        err,
+                                        query: value.to_string(),
+                                    })?;
+                            query.set(val).expect("We checked for initialized");
+                        } else {
+                            warn!("Unknown url query key: {name}");
+                        }
+                    }
+
+                    query.take()
+                };
 
-                let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?;
-                Ok(html_response(issues))
+                if let Some(query) = query {
+                    let issues = generate::issues(&config, &repo_path, &replica, &query)?;
+                    Ok(html_response(issues))
+                } else {
+                    let query = Query::<Snapshot<Issue>>::from_matcher(Matcher::Match {
+                        key_value: MatchKeyValue::Status(Status::Open),
+                    });
+                    Ok(redirect(&format!(
+                        "/{}/issues/?query={}",
+                        repo_path.display(),
+                        query
+                    )))
+                }
             }
             path if path.ends_with("/issues/feed") => {
                 let repo_path = PathBuf::from(
@@ -87,22 +127,30 @@ async fn match_uri(
                 let (repo_path, prefix) = {
                     let split: Vec<&str> = path.split("/issue/").collect();
 
-                    let prefix =
-                        gix::hash::Prefix::from_hex(split[1]).map_err(error::Error::from)?;
+                    let prefix = IdPrefix::from_hex_bytes(split[1].as_bytes()).map_err(|err| {
+                        error::Error::IssuesPrefixParse {
+                            prefix: split[1].to_owned(),
+                            error: err,
+                        }
+                    })?;
 
-                    let repo_path =
-                        PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists"));
+                    let repo_path = PathBuf::from(split[0]);
 
                     (repo_path, prefix)
                 };
                 Ok(html_response(generate::issue(&config, &repo_path, prefix)?))
             }
 
-            other => Ok(responses::html_response_status_content_type(
-                format!("'{}' not found", other),
-                StatusCode::NOT_FOUND,
-                "text/plain",
-            )),
+            other if config.repositories()?.get(&PathBuf::from(other)).is_ok() => {
+                Ok(redirect(&format!("/{other}/issues/?query=status:open")))
+            }
+            other if config.repositories()?.get(&PathBuf::from(other)).is_err() => {
+                Err(error::Error::NotGitBug {
+                    path: PathBuf::from(other),
+                })
+            }
+
+            other => Err(error::Error::NotFound(PathBuf::from(other))),
         }
     };
     match output() {
@@ -111,13 +159,14 @@ async fn match_uri(
     }
 }
 
-pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> {
+pub(crate) async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> {
     let addr: SocketAddr = ([127, 0, 0, 1], 8000).into();
 
     let listener = TcpListener::bind(addr)
         .await
         .map_err(|err| error::Error::TcpBind { addr, err })?;
-    info!("Listening on http://{}", addr);
+    info!("Listening on http://{addr}");
+
     loop {
         let (stream, _) = listener
             .accept()
@@ -131,7 +180,7 @@ pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> {
 
         tokio::task::spawn(async move {
             if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
-                error!("Error serving connection: {:?}", err);
+                error!("Error serving connection: {err:?}");
             }
         });
     }
diff --git a/src/web/responses.rs b/src/web/responses.rs
index bcdcc0a..00af35a 100644
--- a/src/web/responses.rs
+++ b/src/web/responses.rs
@@ -12,10 +12,13 @@
 use std::convert::Infallible;
 
 use bytes::Bytes;
+use git_bug::replica::entity::id::prefix::IdPrefix;
 use http::{Response, StatusCode, Version};
 use http_body_util::{BodyExt, Full, combinators::BoxBody};
+use vy::{IntoHtml, h1, p, pre};
 
-use crate::{error, git_bug::format::HtmlString};
+use super::generate::templates::make_page;
+use crate::error;
 
 pub(super) fn html_response<T: Into<Bytes>>(html_text: T) -> Response<BoxBody<Bytes, Infallible>> {
     html_response_status(html_text, StatusCode::OK)
@@ -28,6 +31,15 @@ pub(super) fn html_response_status<T: Into<Bytes>>(
     html_response_status_content_type(html_text, status, "text/html")
 }
 
+pub(super) fn redirect(target: &str) -> Response<BoxBody<Bytes, Infallible>> {
+    Response::builder()
+        .status(StatusCode::MOVED_PERMANENTLY)
+        .version(Version::HTTP_2)
+        .header("Location", target)
+        .body(full(""))
+        .expect("This is hardcoded and will build")
+}
+
 pub(super) fn html_response_status_content_type<T: Into<Bytes>>(
     html_text: T,
     status: StatusCode,
@@ -36,26 +48,129 @@ pub(super) fn html_response_status_content_type<T: Into<Bytes>>(
     Response::builder()
         .status(status)
         .version(Version::HTTP_2)
-        .header("Content-Type", format!("{}; charset=utf-8", content_type))
+        .header("Content-Type", format!("{content_type}; charset=utf-8"))
         .header("x-content-type-options", "nosniff")
         .header("x-frame-options", "SAMEORIGIN")
         .body(full(html_text))
         .expect("This will always build")
 }
 
-fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> {
+pub(super) fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> {
     Full::new(chunk.into()).boxed()
 }
 
-// FIXME: Not all errors should return `INTERNAL_SERVER_ERROR`. <2025-03-08>
 impl error::Error {
+    #[allow(clippy::too_many_lines)]
     pub fn into_response(self) -> Response<BoxBody<Bytes, Infallible>> {
-        html_response_status(
-            format!(
-                "<h1> Internal server error. </h1> <pre>Error: {}</pre>",
-                HtmlString::from(self.to_string())
+        match self {
+            error::Error::ConfigParse { .. }
+            | error::Error::ProjectListRead { .. }
+            | error::Error::ConfigRead { .. }
+            | error::Error::RepoGetReferences(_)
+            | error::Error::RepoIssueRead(_)
+            | error::Error::RepoIdentityRead(_)
+            | error::Error::RepoFind { .. }
+            | error::Error::RepoRefsIter(_)
+            | error::Error::RepoRefsPrefixed { .. }
+            | error::Error::TcpBind { .. }
+            | error::Error::RepoOpen { .. }
+            | error::Error::TcpAccept { .. } => html_response_status(
+                make_page(
+                    (
+                        h1!("Internal server error"),
+                        pre!(format!("Error {}", self.to_string())),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::INTERNAL_SERVER_ERROR,
+            ),
+            error::Error::NotGitBug { path } => html_response_status(
+                make_page(
+                    (
+                        h1!("Expectation Failed"),
+                        p!(
+                            "Repository '",
+                            path.display().to_string(),
+                            "' has no git bug data to show."
+                        ),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::EXPECTATION_FAILED,
+            ),
+            error::Error::IssuesPrefixMissing { prefix, err } => html_response_status(
+                make_page(
+                    (
+                        h1!("Expectation Failed"),
+                        p!(
+                            "There is no issue associated with the prefix ",
+                            "'",
+                            prefix.to_string(),
+                            "': ",
+                            err.to_string()
+                        ),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::EXPECTATION_FAILED,
+            ),
+            error::Error::IssuesPrefixParse { prefix, error } => html_response_status(
+                make_page(
+                    (
+                        h1!("Expectation Failed"),
+                        p!(
+                            "The prefix '",
+                            prefix,
+                            "' cannot be interperted as a prefix",
+                        ),
+                        p!(error.to_string()),
+                        p!(
+                            "The prefix is composed of  ",
+                            IdPrefix::REQUIRED_LENGTH,
+                            " or more hex chars."
+                        ),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::EXPECTATION_FAILED,
+            ),
+            error::Error::NotFound(path_buf) => html_response_status(
+                make_page(
+                    (
+                        h1!("Expectation Failed"),
+                        p!(
+                            "The path '",
+                            path_buf.display().to_string(),
+                            "' was unkonwn",
+                        ),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::EXPECTATION_FAILED,
+            ),
+            error::Error::InvalidQuery { err, query } => html_response_status(
+                make_page(
+                    (
+                        h1!("Expectation Failed"),
+                        p!("The query '", query, "' was invalid",),
+                        p!(err.to_string()),
+                    ),
+                    "Error page",
+                    Some("Error | Back"),
+                )
+                .into_string(),
+                StatusCode::EXPECTATION_FAILED,
             ),
-            StatusCode::INTERNAL_SERVER_ERROR,
-        )
+        }
     }
 }
diff --git a/templates/issue.html b/templates/issue.html
deleted file mode 100644
index c7e4efc..0000000
--- a/templates/issue.html
+++ /dev/null
@@ -1,74 +0,0 @@
-<!--
-Back - An extremely simple git bug visualization system. Inspired by TVL's
-panettone.
-
-Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-SPDX-License-Identifier: AGPL-3.0-or-later
-
-This file is part of Back.
-
-You should have received a copy of the License along with this program.
-If not, see <https://www.gnu.org/licenses/agpl.txt>.
--->
-
-<!doctype html>
-<html lang="en">
-  <head>
-    <title>{{ HtmlString::from(issue.title.clone()) }} | Back</title>
-    <link
-      href="/style.css"
-      rel="stylesheet"
-      type="text/css"
-    />
-    <meta
-      content="width=device-width,initial-scale=1"
-      name="viewport"
-    />
-  </head>
-  <body>
-    <div class="content">
-      <nav>
-        <a href="/{{repo_path}}/issues/open">Open Issues</a>
-        <a href="/{{repo_path}}/issues/closed">Closed Issues</a>
-      </nav>
-      <header>
-        <h1>{{issue.title|safe}}</h1>
-        <div class="issue-number">{{issue.id}}</div>
-      </header>
-      <main>
-        <div class="issue-info">
-          <span class="created-by-at"
-            >Opened by
-            <span class="user-name">{{issue.author.name|safe}}</span>
-            <span class="user-email">&lt;{{issue.author.email|safe}}&gt;</span>
-            at <span class="timestamp">{{issue.timestamp}}</span></span
-          >
-        </div>
-        {{issue.message|safe}} {% if !issue.comments.is_empty() %}
-        <ol class="issue-history">
-          {% for comment in issue.comments %}
-          <li
-            class="comment"
-            id="{{comment.id}}"
-          >
-            {{comment.message|safe}}
-            <p class="comment-info">
-              <span class="user-name"
-                >{{comment.author.name|safe}} at {{comment.timestamp}}</span
-              >
-            </p>
-          </li>
-          {% endfor %}
-        </ol>
-        {% endif %}
-      </main>
-      <footer>
-        <nav>
-          <a href="/{{repo_path}}/issues/open">Open Issues</a>
-          <a href="{{source_code_repository_url}}">Source code</a>
-          <a href="/{{repo_path}}/issues/closed">Closed Issues</a>
-        </nav>
-      </footer>
-    </div>
-  </body>
-</html>
diff --git a/templates/issues.html b/templates/issues.html
deleted file mode 100644
index 564827c..0000000
--- a/templates/issues.html
+++ /dev/null
@@ -1,77 +0,0 @@
-<!--
-Back - An extremely simple git bug visualization system. Inspired by TVL's
-panettone.
-
-Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-SPDX-License-Identifier: AGPL-3.0-or-later
-
-This file is part of Back.
-
-You should have received a copy of the License along with this program.
-If not, see <https://www.gnu.org/licenses/agpl.txt>.
--->
-
-<!doctype html>
-<html lang="en">
-  <head>
-    <title>Back</title>
-    <link
-      href="/style.css"
-      rel="stylesheet"
-      type="text/css"
-    />
-    <meta
-      content="width=device-width,initial-scale=1"
-      name="viewport"
-    />
-  </head>
-  <body>
-    <div class="content">
-      <header>
-        <h1>{{wanted_status}} Issues</h1>
-      </header>
-      <main>
-        <div class="issue-links">
-          <a href="/{{repo_path}}/issues/{{counter_status|lowercase}}/"
-            >View {{counter_status}} issues</a
-          >
-          <a href="{{source_code_repository_url}}">Source code</a>
-          <!--
-               <form class="issue-search" method="get">
-                   <input name="search" title="Issue search query" type="search">
-                   <input class="sr-only" type="submit" value="Search Issues">
-               </form>
-               -->
-        </div>
-        <ol class="issue-list">
-          {% for issue in issues -%}
-          <li>
-            <a href="/{{repo_path}}/issue/{{issue.id}}">
-              <p>
-                <span class="issue-subject">{{issue.title|safe}}</span>
-              </p>
-              <span class="issue-number">{{issue.id}}</span>
-              <span class="created-by-at"
-                >Opened by {{ " " }}
-                <span class="user-name">{{issue.author.name|safe}}</span>
-                {{ " " }}
-                <span class="user-email"
-                  >&lt;{{issue.author.email|safe}}&gt;</span
-                >
-                {{ "at" }}
-                <span class="timestamp">{{issue.timestamp}}</span>
-              </span>
-              {% if !issue.comments.is_empty() +%}
-              <span class="comment-count">
-                - {{issue.comments.len()}}
-                comment{{issue.comments.len()|pluralize}}</span
-              >
-              {%+ endif %}
-            </a>
-          </li>
-          {%- endfor %}
-        </ol>
-      </main>
-    </div>
-  </body>
-</html>
diff --git a/templates/repos.html b/templates/repos.html
deleted file mode 100644
index 8aa71c4..0000000
--- a/templates/repos.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!--
-Back - An extremely simple git bug visualization system. Inspired by TVL's
-panettone.
-
-Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-SPDX-License-Identifier: AGPL-3.0-or-later
-
-This file is part of Back.
-
-You should have received a copy of the License along with this program.
-If not, see <https://www.gnu.org/licenses/agpl.txt>.
--->
-
-<!doctype html>
-<html lang="en">
-  <head>
-    <title>Back</title>
-    <link
-      href="/style.css"
-      rel="stylesheet"
-      type="text/css"
-    />
-    <meta
-      content="width=device-width,initial-scale=1"
-      name="viewport"
-    />
-  </head>
-  <body>
-    <div class="content">
-      <header>
-        <h1>Repositories</h1>
-      </header>
-      <main>
-        <div class="issue-links">
-          <a href="{{source_code_repository_url}}">Source code</a>
-          <!--
-               <form class="issue-search" method="get">
-                   <input name="search" title="Issue search query" type="search">
-                   <input class="sr-only" type="submit" value="Search Issues">
-               </form>
-               -->
-        </div>
-        <ol class="issue-list">
-          {% for repo in repos -%}
-          <li>
-            <a href="/{{repo.path}}/issues/open">
-              <p>
-                <span class="issue-subject">{{repo.path}}</span>
-              </p>
-              <span class="created-by-at">
-                <span class="timestamp">{{repo.description}}</span>
-                {{ "-" }}
-                <span class="user-name">{{repo.owner}}</span>
-              </span>
-            </a>
-          </li>
-          {%- endfor %}
-        </ol>
-      </main>
-    </div>
-  </body>
-</html>
diff --git a/treefmt.nix b/treefmt.nix
index ebe4157..1768b65 100644
--- a/treefmt.nix
+++ b/treefmt.nix
@@ -11,7 +11,6 @@
 {
   treefmt-nix,
   pkgs,
-  rustfmt,
 }:
 treefmt-nix.lib.evalModule pkgs (
   {pkgs, ...}: {
@@ -22,7 +21,6 @@ treefmt-nix.lib.evalModule pkgs (
       alejandra.enable = true;
       rustfmt = {
         enable = true;
-        package = rustfmt;
         edition = "2024";
       };
       clang-format.enable = true;
@@ -76,8 +74,23 @@ treefmt-nix.lib.evalModule pkgs (
         clang-format = {
           options = ["--style" "GNU"];
         };
-        rustfmt = {
-          options = ["--config-path" "${./rustfmt.toml}"];
+        rustfmt = let
+          config = builtins.fromTOML (builtins.readFile ./rustfmt_config.toml);
+          toValue = value:
+            if builtins.isString value
+            then value
+            else if builtins.isInt value
+            then builtins.toString value
+            else if builtins.isBool value
+            then
+              if value
+              then "true"
+              else "false"
+            else builtins.throw "Unknown value: ${value}";
+          options = pkgs.lib.mapAttrsToList (name: value: "--config=${name}=${toValue value}") config;
+        in {
+          inherit options;
+          excludes = ["vendored/*"];
         };
         shfmt = {
           includes = ["*.bash"];