From 508795437f2b90ea7233f11470d12ecb708c827f Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 16 Aug 2026 16:22:39 +0000 Subject: [PATCH] Mirror the URL space in the frontend build layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build with assetsDir: '_/assets' so hashed assets land under frontend-build/_/assets/ and favicon.ico (from frontend/public) at the build root, then serve the whole build directory at the site root again (frontend.route(app, "/"), cached="/_/assets/"). The explicit favicon route is dropped — the Frontend serves it as an ordinary unhashed (no-cache) file. Manifest paths now carry the _/assets/ prefix, so views.py only prepends a slash. --- AGENTS.md | 12 ++++++++---- docs/design-principles.md | 6 ++++-- frontend/vite.config.js | 7 ++++--- pagerite/app.py | 26 +++++++++----------------- pagerite/views.py | 9 +++++---- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1303f2..ee348c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,15 +21,17 @@ not for the public pages. See `docs/design-principles.md` for the design. - `app.py` — the FastAPI app. FastAPI's built-in API docs are disabled (`docs_url`/`redoc_url`/`openapi_url=None`) because `/docs` belongs to our content. Our own routes (content pages, `/_/api/...`, `/_/f/...`, - `/_/assets/...`, `/_/admin`) are registered BEFORE `frontend.route(app, "/_/assets")` is + `/_/admin`) are registered BEFORE `frontend.route(app, "/")` is called: fastapi-vue inserts its file routes at the position where `route()` was called (during `load()` in the lifespan), so anything defined earlier wins. The one exception is the content catch-all `/{path:path}`, registered AFTER `frontend.route()` so that built frontend assets still take priority over content slugs. The `Frontend` is constructed with `spa=False` explicitly: it only serves the built - asset files under `/_/assets/` without a catch-all; an `index.html` in the build - would become a `/_/assets/` route, so leave it out of the build to keep `/` ours. + files without a catch-all. The build mirrors the URL space — hashed + immutable assets under `/_/assets/`, `favicon.ico` at the site root — + and an `index.html` in the build would become a `/` route, so leave it + out of the build to keep `/` ours. - `data.py` — msgspec Structs for the kanta database. The site structure is a tree: `Data.menu` maps top-level slugs to `Node`s, each with `children` keyed by slug — the URL path is the slug chain. The front @@ -108,7 +110,9 @@ not for the public pages. See `docs/design-principles.md` for the design. In dev, modules load from the Vite dev server (`PAGERITE_VITE_URL`), in prod from the hashed build assets resolved via `frontend-build/.vite/manifest.json`. `vite.config.js` builds with - `manifest: true`, `assetsDir: ''` and JS inputs (`src/main.js` and + `manifest: true`, `assetsDir: '_/assets'` (so the build mirrors the URL + space; `frontend/public/favicon.ico` lands at the build root and is + served at `/favicon.ico`) and JS inputs (`src/main.js` and `src/pagerite.js`) so no `index.html` ends up in the build (it would shadow `/`). All outputs are ES modules. vite-plugin-fastapi.js has an auto-upgrade marker — edit `vite.config.js`, not the plugin. diff --git a/docs/design-principles.md b/docs/design-principles.md index 8baef85..0992569 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -30,7 +30,8 @@ evolves. (`/docs/design-principles`-style). The URL space is the author's, so reserved prefixes must be kept few and deliberate: everything internal lives under `/_/` (the API at `/_/api/`, uploaded files at `/_/f/`, built - assets at `/_/assets/`, and the admin shell at `/_/admin`). + assets at `/_/assets/`, and the admin shell at `/_/admin`). The only + other reserved root path is `/favicon.ico`, served from the build. - **Single user, trusted author.** No auth concerns in the core design. Everything published is public; only editing tools will later sit behind access control (external SSO when that time comes). The author is trusted @@ -75,7 +76,8 @@ evolves. **per-page configurable**: `Node.banner` holds an arbitrary trusted HTML snippet (an image, a styled div, canvas + script — anything), resolved by walking up the node's ancestors to the front page; when nothing in the - chain sets one, the default `/_/assets/banner-*.svg` artwork shows. + chain sets one, the default `banner.svg` artwork (inlined into the + stylesheet by the build) shows. - **Fetch-navigation.** Links are plain ``; a small script (`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a8504b9..ccaf725 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -27,10 +27,11 @@ export default defineConfig({ }, }, build: { - // Emit hashed assets at the root of frontend-build so the backend can - // serve them under /_/assets/{file} without a nested /assets directory. + // Mirror the URL space in the build output: hashed files land under + // frontend-build/_/assets/ and the Frontend serves the build directory + // at the site root (frontend/public/favicon.ico -> /favicon.ico). manifest: true, - assetsDir: '', + assetsDir: '_/assets', rollupOptions: { input: { main: fileURLToPath(new URL('./src/main.js', import.meta.url)), diff --git a/pagerite/app.py b/pagerite/app.py index 064720e..9af1d94 100644 --- a/pagerite/app.py +++ b/pagerite/app.py @@ -1,7 +1,7 @@ """FastAPI application: server-rendered content pages plus Vue assets. Route ordering matters: our routes are defined before -``frontend.route(app, "/_/assets")`` is called, so they take priority over +``frontend.route(app, "/")`` is called, so they take priority over the asset routes that fastapi-vue inserts at that position during ``load()``. The content catch-all (``/{path:path}``) is defined last, so built frontend assets still win over content slugs; anything unmatched falls @@ -21,7 +21,7 @@ from pathlib import Path import blake3 from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect -from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response +from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi_vue import Frontend from kanta import Kanta from pydantic import BaseModel @@ -45,11 +45,11 @@ DB_PATH = os.getenv("PAGERITE_DB", "pagerite.kanta") data = Data() kanta = Kanta(DB_PATH, data) -# Vue build assets served under /_/assets/, no SPA catch-all (assets only). -# With assetsDir: '', all files are emitted at the build root, so cached="/" -# marks every built file immutable. +# 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. BUILD_DIR = Path(__file__).with_name("frontend-build") -frontend = Frontend(BUILD_DIR, spa=False, cached="/") +frontend = Frontend(BUILD_DIR, spa=False, cached="/_/assets/") def _hash_name(body: bytes, orig: str) -> str: @@ -475,23 +475,15 @@ async def admin() -> HTMLResponse: return HTMLResponse(views.render_editor()) -@app.get("/favicon.ico") -async def favicon() -> FileResponse: - """Serve the favicon copied from frontend/public by the Vite build.""" - file = BUILD_DIR / "favicon.ico" - if not file.is_file(): - raise HTTPException(404) - return FileResponse(file) - - @app.get("/") async def front_page(request: Request) -> Response: """Render the front page (slug path "").""" return await show_page(request, "") -# Vue build asset routes are inserted at this position during load(). -frontend.route(app, "/_/assets") +# Vue build asset routes are inserted at this position during load(): the +# build mirrors the URL space (/_/assets/*, /favicon.ico at the root). +frontend.route(app, "/") @app.get("/{path:path}", response_model=None) diff --git a/pagerite/views.py b/pagerite/views.py index 1388851..2a10584 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -243,8 +243,9 @@ def _page_assets() -> tuple[list[str], list[str]]: ) manifest = json.loads((BUILD / ".vite/manifest.json").read_text()) entry = manifest["src/pagerite.js"] - styles = [f"/_/assets/{css}" for css in entry.get("css", [])] - return [f"/_/assets/{entry['file']}"], styles + # Manifest paths already carry the _/assets/ prefix (assetsDir). + styles = [f"/{css}" for css in entry.get("css", [])] + return [f"/{entry['file']}"], styles def _editor_assets() -> tuple[list[str], list[str]]: @@ -260,8 +261,8 @@ def _editor_assets() -> tuple[list[str], list[str]]: ) manifest = json.loads((BUILD / ".vite/manifest.json").read_text()) entry = manifest["src/main.js"] - styles = [f"/_/assets/{css}" for css in entry.get("css", [])] - return [f"/_/assets/{entry['file']}"], styles + styles = [f"/{css}" for css in entry.get("css", [])] + return [f"/{entry['file']}"], styles def render_editor() -> str: