c67e7434

Archive
Tree [c67e7434] [>]
commit
c67e74348fcd191fd051c82ee26e2bae710a9a7b
parent
author
Christopher K. Schmitt <me@shmish.dev>
date
2026-07-29
committer
Christopher K. Schmitt <me@shmish.dev>
date
2026-07-29
changes
23
insertions
191
deletions
149
Replace global OnceLock statics with threaded SiteConfig
Msrc/config.rs
-//! Global application configuration set once at startup.+//! Application configuration set once at startup.~//!-//! Values are stored in `OnceLock` statics and accessed via the corresponding-//! getter functions.  All setters accept `Option` and silently skip `None`.+//! Compile-time constants ([`PKG_NAME`], [`PKG_VERSION`]) are read from+//! `Cargo.toml` via `env!()`.  Runtime configuration is stored in+//! [`SiteConfig`] and threaded through [`AppState`](crate::routes::AppState)+//! rather than held in process-global statics.~-use std::sync::OnceLock;-~/// Package name read from `Cargo.toml` at compile time.~pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");~~/// Package version read from `Cargo.toml` at compile time.~pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");--static HOSTNAME: OnceLock<String> = OnceLock::new();-static SSH_PREFIX: OnceLock<String> = OnceLock::new();-static BASE_URL: OnceLock<String> = OnceLock::new();-static FAVICON: OnceLock<Vec<u8>> = OnceLock::new();-static FAVICON_MIME: OnceLock<&'static str> = OnceLock::new();--/// Returns the configured hostname, or `"index"` if none was provided.-pub fn hostname() -> &'static str {-    HOSTNAME.get().map_or("index", |s| s.as_str())-}--/// Returns the configured SSH prefix, or an empty string if none was provided.-pub fn ssh_prefix() -> &'static str {-    SSH_PREFIX.get().map_or("", |s| s.as_str())-}--/// Returns the configured base URL path prefix, or an empty string if none was-/// provided.-pub fn base_url() -> &'static str {-    BASE_URL.get().map_or("", |s| s.as_str())-}--/// Returns the favicon bytes loaded at startup.-pub fn favicon() -> &'static [u8] {-    FAVICON.get().map_or(&[], |v| v.as_slice())-}--/// Returns the MIME type of the favicon.-pub fn favicon_mime_type() -> &'static str {-    FAVICON_MIME.get().copied().unwrap_or("image/svg+xml")-}--/// Sets the hostname shown in breadcrumbs and clone URLs.-/// Silently skips `None`.-pub fn set_hostname(v: Option<String>) {-    if let Some(v) = v {-        let _ = HOSTNAME.set(v);-    }-}--/// Sets the path prefix used in SSH clone URLs.  Silently skips `None`.-pub fn set_ssh_prefix(v: Option<String>) {-    if let Some(v) = v {-        let _ = SSH_PREFIX.set(v);-    }-}--/// Sets the URL path prefix for reverse-proxy deployments.-/// Silently skips `None`.-pub fn set_base_url(v: Option<String>) {-    if let Some(v) = v {-        let _ = BASE_URL.set(v);-    }-}~-/// Sets the favicon bytes and MIME type served at `/favicon.ico`.-pub fn set_favicon(v: Vec<u8>, mime: &'static str) {-    let _ = FAVICON.set(v);-    let _ = FAVICON_MIME.set(mime);+/// Site-wide configuration set once at startup and shared across all handlers.+#[derive(Clone)]+pub struct SiteConfig {+    pub site_name: String,+    pub hostname: String,+    pub ssh_prefix: String,+    pub base_url: String,+    pub favicon: Vec<u8>,+    pub favicon_mime: &'static str,~}
Msrc/error.rs
~//! [`Result`] type alias.  `Error` implements [`IntoResponse`] via askama's~//! `error.html` template.~+use std::sync::{Arc, OnceLock};+~use askama::Template;~use axum::http::{StatusCode, header};~use axum::response::{IntoResponse, Response};~use thiserror::Error;++use crate::config::SiteConfig;~~/// Unified error type for the application.~///
~~/// Convenience alias for `std::result::Result<T, Error>`.~pub type Result<T> = std::result::Result<T, Error>;++/// Module-level [`SiteConfig`] used only by the error template renderer.+///+/// Set once during startup in [`init_site`].  This exists because+/// [`Error::into_response`] does not have access to [`axum::extract::State`]+/// — it runs after the handler has returned.  All other code paths read from+/// [`AppState`](crate::routes::AppState).+static SITE: OnceLock<Arc<SiteConfig>> = OnceLock::new();++/// Stores the [`SiteConfig`] for use by the error template.+///+/// Called once from `main` before the server starts.+pub fn init_site(site: Arc<SiteConfig>) {+    let _ = SITE.set(site);+}~+fn site() -> Arc<SiteConfig> {+    SITE.get()+        .cloned()+        .unwrap_or_else(|| {+            Arc::new(SiteConfig {+                site_name: crate::PKG_NAME.to_owned(),+                hostname: String::new(),+                ssh_prefix: String::new(),+                base_url: String::new(),+                favicon: Vec::new(),+                favicon_mime: "image/svg+xml",+            })+        })+}+~#[derive(Template)]~#[template(path = "error.html")]~struct ErrorTemplate {~    status: u16,~    kind: &'static str,~    message: String,-    base_url: String,+    site: Arc<SiteConfig>,~}~~impl IntoResponse for Error {
~            status: status.as_u16(),~            kind,~            message,-            base_url: crate::base_url().to_string(),+            site: site(),~        };~~        tpl.render().map_or_else(
~                    self.to_string()~                };~                (status, fallback).into_response()+            },+            |html| {+                (+                    status,+                    [(header::CONTENT_TYPE, "text/html; charset=utf-8")],+                    html,+                )+                    .into_response()~            },-            |html| (status, [(header::CONTENT_TYPE, "text/html; charset=utf-8")], html).into_response(),~        )~    }~}
Msrc/lib.rs
~pub mod theme;~~// Re-exports for askama Template derives (resolved at `crate::`).-pub use config::{PKG_NAME, PKG_VERSION, base_url, hostname, ssh_prefix};+pub use config::{PKG_NAME, PKG_VERSION};
Msrc/main.rs
-//! Binary entry point. Parses CLI arguments, initialises global configuration,-//! builds the CSS theme, constructs the axum router, and starts the HTTP server.+//! Binary entry point. Parses CLI arguments, constructs the application+//! configuration, builds the CSS theme, and starts the HTTP server.~~mod cli;~+use std::sync::Arc;+~use clap::Parser;-use teak::config;+use teak::config::{self, SiteConfig};~use teak::theme::{BASE_CSS, MARKDOWN_CSS};~use teak::{error, routes};~use tower_http::trace::TraceLayer;
~~    let args = cli::Args::parse();~-    config::set_hostname(args.hostname);-    config::set_ssh_prefix(args.ssh_prefix);-    config::set_base_url(args.base_url);-~    let (favicon_data, favicon_mime) = match args.favicon {~        Some(path) => match std::fs::read(&path) {~            Ok(data) => {
~        },~        None => (include_bytes!("../static/favicon.svg").to_vec(), "image/svg+xml"),~    };-    config::set_favicon(favicon_data, favicon_mime);++    let site = Arc::new(SiteConfig {+        site_name: args+            .hostname+            .clone()+            .unwrap_or_else(|| config::PKG_NAME.to_owned()),+        hostname: args.hostname.unwrap_or_default(),+        ssh_prefix: args.ssh_prefix.unwrap_or_default(),+        base_url: args.base_url.unwrap_or_default(),+        favicon: favicon_data,+        favicon_mime,+    });++    error::init_site(site.clone());~~    let theme_vars = match args.theme_file {~        Some(path) => std::fs::read_to_string(path)?,
~    let state = routes::AppState {~        root: args.root,~        css,+        site,~    };~    let router = routes::router(state).layer(TraceLayer::new_for_http());~    let addr = format!("{}:{}", args.bind, args.port);
Msrc/routes.rs
~~use std::collections::HashMap;~use std::path::{Component, PathBuf};+use std::sync::Arc;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use axum::Router;~use axum::extract::{FromRequestParts, Path, State};
~    pub root: PathBuf,~    /// Concatenated CSS served at `/style.css`.~    pub css: String,+    /// Site-wide configuration set once at startup.+    pub site: Arc<SiteConfig>,~}~~/// Serves the concatenated stylesheet.
~}~~/// Serves the favicon bytes loaded at startup, with the correct MIME type.-async fn favicon() -> impl IntoResponse {+async fn favicon(State(state): State<AppState>) -> impl IntoResponse {~    (-        [(header::CONTENT_TYPE, crate::config::favicon_mime_type())],-        crate::config::favicon(),+        [(header::CONTENT_TYPE, state.site.favicon_mime)],+        state.site.favicon.clone(),~    )~}~
Mtemplates/base.html
~    <meta charset="UTF-8">~    <meta name="viewport" content="width=device-width, initial-scale=1.0">~    <title>{% block title %}{% endblock %}</title>-    <link rel="stylesheet" href="{{ crate::base_url() }}/style.css">-    <link rel="icon" type="image/svg+xml" href="{{ crate::base_url() }}/favicon.ico">+    <link rel="stylesheet" href="{{ site.base_url }}/style.css">+    <link rel="icon" type="image/svg+xml" href="{{ site.base_url }}/favicon.ico">~    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">-    <meta property="og:site_name" content="git browse">+    <meta property="og:site_name" content="{{ site.site_name }}">~    <meta property="og:type" content="website">~    <meta property="og:title" content="{% block og_title %}{% endblock %}">~    <meta property="og:description" content="{% block og_description %}{% endblock %}">
Mtemplates/blame.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>~  {% for crumb in breadcrumbs %}~  <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~  {% endfor %}
~  <table class="blame-table">~    {% for line in lines %}~    <tr class="blame-row">-      <td class="blame-sha"><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ line.sha }}" style="color: {{ line.color }}">{{ line.short_sha }}</a></td>+      <td class="blame-sha"><a href="{{ site.base_url }}/{{ repo }}/commits/{{ line.sha }}" style="color: {{ line.color }}">{{ line.short_sha }}</a></td>~      <td class="blame-linenum">{{ line.num }}</td>~      <td class="blame-code">{{ line.content }}</td>~    </tr>
Mtemplates/branch_detail.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}/branches">branches</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/branches">branches</a></li>~  <li>{{ branch }}</li>~</ul>~{% endblock %}
~<details>~  <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~  <ul>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.gz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.xz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.zip"><span class="archive-link">{{ repo }}-{{ branch }}.zip</span><span>zip</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.gz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.xz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.zip"><span class="archive-link">{{ repo }}-{{ branch }}.zip</span><span>zip</span></a></li>~  </ul>~</details>~~<div class="repo-nav">-  <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ tip_sha }}/tree" class="repo-nav-item">+  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ tip_sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>~    <span class="repo-nav-item-meta">[{{ tip_sha[..8] }}]</span>
~{% else %}~<div class="commit-log">~  {% for commit in commits %}-  <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-row">+  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-row">~    <span class="commit-sha">{{ commit.sha[..8] }}</span>~    <span class="commit-message">{{ commit.message }}</span>~    <span class="commit-meta"><span>{{ commit.author }}</span><span>{{ commit.date }}</span></span>
Mtemplates/branch_list.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~  <li>branches</li>~</ul>~{% endblock %}
~{% else %}~<div class="branch-list">~  {% for branch in branches %}-  <a href="{{ crate::base_url() }}/{{ repo }}/branches/{{ branch.name }}" class="branch-row">+  <a href="{{ site.base_url }}/{{ repo }}/branches/{{ branch.name }}" class="branch-row">~    <span class="branch-name">{{ branch.name }}</span>~    <span class="branch-date">{% if branch.is_default %}<span class="branch-default">default</span>{% endif %}{{ branch.date }}</span>~  </a>
Mtemplates/commit.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}/commits">commits</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/commits">commits</a></li>~  <li>{{ sha[..8] }}</li>~</ul>~{% endblock %}
~<details>~  <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~  <ul>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.gz</span><span>tar.gz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.xz</span><span>tar.xz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.zip"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.zip</span><span>zip</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.gz</span><span>tar.gz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.xz</span><span>tar.xz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.zip"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.zip</span><span>zip</span></a></li>~  </ul>~</details>~~<div class="repo-nav">-  <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}/tree" class="repo-nav-item">+  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>~    <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>
~{% if !refs.is_empty() %}~<dl class="tag-info">~  <dt>refs</dt>-  <dd>{% for r in refs %}<div class="commit-ref-row"><a href="{{ crate::base_url() }}/{{ repo }}/{% if r.kind == "branch" %}branches{% else %}tags{% endif %}/{{ r.name }}" class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</a></div>{% endfor %}</dd>+  <dd>{% for r in refs %}<div class="commit-ref-row"><a href="{{ site.base_url }}/{{ repo }}/{% if r.kind == "branch" %}branches{% else %}tags{% endif %}/{{ r.name }}" class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</a></div>{% endfor %}</dd>~</dl>~{% endif %}~~<dl class="tag-info">-  <dt>commit</dt><dd><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha }}</a></dd>+  <dt>commit</dt><dd><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha }}</a></dd>~  <dt>parent{% if parents.len() != 1 %}s{% endif %}</dt>-  <dd>{% for (parent_sha, _) in parents %}<div class="commit-parent"><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ parent_sha }}">{{ parent_sha }}</a></div>{% endfor %}</dd>+  <dd>{% for (parent_sha, _) in parents %}<div class="commit-parent"><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ parent_sha }}">{{ parent_sha }}</a></div>{% endfor %}</dd>~  <dt>author</dt><dd>{{ author }}</dd>~  <dt>date</dt><dd>{{ date }}</dd>~</dl>
Mtemplates/commit_list.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~  <li>commits</li>~</ul>~{% endblock %}
~  <div class="commit-graph-svg">{{ graph_svg|safe }}</div>~  <div class="commit-graph-rows">~    {% for commit in commits %}-    <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-graph-row" style="border-left: 3px solid {{ commit.color }}">+    <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-graph-row" style="border-left: 3px solid {{ commit.color }}">~      <span class="commit-message">{% for r in commit.refs %}<span class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</span> {% endfor %}{{ commit.message }}</span>~      <span class="commit-sha">{{ commit.sha[..8] }}</span>~      <span class="commit-date">{{ commit.date }}</span>
Mtemplates/error.html
~    <meta charset="UTF-8">~    <meta name="viewport" content="width=device-width, initial-scale=1.0">~    <title>{{ status }} - {{ kind }}</title>-    <link rel="stylesheet" href="{{ base_url }}/style.css">+    <link rel="stylesheet" href="{{ site.base_url }}/style.css">~</head>~<body>~    <header>~        <ul>-            <li><a href="{{ base_url }}/">{{ crate::hostname() }}</a></li>+            <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~            <li>error</li>~        </ul>~    </header>
Mtemplates/repo.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ name }}">{{ name }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ name }}">{{ name }}</a></li>~</ul>~{% endblock %}~
~<details>~  <summary><svg class="summary-icon"><use href="#icon-copy"/></svg>Clone</summary>~  <ul>-    <li><button data-url="git@{{ crate::hostname() }}:{{ crate::ssh_prefix() }}{{ name }}.git"><span>git@{{ crate::hostname() }}:{{ crate::ssh_prefix() }}{{ name }}.git</span><span>SSH</span></button></li>-    <li><button data-url="https://{{ crate::hostname() }}{{ crate::base_url() }}/{{ name }}.git"><span>https://{{ crate::hostname() }}/{{ name }}.git</span><span>HTTPS</span></button></li>+    <li><button data-url="git@{{ site.hostname }}:{{ site.ssh_prefix }}{{ name }}.git"><span>git@{{ site.hostname }}:{{ site.ssh_prefix }}{{ name }}.git</span><span>SSH</span></button></li>+    <li><button data-url="https://{{ site.hostname }}{{ site.base_url }}/{{ name }}.git"><span>https://{{ site.hostname }}/{{ name }}.git</span><span>HTTPS</span></button></li>~  </ul>~</details>~
~<details>~  <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~  <ul>-    <li><a href="{{ crate::base_url() }}/{{ name }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.gz"><span class="archive-link">{{ name }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ name }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.xz"><span class="archive-link">{{ name }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ name }}/archive/zip/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.zip"><span class="archive-link">{{ name }}-{{ branch }}.zip</span><span>zip</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.gz"><span class="archive-link">{{ name }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.xz"><span class="archive-link">{{ name }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/archive/zip/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.zip"><span class="archive-link">{{ name }}-{{ branch }}.zip</span><span>zip</span></a></li>~  </ul>~</details>~{% endif %}
~  <summary><svg class="summary-icon"><use href="#icon-branch"/></svg>Branches <span>[{{ branches.len() }}]</span></summary>~  <ul>~    {% for branch in branches.iter().take(5) %}-    <li><a href="{{ crate::base_url() }}/{{ name }}/branches/{{ branch.name }}"><span class="branch-name">{{ branch.name }}</span>{% if branch.is_default %}<span>default</span>{% endif %}<span>{{ branch.date }}</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/branches/{{ branch.name }}"><span class="branch-name">{{ branch.name }}</span>{% if branch.is_default %}<span>default</span>{% endif %}<span>{{ branch.date }}</span></a></li>~    {% endfor %}~    {% if branches.len() > 5 %}-    <li><a href="{{ crate::base_url() }}/{{ name }}/branches" class="see-all"><span>see all {{ branches.len() }} branches</span><span>[&gt;]</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/branches" class="see-all"><span>see all {{ branches.len() }} branches</span><span>[&gt;]</span></a></li>~    {% endif %}~    {% if branches.is_empty() %}~    <li>no branches</li>
~  <summary><svg class="summary-icon"><use href="#icon-tag"/></svg>Tags <span>[{{ tags.len() }}]</span></summary>~  <ul>~    {% for tag in tags.iter().take(5) %}-    <li><a href="{{ crate::base_url() }}/{{ name }}/tags/{{ tag.name }}"><span class="tag-name">{{ tag.name }}</span><span>{{ tag.date }}</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/tags/{{ tag.name }}"><span class="tag-name">{{ tag.name }}</span><span>{{ tag.date }}</span></a></li>~    {% endfor %}~    {% if tags.len() > 5 %}-    <li><a href="{{ crate::base_url() }}/{{ name }}/tags" class="see-all"><span>see all {{ tags.len() }} tags</span><span>[&gt;]</span></a></li>+    <li><a href="{{ site.base_url }}/{{ name }}/tags" class="see-all"><span>see all {{ tags.len() }} tags</span><span>[&gt;]</span></a></li>~    {% endif %}~    {% if tags.is_empty() %}~    <li>no tags</li>
~~<div class="repo-nav">~  {% if let Some(sha) = head_sha %}-  <a href="{{ crate::base_url() }}/{{ name }}/commits/{{ sha }}/tree" class="repo-nav-item">+  <a href="{{ site.base_url }}/{{ name }}/commits/{{ sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>~    <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>~    <span class="repo-nav-item-arrow">[&gt;]</span>~  </a>~  {% endif %}-  <a href="{{ crate::base_url() }}/{{ name }}/commits" class="repo-nav-item">+  <a href="{{ site.base_url }}/{{ name }}/commits" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-commits"/></svg>~    <span>Commits</span>~    <span class="repo-nav-item-meta">[{{ commit_count }}]</span>
Mtemplates/repo_list.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~</ul>~{% endblock %}~
~{% else %}~<div class="repo-list">~  {% for repo in repos %}-  <a href="{{ crate::base_url() }}/{{ repo.name }}" class="repo-card">+  <a href="{{ site.base_url }}/{{ repo.name }}" class="repo-card">~    <span class="repo-name">{{ repo.name }}</span>~    {% if let Some(desc) = &repo.description %}~    <span class="repo-desc">{{ desc }}</span>
Mtemplates/tag_detail.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}/tags">tags</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/tags">tags</a></li>~  <li>{{ tag }}</li>~</ul>~{% endblock %}
~<details>~  <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~  <ul>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.gz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.gz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.gz</span><span>tar.gz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/tar.xz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.xz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.xz</span><span>tar.xz</span></a></li>-    <li><a href="{{ crate::base_url() }}/{{ repo }}/archive/zip/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.zip"><span class="archive-link">{{ repo }}-{{ tag }}.zip</span><span>zip</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.gz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.gz</span><span>tar.gz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.xz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.xz</span><span>tar.xz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.zip"><span class="archive-link">{{ repo }}-{{ tag }}.zip</span><span>zip</span></a></li>~  </ul>~</details>~~<div class="repo-nav">-  <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit_sha }}/tree" class="repo-nav-item">+  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit_sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>~    <span class="repo-nav-item-meta">[{{ commit_sha[..8] }}]</span>
~{% endif %}~~<dl class="tag-info">-  <dt>commit</dt><dd><a class="commit-sha" href="{{ crate::base_url() }}/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>+  <dt>commit</dt><dd><a class="commit-sha" href="{{ site.base_url }}/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>~  <dt>author</dt><dd>{{ commit_author }}</dd>~  <dt>date</dt><dd>{{ commit_date }}</dd>~</dl>
Mtemplates/tag_list.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~  <li>tags</li>~</ul>~{% endblock %}
~{% else %}~<div class="tag-list">~  {% for tag in tags %}-  <a href="{{ crate::base_url() }}/{{ repo }}/tags/{{ tag.name }}" class="tag-row">+  <a href="{{ site.base_url }}/{{ repo }}/tags/{{ tag.name }}" class="tag-row">~    <span class="tag-name">{{ tag.name }}</span>~    <span class="tag-date">{{ tag.date }}</span>~  </a>
Mtemplates/tree.html
~~{% block breadcrumbs %}~<ul>-  <li><a href="{{ crate::base_url() }}/">{{ crate::hostname() }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>~  {% for crumb in breadcrumbs %}~  <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~  {% endfor %}
~      <span>Raw</span>~      <span class="repo-nav-item-arrow">[&gt;]</span>~    </a>-    <a href="{{ crate::base_url() }}/{{ repo }}/commits/{{ sha }}/blame/{{ path }}" class="repo-nav-item">+    <a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}/blame/{{ path }}" class="repo-nav-item">~      <svg class="summary-icon"><use href="#icon-blame"/></svg>~      <span>Blame</span>~      <span class="repo-nav-item-arrow">[&gt;]</span>
Msrc/routes/blame.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, Breadcrumb, RepoName};
~    tree_url: String,~    breadcrumbs: Vec<Breadcrumb>,~    lines: Vec<git::BlameEntry>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders the blame view for a file.
~    let git_repo = git::open_repo(&state.root, &name)?;~    let lines = git::load_blame(&git_repo, &params.sha, &params.path)?;~-    let b = crate::config::base_url();+    let b = &state.site.base_url;~    let tree_url = format!("{b}/{name}/commits/{}/tree/{}", params.sha, params.path);~    let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, &params.sha, &params.path);~
~        tree_url,~        breadcrumbs,~        lines,+        site: state.site,~    })~}
Msrc/routes/branch.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, RepoName};
~struct BranchList {~    repo: String,~    branches: Vec<git::BranchInfo>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Template context for the branch detail (log) page.
~    commits: Vec<git::CommitSummary>,~    after: Option<String>,~    next_cursor: Option<String>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders the full list of branches for a repository.
~) -> Result<impl IntoResponse> {~    let git_repo = git::open_repo(&state.root, &repo)?;~    let branches = git::load_branches(&git_repo);-    Ok(BranchList { repo, branches })+    Ok(BranchList {+        repo,+        branches,+        site: state.site,+    })~}~~/// Renders a paginated log of commits reachable from the tip of the given branch.
~        commits: log.commits,~        after,~        next_cursor: log.next_cursor,+        site: state.site,~    })~}
Msrc/routes/commit.rs
~use gix::bstr::ByteSlice;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use crate::git::{self, GitResultExt as _, commit_date, committer_date};~use crate::routes::{AppState, RepoName};
~    after: Option<String>,~    /// Cursor for the next page of older commits (`None` means no more pages).~    next_cursor: Option<String>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders a paginated commit log with an SVG ancestry graph.
~        commits,~        after,~        next_cursor,+        site: state.site,~    })~}~
~    lines_added: u64,~    lines_removed: u64,~    file_changes: Vec<FileChange>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Displays a single commit: metadata, diff stat, and per-file inline diffs.
~        lines_added,~        lines_removed,~        file_changes,+        site: state.site,~    })~}
Msrc/routes/repo.rs
~~use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html};~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git::{self, commit_date};~use crate::routes::{AppState, RepoName};
~    head_sha: Option<String>,~    default_branch: Option<String>,~    readme: Option<String>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders the detail page for a single repository, including its branches,
~~    let head_sha = head_id.map(|id| id.to_string());~+    let base_url = state.site.base_url.clone();+    let site = state.site;+~    let readme = ["HEAD:README.md", "HEAD:README", "HEAD:readme.md"]~        .iter()~        .find_map(|&spec| {
~                    let dest_url = match &head_sha {~                        Some(sha) if is_relative_url(&dest_url) => format!(~                            "{}/{}/commits/{}/raw/{}",-                            crate::config::base_url(),+                            base_url,~                            name,~                            sha,~                            dest_url
~                    let dest_url = match &head_sha {~                        Some(sha) if is_relative_url(&dest_url) => format!(~                            "{}/{}/commits/{}/tree/{}",-                            crate::config::base_url(),+                            base_url,~                            name,~                            sha,~                            dest_url
~        head_sha,~        default_branch,~        readme,+        site,~    })~}~
~#[template(path = "repo_list.html")]~struct RepoList {~    repos: Vec<RepoSummary>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders the repository list page by scanning the root directory for git
~~    repos.sort_by(|a, b| b.last_commit.cmp(&a.last_commit));~-    Ok(RepoList { repos })+    Ok(RepoList { repos, site: state.site })~}
Msrc/routes/tag.rs
~use axum::response::IntoResponse;~use serde::Deserialize;~+use crate::config::SiteConfig;~use crate::error::Result;~use crate::git;~use crate::routes::{AppState, RepoName};
~struct TagList {~    repo: String,~    tags: Vec<git::TagInfo>,+    site: std::sync::Arc<SiteConfig>,~}~~/// Template context for the tag detail page.
~    commit_sha: String,~    commit_author: String,~    commit_date: String,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders the full list of tags for a repository, sorted newest first.
~) -> Result<impl IntoResponse> {~    let git_repo = git::open_repo(&state.root, &repo)?;~    let tags = git::load_tags(&git_repo);-    Ok(TagList { repo, tags })+    Ok(TagList {+        repo,+        tags,+        site: state.site,+    })~}~~/// Renders the detail page for a single tag.
~        commit_sha: commit.sha,~        commit_author: commit.author,~        commit_date: commit.date,+        site: state.site,~    })~}
Msrc/routes/tree.rs
~use syntect::html::{ClassStyle, ClassedHTMLGenerator};~use syntect::parsing::SyntaxSet;~+use crate::config::SiteConfig;~use crate::error::{Error, Result};~use crate::git::{self, GitResultExt as _};~use crate::routes::{AppState, Breadcrumb, RepoName};
~    content_html: String,~    raw_url: String,~    parent_url: String,+    site: std::sync::Arc<SiteConfig>,~}~~/// Renders a tree or file at `{sha}:{path}` in the given repository.
~        (obj, path)~    };~-    let b = crate::config::base_url();+    let b = &state.site.base_url;~    let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, &params.sha, &resolved_path);~    let raw_url = if resolved_path.is_empty() {~        String::new()
~                content_html: String::new(),~                raw_url,~                parent_url,+                site: state.site,~            }~            .into_response())~        }
~                content_html,~                raw_url,~                parent_url,+                site: state.site,~            }~            .into_response())~        }