wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

Commit: 6e060abc1f25c2e1b79fe06bfa8b72cd26952ee1 (tree)
Parent: f3f2b40f0ffae5de2e6d3f661e32b582274bae49 (tree)
Author: wayverd
Date: 2026 M02 19, Thu 17:51:47 -0500
55 files changed; 1661 insertions 1191 deletions
large refactoring

  - moved git module out of utils

  - moved extractors and response out of utils and into http

  - moved error out of utils

  - overhaul of error handling with dedicated spawn method

  - removed need to global config and syntax variables, added them to a new state struct

    - this helps make fuzzing a bit more efficient

diff --git a/Cargo.lock b/Cargo.lock
index 488a245..0380a46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -154,6 +154,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
 name = "atomic"
 version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -221,6 +232,27 @@ dependencies = [
 ]
 
 [[package]]
+name = "axum-extra"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
 name = "axum-response-cache"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -257,6 +289,7 @@ dependencies = [
  "anyhow",
  "askama",
  "axum",
+ "axum-extra",
  "axum-response-cache",
  "clap",
  "comrak",
@@ -266,6 +299,7 @@ dependencies = [
  "jiff",
  "mimalloc",
  "mime",
+ "moka",
  "num-conv",
  "serde",
  "syntect",
@@ -452,6 +486,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
 name = "crc32fast"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -461,6 +504,30 @@ dependencies = [
 ]
 
 [[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
 name = "darling"
 version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -538,6 +605,27 @@ dependencies = [
 ]
 
 [[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
 name = "fancy-regex"
 version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -706,6 +794,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
 name = "git2"
 version = "0.20.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -904,6 +1005,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
 name = "ident_case"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -938,6 +1045,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
 dependencies = [
  "equivalent",
  "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
 ]
 
 [[package]]
@@ -1011,7 +1120,7 @@ version = "0.1.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
 dependencies = [
- "getrandom",
+ "getrandom 0.3.4",
  "libc",
 ]
 
@@ -1032,6 +1141,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
 name = "libc"
 version = "0.2.182"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1084,6 +1199,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
 
 [[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
 name = "log"
 version = "0.4.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1147,6 +1271,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "moka"
+version = "0.12.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
+dependencies = [
+ "async-lock",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "equivalent",
+ "event-listener",
+ "futures-util",
+ "parking_lot",
+ "portable-atomic",
+ "smallvec",
+ "tagptr",
+ "uuid",
+]
+
+[[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1174,6 +1318,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
 
 [[package]]
+name = "onig"
+version = "6.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
 name = "pear"
 version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1323,6 +1518,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
 [[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
 name = "proc-macro2"
 version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1369,6 +1574,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
 
 [[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
 name = "regex"
 version = "1.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1425,6 +1639,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
 name = "serde"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1609,6 +1835,7 @@ dependencies = [
  "flate2",
  "fnv",
  "once_cell",
+ "onig",
  "plist",
  "regex-syntax",
  "serde",
@@ -1620,6 +1847,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
 name = "thiserror"
 version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1974,6 +2207,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
 name = "unicode_categories"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2004,6 +2243,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
+name = "uuid"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
+dependencies = [
+ "getrandom 0.4.1",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "valuable"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2047,6 +2297,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
 name = "wasm-bindgen"
 version = "0.2.108"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2092,6 +2351,40 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
 name = "web-time"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2213,6 +2506,88 @@ name = "wit-bindgen"
 version = "0.51.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
 
 [[package]]
 name = "writeable"
diff --git a/Cargo.toml b/Cargo.toml
index 418b3c3..f33d8f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ ansi-to-html = "=0.2.2"
 anyhow = "=1.0.101"
 askama = "=0.15.4"
 axum = { version = "=0.8.8", features = ["tracing"] }
+axum-extra = "=0.12.5"
 axum-response-cache = "=0.4.0"
 clap = { version = "=4.5.59", features = ["derive", "string"] }
 comrak = { version = "=0.50.0", default-features = false }
@@ -27,9 +28,10 @@ http = "=1.4.0"
 jiff = "=0.2.20"
 mimalloc = "=0.1.48"
 mime = "=0.3.17"
+moka = { version = "=0.12.13", features = ["future"] }
 num-conv = "=0.2.0"
 serde = { version = "=1.0.228", features = ["derive"] }
-syntect = { version = "=5.3.0", default-features = false, features = ["default-fancy"] }
+syntect = { version = "=5.3.0", default-features = false, features = ["default-onig"] }
 thiserror = "=2.0.18"
 tokio = { version = "=1.49.0", features = ["macros", "rt-multi-thread", "signal", "fs"] }
 tower = "=0.5.3"
@@ -43,3 +45,6 @@ two-face = { version = "=0.5.1", default-features = false, features = ["syntect-
 [profile.release]
 codegen-units = 1
 lto = true
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
diff --git a/assets/style.css b/assets/style.css
index b403d04..f2deae8 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -184,6 +184,9 @@ a.feed > img {
   width: 100%;
 }
 
+#log .commit-author-email {
+  text-wrap: nowrap;
+}
 #log .commit-files-modified,
 #log .commit-lines-added,
 #log .commit-lines-removed {
diff --git a/src/config.rs b/src/config.rs
index d626c03..0f28d85 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -35,7 +35,7 @@ pub struct Config {
 }
 
 impl Config {
-    pub fn load() -> crate::utils::error::Result<Self> {
+    pub fn load() -> crate::error::Result<Self> {
         let config: Self = Figment::new()
             .merge(Serialized::defaults(Self::parse()))
             .merge(Toml::file("bile.toml"))
@@ -45,7 +45,7 @@ impl Config {
         Ok(config)
     }
 
-    pub fn finalize(self) -> crate::utils::error::Result<Self> {
+    pub fn finalize(self) -> crate::error::Result<Self> {
         Ok(Self {
             port: self.port,
             project_root: self.project_root.canonicalize()?,
diff --git a/src/utils/error.rs b/src/error.rs
similarity index 91%
rename from src/utils/error.rs
rename to src/error.rs
index a21e323..3d7190b 100644
--- a/src/utils/error.rs
+++ b/src/error.rs
@@ -1,7 +1,5 @@
 use std::{convert, fmt};
 
-use axum::response;
-use http::StatusCode;
 use tracing_error::SpanTrace;
 
 pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -67,16 +65,6 @@ impl tracing_error::ExtractSpanTrace for Error {
     }
 }
 
-impl response::IntoResponse for Error {
-    fn into_response(self) -> response::Response {
-        super::Error::Failure {
-            status: StatusCode::INTERNAL_SERVER_ERROR,
-            err: self,
-        }
-        .into_response()
-    }
-}
-
 pub trait Context<T, E> {
     fn context<C>(self, context: C) -> Result<T, Error>
     where
diff --git a/src/utils/git/branch.rs b/src/git/branch.rs
similarity index 84%
rename from src/utils/git/branch.rs
rename to src/git/branch.rs
index e591b5d..f0c6751 100644
--- a/src/utils/git/branch.rs
+++ b/src/git/branch.rs
@@ -1,10 +1,10 @@
 use git2::{Branch, BranchType, Reference};
 
-use crate::utils::git::Repository;
+use crate::git::Repository;
 
 impl Repository {
     #[tracing::instrument(skip_all)]
-    pub fn branches(&self) -> crate::utils::error::Result<Vec<Reference<'_>>> {
+    pub(crate) fn branches(&self) -> crate::error::Result<Vec<Reference<'_>>> {
         let references = self.inner.references()?;
 
         let branches = references
@@ -16,10 +16,10 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn branches_of_type(
+    pub(crate) fn branches_of_type(
         &self,
         typ: BranchType,
-    ) -> crate::utils::error::Result<Vec<Branch<'_>>> {
+    ) -> crate::error::Result<Vec<Branch<'_>>> {
         let branches = self
             .inner
             .branches(Some(typ))?
diff --git a/src/utils/git/commit.rs b/src/git/commit.rs
similarity index 83%
rename from src/utils/git/commit.rs
rename to src/git/commit.rs
index 6d79c61..4f2b456 100644
--- a/src/utils/git/commit.rs
+++ b/src/git/commit.rs
@@ -2,14 +2,11 @@ use std::ffi::CString;
 
 use git2::{Commit, Diff, DiffOptions, DiffStats, Sort, Tree};
 
-use crate::utils::{
-    error::{Context as _, Result},
-    git::Repository,
-};
+use crate::{error::Context as _, error::Result, git::Repository, http::extractor::ObjectName};
 
 impl Repository {
     #[tracing::instrument(skip_all)]
-    pub fn commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
+    pub(crate) fn commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
         let obj = match self.inner.revparse_single(spec) {
             Ok(obj) => obj,
             Err(err) => {
@@ -30,7 +27,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commit_diff(&self, commit: &Commit<'_>) -> Result<Diff<'_>> {
+    pub(crate) fn commit_diff(&self, commit: &Commit<'_>) -> Result<Diff<'_>> {
         let mut options = DiffOptions::new();
 
         // This is identical to getting "commit^" and on merges this will be the
@@ -47,7 +44,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commit_stats(&self, commit: &Commit<'_>) -> Result<DiffStats> {
+    pub(crate) fn commit_stats(&self, commit: &Commit<'_>) -> Result<DiffStats> {
         let diff = self.commit_diff(commit)?;
 
         let stats = diff.stats()?;
@@ -56,7 +53,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commit_tree(&self, spec: &str) -> Result<Option<(Commit<'_>, Tree<'_>)>> {
+    pub(crate) fn commit_tree(&self, spec: &str) -> Result<Option<(Commit<'_>, Tree<'_>)>> {
         let Some(commit) = self.commit(spec)? else {
             return Ok(None);
         };
@@ -67,7 +64,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commits(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
+    pub(crate) fn commits(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
         if self.is_shallow() {
             return self.commits_shallow();
         }
@@ -76,7 +73,11 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commits_full(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
+    pub(crate) fn commits_full(
+        &self,
+        spec: &str,
+        amount: usize,
+    ) -> Result<Option<Vec<Commit<'_>>>> {
         let mut revwalk = self.inner.revwalk()?;
 
         let Some(commit) = self.commit(spec)? else {
@@ -96,13 +97,12 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commits_for_obj(
+    pub(crate) fn commits_for_obj(
         &self,
         spec: &str,
         amount: usize,
-        obj: Option<&str>,
+        obj: Option<&ObjectName>,
     ) -> Result<Option<Vec<Commit<'_>>>> {
-        tracing::info!("is_shallow");
         if self.is_shallow() {
             return self
                 .commits_shallow()
@@ -112,7 +112,6 @@ impl Repository {
         let mut revwalk = self.inner.revwalk().context("failed to create revwalk")?;
 
         let Some(commit) = self.commit(spec).context("failed to get commit")? else {
-            tracing::info!("commit None");
             return Ok(None);
         };
 
@@ -127,7 +126,7 @@ impl Repository {
         let commits =
             revwalk.filter_map(|oid| oid.ok().and_then(|oid| self.inner.find_commit(oid).ok()));
 
-        let Some(Ok(path)) = obj.map(CString::new) else {
+        let Some(Ok(path)) = obj.map(|n| n.0.as_str()).map(CString::new) else {
             return Ok(Some(commits.take(amount).collect()));
         };
 
@@ -186,7 +185,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn commits_shallow(&self) -> Result<Option<Vec<Commit<'_>>>> {
+    pub(crate) fn commits_shallow(&self) -> Result<Option<Vec<Commit<'_>>>> {
         tracing::warn!("repository {:?} is only a shallow clone", self.inner.path());
         let commits = self
             .inner
@@ -199,7 +198,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn file_last_commit<P: git2::IntoCString>(
+    pub(crate) fn file_last_commit<P: git2::IntoCString>(
         &self,
         spec: &str,
         path: P,
diff --git a/src/utils/git/core.rs b/src/git/core.rs
similarity index 86%
rename from src/utils/git/core.rs
rename to src/git/core.rs
index 1ef7db0..43e1520 100644
--- a/src/utils/git/core.rs
+++ b/src/git/core.rs
@@ -1,15 +1,13 @@
 use std::path::Path;
 
 use git2::{Reference, Time};
+use syntect::parsing::SyntaxSet;
 
-use crate::utils::{
-    error::{Context as _, Result},
-    git::Repository,
-};
+use crate::{error::Context as _, error::Result, git::Repository, http::extractor::Ref};
 
 impl Repository {
     #[must_use]
-    pub fn description(&self) -> String {
+    pub(crate) fn description(&self) -> String {
         let content = std::fs::read_to_string(self.path().join("description")).unwrap_or_default();
 
         let first = content.lines().next().unwrap_or_default();
@@ -18,24 +16,24 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn head(&self) -> Result<Reference<'_>> {
+    pub(crate) fn head(&self) -> Result<Reference<'_>> {
         let head = self.inner.head()?;
 
         Ok(head)
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn is_empty(&self) -> Result<bool> {
+    pub(crate) fn is_empty(&self) -> Result<bool> {
         Ok(self.inner.is_empty()?)
     }
 
     #[must_use]
-    pub fn is_shallow(&self) -> bool {
+    pub(crate) fn is_shallow(&self) -> bool {
         self.inner.is_shallow()
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn last_modified(&self) -> Result<Time> {
+    pub(crate) fn last_modified(&self) -> Result<Time> {
         let head = self.head()?;
         let commit = head.peel_to_commit()?;
         let time = commit.committer().when();
@@ -43,7 +41,7 @@ impl Repository {
         Ok(time)
     }
 
-    pub fn name(&self) -> Option<&str> {
+    pub(crate) fn name(&self) -> Option<&str> {
         self.inner
             .workdir()
             // use the path for bare repositories
@@ -53,7 +51,7 @@ impl Repository {
     }
 
     #[must_use]
-    pub fn owner(&self) -> String {
+    pub(crate) fn owner(&self) -> String {
         self.inner
             .config()
             .and_then(|config| config.get_string("gitweb.owner"))
@@ -61,12 +59,12 @@ impl Repository {
     }
 
     #[must_use]
-    pub fn path(&self) -> &Path {
+    pub(crate) fn path(&self) -> &Path {
         self.inner.path()
     }
 
     #[must_use]
-    pub fn readme(&self) -> String {
+    pub(crate) fn readme(&self, syntaxes: &SyntaxSet) -> String {
         use askama::filters::Escaper as _;
 
         enum ReadmeFormat {
@@ -118,18 +116,18 @@ impl Repository {
                     // already is HTML
                     ReadmeFormat::Html => text.to_string(),
                     // render Markdown to HTML
-                    ReadmeFormat::Markdown => crate::utils::markdown::render(text),
+                    ReadmeFormat::Markdown => crate::utils::markdown::render(syntaxes, text),
                 }
             })
             .unwrap_or_default()
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn ref_or_head_shorthand(&self, r#ref: Option<&str>) -> Result<String> {
+    pub(crate) fn ref_or_head_shorthand(&self, r#ref: Option<&Ref>) -> Result<String> {
         let head = self.head()?;
 
         let spec = r#ref
-            .map(str::to_string)
+            .map(|r| r.0.clone())
             .or_else(move || head.shorthand().map(str::to_string))
             .context("failed to get repo ref spec")?;
 
diff --git a/src/utils/git/mod.rs b/src/git/mod.rs
similarity index 88%
rename from src/utils/git/mod.rs
rename to src/git/mod.rs
index 49498a6..fa5b97b 100644
--- a/src/utils/git/mod.rs
+++ b/src/git/mod.rs
@@ -4,13 +4,13 @@ mod core;
 mod tag;
 mod tree;
 
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 use git2::{Object, Signature};
 
-use crate::utils::error::{Context as _, Result};
+use crate::{config::Config, error::Context as _, error::Result, http::extractor::RepoName};
 
-pub struct TagEntry {
+pub(crate) struct TagEntry {
     pub link: String,
     pub tag: String,
     pub message: String,
@@ -57,20 +57,17 @@ impl TagEntry {
     }
 }
 
-pub struct Repository {
+pub(crate) struct Repository {
     inner: git2::Repository,
 }
 
 impl Repository {
     #[tracing::instrument(skip_all)]
-    pub fn open<P>(path: P) -> Result<Option<Self>>
-    where
-        P: AsRef<Path>,
-    {
-        let config = crate::config();
-
-        let path = path.as_ref();
+    pub(crate) fn open(config: &Config, name: &RepoName) -> Result<Option<Self>> {
+        Self::open_path(config, &PathBuf::from(&name.0))
+    }
 
+    pub(crate) fn open_path(config: &Config, path: &Path) -> Result<Option<Self>> {
         let path = config.project_root.join(path);
 
         if !path.exists() {
@@ -109,7 +106,7 @@ impl Repository {
     }
 
     #[must_use]
-    pub const fn as_inner(&self) -> &git2::Repository {
+    pub(crate) const fn as_inner(&self) -> &git2::Repository {
         &self.inner
     }
 }
diff --git a/src/utils/git/tag.rs b/src/git/tag.rs
similarity index 85%
rename from src/utils/git/tag.rs
rename to src/git/tag.rs
index 3152ac6..67b8abd 100644
--- a/src/utils/git/tag.rs
+++ b/src/git/tag.rs
@@ -1,13 +1,14 @@
-use git2::{ObjectType, Tag};
+use git2::ObjectType;
 
-use crate::utils::{
+use crate::{
     error::Result,
     git::{Repository, TagEntry},
+    http::extractor::Tag,
 };
 
 impl Repository {
     #[tracing::instrument(skip_all)]
-    pub fn tag_entries(&self) -> Result<Vec<TagEntry>> {
+    pub(crate) fn tag_entries(&self) -> Result<Vec<TagEntry>> {
         let mut tags = Vec::new();
 
         self.inner.tag_foreach(|oid, name_bytes| {
@@ -40,8 +41,8 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn tag(&self, spec: &str) -> Result<Tag<'_>> {
-        let tag = self.inner.revparse_single(spec)?.peel_to_tag()?;
+    pub(crate) fn tag(&self, spec: &Tag) -> Result<git2::Tag<'_>> {
+        let tag = self.inner.revparse_single(&spec.0)?.peel_to_tag()?;
 
         Ok(tag)
     }
diff --git a/src/utils/git/tree.rs b/src/git/tree.rs
similarity index 78%
rename from src/utils/git/tree.rs
rename to src/git/tree.rs
index 7032199..dcf7584 100644
--- a/src/utils/git/tree.rs
+++ b/src/git/tree.rs
@@ -1,15 +1,13 @@
 use std::path::Path;
 
+use anyhow::Context as _;
 use git2::{Blob, Object, Tree};
 
-use crate::utils::{
-    error::{Context as _, Result},
-    git::Repository,
-};
+use crate::{error::Result, git::Repository};
 
 impl Repository {
     #[tracing::instrument(skip_all)]
-    pub fn tree_blob(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Blob<'_>>> {
+    pub(crate) fn tree_blob(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Blob<'_>>> {
         let Some(obj) = self.tree_object(tree, path)? else {
             return Ok(None);
         };
@@ -20,7 +18,7 @@ impl Repository {
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn tree_object(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Object<'_>>> {
+    pub(crate) fn tree_object(&self, tree: &Tree<'_>, path: &Path) -> Result<Option<Object<'_>>> {
         let entry = match tree
             .get_path(path)
             .context("failed to get object from tree")
diff --git a/src/handlers/git.rs b/src/handlers/git.rs
new file mode 100644
index 0000000..c593a78
--- /dev/null
+++ b/src/handlers/git.rs
@@ -0,0 +1,90 @@
+use axum::{
+    extract::{Path, State},
+    http::{HeaderValue, StatusCode, Uri, header},
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::RepoName,
+        response::{ErrorPage, Result},
+    },
+};
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(
+    state: State<BileState>,
+    uri: Uri,
+    Path(repo_name): Path<RepoName>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &uri, &repo_name))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+    state: State<BileState>,
+    uri: Uri,
+    Path((repo_name, _)): Path<(RepoName, String)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &uri, &repo_name))
+        .await
+}
+
+fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let path = uri
+        .path()
+        .strip_prefix(&format!("/{repo_name}/"))
+        .unwrap_or_default();
+
+    let path = repo.path().join(path);
+
+    // cant canonicalize if it doesnt exist
+    if !path.exists() {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    }
+
+    let path = path.canonicalize().context("canonicalize new path")?;
+
+    // that path got us outside of the repository structure somehow
+    if !path.starts_with(repo.path()) {
+        tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::FORBIDDEN)
+            .into_response());
+    }
+
+    // Either the requested resource does not exist or it is not
+    // a file, i.e. a directory.
+    if !path.is_file() {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    }
+
+    let body = std::fs::read(&path).context("reading file")?;
+
+    Ok((
+        StatusCode::OK,
+        [(
+            header::CONTENT_TYPE,
+            HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
+        )],
+        body,
+    )
+        .into_response())
+}
diff --git a/src/handlers/git.rs b/src/handlers/git.rs
deleted file mode 100644
index 21df48a..0000000
--- a/src/handlers/git.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use axum::{
-    extract::Path,
-    http::{HeaderValue, StatusCode, Uri, header},
-    response::{IntoResponse as _, Response},
-};
-
-use crate::utils::{
-    Error, Result, error::Context as _, extractor::repo_name_checks, git::Repository,
-    spawn_blocking,
-};
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(uri: Uri, Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&uri, &repo_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(uri: Uri, Path((repo_name, _)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&uri, &repo_name).into_response()).await
-}
-
-fn inner(uri: &Uri, repo_name: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    let path = uri
-        .path()
-        .strip_prefix(&format!("/{repo_name}/"))
-        .unwrap_or_default();
-
-    let path = repo.path().join(path);
-
-    // cant canonicalize if it doesnt exist
-    if !path.exists() {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "This page does not exist.",
-        ));
-    }
-
-    let path = path.canonicalize().context("canonicalize new path")?;
-
-    // that path got us outside of the repository structure somehow
-    if !path.starts_with(repo.path()) {
-        tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
-        return Err(Error::new(
-            StatusCode::FORBIDDEN,
-            "You do not have access to this file.",
-        ));
-    }
-
-    // Either the requested resource does not exist or it is not
-    // a file, i.e. a directory.
-    if !path.is_file() {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "This page does not exist.",
-        ));
-    }
-
-    let body = std::fs::read(&path).context("reading file")?;
-
-    Ok((
-        StatusCode::OK,
-        [(
-            header::CONTENT_TYPE,
-            HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()),
-        )],
-        body,
-    )
-        .into_response())
-}
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
new file mode 100644
index 0000000..af5a9e1
--- /dev/null
+++ b/src/handlers/index.rs
@@ -0,0 +1,63 @@
+use std::fs;
+
+use axum::{
+    extract::State,
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::response::{Html, Result},
+    utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "index.html")]
+struct IndexTemplate<'a> {
+    config: &'a Config,
+    repos: Vec<Repository>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(state: State<BileState>) -> Response {
+    state.spawn(move |state| inner(&state)).await
+}
+
+fn inner(state: &BileState) -> Result<Response> {
+    let Ok(read) = fs::read_dir(&state.config.project_root) else {
+        return Ok(Html(IndexTemplate {
+            config: &state.config,
+            repos: Vec::new(),
+        })
+        .into_response());
+    };
+
+    let mut repos = Vec::new();
+
+    for entry in read {
+        let entry = entry.context("failed to open directory entry")?;
+
+        let Some(repo) = Repository::open_path(&state.config, &entry.path())
+            .context("failed to open repository")?
+        else {
+            continue;
+        };
+
+        // check for the export file in the git directory
+        // (the .git subfolder for non-bare repos)
+        if !repo.path().join(&state.config.export_ok).exists() {
+            continue;
+        }
+
+        repos.push(repo);
+    }
+
+    Ok(Html(IndexTemplate {
+        config: &state.config,
+        repos,
+    })
+    .into_response())
+}
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
deleted file mode 100644
index 0f6004d..0000000
--- a/src/handlers/index.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use std::fs;
-
-use axum::response::{IntoResponse as _, Response};
-
-use crate::utils::{
-    Result, error::Context as _, filters, git::Repository, response::Html, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "index.html")]
-struct IndexTemplate {
-    repos: Vec<Repository>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get() -> Response {
-    spawn_blocking(move || inner().into_response()).await
-}
-
-fn inner() -> Result {
-    let Ok(read) = fs::read_dir(&crate::config().project_root) else {
-        return Ok(Html(IndexTemplate { repos: Vec::new() }).into_response());
-    };
-
-    let config = crate::config();
-
-    let mut repos = Vec::new();
-
-    for entry in read {
-        let entry = entry.context("failed to open directory entry")?;
-
-        let Some(repo) = Repository::open(entry.path()).context("failed to open repository")?
-        else {
-            continue;
-        };
-
-        // check for the export file in the git directory
-        // (the .git subfolder for non-bare repos)
-        if !repo.path().join(&config.export_ok).exists() {
-            continue;
-        }
-
-        repos.push(repo);
-    }
-
-    Ok(Html(IndexTemplate { repos }).into_response())
-}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
new file mode 100644
index 0000000..8f6333b
--- /dev/null
+++ b/src/handlers/mod.rs
@@ -0,0 +1,11 @@
+pub(crate) mod git;
+pub(crate) mod index;
+pub(crate) mod repo_commit;
+pub(crate) mod repo_file;
+pub(crate) mod repo_file_raw;
+pub(crate) mod repo_home;
+pub(crate) mod repo_log;
+pub(crate) mod repo_log_feed;
+pub(crate) mod repo_refs;
+pub(crate) mod repo_refs_feed;
+pub(crate) mod repo_tag;
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
deleted file mode 100644
index 59cd646..0000000
--- a/src/handlers/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-pub mod git;
-pub mod index;
-pub mod repo_commit;
-pub mod repo_file;
-pub mod repo_file_raw;
-pub mod repo_home;
-pub mod repo_log;
-pub mod repo_log_feed;
-pub mod repo_refs;
-pub mod repo_refs_feed;
-pub mod repo_tag;
diff --git a/src/handlers/repo_commit.rs b/src/handlers/repo_commit.rs
index 5ed4344..823424d 100644
--- a/src/handlers/repo_commit.rs
+++ b/src/handlers/repo_commit.rs
@@ -1,37 +1,37 @@
 use std::fmt::Write as _;
 
 use axum::{
-    extract::Path,
+    extract::{Path, State},
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
-use git2::{
-    BranchType, Commit, DescribeFormatOptions, DescribeOptions, Diff, DiffFindOptions, DiffFormat,
-};
+use git2::{BranchType, DescribeFormatOptions, DescribeOptions, DiffFindOptions, DiffFormat};
 use syntect::{
     html::{ClassStyle, ClassedHTMLGenerator},
+    parsing::SyntaxSet,
     util::LinesWithEndings,
 };
 
 use crate::{
-    SYNTAXES,
-    utils::{
-        Error, Result,
-        error::Context as _,
-        extractor::{commit_checks, repo_name_checks},
-        filters,
-        git::Repository,
-        response::Html,
-        spawn_blocking,
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{Commit, RepoName},
+        response::{ErrorPage, Html, Result},
     },
+    utils::filters,
 };
 
 #[derive(askama::Template)]
 #[template(path = "commit.html")]
 struct RepoCommitTemplate<'a> {
+    config: &'a Config,
+    syntaxes: &'a SyntaxSet,
     repo: &'a Repository,
-    commit: Commit<'a>,
-    diff: &'a Diff<'a>,
+    commit: git2::Commit<'a>,
+    diff: &'a git2::Diff<'a>,
 }
 
 impl RepoCommitTemplate<'_> {
@@ -61,12 +61,13 @@ impl RepoCommitTemplate<'_> {
         });
 
         // highlight the diff
-        let syntax = SYNTAXES
+        let syntax = self
+            .syntaxes
             .find_syntax_by_name("Diff")
             .expect("diff syntax missing");
 
         let mut highlighter =
-            ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
+            ClassedHTMLGenerator::new_with_class_style(syntax, self.syntaxes, ClassStyle::Spaced);
 
         LinesWithEndings::from(&buf).for_each(|line| {
             if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
@@ -127,24 +128,28 @@ impl RepoCommitTemplate<'_> {
 }
 
 #[tracing::instrument(skip_all)]
-pub async fn get(Path((repo_name, commit)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&repo_name, &commit).into_response()).await
+pub(crate) async fn get(
+    state: State<BileState>,
+    Path((repo_name, commit)): Path<(RepoName, Commit)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, &commit))
+        .await
 }
 
 #[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, commit: &str) -> Result {
-    repo_name_checks(repo_name)?;
-    commit_checks(commit)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName, commit: &Commit) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
     };
 
-    let Some(commit) = repo.commit(commit).context("failed to get commit")? else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "commit does not exist in repo",
-        ));
+    let Some(commit) = repo.commit(&commit.0).context("failed to get commit")? else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
     };
 
     let mut diff = repo
@@ -159,6 +164,8 @@ fn inner(repo_name: &str, commit: &str) -> Result {
     }
 
     Ok(Html(RepoCommitTemplate {
+        config: &state.config,
+        syntaxes: &state.syntax,
         repo: &repo,
         commit,
         diff: &diff,
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
new file mode 100644
index 0000000..e83fe92
--- /dev/null
+++ b/src/handlers/repo_file.rs
@@ -0,0 +1,239 @@
+use std::{fmt::Write as _, path};
+
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{IntoResponse as _, Response},
+};
+use syntect::{
+    html::{ClassStyle, ClassedHTMLGenerator},
+    parsing::SyntaxSet,
+    util::LinesWithEndings,
+};
+
+use crate::{
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{ObjectName, Ref, RepoName},
+        response::{ErrorPage, Html, Redirect, Result},
+    },
+    utils::{blob_mime, filters},
+};
+
+#[derive(askama::Template)]
+#[template(path = "tree.html")]
+struct RepoTreeTemplate<'a> {
+    config: &'a Config,
+    repo: &'a Repository,
+    tree: git2::Tree<'a>,
+    path: &'a path::Path,
+    spec: &'a str,
+    last_commit: git2::Commit<'a>,
+}
+
+#[derive(askama::Template)]
+#[template(path = "file.html")]
+struct RepoFileTemplate<'a> {
+    config: &'a Config,
+    repo: &'a Repository,
+    path: &'a path::Path,
+    file_text: &'a str,
+    spec: &'a str,
+    last_commit: git2::Commit<'a>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, None, None))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+    state: State<BileState>,
+    Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), None))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_3(
+    state: State<BileState>,
+    Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), Some(&object_name)))
+        .await
+}
+
+fn inner(
+    state: &BileState,
+    repo_name: &RepoName,
+    r#ref: Option<&Ref>,
+    object_name: Option<&ObjectName>,
+) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    if repo.is_empty()? {
+        return Ok(Redirect::permanent(&format!("/{repo_name}"))
+            .unwrap_or(Redirect::PERMANENT_ROOT)
+            .into_response());
+    }
+
+    let spec = repo.ref_or_head_shorthand(r#ref)?;
+    let Some((commit, tree)) = repo
+        .commit_tree(&spec)
+        .context("failed to get commit tree")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let (path, tree_obj) = if let Some(path) = object_name {
+        let path = path::Path::new(&path.0);
+
+        (path, repo.tree_object(&tree, path)?)
+    } else {
+        (path::Path::new(""), Some(tree.into_object()))
+    };
+
+    let Some(tree_obj) = tree_obj else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let tree_obj = match tree_obj.into_tree() {
+        // this is a subtree
+        Ok(sub_tree) => {
+            return Ok(Html(RepoTreeTemplate {
+                config: &state.config,
+                repo: &repo,
+                tree: sub_tree,
+                path,
+                spec: &spec,
+                last_commit,
+            })
+            .into_response());
+        }
+        // this is not a subtree, so it should be a blob i.e. file
+        Err(tree_obj) => tree_obj,
+    };
+
+    let Some(blob) = tree_obj.as_blob() else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let output = render(&state.syntax, repo_name, path, &spec, &commit, blob)?;
+
+    Ok(Html(RepoFileTemplate {
+        config: &state.config,
+        repo: &repo,
+        path,
+        file_text: &output,
+        spec: &spec,
+        last_commit,
+    })
+    .into_response())
+}
+
+// TODO: make sure I am escaping html properly here
+// TODO: allow disabling of syntax highlighting
+// TODO: -- dont pull in memory, use iterators if possible
+fn render(
+    syntaxes: &SyntaxSet,
+    repo_name: &RepoName,
+    path: &path::Path,
+    spec: &str,
+    commit: &git2::Commit<'_>,
+    blob: &git2::Blob<'_>,
+) -> crate::error::Result<String> {
+    let extension = path
+        .extension()
+        .and_then(std::ffi::OsStr::to_str)
+        .unwrap_or_default();
+
+    if blob.is_binary() {
+        // this is not a text file, but try to serve the file if the MIME type
+        // can give a hint at how
+        let mime = blob_mime(blob, extension);
+
+        let output = match mime.type_() {
+            mime::TEXT => unreachable!("git detected this file as binary"),
+            mime::IMAGE => format!(
+                "<img src=\"/{}/tree/{}/raw/{}\" />",
+                repo_name,
+                spec,
+                path.display()
+            ),
+            tag @ (mime::AUDIO | mime::VIDEO) => format!(
+                "<{} src=\"/{}/tree/{}/raw/{}\" controls>Your browser does not have support for playing this {0} file.</{0}>",
+                tag,
+                repo_name,
+                spec,
+                path.display()
+            ),
+            _ => "Cannot display binary file.".to_string(),
+        };
+
+        return Ok(output);
+    }
+
+    let syntax = syntaxes
+        .find_syntax_by_extension(extension)
+        .unwrap_or_else(|| syntaxes.find_syntax_plain_text());
+
+    // get file contents from git object
+    let file_string = str::from_utf8(blob.content())?;
+
+    // create a highlighter that uses CSS classes so we can use prefers-color-scheme
+    let mut highlighter =
+        ClassedHTMLGenerator::new_with_class_style(syntax, syntaxes, ClassStyle::Spaced);
+    LinesWithEndings::from(file_string).for_each(|line| {
+        if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
+            tracing::error!(err=?err, "failed to highlight code");
+        }
+    });
+
+    // use oid so it is a permalink
+    let prefix = format!(
+        "/{}/tree/{}/item/{}",
+        repo_name,
+        commit.id(),
+        path.display()
+    );
+
+    let mut output = String::from("<pre>\n");
+    for (n, line) in highlighter.finalize().lines().enumerate() {
+        let _ = writeln!(
+            &mut output,
+            "<a href='{1}#L{0}' id='L{0}' class='line'>{0}</a>{2}",
+            n + 1,
+            prefix,
+            line,
+        );
+    }
+    output.push_str("</pre>\n");
+
+    Ok(output)
+}
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
deleted file mode 100644
index 65db580..0000000
--- a/src/handlers/repo_file.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use std::{fmt::Write as _, path};
-
-use axum::{
-    extract::Path,
-    http::StatusCode,
-    response::{IntoResponse as _, Response},
-};
-use git2::{Blob, Commit, Tree};
-use syntect::{
-    html::{ClassStyle, ClassedHTMLGenerator},
-    util::LinesWithEndings,
-};
-
-use crate::{
-    SYNTAXES,
-    utils::{
-        Error, Result, blob_mime,
-        error::Context as _,
-        extractor::repo_name_checks,
-        filters,
-        git::Repository,
-        response::{Html, Redirect},
-        spawn_blocking,
-    },
-};
-
-#[derive(askama::Template)]
-#[template(path = "tree.html")]
-struct RepoTreeTemplate<'a> {
-    repo: &'a Repository,
-    tree: Tree<'a>,
-    path: &'a path::Path,
-    spec: &'a str,
-    last_commit: Commit<'a>,
-}
-
-#[derive(askama::Template)]
-#[template(path = "file.html")]
-struct RepoFileTemplate<'a> {
-    repo: &'a Repository,
-    path: &'a path::Path,
-    file_text: &'a str,
-    spec: &'a str,
-    last_commit: Commit<'a>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name, None, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&repo_name, Some(&r#ref), None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_3(
-    Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
-    spawn_blocking(move || inner(&repo_name, Some(&r#ref), Some(&object_name)).into_response())
-        .await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>, object_name: Option<&str>) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    if repo.is_empty()? {
-        return Ok(Redirect::permanent(&format!("/{repo_name}"))
-            .unwrap_or(Redirect::PERMANENT_ROOT)
-            .into_response());
-    }
-
-    let spec = repo.ref_or_head_shorthand(r#ref)?;
-    let Some((commit, tree)) = repo
-        .commit_tree(&spec)
-        .context("failed to get commit tree")?
-    else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "commit does not exist in repo",
-        ));
-    };
-
-    let (path, tree_obj) = if let Some(path) = object_name {
-        let path = path::Path::new(path);
-
-        (path, repo.tree_object(&tree, path)?)
-    } else {
-        (path::Path::new(""), Some(tree.into_object()))
-    };
-
-    let Some(tree_obj) = tree_obj else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "file does not exist into repo",
-        ));
-    };
-
-    let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "commit does not exist in repo",
-        ));
-    };
-
-    let tree_obj = match tree_obj.into_tree() {
-        // this is a subtree
-        Ok(sub_tree) => {
-            return Ok(Html(RepoTreeTemplate {
-                repo: &repo,
-                tree: sub_tree,
-                path,
-                spec: &spec,
-                last_commit,
-            })
-            .into_response());
-        }
-        // this is not a subtree, so it should be a blob i.e. file
-        Err(tree_obj) => tree_obj,
-    };
-
-    let Some(blob) = tree_obj.as_blob() else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "File not found"));
-    };
-
-    let output = render(repo_name, path, &spec, &commit, blob)?;
-
-    Ok(Html(RepoFileTemplate {
-        repo: &repo,
-        path,
-        file_text: &output,
-        spec: &spec,
-        last_commit,
-    })
-    .into_response())
-}
-
-// TODO: make sure I am escaping html properly here
-// TODO: allow disabling of syntax highlighting
-// TODO: -- dont pull in memory, use iterators if possible
-fn render(
-    repo_name: &str,
-    path: &path::Path,
-    spec: &str,
-    commit: &Commit<'_>,
-    blob: &Blob<'_>,
-) -> crate::utils::error::Result<String> {
-    let extension = path
-        .extension()
-        .and_then(std::ffi::OsStr::to_str)
-        .unwrap_or_default();
-
-    if blob.is_binary() {
-        // this is not a text file, but try to serve the file if the MIME type
-        // can give a hint at how
-        let mime = blob_mime(blob, extension);
-
-        let output = match mime.type_() {
-            mime::TEXT => unreachable!("git detected this file as binary"),
-            mime::IMAGE => format!(
-                "<img src=\"/{}/tree/{}/raw/{}\" />",
-                repo_name,
-                spec,
-                path.display()
-            ),
-            tag @ (mime::AUDIO | mime::VIDEO) => format!(
-                "<{} src=\"/{}/tree/{}/raw/{}\" controls>Your browser does not have support for playing this {0} file.</{0}>",
-                tag,
-                repo_name,
-                spec,
-                path.display()
-            ),
-            _ => "Cannot display binary file.".to_string(),
-        };
-
-        return Ok(output);
-    }
-
-    let syntax = SYNTAXES
-        .find_syntax_by_extension(extension)
-        .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
-
-    // get file contents from git object
-    let file_string = str::from_utf8(blob.content())?;
-
-    // create a highlighter that uses CSS classes so we can use prefers-color-scheme
-    let mut highlighter =
-        ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
-    LinesWithEndings::from(file_string).for_each(|line| {
-        if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
-            tracing::error!(err=?err, "failed to highlight code");
-        }
-    });
-
-    // use oid so it is a permalink
-    let prefix = format!(
-        "/{}/tree/{}/item/{}",
-        repo_name,
-        commit.id(),
-        path.display()
-    );
-
-    let mut output = String::from("<pre>\n");
-    for (n, line) in highlighter.finalize().lines().enumerate() {
-        let _ = writeln!(
-            &mut output,
-            "<a href='{1}#L{0}' id='L{0}' class='line'>{0}</a>{2}",
-            n + 1,
-            prefix,
-            line,
-        );
-    }
-    output.push_str("</pre>\n");
-
-    Ok(output)
-}
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
new file mode 100644
index 0000000..9072050
--- /dev/null
+++ b/src/handlers/repo_file_raw.rs
@@ -0,0 +1,83 @@
+use std::path;
+
+use axum::{
+    extract::{Path, State},
+    http::{StatusCode, header},
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{ObjectName, Ref, RepoName},
+        response::{ErrorPage, Result},
+    },
+    utils::blob_mime,
+};
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(
+    state: State<BileState>,
+    Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, &r#ref, &object_name))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(
+    state: &BileState,
+    repo_name: &RepoName,
+    r#ref: &Ref,
+    object_name: &ObjectName,
+) -> Result<Response> {
+    let repo = match Repository::open(&state.config, repo_name).context("opening repository") {
+        Ok(Some(repo)) => repo,
+        Ok(None) => {
+            return Ok(ErrorPage::new(&state.config)
+                .with_status(StatusCode::NOT_FOUND)
+                .into_response());
+        }
+        Err(err) => {
+            tracing::error!(err=?err, "failed to open repository");
+
+            return Ok(ErrorPage::new(&state.config)
+                .with_status(StatusCode::NOT_FOUND)
+                .into_response());
+        }
+    };
+
+    let path = path::Path::new(&object_name.0);
+
+    let Some((_, tree)) = repo
+        .commit_tree(&r#ref.0)
+        .context("failed to get commit tree")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let Some(blob) = repo.tree_blob(&tree, path)? else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let extension = path
+        .extension()
+        .and_then(std::ffi::OsStr::to_str)
+        .unwrap_or_default();
+
+    let mime = blob_mime(&blob, extension);
+
+    Ok((
+        StatusCode::OK,
+        [(header::CONTENT_TYPE, mime.as_ref())],
+        blob.content().to_vec(),
+    )
+        .into_response())
+}
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
deleted file mode 100644
index 666981d..0000000
--- a/src/handlers/repo_file_raw.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-use std::path;
-
-use axum::{
-    extract::Path,
-    http::{StatusCode, header},
-    response::{IntoResponse as _, Response},
-};
-
-use crate::utils::{
-    Error, Result, blob_mime, error::Context as _, extractor::repo_name_checks, git::Repository,
-    spawn_blocking,
-};
-
-#[tracing::instrument(skip_all)]
-pub async fn get(
-    Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
-    spawn_blocking(move || inner(&repo_name, &r#ref, &object_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, r#ref: &str, object_name: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    let path = path::Path::new(&object_name);
-
-    let Some((_, tree)) = repo
-        .commit_tree(r#ref)
-        .context("failed to get commit tree")?
-    else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "commit does not exist in repo",
-        ));
-    };
-
-    let Some(blob) = repo.tree_blob(&tree, path)? else {
-        return Err(Error::new(
-            StatusCode::NOT_FOUND,
-            "file does not exist into repo",
-        ));
-    };
-
-    let extension = path
-        .extension()
-        .and_then(std::ffi::OsStr::to_str)
-        .unwrap_or_default();
-
-    let mime = blob_mime(&blob, extension);
-
-    Ok((
-        StatusCode::OK,
-        [(header::CONTENT_TYPE, mime.as_ref())],
-        blob.content().to_vec(),
-    )
-        .into_response())
-}
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
new file mode 100644
index 0000000..503b851
--- /dev/null
+++ b/src/handlers/repo_home.rs
@@ -0,0 +1,59 @@
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::RepoName,
+        response::{ErrorPage, Html, Result},
+    },
+    utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "repo.html")]
+struct RepoHomeTemplate<'a> {
+    config: &'a Config,
+    repo: &'a Repository,
+    commits: Vec<git2::Commit<'a>>,
+    readme_text: String,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state.spawn(move |state| inner(&state, &repo_name)).await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let readme_text = repo.readme(&state.syntax);
+
+    // TODO: let r = req.param("ref").unwrap_or("HEAD");
+    let r = "HEAD";
+    let Some(commits) = repo.commits(r, 3)? else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    Ok(Html(RepoHomeTemplate {
+        config: &state.config,
+        repo: &repo,
+        commits,
+        readme_text,
+    })
+    .into_response())
+}
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
deleted file mode 100644
index fc5504b..0000000
--- a/src/handlers/repo_home.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use axum::{
-    extract::Path,
-    http::StatusCode,
-    response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
-    Error, Result, error::Context as _, extractor::repo_name_checks, filters, git::Repository,
-    response::Html, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "repo.html")]
-struct RepoHomeTemplate<'a> {
-    repo: &'a Repository,
-    commits: Vec<Commit<'a>>,
-    readme_text: String,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    let readme_text = repo.readme();
-
-    // TODO: let r = req.param("ref").unwrap_or("HEAD");
-    let r = "HEAD";
-    let Some(commits) = repo.commits(r, 3)? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "crepo does not exist"));
-    };
-
-    Ok(Html(RepoHomeTemplate {
-        repo: &repo,
-        commits,
-        readme_text,
-    })
-    .into_response())
-}
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
new file mode 100644
index 0000000..d41fb39
--- /dev/null
+++ b/src/handlers/repo_log.rs
@@ -0,0 +1,117 @@
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{ObjectName, Ref, RepoName},
+        response::{ErrorPage, Html, Redirect, Result},
+    },
+    utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "log.html")]
+struct RepoLogTemplate<'a> {
+    config: &'a Config,
+    repo: &'a Repository,
+    commits: Vec<git2::Commit<'a>>,
+    branch: String,
+    // the spec the user should be linked to to see the next page of commits
+    next_page: Option<String>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, None, None))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+    state: State<BileState>,
+    Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), None))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_3(
+    state: State<BileState>,
+    Path((repo_name, r#ref, object_name)): Path<(RepoName, Ref, ObjectName)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref), Some(&object_name)))
+        .await
+}
+
+fn inner(
+    state: &BileState,
+    repo_name: &RepoName,
+    r#ref: Option<&Ref>,
+    object_name: Option<&ObjectName>,
+) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    if repo.is_empty()? {
+        return Ok(Redirect::permanent(&format!("/{repo_name}"))
+            .unwrap_or(Redirect::PERMANENT_ROOT)
+            .into_response());
+    }
+
+    let r = r#ref.map_or("HEAD", |r| r.0.as_str());
+
+    let next_page_spec = if repo.is_shallow() {
+        String::new()
+    } else if let Some(i) = r.rfind('~') {
+        // there is a tilde, try to find a number too
+        let n = r[i + 1..].parse::<usize>().ok().unwrap_or(1);
+        format!("{}~{}", &r[..i], n + state.config.log_per_page)
+    } else {
+        // there was no tilde
+        format!("{}~{}", r, state.config.log_per_page)
+    };
+
+    let Some(mut commits) = repo
+        .commits_for_obj(r, state.config.log_per_page + 1, object_name)
+        .context("failed to get commits for object")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    // check if there even is a next page
+    let next_page = if commits.len() < state.config.log_per_page + 1 {
+        None
+    } else {
+        // remove additional commit from next page check
+        commits.pop();
+        Some(next_page_spec)
+    };
+
+    let branch = repo.ref_or_head_shorthand(r#ref)?;
+
+    Ok(Html(RepoLogTemplate {
+        config: &state.config,
+        repo: &repo,
+        commits,
+        branch,
+        next_page,
+    })
+    .into_response())
+}
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
deleted file mode 100644
index 28867ef..0000000
--- a/src/handlers/repo_log.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-use axum::{
-    extract::Path,
-    http::StatusCode,
-    response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
-    Error, Result,
-    error::Context as _,
-    extractor::repo_name_checks,
-    filters,
-    git::Repository,
-    response::{Html, Redirect},
-    spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "log.html")]
-struct RepoLogTemplate<'a> {
-    repo: &'a Repository,
-    commits: Vec<Commit<'a>>,
-    branch: String,
-    // the spec the user should be linked to to see the next page of commits
-    next_page: Option<String>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name, None, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&repo_name, Some(&r#ref), None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_3(
-    Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
-) -> Response {
-    spawn_blocking(move || inner(&repo_name, Some(&r#ref), Some(&object_name)).into_response())
-        .await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>, object_name: Option<&str>) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let config = crate::config();
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    if repo.is_empty()? {
-        return Ok(Redirect::permanent(&format!("/{repo_name}"))
-            .unwrap_or(Redirect::PERMANENT_ROOT)
-            .into_response());
-    }
-
-    let r = r#ref.unwrap_or("HEAD");
-
-    let next_page_spec = if repo.is_shallow() {
-        String::new()
-    } else if let Some(i) = r.rfind('~') {
-        // there is a tilde, try to find a number too
-        let n = r[i + 1..].parse::<usize>().ok().unwrap_or(1);
-        format!("{}~{}", &r[..i], n + config.log_per_page)
-    } else {
-        // there was no tilde
-        format!("{}~{}", r, config.log_per_page)
-    };
-
-    let Some(mut commits) = repo
-        .commits_for_obj(r, config.log_per_page + 1, object_name)
-        .context("failed to get commits for object")?
-    else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "entry does not exist"));
-    };
-
-    // check if there even is a next page
-    let next_page = if commits.len() < config.log_per_page + 1 {
-        None
-    } else {
-        // remove additional commit from next page check
-        commits.pop();
-        Some(next_page_spec)
-    };
-
-    let branch = repo.ref_or_head_shorthand(r#ref)?;
-
-    Ok(Html(RepoLogTemplate {
-        repo: &repo,
-        commits,
-        branch,
-        next_page,
-    })
-    .into_response())
-}
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
new file mode 100644
index 0000000..cd306d8
--- /dev/null
+++ b/src/handlers/repo_log_feed.rs
@@ -0,0 +1,76 @@
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{IntoResponse as _, Response},
+};
+
+use crate::{
+    BileState,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{Ref, RepoName},
+        response::{ErrorPage, Result, Xml},
+    },
+    utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "log.xml")]
+struct RepoLogFeedTemplate<'a> {
+    repo: &'a Repository,
+    commits: Vec<git2::Commit<'a>>,
+    branch: String,
+    base_url: &'a str,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_1(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, None))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_2(
+    state: State<BileState>,
+    Path((repo_name, r#ref)): Path<(RepoName, Ref)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, Some(&r#ref)))
+        .await
+}
+
+fn inner(state: &BileState, repo_name: &RepoName, r#ref: Option<&Ref>) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    if repo.is_empty()? {
+        // show a server error
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::SERVICE_UNAVAILABLE)
+            .into_response());
+    }
+
+    let r = r#ref.map_or("HEAD", |r| r.0.as_str());
+
+    let Some(commits) = repo.commits(r, state.config.log_per_page)? else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let branch = repo.ref_or_head_shorthand(r#ref)?;
+
+    Ok(Xml(RepoLogFeedTemplate {
+        repo: &repo,
+        commits,
+        branch,
+        base_url: &format!("/{repo_name}"),
+    })
+    .into_response())
+}
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
deleted file mode 100644
index f21bbc9..0000000
--- a/src/handlers/repo_log_feed.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use axum::{
-    extract::Path,
-    http::StatusCode,
-    response::{IntoResponse as _, Response},
-};
-use git2::Commit;
-
-use crate::utils::{
-    Error, Result, error::Context as _, extractor::repo_name_checks, filters, git::Repository,
-    response::Xml, spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "log.xml")]
-struct RepoLogFeedTemplate<'a> {
-    repo: &'a Repository,
-    commits: Vec<Commit<'a>>,
-    branch: String,
-    base_url: &'a str,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_1(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name, None).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&repo_name, Some(&r#ref)).into_response()).await
-}
-
-fn inner(repo_name: &str, r#ref: Option<&str>) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    if repo.is_empty()? {
-        // show a server error
-        return Err(Error::new(
-            StatusCode::SERVICE_UNAVAILABLE,
-            "Cannot show feed because there are no commits.",
-        ));
-    }
-
-    let r = r#ref.unwrap_or("HEAD");
-
-    let Some(commits) = repo.commits(r, crate::config().log_per_page)? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "crepo does not exist"));
-    };
-
-    let branch = repo.ref_or_head_shorthand(r#ref)?;
-
-    Ok(Xml(RepoLogFeedTemplate {
-        repo: &repo,
-        commits,
-        branch,
-        base_url: &format!("/{repo_name}"),
-    })
-    .into_response())
-}
diff --git a/src/handlers/repo_refs.rs b/src/handlers/repo_refs.rs
index d88c463..efff748 100644
--- a/src/handlers/repo_refs.rs
+++ b/src/handlers/repo_refs.rs
@@ -1,39 +1,42 @@
 use axum::{
-    extract::Path,
+    extract::{Path, State},
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
-use git2::Reference;
 
-use crate::utils::{
-    Error, Result,
+use crate::{
+    BileState,
+    config::Config,
     error::Context as _,
-    extractor::repo_name_checks,
-    filters,
     git::{Repository, TagEntry},
-    response::{Html, Redirect},
-    spawn_blocking,
+    http::{
+        extractor::RepoName,
+        response::{ErrorPage, Html, Redirect, Result},
+    },
+    utils::filters,
 };
 
 #[derive(askama::Template)]
 #[template(path = "refs.html")]
 struct RepoRefTemplate<'a> {
+    config: &'a Config,
     repo: &'a Repository,
-    branches: Vec<Reference<'a>>,
+    branches: Vec<git2::Reference<'a>>,
     tags: Vec<TagEntry>,
 }
 
 #[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name).into_response()).await
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state.spawn(move |state| inner(&state, &repo_name)).await
 }
 
 #[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
     };
 
     if repo.is_empty()? {
@@ -50,6 +53,7 @@ fn inner(repo_name: &str) -> Result {
     tags.sort_unstable_by(|a, b| a.signature.when().cmp(&b.signature.when()).reverse());
 
     Ok(Html(RepoRefTemplate {
+        config: &state.config,
         repo: &repo,
         branches,
         tags,
diff --git a/src/handlers/repo_refs_feed.rs b/src/handlers/repo_refs_feed.rs
index fd04b99..203658a 100644
--- a/src/handlers/repo_refs_feed.rs
+++ b/src/handlers/repo_refs_feed.rs
@@ -1,17 +1,18 @@
 use axum::{
-    extract::Path,
+    extract::{Path, State},
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
 
-use crate::utils::{
-    Error, Result,
+use crate::{
+    BileState,
     error::Context as _,
-    extractor::repo_name_checks,
-    filters,
     git::{Repository, TagEntry},
-    response::Xml,
-    spawn_blocking,
+    http::{
+        extractor::RepoName,
+        response::{ErrorPage, Result, Xml},
+    },
+    utils::filters,
 };
 
 #[derive(askama::Template)]
@@ -23,24 +24,24 @@ struct RepoRefFeedTemplate<'a> {
 }
 
 #[tracing::instrument(skip_all)]
-pub async fn get(Path(repo_name): Path<String>) -> Response {
-    spawn_blocking(move || inner(&repo_name).into_response()).await
+pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>) -> Response {
+    state.spawn(move |state| inner(&state, &repo_name)).await
 }
 
 #[tracing::instrument(skip_all)]
-fn inner(repo_name: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
+fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
     };
 
     if repo.is_empty()? {
         // show a server error
-        return Err(Error::new(
-            StatusCode::SERVICE_UNAVAILABLE,
-            "Cannot show feed because there is nothing here.",
-        ));
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::SERVICE_UNAVAILABLE)
+            .into_response());
     }
 
     let mut tags = repo.tag_entries()?;
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
new file mode 100644
index 0000000..d78a4ca
--- /dev/null
+++ b/src/handlers/repo_tag.rs
@@ -0,0 +1,58 @@
+use axum::{
+    extract::{Path, State},
+    http::StatusCode,
+    response::{IntoResponse, Response},
+};
+
+use crate::{
+    BileState,
+    config::Config,
+    error::Context as _,
+    git::Repository,
+    http::{
+        extractor::{RepoName, Tag},
+        response::{ErrorPage, Html, Redirect, Result},
+    },
+    utils::filters,
+};
+
+#[derive(askama::Template)]
+#[template(path = "tag.html")]
+struct Template<'a> {
+    config: &'a Config,
+    repo: &'a Repository,
+    tag: git2::Tag<'a>,
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get(
+    state: State<BileState>,
+    Path((repo_name, tag)): Path<(RepoName, Tag)>,
+) -> Response {
+    state
+        .spawn(move |state| inner(&state, &repo_name, &tag))
+        .await
+}
+
+#[tracing::instrument(skip_all)]
+fn inner(state: &BileState, repo_name: &RepoName, tag: &Tag) -> Result<Response> {
+    let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
+    else {
+        return Ok(ErrorPage::new(&state.config)
+            .with_status(StatusCode::NOT_FOUND)
+            .into_response());
+    };
+
+    let Ok(repo_tag) = repo.tag(tag) else {
+        return Ok(Redirect::permanent(&format!("/{repo_name}/commit/{tag}"))
+            .unwrap_or(Redirect::PERMANENT_ROOT)
+            .into_response());
+    };
+
+    Ok(Html(Template {
+        config: &state.config,
+        repo: &repo,
+        tag: repo_tag,
+    })
+    .into_response())
+}
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
deleted file mode 100644
index 2d7882a..0000000
--- a/src/handlers/repo_tag.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use axum::{
-    extract::Path,
-    http::StatusCode,
-    response::{IntoResponse, Response},
-};
-use git2::Tag;
-
-use crate::utils::{
-    Error, Result,
-    error::Context as _,
-    extractor::repo_name_checks,
-    filters,
-    git::Repository,
-    response::{Html, Redirect},
-    spawn_blocking,
-};
-
-#[derive(askama::Template)]
-#[template(path = "tag.html")]
-struct Template<'a> {
-    repo: &'a Repository,
-    tag: Tag<'a>,
-}
-
-#[tracing::instrument(skip_all)]
-pub async fn get(Path((repo_name, tag)): Path<(String, String)>) -> Response {
-    spawn_blocking(move || inner(&repo_name, &tag).into_response()).await
-}
-
-#[tracing::instrument(skip_all)]
-fn inner(repo_name: &str, tag: &str) -> Result {
-    repo_name_checks(repo_name)?;
-
-    let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    };
-
-    let Ok(repo_tag) = repo.tag(tag) else {
-        return Ok(Redirect::permanent(&format!("/{repo_name}/commit/{tag}"))
-            .unwrap_or(Redirect::PERMANENT_ROOT)
-            .into_response());
-    };
-
-    Ok(Html(Template {
-        repo: &repo,
-        tag: repo_tag,
-    })
-    .into_response())
-}
diff --git a/src/http/extractor.rs b/src/http/extractor.rs
new file mode 100644
index 0000000..d674c3f
--- /dev/null
+++ b/src/http/extractor.rs
@@ -0,0 +1,147 @@
+use std::fmt;
+
+use serde::de::Error as _;
+
+pub(crate) struct Commit(pub String);
+
+impl fmt::Display for Commit {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for Commit {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid commit ref"));
+        }
+
+        for c in value.bytes() {
+            if !c.is_ascii_hexdigit() {
+                return Err(D::Error::custom("invalid commit ref"));
+            }
+        }
+
+        Ok(Self(value))
+    }
+}
+
+pub(crate) struct Obj(pub String);
+
+impl fmt::Display for Obj {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for Obj {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid object ref"));
+        }
+
+        Ok(Self(value))
+    }
+}
+
+pub(crate) struct ObjectName(pub String);
+
+impl fmt::Display for ObjectName {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for ObjectName {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid object name"));
+        }
+
+        Ok(Self(value))
+    }
+}
+
+pub(crate) struct Ref(pub String);
+
+impl fmt::Display for Ref {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for Ref {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid ref"));
+        }
+
+        Ok(Self(value))
+    }
+}
+
+pub(crate) struct RepoName(pub String);
+
+impl fmt::Display for RepoName {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for RepoName {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid repo name"));
+        }
+
+        Ok(Self(value))
+    }
+}
+
+pub(crate) struct Tag(pub String);
+
+impl fmt::Display for Tag {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for Tag {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+
+        if value.is_empty() {
+            return Err(D::Error::custom("invalid tag"));
+        }
+
+        Ok(Self(value))
+    }
+}
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 0000000..83e27db
--- /dev/null
+++ b/src/http/mod.rs
@@ -0,0 +1,61 @@
+pub(crate) mod extractor;
+pub(crate) mod response;
+
+use std::sync::Arc;
+
+use axum::response::{IntoResponse as _, Response};
+use http::StatusCode;
+use syntect::parsing::SyntaxSet;
+
+use crate::{config::Config, error::Result, http::response::ErrorPage};
+
+#[derive(Clone)]
+pub(crate) struct BileState {
+    pub(crate) config: Arc<Config>,
+    pub(crate) syntax: Arc<SyntaxSet>,
+}
+
+impl BileState {
+    pub(crate) fn new(config: Config, syntax: SyntaxSet) -> Self {
+        Self {
+            config: Arc::new(config),
+            syntax: Arc::new(syntax),
+        }
+    }
+
+    pub(crate) async fn spawn<F>(&self, f: F) -> Response
+    where
+        F: FnOnce(Self) -> Result<Response> + Send + 'static,
+    {
+        let span = tracing::Span::current();
+
+        let this = self.clone();
+
+        spawn_blocking(move || span.in_scope(|| wrap_err(&this.config.clone(), f(this)))).await
+    }
+}
+
+// TODO: https://github.com/rust-lang/rust/issues/110011
+// #[track_caller]
+async fn spawn_blocking<F, R>(f: F) -> R
+where
+    F: FnOnce() -> R + Send + 'static,
+    R: Send + 'static,
+{
+    tokio::task::spawn_blocking(f)
+        .await
+        .expect("failed to join spawn_blocking call, this should only happen due to a panic")
+}
+
+fn wrap_err(config: &Config, res: Result<Response>) -> Response {
+    match res {
+        Ok(res) => res,
+        Err(err) => {
+            tracing::error!(err=?err, "failed to handle response");
+
+            ErrorPage::new(config)
+                .with_status(StatusCode::INTERNAL_SERVER_ERROR)
+                .into_response()
+        }
+    }
+}
diff --git a/src/utils/response.rs b/src/http/response.rs
similarity index 78%
rename from src/utils/response.rs
rename to src/http/response.rs
index eff1bec..f4d8530 100644
--- a/src/utils/response.rs
+++ b/src/http/response.rs
@@ -1,12 +1,16 @@
 use axum::{
     http::{HeaderValue, StatusCode, header},
-    response::IntoResponse,
+    response::{IntoResponse, Response},
 };
 
-pub struct Css<T>(pub T);
+use crate::config::Config;
+
+pub(crate) type Result<T = Response, E = crate::error::Error> = std::result::Result<T, E>;
+
+pub(crate) struct Css<T>(pub T);
 
 impl<T: IntoResponse> IntoResponse for Css<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (
             [
                 (
@@ -24,10 +28,10 @@ impl<T: IntoResponse> IntoResponse for Css<T> {
     }
 }
 
-pub struct Ico<T>(pub T);
+pub(crate) struct Ico<T>(pub T);
 
 impl<T: IntoResponse> IntoResponse for Ico<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (
             [
                 (
@@ -45,10 +49,10 @@ impl<T: IntoResponse> IntoResponse for Ico<T> {
     }
 }
 
-pub struct Json<T>(pub T);
+pub(crate) struct Json<T>(pub T);
 
 impl<T: IntoResponse> IntoResponse for Json<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (
             [
                 (
@@ -66,10 +70,10 @@ impl<T: IntoResponse> IntoResponse for Json<T> {
     }
 }
 
-pub struct Png<T>(pub T);
+pub(crate) struct Png<T>(pub T);
 
 impl<T: IntoResponse> IntoResponse for Png<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (
             [
                 (
@@ -87,10 +91,10 @@ impl<T: IntoResponse> IntoResponse for Png<T> {
     }
 }
 
-pub struct Text<T>(pub T);
+pub(crate) struct Text<T>(pub T);
 
 impl<T: IntoResponse> IntoResponse for Text<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (
             [
                 (
@@ -108,10 +112,10 @@ impl<T: IntoResponse> IntoResponse for Text<T> {
     }
 }
 
-pub struct Html<T: askama::Template>(pub T);
+pub(crate) struct Html<T: askama::Template>(pub T);
 
 impl<T: askama::Template> IntoResponse for Html<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         match self.0.render() {
             Ok(rendered) => (
                 [
@@ -144,10 +148,10 @@ impl<T: askama::Template> IntoResponse for Html<T> {
     }
 }
 
-pub struct Xml<T: askama::Template>(pub T);
+pub(crate) struct Xml<T: askama::Template>(pub T);
 
 impl<T: askama::Template> IntoResponse for Xml<T> {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         match self.0.render() {
             Ok(rendered) => (
                 [
@@ -182,33 +186,33 @@ impl<T: askama::Template> IntoResponse for Xml<T> {
 
 #[must_use = "needs to be returned from a handler or otherwise turned into a Response to be useful"]
 #[derive(Debug, Clone)]
-pub struct Redirect {
+pub(crate) struct Redirect {
     status_code: StatusCode,
     location: HeaderValue,
 }
 
 impl Redirect {
-    pub const PERMANENT_ROOT: Self = Self {
+    pub(crate) const PERMANENT_ROOT: Self = Self {
         status_code: StatusCode::PERMANENT_REDIRECT,
         location: HeaderValue::from_static("/"),
     };
-    pub const TEMPORARY_ROOT: Self = Self {
+    pub(crate) const TEMPORARY_ROOT: Self = Self {
         status_code: StatusCode::TEMPORARY_REDIRECT,
         location: HeaderValue::from_static("/"),
     };
 
     #[tracing::instrument(skip_all)]
-    pub fn to(uri: &str) -> Option<Self> {
+    pub(crate) fn to(uri: &str) -> Option<Self> {
         Self::with_status_code(StatusCode::SEE_OTHER, uri)
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn temporary(uri: &str) -> Option<Self> {
+    pub(crate) fn temporary(uri: &str) -> Option<Self> {
         Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri)
     }
 
     #[tracing::instrument(skip_all)]
-    pub fn permanent(uri: &str) -> Option<Self> {
+    pub(crate) fn permanent(uri: &str) -> Option<Self> {
         Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri)
     }
 
@@ -236,7 +240,36 @@ impl Redirect {
 }
 
 impl IntoResponse for Redirect {
-    fn into_response(self) -> axum::response::Response {
+    fn into_response(self) -> Response {
         (self.status_code, [(header::LOCATION, self.location)]).into_response()
     }
 }
+
+#[derive(askama::Template)]
+#[template(path = "error.html")]
+pub(crate) struct ErrorPage<'c> {
+    config: &'c Config,
+    status: StatusCode,
+}
+
+impl<'c> ErrorPage<'c> {
+    pub(crate) const fn new(config: &'c Config) -> Self {
+        Self {
+            config,
+            status: StatusCode::INTERNAL_SERVER_ERROR,
+        }
+    }
+
+    pub(crate) const fn with_status(self, status: StatusCode) -> Self {
+        Self {
+            config: self.config,
+            status,
+        }
+    }
+}
+
+impl IntoResponse for ErrorPage<'_> {
+    fn into_response(self) -> Response {
+        (self.status, Html(self)).into_response()
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index d205dbd..73f9c46 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -225,36 +225,32 @@
     clippy::wildcard_imports,
     clippy::zero_sized_map_values
 )]
-#![allow(clippy::missing_errors_doc)]
+#![allow(clippy::missing_errors_doc, clippy::redundant_pub_crate)]
 
-pub mod handlers;
-pub mod utils;
+pub(crate) mod git;
+pub(crate) mod handlers;
+pub(crate) mod http;
+pub(crate) mod utils;
 
 pub mod config;
+pub mod error;
 
-use std::{
-    str,
-    sync::{LazyLock, OnceLock},
-    time::Duration,
-};
+use std::{str, time::Duration};
 
 use axum::{Router, http::StatusCode, routing::get};
 use axum_response_cache::CacheLayer;
-use syntect::parsing::SyntaxSet;
 use tower_helmet::HelmetLayer;
 use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
 
 use crate::{
     config::Config,
-    utils::response::{Css, Ico, Json, Png, Text},
+    http::BileState,
+    http::response::{Css, Ico, Json, Png, Text},
 };
 
 #[global_allocator]
 static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
 
-static CONFIG: OnceLock<Config> = OnceLock::new();
-static SYNTAXES: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
-
 static APPLE_TOUCH_ICON_PNG: &[u8] = include_bytes!("../assets/apple-touch-icon.png");
 static FAVICON_ICO: &[u8] = include_bytes!("../assets/favicon.ico");
 static ICON_192_MASKABLE: &[u8] = include_bytes!("../assets/icon-192-maskable.png");
@@ -267,14 +263,10 @@ static STYLE_CSS: &str = include_str!("../assets/style.css");
 
 static META_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
 
-pub(crate) fn config() -> &'static Config {
-    CONFIG
-        .get()
-        .unwrap_or_else(|| unreachable!("failed to get global config, this should not happen"))
-}
-
 #[allow(missing_copy_implementations, clippy::empty_structs_with_brackets)]
-pub struct Bile {}
+pub struct Bile {
+    state: BileState,
+}
 
 impl Bile {
     /// Create a new Bile 'instance'
@@ -284,10 +276,11 @@ impl Bile {
     /// This will panic if you create multiple Bile instances.
     ///
     /// Which you shouldn't BTW.
+    #[must_use]
     pub fn init(config: Config) -> Self {
-        CONFIG.set(config).expect("config already set");
-
-        Self {}
+        Self {
+            state: BileState::new(config, two_face::syntax::extra_newlines()),
+        }
     }
 
     #[rustfmt::skip]
@@ -352,6 +345,8 @@ impl Bile {
             .route("/{repo_name}/tree/{ref}/item/{*object_name}", get(handlers::repo_file::get_3))
             .route("/{repo_name}/tree/{ref}/raw/{*object_name}", get(handlers::repo_file_raw::get))
             //
+            .with_state(self.state.clone())
+            //
             .layer((
                 TraceLayer::new_for_http(),
                 TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(10)),
diff --git a/src/main.rs b/src/main.rs
index 41b00eb..af9318f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,7 @@ use bile::{Bile, config::Config};
 use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
 
 #[tokio::main]
-async fn main() -> bile::utils::error::Result<()> {
+async fn main() -> bile::error::Result<()> {
     tracing_subscriber::registry()
         .with(
             tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/utils/cache.rs
diff --git a/src/utils/extractor.rs b/src/utils/extractor.rs
deleted file mode 100644
index dcb6222..0000000
--- a/src/utils/extractor.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use axum::{
-    extract,
-    http::{self, StatusCode, request},
-    response::{self, IntoResponse as _},
-};
-
-use crate::utils::{Error, Result};
-
-fn get_param<'x>(ext: &'x http::Extensions, name: &str) -> Option<&'x str> {
-    let raw = ext.get::<extract::RawPathParams>()?;
-
-    for (key, value) in raw {
-        if key == name {
-            return Some(value);
-        }
-    }
-
-    None
-}
-
-pub struct Commit(pub String);
-
-impl<S> extract::FromRequestParts<S> for Commit
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "commit") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(
-                Error::new(StatusCode::NOT_FOUND, "commit does not exist in repo").into_response(),
-            );
-        }
-
-        for c in raw.bytes() {
-            if !c.is_ascii_hexdigit() {
-                return Err(
-                    Error::new(StatusCode::NOT_FOUND, "commit does not exist in repo")
-                        .into_response(),
-                );
-            }
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub struct Obj(pub String);
-
-impl<S> extract::FromRequestParts<S> for Obj
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "obj") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(
-                Error::new(StatusCode::NOT_FOUND, "object does not exist in repo").into_response(),
-            );
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub struct ObjectName(pub String);
-
-impl<S> extract::FromRequestParts<S> for ObjectName
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "object_name") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(
-                Error::new(StatusCode::NOT_FOUND, "object does not exist in repo").into_response(),
-            );
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub struct Ref(pub String);
-
-impl<S> extract::FromRequestParts<S> for Ref
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "ref") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(
-                Error::new(StatusCode::NOT_FOUND, "ref does not exist in repo").into_response(),
-            );
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub struct RepoName(pub String);
-
-impl<S> extract::FromRequestParts<S> for RepoName
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "repo_name") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist").into_response());
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub struct Tag(pub String);
-
-impl<S> extract::FromRequestParts<S> for Tag
-where
-    S: Sync,
-{
-    type Rejection = response::Response;
-
-    async fn from_request_parts(
-        parts: &mut request::Parts,
-        _state: &S,
-    ) -> Result<Self, Self::Rejection> {
-        let Some(raw) = get_param(&parts.extensions, "tag") else {
-            return Err(Error::new(StatusCode::NOT_FOUND, "page not found").into_response());
-        };
-
-        if raw.is_empty() {
-            return Err(
-                Error::new(StatusCode::NOT_FOUND, "tag does not exist in repo").into_response(),
-            );
-        }
-
-        Ok(Self(raw.to_string()))
-    }
-}
-
-pub fn repo_name_checks(name: &str) -> Result<()> {
-    let name = name.trim();
-
-    if name.is_empty() {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    }
-
-    Ok(())
-}
-
-pub fn commit_checks(name: &str) -> Result<()> {
-    let name = name.trim();
-
-    if name.is_empty() {
-        return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-    }
-
-    for c in name.bytes() {
-        if !c.is_ascii_hexdigit() {
-            return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
-        }
-    }
-
-    Ok(())
-}
diff --git a/src/utils/filters.rs b/src/utils/filters.rs
index fbada2b..3643db8 100644
--- a/src/utils/filters.rs
+++ b/src/utils/filters.rs
@@ -1,4 +1,10 @@
-#![allow(clippy::inline_always, reason = "generated by askama filter_fn")]
+#![allow(
+    unreachable_pub,
+    clippy::inline_always,
+    clippy::unused_self,
+    clippy::unnecessary_wraps,
+    reason = "generated by askama filter_fn"
+)]
 
 use git2::{Commit, Signature, Time};
 use jiff::{
@@ -7,11 +13,11 @@ use jiff::{
 };
 use num_conv::Truncate as _;
 
-use crate::utils::git::Repository;
+use crate::git::Repository;
 
 #[askama::filter_fn]
 #[tracing::instrument(skip_all)]
-pub fn format_datetime<'f>(
+pub(crate) fn format_datetime<'f>(
     time: Time,
     _: &dyn askama::Values,
     format: &'f str,
@@ -32,7 +38,7 @@ pub fn format_datetime<'f>(
 }
 
 #[askama::filter_fn]
-pub fn unix_perms(m: i32, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn unix_perms(m: i32, _: &dyn askama::Values) -> askama::Result<String> {
     // https://unix.stackexchange.com/questions/450480/file-permission-with-six-bytes-in-git-what-does-it-mean
     // Git doesn't store arbitrary modes, only a subset of the values are
     // allowed. Since the number of possible values is quite small, it is
@@ -49,18 +55,21 @@ pub fn unix_perms(m: i32, _: &dyn askama::Values) -> askama::Result<String> {
 }
 
 #[askama::filter_fn]
-pub fn repo_name<'r>(repo: &'r Repository, _: &dyn askama::Values) -> askama::Result<&'r str> {
+pub(crate) fn repo_name<'r>(
+    repo: &'r Repository,
+    _: &dyn askama::Values,
+) -> askama::Result<&'r str> {
     repo.name().ok_or(askama::Error::Fmt)
 }
 
 #[askama::filter_fn]
-pub fn description(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn description(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
     Ok(repo.description())
 }
 
 #[askama::filter_fn]
 #[tracing::instrument(skip_all)]
-pub fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama::Result<Time> {
+pub(crate) fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama::Result<Time> {
     repo.last_modified().map_err(|err| {
         tracing::error!(err=?err, "failed to get repo's last modified date");
         askama::Error::Fmt
@@ -68,12 +77,12 @@ pub fn last_modified(repo: &Repository, _: &dyn askama::Values) -> askama::Resul
 }
 
 #[askama::filter_fn]
-pub fn repo_owner(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn repo_owner(repo: &Repository, _: &dyn askama::Values) -> askama::Result<String> {
     Ok(repo.owner())
 }
 
 #[askama::filter_fn]
-pub fn signature_email_link(
+pub(crate) fn signature_email_link(
     signature: &Signature<'_>,
     _: &dyn askama::Values,
 ) -> askama::Result<String> {
@@ -90,7 +99,7 @@ pub fn signature_email_link(
 
 #[askama::filter_fn]
 #[tracing::instrument(skip_all)]
-pub fn short_id(commit: &Commit<'_>, _: &dyn askama::Values) -> askama::Result<String> {
+pub(crate) fn short_id(commit: &Commit<'_>, _: &dyn askama::Values) -> askama::Result<String> {
     let id = match commit.as_object().short_id() {
         Ok(id) => id,
         Err(err) => {
diff --git a/src/utils/markdown.rs b/src/utils/markdown.rs
index 6e02d11..2b4cb1e 100644
--- a/src/utils/markdown.rs
+++ b/src/utils/markdown.rs
@@ -15,12 +15,10 @@ use syntect::{
     util::LinesWithEndings,
 };
 
-use crate::SYNTAXES;
-
 #[tracing::instrument(skip_all)]
-pub fn render(input: &str) -> String {
+pub(crate) fn render(syntaxes: &SyntaxSet, input: &str) -> String {
     let adaptor = SyntectAdapter {
-        syntax_set: &SYNTAXES,
+        syntax_set: syntaxes,
     };
 
     let options = Options::default();
@@ -32,11 +30,11 @@ pub fn render(input: &str) -> String {
     markdown_to_html_with_plugins(input, &options, &plugins)
 }
 
-struct SyntectAdapter {
-    syntax_set: &'static SyntaxSet,
+struct SyntectAdapter<'s> {
+    syntax_set: &'s SyntaxSet,
 }
 
-impl SyntectAdapter {
+impl SyntectAdapter<'_> {
     fn highlight_html(&self, code: &str, syntax: &SyntaxReference) -> Result<String, Error> {
         let mut html_generator =
             ClassedHTMLGenerator::new_with_class_style(syntax, self.syntax_set, ClassStyle::Spaced);
@@ -47,7 +45,7 @@ impl SyntectAdapter {
     }
 }
 
-impl SyntaxHighlighterAdapter for SyntectAdapter {
+impl SyntaxHighlighterAdapter for SyntectAdapter<'_> {
     fn write_highlighted(
         &self,
         output: &mut dyn Write,
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 0000000..4b1ae7e
--- /dev/null
+++ b/src/utils/mod.rs
@@ -0,0 +1,14 @@
+pub(crate) mod cache;
+pub(crate) mod filters;
+pub(crate) mod markdown;
+
+#[must_use]
+pub(crate) fn blob_mime(blob: &git2::Blob<'_>, extension: &str) -> mime::Mime {
+    extension.parse().unwrap_or_else(|_| {
+        if blob.is_binary() {
+            mime::APPLICATION_OCTET_STREAM
+        } else {
+            mime::TEXT_PLAIN_UTF_8
+        }
+    })
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
deleted file mode 100644
index 19fef1c..0000000
--- a/src/utils/mod.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-pub mod git;
-
-pub mod error;
-pub mod extractor;
-pub mod filters;
-pub mod markdown;
-pub mod response;
-
-use axum::{
-    http::StatusCode,
-    response::{IntoResponse, Response},
-};
-
-use crate::utils::response::Html;
-
-pub type Result<T = Response, E = Error> = std::result::Result<T, E>;
-
-// TODO: https://github.com/rust-lang/rust/issues/110011
-// #[track_caller]
-pub async fn spawn_blocking<F, R>(f: F) -> R
-where
-    F: FnOnce() -> R + Send + 'static,
-    R: Send + 'static,
-{
-    tokio::task::spawn_blocking(f)
-        .await
-        .expect("failed to join spawn_blocking call, this should only happen due to a panic")
-}
-
-#[derive(askama::Template)]
-#[template(path = "error.html")]
-pub enum Error {
-    Failure {
-        status: StatusCode,
-        err: error::Error,
-    },
-    Custom {
-        status: StatusCode,
-        message: String,
-    },
-}
-
-impl Error {
-    pub fn new<M: ToString + ?Sized>(status: StatusCode, message: &M) -> Self {
-        Self::Custom {
-            status,
-            message: message.to_string(),
-        }
-    }
-}
-
-impl IntoResponse for Error {
-    fn into_response(self) -> Response {
-        let status = match &self {
-            Self::Failure { status, err } => {
-                tracing::error!(err=?err, "failed to respond to request");
-
-                status
-            }
-            Self::Custom { status, .. } => status,
-        };
-
-        (*status, Html(self)).into_response()
-    }
-}
-
-impl<E> From<E> for Error
-where
-    E: Into<error::Error>,
-{
-    fn from(err: E) -> Self {
-        Self::Failure {
-            status: StatusCode::INTERNAL_SERVER_ERROR,
-            err: err.into(),
-        }
-    }
-}
-
-#[must_use]
-pub fn blob_mime(blob: &git2::Blob<'_>, extension: &str) -> mime::Mime {
-    extension.parse().unwrap_or_else(|_| {
-        if blob.is_binary() {
-            mime::APPLICATION_OCTET_STREAM
-        } else {
-            mime::TEXT_PLAIN_UTF_8
-        }
-    })
-}
diff --git a/templates/base.html b/templates/base.html
index 30484c2..5796569 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -13,7 +13,7 @@
   <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
   <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
   <meta name="description" content="My self-hosted git repositories">
-  <title>{% block title %}{{crate::config().site_name}}{% endblock %}</title>
+  <title>{% block title %}{{config.site_name}}{% endblock %}</title>
   <link rel="manifest" href="manifest.json">
   <link rel="icon" href="/favicon.ico" sizes="any">
   <link rel="apple-touch-icon" href="/apple-touch-icon.png">
@@ -23,7 +23,7 @@
 
 <body>
   <div class="page-title">
-    <h1><a href="/">{{crate::config().site_name}}</a></h1>
+    <h1><a href="/">{{config.site_name}}</a></h1>
   </div>
   <hr />
   <div id="content">
diff --git a/templates/commit.html b/templates/commit.html
index 49bb891..497e3c4 100644
--- a/templates/commit.html
+++ b/templates/commit.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} commit {{commit|short_id}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} commit {{commit|short_id}} - {{config.site_name}}{% endblock %}
 
 {% block content %}
   {% include "repo-navbar.html" %}
diff --git a/templates/error-404.html b/templates/error-404.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/templates/error-404.html
diff --git a/templates/error.html b/templates/error-old.html
similarity index 86%
rename from templates/error.html
rename to templates/error-old.html
index 5d0456b..e1c0478 100644
--- a/templates/error.html
+++ b/templates/error-old.html
@@ -6,8 +6,8 @@
   </div>
   <br>
   <div>
-    {%- match self -%}
-      {%- when Self::Failure { status, err } -%}
+    {%- match self.kind -%}
+      {%- when ErrorKind::Failure { status, err } -%}
         {% let debug = format!("{:?}", err) %}
         {% let err = ansi_to_html::convert(&debug) %}
         {%- match err -%}
@@ -16,7 +16,7 @@
           {%- when Err(_) -%}
             {{status}} &mdash; {{ debug }}
         {%- endmatch -%}
-      {%- when Self::Custom { status, message } -%}
+      {%- when ErrorKind::Custom { status, message } -%}
         {{status}} &mdash; {{ message }}
     {%- endmatch -%}
   </div>
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..70fe79b
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block content %}
+  <div class="page-title">
+    <h1>Hmm, that did not work.</h1>
+  </div>
+  <br>
+  <div>
+  </div>
+{% endblock %}<
\ No newline at end of file
diff --git a/templates/file.html b/templates/file.html
index ec9e8fd..744dacf 100644
--- a/templates/file.html
+++ b/templates/file.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} {{path.display()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} {{path.display()}} - {{config.site_name}}{% endblock %}
 
 {% block content %}
   {% include "repo-navbar.html" %}
diff --git a/templates/log.html b/templates/log.html
index 4719ab8..6ebc3a6 100644
--- a/templates/log.html
+++ b/templates/log.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} log at {{branch}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} log at {{branch}} - {{config.site_name}}{% endblock %}
 
 {% block head %}
 <link rel="alternate" type="application/rss+xml" title="{{repo|repo_name}} {{branch}} commits" href="log.xml">
@@ -17,7 +17,7 @@
     </tbody>
   </table>
   {% if next_page.is_some() %}
-    <a href="{{next_page.as_ref().unwrap()}}">older commits &rarr;</a>
+    <a href="/{{repo|repo_name|urlencode_strict}}/log/{{next_page.as_ref().unwrap()}}">older commits &rarr;</a>
   {% endif %}
   <hr>
   <table id="log">
@@ -38,6 +38,6 @@
     </tbody>
   </table>
   {% if next_page.is_some() %}
-    <a href="{{next_page.as_ref().unwrap()}}">older commits &rarr;</a>
+    <a href="/{{repo|repo_name|urlencode_strict}}/log/{{next_page.as_ref().unwrap()}}">older commits &rarr;</a>
   {% endif %}
 {% endblock %}=
\ No newline at end of file
diff --git a/templates/refs.html b/templates/refs.html
index 283e1c9..ed0d0fa 100644
--- a/templates/refs.html
+++ b/templates/refs.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} refs - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} refs - {{config.site_name}}{% endblock %}
 
 {% block head %}
 <link rel="alternate" type="application/rss+xml" title="{{repo|repo_name}} tags" href="refs.xml">
diff --git a/templates/repo-navbar.html b/templates/repo-navbar.html
index bd85681..79f03e0 100644
--- a/templates/repo-navbar.html
+++ b/templates/repo-navbar.html
@@ -1,8 +1,8 @@
-<table>
+<table class="repo">
   <tbody>
-    <tr><td><h1>{{repo|repo_name}}</h1></td></tr>
-    <tr><td>{{repo|description}}</td></tr>
-    <tr class="clone-url"><td>git clone <a href="{{crate::config().clone_base}}/{{repo|repo_name}}">{{crate::config().clone_base}}/{{repo|repo_name}}</a></td></tr>
+    <tr><td class="repo-link"><h1>{{repo|repo_name}}</h1></td></tr>
+    <tr><td class="repo-description">{{repo|description}}</td></tr>
+    <tr class="clone-url"><td>git clone <a href="{{config.clone_base}}/{{repo|repo_name}}">{{config.clone_base}}/{{repo|repo_name}}</a></td></tr>
     <tr class="navbar"><td><a href="/{{repo|repo_name|urlencode_strict}}">README</a> | <a href="/{{repo|repo_name|urlencode_strict}}/tree">tree</a> | <a href="/{{repo|repo_name|urlencode_strict}}/log">log</a> | <a href="/{{repo|repo_name|urlencode_strict}}/refs">refs</a></td></tr>
   </tbody>
 </table>
diff --git a/templates/repo.html b/templates/repo.html
index c5ff7ec..eca145e 100644
--- a/templates/repo.html
+++ b/templates/repo.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} - {{config.site_name}}{% endblock %}
 
 {% block content %}
   {% include "repo-navbar.html" %}
diff --git a/templates/tag.html b/templates/tag.html
index 1015bc1..e30669a 100644
--- a/templates/tag.html
+++ b/templates/tag.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} tag {{tag.name().unwrap()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} tag {{tag.name().unwrap()}} - {{config.site_name}}{% endblock %}
 
 {% block content %}
   {% include "repo-navbar.html" %}
diff --git a/templates/tree.html b/templates/tree.html
index c47e1a6..aa476f7 100644
--- a/templates/tree.html
+++ b/templates/tree.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
-{% block title %}{{repo|repo_name}} {{path.display()}} - {{crate::config().site_name}}{% endblock %}
+{% block title %}{{repo|repo_name}} {{path.display()}} - {{config.site_name}}{% endblock %}
 
 {% block content %}
   {% include "repo-navbar.html" %}