From 986e28c220214b50b05f7cf2465f114c44f4e08d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 4 Sep 2026 14:59:38 +0000 Subject: [PATCH] Serve /favicon.ico as a redirect to the configured site icon --- AGENTS.md | 2 +- pagerite/app.py | 4 ++-- pagerite/data.py | 4 ++-- pagerite/files.py | 22 +++++++++++++++++++--- pagerite/views.py | 4 ++-- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 96469bf..2674238 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,6 @@ Server run by CLI entry point `uv run pagerite` (no auto reloads, build needed). ## Conventions - Keep dependencies minimal; add via `uv add` and mention it. -- The public URL space belongs to content (pretty slugs at root). Reserve only `/_` for the machinery (`/_api/`, `/_f/`, `/_assets/`), plus `/favicon.ico` from the build. Slugs are lowercase ASCII letters, digits, hyphens and underscores `[a-z0-9_-]` (the site editor filters input live via `slugify.js`, built on the `transliteration` npm package — unicode folds to ASCII, spaces become hyphens; an empty slug on a new page is derived from its title), may not begin with `_` or `.`, and such URLs are never looked up as content. +- The public URL space belongs to content (pretty slugs at root). Reserve only `/_` for the machinery (`/_api/`, `/_f/`, `/_assets/`), plus `/favicon.ico` (backend redirect to the configured site icon). Slugs are lowercase ASCII letters, digits, hyphens and underscores `[a-z0-9_-]` (the site editor filters input live via `slugify.js`, built on the `transliteration` npm package — unicode folds to ASCII, spaces become hyphens; an empty slug on a new page is derived from its title), may not begin with `_` or `.`, and such URLs are never looked up as content. - No auth in core code; the SSO/reverse proxy gates all of `/_api` (forward-auth) and owns `/auth/` (login/logout, session validation). Pages render identically for everyone; pagerite.js adds the editing UI only after the auth server validates the session. The one keyed exception is `/_translate/{key}` (translator service; `Data.translate_keys`, see docs/localization.md). - Update the relevant MarkDown files when architecture, tooling, or conventions change. diff --git a/pagerite/app.py b/pagerite/app.py index 65c9d4b..8ca2379 100644 --- a/pagerite/app.py +++ b/pagerite/app.py @@ -48,7 +48,7 @@ logger = logging.getLogger(__name__) # Vue build served at the site root, no SPA catch-all (assets only). The # build mirrors the URL space: hashed, immutable files live under -# /_assets/ (assetsDir: '_/assets'), the favicon at /favicon.ico. +# /_assets/ (assetsDir: '_/assets'). frontend = Frontend( Path(__file__).with_name("frontend-build"), spa=False, cached="/_assets/" ) @@ -124,7 +124,7 @@ app.include_router(tracking.router) app.include_router(files.router) # Vue build asset routes are inserted at this position during load(): the -# build mirrors the URL space (/_assets/*, /favicon.ico at the root). +# build mirrors the URL space (/_assets/*). frontend.route(app, "/") # The content catch-all goes last: built assets win over content slugs, diff --git a/pagerite/data.py b/pagerite/data.py index ea3e987..aebfe1b 100644 --- a/pagerite/data.py +++ b/pagerite/data.py @@ -98,8 +98,8 @@ class Data(msgspec.Struct): #: Trusted author content; not sanitized. custom_css: str = "" #: Favicon: content-addressed file name (served at "/_f/{name}"), - #: linked as on every page. Empty = the build's - #: /favicon.ico. + #: linked as on every page; /favicon.ico redirects + #: to it. Empty = no icon (and /favicon.ico 404s). favicon: str = "" #: API keys gating the translator service WebSocket (/_translate/{key}; #: the external forward-auth does not cover that route): key -> display diff --git a/pagerite/files.py b/pagerite/files.py index 1b2e657..73f3c65 100644 --- a/pagerite/files.py +++ b/pagerite/files.py @@ -6,7 +6,8 @@ when compression shrinks the body), served immutable at ``/_f/``. Raster images and SVGs are recompressed into AVIF/WebP/JPEG derivatives (``store_image`` and helpers); the untouched original is kept alongside as ``.orig`` (never served). Routes: upload/delete under -``/_api/files``, the favicon settings endpoints, the ``/_f/`` server with +``/_api/files``, the favicon settings endpoints, the /favicon.ico +redirect to the configured icon, the ``/_f/`` server with Accept-negotiated formats, and the user assets (``/_themes/``, ``/_fonts/``). """ @@ -19,7 +20,7 @@ from pathlib import Path import blake3 from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import Response +from fastapi.responses import RedirectResponse, Response from mediapreview import dispatch from pagerite import views @@ -252,6 +253,20 @@ async def delete_file(name: str) -> None: file_store.delete(name) +@router.get("/favicon.ico", include_in_schema=False) +async def favicon_ico() -> Response: + """The conventional /favicon.ico: redirect to the configured site icon. + + Browsers request this path on their own (tabs, bookmarks, feeds and + other non-HTML contexts) regardless of the pages + carry. Redirect to the icon's store URL, which negotiates the format + and caches immutably; 404 when no custom icon is configured. + """ + if not data.favicon: + raise HTTPException(404) + return RedirectResponse(f"/_f/{data.favicon}") + + @router.put("/_api/settings/favicon") async def put_favicon(request: Request) -> dict[str, str]: """Upload a favicon into the content-addressed store and activate it. @@ -276,7 +291,8 @@ async def put_favicon(request: Request) -> dict[str, str]: @router.delete("/_api/settings/favicon", status_code=204) async def delete_favicon(request: Request) -> None: - """Clear the custom favicon (back to the build's /favicon.ico). + """Clear the custom favicon (/favicon.ico goes back to 404, pages drop + the ). The blob stays in the content-addressed store; only the reference goes. """ diff --git a/pagerite/views.py b/pagerite/views.py index 52b44a3..cc62cba 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -372,8 +372,8 @@ def _layout( doc.meta(property=key, content=value) else: doc.meta(name=key, content=value) - # A custom favicon (from the site editor) is linked explicitly; without - # one, browsers fall back to the build's /favicon.ico by convention. + # A custom favicon (from the site editor) is linked explicitly; + # /favicon.ico redirects to the same store file for non-HTML contexts. if favicon: doc.link(rel="icon", href=f"/_f/{favicon}", id="pagerite-favicon") # Asset URLs for the on-demand bundles (editor, analytics) for