diff --git a/.gitignore b/.gitignore index 483cd46..603cfc5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ wheels/ .venv .ruff_cache/ /pagerite/frontend-build -pagerite.kanta + +# Site +*.kantadb diff --git a/AGENTS.md b/AGENTS.md index b19b1ae..e78a8cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,8 +20,8 @@ not for the public pages. See `docs/design-principles.md` for the design. - Avoid running the server yourself, ask the user to test - `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/...`, - `/_/admin`) are registered BEFORE `frontend.route(app, "/")` is + our content. Our own routes (content pages, `/_api/...`, `/_f/...`, + `/_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 @@ -29,7 +29,7 @@ not for the public pages. See `docs/design-principles.md` for the design. frontend assets still take priority over content slugs. The `Frontend` is constructed with `spa=False` explicitly: it only serves the built files without a catch-all. The build mirrors the URL space — hashed - immutable assets under `/_/assets/`, `favicon.ico` at the site root — + 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 @@ -49,15 +49,15 @@ not for the public pages. See `docs/design-principles.md` for the design. the `Data` object; reads are plain attribute access, writes in `kanta.transaction(...)`. `Data.files` is a content-addressed store (blake3[:12] + extension) - mapping file names to bytes, served at `/_/f/{name}` with immutable - caching; pages reference files by absolute `/_/f/` URLs so hierarchy + mapping file names to bytes, served at `/_f/{name}` with immutable + caching; pages reference files by absolute `/_f/` URLs so hierarchy moves never break them. `Node.banner` is a raw trusted HTML snippet for the header banner (img, styled div, canvas+script...); empty inherits from the node's ancestors (front page last), then the default banner.svg artwork. `Data.version` is bumped on every write and embedded in page ETags so nav-affecting changes invalidate caches. `Data.brand` is the site name (header link + `` suffix), editable - in the site editor via `/_/api/settings`; empty = no header link and + in the site editor via `/_api/settings`; empty = no header link and no `<title>` suffix. - `markdown.py` — markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists plugins). Custom image rule: relative srcs @@ -80,20 +80,20 @@ not for the public pages. See `docs/design-principles.md` for the design. code copy buttons. The backend links the shared CSS as two separate stylesheets (base and theme) so they can be swapped or augmented. - `assets/` — shared styles and data files built by Vite and served hashed - under `/_/assets/`: `pagerite.css` (base layout + conservative variables), + under `/_assets/`: `pagerite.css` (base layout + conservative variables), `themes/purple/theme.css` (the purple/dark theme override), `pygments.css`, `banner.svg` and `fonts/` (self-hosted Fraunces/Literata/Fira Code variable woff2). The `::view-transition*` block at the end of `pagerite.css` (from termotohtori.fi) is fragile — do not tweak. - Vite builds ES-module `.js` outputs; the backend renders `<script type="module">` for them (module scripts defer by default). - - The database file is `pagerite.kanta` in the cwd (`PAGERITE_DB` + - The database file is `pagerite.kantadb` in the cwd (`PAGERITE_DB` overrides); gitignored. Do not delete it without asking. - `scripts/fastapi-vue/` — helper scripts from the fastapi-vue template (build hook etc.), do not edit. - `frontend/` — the Vue editor as **two separate apps** mounted in their own host divs created inside the static document: `PageEditor.vue` - (CodeMirror + server-rendered preview over WebSocket `/_/api/ws/editor`, + (CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor scroll drives document scroll) opened by the article pen — it edits content and title only, never the path — and `SiteEditor.vue` (site brand + banner HTML edited in @@ -115,7 +115,7 @@ not for the public pages. See `docs/design-principles.md` for the design. two pens swap the docked panel for the other editor; clicking the open editor's own pen closes it. Normally dynamic-imported onto the content page by pagerite.js when a 🖊️ edit link is clicked (the link carries - `data-editor-src`/`data-editor-css`/`data-editor-mode`); the `/_/admin` + `data-editor-src`/`data-editor-css`/`data-editor-mode`); the `/_admin` route (page selected by location hash) is the no-JS-import fallback shell rendered by `views.render_editor` and keeps its own preview pane. In dev, modules load from the Vite dev server (`PAGERITE_VITE_URL`), @@ -172,7 +172,7 @@ not for the public pages. See `docs/design-principles.md` for the design. - 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 (files, API, built assets, admin), plus + only `/_` for the machinery (`/_api/`, `/_f/`, `/_assets/`, `/_admin`), 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 diff --git a/README.md b/README.md index 50c72b2..c73fc26 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ uv run pagerite # serves the built frontend uv run scripts/devserver.py # dev mode with auto reloads (no build needed) ``` -The database lives in `pagerite.kanta` in the working directory +The database lives in `pagerite.kantadb` in the working directory (`PAGERITE_DB` overrides). On startup, demo pages from `pagerite/seed.py` are added only if missing. diff --git a/docs/design-principles.md b/docs/design-principles.md index 1803627..a485f05 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -29,8 +29,7 @@ evolves. directly at the site root; structured content may nest (`/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`). The only + lives under `/_` (`/_api/`, `/_f/`, `/_assets/`, `/_admin`). The only other reserved root path is `/favicon.ico`, served from the build. Slugs are lowercase ASCII letters, digits, hyphens and underscores (`[a-z0-9_-]`; input is transliterated and filtered as you type, and a @@ -55,16 +54,16 @@ evolves. lists, task lists, brace-attributes; tables and strikethrough from the default preset), with `html=True` for raw passthrough. Fenced code blocks are highlighted server-side with **Pygments** (github-dark palette in - `/_/assets/pygments-*.css`); a JS copy button appears on hover. Should this + `/_assets/pygments-*.css`); a JS copy button appears on hover. Should this prove limiting, we implement our own renderer on top of html5tagger, which we already use for all HTML generation. -- **Files are content-addressed.** Uploads (`PUT /_/api/files/{filename}`) +- **Files are content-addressed.** Uploads (`PUT /_api/files/{filename}`) are stored by content hash — blake3, first 6 bytes hex + original - extension — and served immutable from `/_/f/{hash}.ext`. Absolute URLs + extension — and served immutable from `/_f/{hash}.ext`. Absolute URLs that survive page renames and dedupe identical content; pages no longer own files. An image with a title becomes a `<figure>` with `<figcaption>`. Positioning is by attribute classes: - `![alt](/_/f/….avif "Caption"){.right}` — `{.right}`, `{.left}` float, + `![alt](/_f/….avif "Caption"){.right}` — `{.right}`, `{.left}` float, `{.wide}` goes full bleed (viewport edge to edge, or up to the docked editor; the sidebar stacks on top of it); plain attributes like `width=300` work too. @@ -116,7 +115,7 @@ evolves. fresh value halfway between its new siblings; all other items keep theirs). New pages append at the end of their menu. Structure edits (reorder, move/rename with the whole subtree, retitle) go through - `POST /_/api/structure` and the editor's structure panel. + `POST /_api/structure` and the editor's structure panel. - Unpublished pages are hidden from both nav and URL access (404). ## Reading experience @@ -139,7 +138,7 @@ evolves. where needed. - Fonts, the shared stylesheet, pygments styles and the default banner SVG live under `frontend/src/assets/` and are emitted as hashed assets under - `/_/assets/` (Fraunces for headings, Literata for body, Fira Code for code — + `/_assets/` (Fraunces for headings, Literata for body, Fira Code for code — variable woff2 files with local `@font-face`). No third-party requests. ## Editing @@ -160,14 +159,14 @@ evolves. reloads the page). The pens are `<button>`s wired up by `pagerite.js` — editing is an action, not a navigation. The editor's WebSocket **reconnects automatically** with local text and pending saves preserved. - A standalone shell also exists at `/_/admin#/path` with its own preview + A standalone shell also exists at `/_admin#/path` with its own preview pane. (All users are trusted authors for now; access control later with SSO.) - **CodeMirror 6** for Markdown editing (no WYSIWYG), title/published controls. Images can be pasted straight into the editor or chosen via a file - input: they upload to the content store (`PUT /_/api/files/...`) and - insert `![alt](/_/f/hash.ext)` at the cursor. + input: they upload to the content store (`PUT /_api/files/...`) and + insert `![alt](/_f/hash.ext)` at the cursor. - The **structure panel** (vue-draggable tree of the whole site, in site mode) covers page management: reorder any menu level, drag across sections, add, delete (two clicks: the button arms, then deletes — no @@ -187,11 +186,11 @@ evolves. is the root row with an empty slug — renaming it away leaves no front page ("/" redirects to the first nav item), and giving another top-level row the empty slug makes it the front page. -- Preview and saving go over a **WebSocket** (`/_/api/ws/editor`) with a +- Preview and saving go over a **WebSocket** (`/_api/ws/editor`) with a stateless JSON protocol (`open`/`render`/`save`; on save all fields are optional and absent ones keep their old values, `move_from` renames), avoiding REST polling and races. Rendering always stays server-side. -- A REST API also exists for scripting, all under `/_/api/`: +- A REST API also exists for scripting, all under `/_api/`: `GET pages` (the full tree), `PUT/DELETE pages/{path}`, `GET/PUT settings` (site brand), `POST structure` (reorder/move/ retitle), file upload/removal via `PUT/DELETE files/{name}`. diff --git a/frontend/src/PageEditor.vue b/frontend/src/PageEditor.vue index 01523b2..5f0a3df 100644 --- a/frontend/src/PageEditor.vue +++ b/frontend/src/PageEditor.vue @@ -1,8 +1,8 @@ <script setup> // Page editor: CodeMirror for Markdown, live server-rendered preview // applied straight into the visible article, saving over one WebSocket -// (/_/api/ws/editor). Docked left of the article on the page itself -// (main.js openEditor) or standalone at /_/admin with its own preview pane. +// (/_api/ws/editor). Docked left of the article on the page itself +// (main.js openEditor) or standalone at /_admin with its own preview pane. // The socket connects when the editor is opened and reconnects with // exponential backoff after a failure; unsaved text and pending saves // survive a disconnect. Editor scroll drives the document scroll, keeping the @@ -109,7 +109,7 @@ function insertAtCursor(text) { async function uploadImage(file) { if (!file) return const name = file.name.replace(/[^\w.-]/g, '-') - const res = await fetch(`/_/api/files/${encodeURIComponent(name)}`, { method: 'PUT', body: file }) + const res = await fetch(`/_api/files/${encodeURIComponent(name)}`, { method: 'PUT', body: file }) if (res.ok) { const { path: stored } = await res.json() const alt = name.replace(/\.[^.]+$/, '') @@ -223,7 +223,7 @@ function syncScroll() { function connect() { ws = new WebSocket( - `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_/api/ws/editor`, + `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_api/ws/editor`, ) ws.onmessage = onMessage ws.onopen = () => { diff --git a/frontend/src/SiteEditor.vue b/frontend/src/SiteEditor.vue index 19601e8..a71fb95 100644 --- a/frontend/src/SiteEditor.vue +++ b/frontend/src/SiteEditor.vue @@ -6,7 +6,7 @@ // no save button, no edit mode. Focusing a page's row navigates to it in // place (no transitions). // -// The tree comes from the server nested (GET /_/api/pages); every node is +// The tree comes from the server nested (GET /_api/pages); every node is // real — a label with a title and slug, with content (landing page) or // without (category whose URL renders a placeholder page). The front page // is a top-level row with an empty slug, not the parent of the others. @@ -206,7 +206,7 @@ async function commitPending() { const loc = locatePending(tree.value, '') const parentPath = loc?.parentPath ?? '' const newPath = parentPath ? `${parentPath}/${slug}` : slug - const res = await fetch(`/_/api/pages/${newPath}`, { + const res = await fetch(`/_api/pages/${newPath}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ @@ -246,7 +246,7 @@ const brand = ref('') async function loadSettings() { try { - brand.value = (await (await fetch('/_/api/settings')).json()).brand + brand.value = (await (await fetch('/_api/settings')).json()).brand } catch { /* keep default */ } } @@ -278,7 +278,7 @@ function onBrandInput() { } async function saveBrand() { - const res = await fetch('/_/api/settings', { + const res = await fetch('/_api/settings', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ brand: brand.value }), @@ -308,7 +308,7 @@ function armRemove(node) { } async function removePage(node) { - const res = await fetch(`/_/api/pages/${node.path}`, { method: 'DELETE' }) + const res = await fetch(`/_api/pages/${node.path}`, { method: 'DELETE' }) if (res.ok) { saveError.value = '' refreshPages() @@ -329,7 +329,7 @@ async function removePage(node) { // --- Site structure tree (drag-and-drop ordering/moving) ---------------- async function refreshPages() { try { - tree.value = await (await fetch('/_/api/pages')).json() + tree.value = await (await fetch('/_api/pages')).json() } catch { /* list stays stale; not fatal */ } } @@ -341,7 +341,7 @@ async function errorDetail(res) { } async function postStructure(op) { - const res = await fetch('/_/api/structure', { + const res = await fetch('/_api/structure', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(op), @@ -469,7 +469,7 @@ async function uploadBannerMedia(file) { // Banner media goes to the shared content store, like article images. if (!file || !/^(image|video)\//.test(file.type)) return const name = file.name.replace(/[^\w.-]/g, '-') - const res = await fetch(`/_/api/files/${encodeURIComponent(name)}`, { method: 'PUT', body: file }) + const res = await fetch(`/_api/files/${encodeURIComponent(name)}`, { method: 'PUT', body: file }) if (!res.ok) { return } @@ -526,7 +526,7 @@ function onKeydown(ev) { function connect() { ws = new WebSocket( - `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_/api/ws/editor`, + `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_api/ws/editor`, ) ws.onmessage = onMessage ws.onopen = () => { diff --git a/frontend/src/StructureTree.vue b/frontend/src/StructureTree.vue index 98b0ec2..a45aed1 100644 --- a/frontend/src/StructureTree.vue +++ b/frontend/src/StructureTree.vue @@ -1,6 +1,6 @@ <script setup> // Recursive site-structure tree with drag-and-drop ordering (vue-draggable). -// Nodes come from the server (GET /_/api/pages via SiteEditor.vue) as +// Nodes come from the server (GET /_api/pages via SiteEditor.vue) as // {slug, path, title, order, published, has_content, children}. // Every node is real: a label whose title and slug are always editable // inline — the title saves while typing (and focusing it opens the page), diff --git a/frontend/src/main.js b/frontend/src/main.js index e265084..97ebede 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -4,7 +4,7 @@ // editing with the preview rendered into the visible article. // - SiteEditor ("site" mode): pen on the banner — banner HTML editing // (previewed into the real banner) and the site structure tree. -// The standalone /_/admin shell (#app in the DOM) mounts PageEditor with the +// The standalone /_admin shell (#app in the DOM) mounts PageEditor with the // page selected by location hash, as a no-dynamic-import fallback. if (import.meta.env.DEV) { import("./assets/pagerite.css"); @@ -50,7 +50,7 @@ export function closeEditor() { const shell = document.getElementById('app') if (shell) { - // Standalone /_/admin shell: mount into it and follow the location hash. + // Standalone /_admin shell: mount into it and follow the location hash. host = shell createApp(PageEditor, { pagePath: location.hash.replace(/^#\/?/, '').replace(/\/$/, ''), diff --git a/frontend/src/pagerite.js b/frontend/src/pagerite.js index 05e7d8c..c1f3538 100644 --- a/frontend/src/pagerite.js +++ b/frontend/src/pagerite.js @@ -132,7 +132,7 @@ function preload() { const urls = new Set(); for (const a of document.querySelectorAll('#nav a[href^="/"], #main a[href^="/"]')) { - if (!a.pathname.startsWith("/_/admin")) urls.add(a.pathname); + if (!a.pathname.startsWith("/_admin")) urls.add(a.pathname); } for (const url of urls) { if (url === location.pathname) continue; @@ -251,7 +251,7 @@ if (url.origin !== location.origin) return; // Same-page anchor links (footnotes etc.): let the browser handle them if (url.pathname === location.pathname && url.hash) return; - if (url.pathname.startsWith("/_/")) return; + if (url.pathname.startsWith("/_")) return; ev.preventDefault(); load(url); }); @@ -270,7 +270,7 @@ try { const body = { path, index }; if (editor) body.markdown = editor.getMarkdown(); - const res = await fetch("/_/api/toggle-task", { + const res = await fetch("/_api/toggle-task", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c1346e3..9021c18 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -9,29 +9,29 @@ const backendUrl = process.env.PAGERITE_BACKEND_URL || 'http://localhost:3200' // Proxy content pages (/slug, /path/to/slug) to the FastAPI backend in dev. // Excludes Vite internals (/@..., /src, /node_modules, /__...) and the -// backend's /_ prefix. /_/api and /_/f are handled by the fastapi-vue plugin; -// /_/admin is proxied explicitly below. +// backend's /_ prefix. /_api and /_f are handled by the fastapi-vue plugin; +// /_admin is proxied explicitly below. const CONTENT_PROXY = '^\\/(?!_|@|src|node_modules|__)(?:[^./?]+(?:\\/[^./?]+)*)?(?:\\?.*)?$' // https://vite.dev/config/ export default defineConfig({ plugins: [ - fastapiVue({ paths: ["/_/api", "/_/f"] }), + fastapiVue({ paths: ["/_api", "/_f"] }), vue(), vueDevTools(), ], server: { proxy: { - "/_/admin": { target: backendUrl, changeOrigin: false }, + "/_admin": { target: backendUrl, changeOrigin: false }, [CONTENT_PROXY]: { target: backendUrl, changeOrigin: false }, }, }, build: { // Mirror the URL space in the build output: hashed files land under - // frontend-build/_/assets/ and the Frontend serves the build directory + // frontend-build/_assets/ and the Frontend serves the build directory // at the site root (frontend/public/favicon.ico -> /favicon.ico). manifest: true, - assetsDir: '_/assets', + 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 94dde4f..b7989a9 100644 --- a/pagerite/app.py +++ b/pagerite/app.py @@ -40,7 +40,7 @@ from pagerite.data import ( ) from pagerite.markdown import has_h1, render, toggle_task -DB_PATH = os.getenv("PAGERITE_DB", "pagerite.kanta") +DB_PATH = os.getenv("PAGERITE_DB", "pagerite.kantadb") # Our own data root; kanta edits it in place, reads are plain attribute access. data = Data() @@ -48,9 +48,9 @@ kanta = Kanta(DB_PATH, data) # 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'), the favicon at /favicon.ico. BUILD_DIR = Path(__file__).with_name("frontend-build") -frontend = Frontend(BUILD_DIR, spa=False, cached="/_/assets/") +frontend = Frontend(BUILD_DIR, spa=False, cached="/_assets/") def _hash_name(body: bytes, orig: str) -> str: @@ -60,12 +60,12 @@ def _hash_name(body: bytes, orig: str) -> str: def _store_seed_file(markdown: str, banner: str, orig: str, body: bytes) -> tuple[str, str]: - """Store a seed file content-addressed and point references at /_/f/.""" + """Store a seed file content-addressed and point references at /_f/.""" name = _hash_name(body, orig) data.files.setdefault(name, body) - markdown = markdown.replace(f"]({orig}", f"](/_/f/{name}") - banner = banner.replace(f'src="/{orig}"', f'src="/_/f/{name}"') - banner = banner.replace(f'src="{orig}"', f'src="/_/f/{name}"') + markdown = markdown.replace(f"]({orig}", f"](/_f/{name}") + banner = banner.replace(f'src="/{orig}"', f'src="/_f/{name}"') + banner = banner.replace(f'src="{orig}"', f'src="/_f/{name}"') return markdown, banner @@ -163,13 +163,13 @@ class PageIn(BaseModel): banner: str | None = None # None keeps the existing banner -@app.get("/_/api/health") +@app.get("/_api/health") async def health_check() -> dict[str, str]: """Return backend status for health monitoring.""" return {"status": "ok"} -@app.get("/_/api/pages") +@app.get("/_api/pages") async def list_pages() -> list[dict]: """The site tree for the structure editor (all nodes, drafts included). @@ -194,7 +194,7 @@ async def list_pages() -> list[dict]: return dump(data.menu, "") -@app.put("/_/api/pages/{path:path}", status_code=204) +@app.put("/_api/pages/{path:path}", status_code=204) async def save_page(path: str, page: PageIn) -> None: """Create or replace the page at a slug path ("" or "/" = front page). @@ -236,7 +236,7 @@ class StructureOp(BaseModel): title: str | None = None -@app.post("/_/api/structure", status_code=204) +@app.post("/_api/structure", status_code=204) async def update_structure(op: StructureOp) -> None: """Apply one structure operation (see StructureOp).""" path = op.path.strip("/") @@ -279,7 +279,7 @@ async def update_structure(op: StructureOp) -> None: data.version += 1 -@app.get("/_/api/settings") +@app.get("/_api/settings") async def get_settings() -> dict[str, str]: """Site-wide settings (the brand text).""" return {"brand": data.brand} @@ -291,7 +291,7 @@ class SettingsIn(BaseModel): brand: str -@app.put("/_/api/settings", status_code=204) +@app.put("/_api/settings", status_code=204) async def put_settings(settings: SettingsIn) -> None: """Update site-wide settings; bumps the version so ETags invalidate.""" with kanta.transaction("update settings"): @@ -307,7 +307,7 @@ class ToggleTaskIn(BaseModel): markdown: str | None = None -@app.post("/_/api/toggle-task") +@app.post("/_api/toggle-task") async def toggle_task_endpoint(body: ToggleTaskIn) -> dict[str, str]: """Toggle the Nth task-list checkbox in a page's Markdown source. @@ -337,12 +337,12 @@ async def toggle_task_endpoint(body: ToggleTaskIn) -> dict[str, str]: return {"markdown": new_markdown} -@app.put("/_/api/files/{name}") +@app.put("/_api/files/{name}") async def upload_file(name: str, request: Request) -> dict[str, str]: """Store an upload (image, video...) in the content-addressed store. The stored name is a blake3 hash prefix + the original extension, - served immutable at "/_/f/{name}"; returns {"path": "/_/f/..."}. + served immutable at "/_f/{name}"; returns {"path": "/_f/..."}. """ if "/" in name or name in {".", ".."}: raise HTTPException(400, "bad file name") @@ -351,10 +351,10 @@ async def upload_file(name: str, request: Request) -> dict[str, str]: with kanta.transaction("upload file", extra=name): data.files[stored] = body data.version += 1 - return {"path": f"/_/f/{stored}"} + return {"path": f"/_f/{stored}"} -@app.delete("/_/api/files/{name}", status_code=204) +@app.delete("/_api/files/{name}", status_code=204) async def delete_file(name: str) -> None: """Remove a file from the content-addressed store (no refcounting: other pages referencing the same content will 404).""" @@ -365,7 +365,7 @@ async def delete_file(name: str) -> None: data.version += 1 -@app.get("/_/f/{name}") +@app.get("/_f/{name}") async def stored_file(name: str, request: Request) -> Response: """Serve a file from the content-addressed store (immutable: the name is its own hash, so cache forever).""" @@ -382,7 +382,7 @@ async def stored_file(name: str, request: Request) -> Response: ) -@app.delete("/_/api/pages/{path:path}", status_code=204) +@app.delete("/_api/pages/{path:path}", status_code=204) async def delete_page(path: str) -> None: """Delete a node by slug path. @@ -426,7 +426,7 @@ def _check_reserved(path: str) -> None: ) -@app.websocket("/_/api/ws/editor") +@app.websocket("/_api/ws/editor") async def editor_ws(ws: WebSocket) -> None: """Editor session: open pages, render previews, save — over one socket. @@ -549,7 +549,7 @@ async def editor_ws(ws: WebSocket) -> None: pass -@app.get("/_/admin", response_class=HTMLResponse) +@app.get("/_admin", response_class=HTMLResponse) async def admin() -> HTMLResponse: """Serve the editor app shell (Vue mounts into #app).""" return HTMLResponse(views.render_editor()) @@ -562,7 +562,7 @@ async def front_page(request: Request) -> Response: # 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/*, /favicon.ico at the root). frontend.route(app, "/") diff --git a/pagerite/data.py b/pagerite/data.py index 99a75df..10a2aa6 100644 --- a/pagerite/data.py +++ b/pagerite/data.py @@ -70,7 +70,7 @@ class Data(msgspec.Struct): #: Top-level menu items by slug; "" is the front page. menu: dict[str, Node] = {} #: Content-addressed file store: name (blake3 hash prefix + extension) - #: -> bytes, served immutable at "/_/{name}". Absolute URLs that stay + #: -> bytes, served immutable at "/_f/{name}". Absolute URLs that stay #: valid when pages move. files: dict[str, bytes] = {} #: Bumped on every structure/content write, so page ETags (which embed diff --git a/pagerite/markdown.py b/pagerite/markdown.py index fb29a78..8b3fa8f 100644 --- a/pagerite/markdown.py +++ b/pagerite/markdown.py @@ -27,7 +27,7 @@ from pygments.formatters import HtmlFormatter from pygments.lexers import get_lexer_by_name from pygments.util import ClassNotFound -# Styles in /_/assets/pygments-*.css match this formatter (regenerate: +# Styles in /_assets/pygments-*.css match this formatter (regenerate: # HtmlFormatter(style="github-dark").get_style_defs("pre code")) _formatter = HtmlFormatter(style="github-dark", nowrap=True) diff --git a/pagerite/seed.py b/pagerite/seed.py index 0e2615a..a5e038c 100644 --- a/pagerite/seed.py +++ b/pagerite/seed.py @@ -53,12 +53,12 @@ site's layout or scroll effects. ## Images -Upload a file (`PUT /_/api/files/{filename}`) and it lands in the -content-addressed store, served immutable from `/_/f/{hash}.ext` — an +Upload a file (`PUT /_api/files/{filename}`) and it lands in the +content-addressed store, served immutable from `/_f/{hash}.ext` — an absolute URL that survives page moves: ``` -![Abstract shapes](/_/f/....svg "A captioned figure"){.right width=280} +![Abstract shapes](/_f/....svg "A captioned figure"){.right width=280} ``` ![Abstract shapes](shapes.svg "A captioned figure, floated right with an attribute class"){.right width=280} diff --git a/scripts/devserver.py b/scripts/devserver.py index 453a694..cef6b49 100755 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -22,7 +22,7 @@ from devutil import ( DEFAULT_VITE_PORT = 3100 DEFAULT_DEV_PORT = 3200 -HEALTH = "/_/api/health?from=devserver.py" +HEALTH = "/_api/health?from=devserver.py" async def run_devserver(