Serve /favicon.ico as a redirect to the configured site icon
This commit is contained in:
@@ -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.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 <link rel="icon"> on every page. Empty = the build's
|
||||
#: /favicon.ico.
|
||||
#: linked as <link rel="icon"> 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
|
||||
|
||||
+19
-3
@@ -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
|
||||
``<hash>.orig<ext>`` (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 <link rel="icon"> 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 <link rel="icon">).
|
||||
|
||||
The blob stays in the content-addressed store; only the reference goes.
|
||||
"""
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user