From eabe866d31e4d4e6a59a137a35d081958a5cffa4 Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 13 Jan 2026 14:17:42 -0600 Subject: [PATCH] feat: enforce canonical URLs by redirecting trailing slashes --- src/proxy.rs | 10 ++++++++++ web/src/routes/+layout.server.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/proxy.rs b/src/proxy.rs index a73042a..1b80575 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -22,6 +22,16 @@ pub async fn isr_handler(State(state): State>, req: Request) -> Re let path = uri.path(); let query = uri.query(); + // Redirect trailing slashes to non-trailing (except root) + if path.len() > 1 && path.ends_with('/') { + let normalized = path.trim_end_matches('/'); + let redirect_uri = match query { + Some(q) => format!("{normalized}?{q}"), + None => normalized.to_string(), + }; + return axum::response::Redirect::permanent(&redirect_uri).into_response(); + } + if method != axum::http::Method::GET && method != axum::http::Method::HEAD { tracing::warn!(method = %method, path = %path, "Non-GET/HEAD request to non-API route"); diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts index 25c7c26..775c7c8 100644 --- a/web/src/routes/+layout.server.ts +++ b/web/src/routes/+layout.server.ts @@ -4,6 +4,8 @@ import { apiFetch } from "$lib/api.server"; import type { SiteSettings } from "$lib/admin-types"; import { building } from "$app/environment"; +export const trailingSlash = "never"; + const DEFAULT_SETTINGS: SiteSettings = { identity: { siteTitle: "xevion.dev",