Mirror the URL space in the frontend build layout
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.
This commit is contained in:
@@ -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
|
- `app.py` — the FastAPI app. FastAPI's built-in API docs are disabled
|
||||||
(`docs_url`/`redoc_url`/`openapi_url=None`) because `/docs` belongs to
|
(`docs_url`/`redoc_url`/`openapi_url=None`) because `/docs` belongs to
|
||||||
our content. Our own routes (content pages, `/_/api/...`, `/_/f/...`,
|
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
|
called: fastapi-vue inserts its file routes at the position where
|
||||||
`route()` was called (during `load()` in the lifespan), so anything
|
`route()` was called (during `load()` in the lifespan), so anything
|
||||||
defined earlier wins. The one exception is the content catch-all
|
defined earlier wins. The one exception is the content catch-all
|
||||||
`/{path:path}`, registered AFTER `frontend.route()` so that built
|
`/{path:path}`, registered AFTER `frontend.route()` so that built
|
||||||
frontend assets still take priority over content slugs. The `Frontend`
|
frontend assets still take priority over content slugs. The `Frontend`
|
||||||
is constructed with `spa=False` explicitly: it only serves the built
|
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
|
files without a catch-all. The build mirrors the URL space — hashed
|
||||||
would become a `/_/assets/` route, so leave it out of the build to keep `/` ours.
|
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
|
- `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
|
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
|
`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 dev, modules load from the Vite dev server (`PAGERITE_VITE_URL`),
|
||||||
in prod from the hashed build assets resolved via
|
in prod from the hashed build assets resolved via
|
||||||
`frontend-build/.vite/manifest.json`. `vite.config.js` builds with
|
`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
|
`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
|
`/`). All outputs are ES modules. vite-plugin-fastapi.js has an
|
||||||
auto-upgrade marker — edit `vite.config.js`, not the plugin.
|
auto-upgrade marker — edit `vite.config.js`, not the plugin.
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ evolves.
|
|||||||
(`/docs/design-principles`-style). The URL space is the author's, so
|
(`/docs/design-principles`-style). The URL space is the author's, so
|
||||||
reserved prefixes must be kept few and deliberate: everything internal
|
reserved prefixes must be kept few and deliberate: everything internal
|
||||||
lives under `/_/` (the API at `/_/api/`, uploaded files at `/_/f/`, built
|
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.
|
- **Single user, trusted author.** No auth concerns in the core design.
|
||||||
Everything published is public; only editing tools will later sit behind
|
Everything published is public; only editing tools will later sit behind
|
||||||
access control (external SSO when that time comes). The author is trusted
|
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
|
**per-page configurable**: `Node.banner` holds an arbitrary trusted HTML
|
||||||
snippet (an image, a styled div, canvas + script — anything), resolved by
|
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
|
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 href>`; a small script
|
- **Fetch-navigation.** Links are plain `<a href>`; a small script
|
||||||
(`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the
|
(`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the
|
||||||
page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions
|
page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Emit hashed assets at the root of frontend-build so the backend can
|
// Mirror the URL space in the build output: hashed files land under
|
||||||
// serve them under /_/assets/{file} without a nested /assets directory.
|
// frontend-build/_/assets/ and the Frontend serves the build directory
|
||||||
|
// at the site root (frontend/public/favicon.ico -> /favicon.ico).
|
||||||
manifest: true,
|
manifest: true,
|
||||||
assetsDir: '',
|
assetsDir: '_/assets',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: fileURLToPath(new URL('./src/main.js', import.meta.url)),
|
main: fileURLToPath(new URL('./src/main.js', import.meta.url)),
|
||||||
|
|||||||
+9
-17
@@ -1,7 +1,7 @@
|
|||||||
"""FastAPI application: server-rendered content pages plus Vue assets.
|
"""FastAPI application: server-rendered content pages plus Vue assets.
|
||||||
|
|
||||||
Route ordering matters: our routes are defined before
|
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 asset routes that fastapi-vue inserts at that position during ``load()``.
|
||||||
The content catch-all (``/{path:path}``) is defined last, so built
|
The content catch-all (``/{path:path}``) is defined last, so built
|
||||||
frontend assets still win over content slugs; anything unmatched falls
|
frontend assets still win over content slugs; anything unmatched falls
|
||||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import blake3
|
import blake3
|
||||||
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
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 fastapi_vue import Frontend
|
||||||
from kanta import Kanta
|
from kanta import Kanta
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -45,11 +45,11 @@ DB_PATH = os.getenv("PAGERITE_DB", "pagerite.kanta")
|
|||||||
data = Data()
|
data = Data()
|
||||||
kanta = Kanta(DB_PATH, data)
|
kanta = Kanta(DB_PATH, data)
|
||||||
|
|
||||||
# Vue build assets served under /_/assets/, no SPA catch-all (assets only).
|
# Vue build served at the site root, no SPA catch-all (assets only). The
|
||||||
# With assetsDir: '', all files are emitted at the build root, so cached="/"
|
# build mirrors the URL space: hashed, immutable files live under
|
||||||
# marks every built file immutable.
|
# /_/assets/ (assetsDir: '_/assets'), the favicon at /favicon.ico.
|
||||||
BUILD_DIR = Path(__file__).with_name("frontend-build")
|
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:
|
def _hash_name(body: bytes, orig: str) -> str:
|
||||||
@@ -475,23 +475,15 @@ async def admin() -> HTMLResponse:
|
|||||||
return HTMLResponse(views.render_editor())
|
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("/")
|
@app.get("/")
|
||||||
async def front_page(request: Request) -> Response:
|
async def front_page(request: Request) -> Response:
|
||||||
"""Render the front page (slug path "")."""
|
"""Render the front page (slug path "")."""
|
||||||
return await show_page(request, "")
|
return await show_page(request, "")
|
||||||
|
|
||||||
|
|
||||||
# Vue build asset routes are inserted at this position during load().
|
# Vue build asset routes are inserted at this position during load(): the
|
||||||
frontend.route(app, "/_/assets")
|
# build mirrors the URL space (/_/assets/*, /favicon.ico at the root).
|
||||||
|
frontend.route(app, "/")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{path:path}", response_model=None)
|
@app.get("/{path:path}", response_model=None)
|
||||||
|
|||||||
+5
-4
@@ -243,8 +243,9 @@ def _page_assets() -> tuple[list[str], list[str]]:
|
|||||||
)
|
)
|
||||||
manifest = json.loads((BUILD / ".vite/manifest.json").read_text())
|
manifest = json.loads((BUILD / ".vite/manifest.json").read_text())
|
||||||
entry = manifest["src/pagerite.js"]
|
entry = manifest["src/pagerite.js"]
|
||||||
styles = [f"/_/assets/{css}" for css in entry.get("css", [])]
|
# Manifest paths already carry the _/assets/ prefix (assetsDir).
|
||||||
return [f"/_/assets/{entry['file']}"], styles
|
styles = [f"/{css}" for css in entry.get("css", [])]
|
||||||
|
return [f"/{entry['file']}"], styles
|
||||||
|
|
||||||
|
|
||||||
def _editor_assets() -> tuple[list[str], list[str]]:
|
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())
|
manifest = json.loads((BUILD / ".vite/manifest.json").read_text())
|
||||||
entry = manifest["src/main.js"]
|
entry = manifest["src/main.js"]
|
||||||
styles = [f"/_/assets/{css}" for css in entry.get("css", [])]
|
styles = [f"/{css}" for css in entry.get("css", [])]
|
||||||
return [f"/_/assets/{entry['file']}"], styles
|
return [f"/{entry['file']}"], styles
|
||||||
|
|
||||||
|
|
||||||
def render_editor() -> str:
|
def render_editor() -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user