Remove stale /_admin app and disable SPA fallback
- Remove the /_admin route, views.render_editor, and standalone mode from the Vue editor (main.js / PageEditor.vue) - Remove /_admin dev proxy and preload exclusion - Set vite appType to 'mpa' and delete frontend/index.html so unknown /_ paths return 404 instead of an empty Vue shell - Update AGENTS.md and docs/design-principles.md
This commit is contained in:
@@ -20,9 +20,9 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
|||||||
- Avoid running the server yourself, ask the user to test
|
- Avoid running the server yourself, ask the user to test
|
||||||
- `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/...`) are
|
||||||
`/_admin`) are registered BEFORE `frontend.route(app, "/")` is
|
registered BEFORE `frontend.route(app, "/")` is called: fastapi-vue
|
||||||
called: fastapi-vue inserts its file routes at the position where
|
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
|
||||||
@@ -115,17 +115,17 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
|||||||
two pens swap the docked
|
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
|
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
|
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`).
|
||||||
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`),
|
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` sets
|
||||||
`manifest: true`, `assetsDir: '_/assets'` (so the build mirrors the URL
|
`appType: 'mpa'` (no SPA fallback) and builds with `manifest: true`,
|
||||||
space; `frontend/public/favicon.ico` lands at the build root and is
|
`assetsDir: '_/assets'` (so the build mirrors the URL space;
|
||||||
served at `/favicon.ico`) and JS inputs (`src/main.js` and
|
`frontend/public/favicon.ico` lands at the build root and is served at
|
||||||
`src/pagerite.js`) so no `index.html` ends up in the build (it would shadow
|
`/favicon.ico`). JS inputs are `src/main.js` and `src/pagerite.js`; there
|
||||||
`/`). All outputs are ES modules. vite-plugin-fastapi.js has an
|
is no `index.html` source (it would shadow `/` and turn missing dev paths
|
||||||
|
into an empty Vue shell). 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.
|
||||||
- `docs/` — design documentation.
|
- `docs/` — design documentation.
|
||||||
|
|
||||||
@@ -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.
|
- Keep dependencies minimal; add via `uv add` and mention it.
|
||||||
- The public URL space belongs to content (pretty slugs at root). Reserve
|
- The public URL space belongs to content (pretty slugs at root). Reserve
|
||||||
only `/_` for the machinery (`/_api/`, `/_f/`, `/_assets/`, `/_admin`), plus
|
only `/_` for the machinery (`/_api/`, `/_f/`, `/_assets/`), plus
|
||||||
`/favicon.ico` from the build. Slugs are lowercase ASCII letters, digits,
|
`/favicon.ico` from the build. Slugs are lowercase ASCII letters, digits,
|
||||||
hyphens and underscores `[a-z0-9_-]` (the site editor filters input live
|
hyphens and underscores `[a-z0-9_-]` (the site editor filters input live
|
||||||
via `slugify.js`, built on the `transliteration` npm package — unicode
|
via `slugify.js`, built on the `transliteration` npm package — unicode
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ evolves.
|
|||||||
Python with **html5tagger**. There is no client-side templating or SPA for
|
Python with **html5tagger**. There is no client-side templating or SPA for
|
||||||
the public site.
|
the public site.
|
||||||
- **Vue only where interactivity demands it.** Small interactive islands
|
- **Vue only where interactivity demands it.** Small interactive islands
|
||||||
(editing tools mainly) may be Vue components, either mounted into specific
|
(editing tools mainly) are Vue components mounted into specific elements of
|
||||||
elements of the server-rendered pages or served as standalone apps
|
the server-rendered pages. The public reading experience has no scripting
|
||||||
(e.g. an admin panel). The public reading experience has no scripting
|
|
||||||
requirement.
|
requirement.
|
||||||
- **Persistence via kanta.** Content is stored in an asyncio-friendly kanta
|
- **Persistence via kanta.** Content is stored in an asyncio-friendly kanta
|
||||||
database. Rendering happens on the fly on each request — there are no
|
database. Rendering happens on the fly on each request — there are no
|
||||||
@@ -29,7 +28,7 @@ evolves.
|
|||||||
directly at the site root; structured content may nest
|
directly at the site root; structured content may nest
|
||||||
(`/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 `/_` (`/_api/`, `/_f/`, `/_assets/`, `/_admin`). The only
|
lives under `/_` (`/_api/`, `/_f/`, `/_assets/`). The only
|
||||||
other reserved root path is `/favicon.ico`, served from the build.
|
other reserved root path is `/favicon.ico`, served from the build.
|
||||||
Slugs are lowercase ASCII letters, digits, hyphens and underscores
|
Slugs are lowercase ASCII letters, digits, hyphens and underscores
|
||||||
(`[a-z0-9_-]`; input is transliterated and filtered as you type, and a
|
(`[a-z0-9_-]`; input is transliterated and filtered as you type, and a
|
||||||
@@ -159,8 +158,7 @@ evolves.
|
|||||||
reloads the page). The pens are `<button>`s wired up by `pagerite.js` —
|
reloads the page). The pens are `<button>`s wired up by `pagerite.js` —
|
||||||
editing is an action, not a navigation. The editor's WebSocket
|
editing is an action, not a navigation. The editor's WebSocket
|
||||||
**reconnects automatically** with local text and pending saves preserved.
|
**reconnects automatically** with local text and pending saves preserved.
|
||||||
A standalone shell also exists at `/_admin#/path` with its own preview
|
(All users are trusted authors for now; access control later with
|
||||||
pane. (All users are trusted authors for now; access control later with
|
|
||||||
SSO.)
|
SSO.)
|
||||||
- **CodeMirror 6** for Markdown editing (no WYSIWYG), title/published
|
- **CodeMirror 6** for Markdown editing (no WYSIWYG), title/published
|
||||||
controls.
|
controls.
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Pagerite</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+11
-63
@@ -1,13 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// Page editor: CodeMirror for Markdown, live server-rendered preview
|
// Page editor: CodeMirror for Markdown, live server-rendered preview
|
||||||
// applied straight into the visible article, saving over one WebSocket
|
// applied straight into the visible article, saving over one WebSocket
|
||||||
// (/_api/ws/editor). Docked left of the article on the page itself
|
// (/_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
|
// The socket connects when the editor is opened and reconnects with
|
||||||
// exponential backoff after a failure; unsaved text and pending saves
|
// exponential backoff after a failure; unsaved text and pending saves
|
||||||
// survive a disconnect. Editor scroll drives the document scroll, keeping the
|
// survive a disconnect. Editor scroll drives the document scroll, keeping the
|
||||||
// rendered article at the cursor's position.
|
// rendered article at the cursor's position.
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { EditorView, basicSetup } from 'codemirror'
|
import { EditorView, basicSetup } from 'codemirror'
|
||||||
import { EditorState } from '@codemirror/state'
|
import { EditorState } from '@codemirror/state'
|
||||||
import { markdown } from '@codemirror/lang-markdown'
|
import { markdown } from '@codemirror/lang-markdown'
|
||||||
@@ -15,7 +14,6 @@ import { cmHighlight, cmTheme } from './cmtheme'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pagePath: { type: String, default: '' },
|
pagePath: { type: String, default: '' },
|
||||||
standalone: { type: Boolean, default: false },
|
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -23,10 +21,7 @@ const path = ref('')
|
|||||||
const title = ref('')
|
const title = ref('')
|
||||||
const published = ref(true)
|
const published = ref(true)
|
||||||
const saveError = ref('')
|
const saveError = ref('')
|
||||||
const previewHtml = ref('')
|
|
||||||
const previewHasH1 = ref(false)
|
|
||||||
const editorEl = ref(null)
|
const editorEl = ref(null)
|
||||||
const previewEl = ref(null)
|
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
|
||||||
let ws = null
|
let ws = null
|
||||||
@@ -40,10 +35,6 @@ let everConnected = false
|
|||||||
let dirty = false
|
let dirty = false
|
||||||
let syncingScroll = false
|
let syncingScroll = false
|
||||||
|
|
||||||
function currentPath() {
|
|
||||||
return location.hash.replace(/^#\/?/, '').replace(/\/$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(msg) {
|
function send(msg) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify(msg))
|
ws.send(JSON.stringify(msg))
|
||||||
@@ -87,10 +78,9 @@ function save() {
|
|||||||
async function saveAndClose() {
|
async function saveAndClose() {
|
||||||
await save()
|
await save()
|
||||||
// Reload so nav/sidebar changes apply, then the editor is gone.
|
// Reload so nav/sidebar changes apply, then the editor is gone.
|
||||||
// In standalone mode replace the current history entry so the admin
|
dirty = false
|
||||||
// shell does not remain in the back-button stack.
|
emit('close')
|
||||||
if (props.standalone) location.replace(`/${path.value}`)
|
location.reload()
|
||||||
else { dirty = false; emit('close'); location.reload() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -98,7 +88,7 @@ function close() {
|
|||||||
const stale = dirty
|
const stale = dirty
|
||||||
dirty = false
|
dirty = false
|
||||||
emit('close')
|
emit('close')
|
||||||
if (stale && !props.standalone) location.reload()
|
if (stale) location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertAtCursor(text) {
|
function insertAtCursor(text) {
|
||||||
@@ -128,11 +118,6 @@ function setDocument(text, preserveSelection = false) {
|
|||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHashChange() {
|
|
||||||
const p = currentPath()
|
|
||||||
if (p !== path.value) openPath(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
function runScripts(root) {
|
function runScripts(root) {
|
||||||
// Scripts injected via innerHTML do not execute; re-create them.
|
// Scripts injected via innerHTML do not execute; re-create them.
|
||||||
for (const old of root.querySelectorAll('script')) {
|
for (const old of root.querySelectorAll('script')) {
|
||||||
@@ -144,15 +129,6 @@ function runScripts(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previewIntoArticle(html, hasH1) {
|
function previewIntoArticle(html, hasH1) {
|
||||||
// Docked mode previews into the article on the page itself;
|
|
||||||
// standalone mode has its own preview pane. When the markdown owns its
|
|
||||||
// h1, the title-derived h1 is hidden (matching server-side rendering).
|
|
||||||
previewHasH1.value = hasH1
|
|
||||||
if (props.standalone) {
|
|
||||||
previewHtml.value = html
|
|
||||||
nextTick(() => { if (previewEl.value) runScripts(previewEl.value) })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const article = document.querySelector('#main article')
|
const article = document.querySelector('#main article')
|
||||||
if (!article) return
|
if (!article) return
|
||||||
const h1 = article.querySelector('h1')
|
const h1 = article.querySelector('h1')
|
||||||
@@ -210,13 +186,8 @@ function syncScroll() {
|
|||||||
const scroller = view.scrollDOM
|
const scroller = view.scrollDOM
|
||||||
const max = scroller.scrollHeight - scroller.clientHeight
|
const max = scroller.scrollHeight - scroller.clientHeight
|
||||||
const pct = max > 0 ? scroller.scrollTop / max : 0
|
const pct = max > 0 ? scroller.scrollTop / max : 0
|
||||||
if (props.standalone) {
|
const doc = document.documentElement
|
||||||
const pv = previewEl.value
|
window.scrollTo(0, pct * (doc.scrollHeight - innerHeight))
|
||||||
if (pv) pv.scrollTop = pct * (pv.scrollHeight - pv.clientHeight)
|
|
||||||
} else {
|
|
||||||
const doc = document.documentElement
|
|
||||||
window.scrollTo(0, pct * (doc.scrollHeight - innerHeight))
|
|
||||||
}
|
|
||||||
syncingScroll = false
|
syncingScroll = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -234,7 +205,7 @@ function connect() {
|
|||||||
requestRender()
|
requestRender()
|
||||||
if (pendingSave) send(pendingSave)
|
if (pendingSave) send(pendingSave)
|
||||||
} else {
|
} else {
|
||||||
openPath(props.standalone ? currentPath() : normPath(props.pagePath))
|
openPath(normPath(props.pagePath))
|
||||||
}
|
}
|
||||||
everConnected = true
|
everConnected = true
|
||||||
}
|
}
|
||||||
@@ -281,7 +252,6 @@ onMounted(() => {
|
|||||||
setMarkdown: (text) => setDocument(text, true),
|
setMarkdown: (text) => setDocument(text, true),
|
||||||
path: () => path.value,
|
path: () => path.value,
|
||||||
}
|
}
|
||||||
if (props.standalone) addEventListener('hashchange', onHashChange)
|
|
||||||
addEventListener('keydown', onKeydown)
|
addEventListener('keydown', onKeydown)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -293,13 +263,12 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
view?.destroy()
|
view?.destroy()
|
||||||
delete window.__pageritePageEditor
|
delete window.__pageritePageEditor
|
||||||
if (props.standalone) removeEventListener('hashchange', onHashChange)
|
|
||||||
removeEventListener('keydown', onKeydown)
|
removeEventListener('keydown', onKeydown)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor-root" :class="{ overlay: !standalone }">
|
<div class="editor-root overlay">
|
||||||
<header class="toolbar">
|
<header class="toolbar">
|
||||||
<input v-model="title" placeholder="Title" class="title" @input="requestRender" />
|
<input v-model="title" placeholder="Title" class="title" @input="requestRender" />
|
||||||
<label><input v-model="published" type="checkbox" /> published</label>
|
<label><input v-model="published" type="checkbox" /> published</label>
|
||||||
@@ -312,18 +281,11 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<button type="button" @click="fileInput.click()">image</button>
|
<button type="button" @click="fileInput.click()">image</button>
|
||||||
<button type="button" @click="saveAndClose">save</button>
|
<button type="button" @click="saveAndClose">save</button>
|
||||||
<button v-if="!standalone" type="button" class="close" title="close" @click="close">✕</button>
|
<button type="button" class="close" title="close" @click="close">✕</button>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="saveError">{{ saveError }}</div>
|
<div v-if="saveError">{{ saveError }}</div>
|
||||||
<div class="panes">
|
<div class="panes">
|
||||||
<div ref="editorEl" class="editor" />
|
<div ref="editorEl" class="editor" />
|
||||||
<div v-if="standalone" ref="previewEl" class="preview">
|
|
||||||
<article>
|
|
||||||
<h1 v-if="!previewHasH1">{{ title }}</h1>
|
|
||||||
<!-- server-rendered markdown preview -->
|
|
||||||
<div class="body" v-html="previewHtml" />
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -418,18 +380,4 @@ onUnmounted(() => {
|
|||||||
.editor :deep(.cm-gutters) {
|
.editor :deep(.cm-gutters) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview article {
|
|
||||||
max-width: 44rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+6
-23
@@ -4,8 +4,6 @@
|
|||||||
// editing with the preview rendered into the visible article.
|
// editing with the preview rendered into the visible article.
|
||||||
// - SiteEditor ("site" mode): pen on the banner — banner HTML editing
|
// - SiteEditor ("site" mode): pen on the banner — banner HTML editing
|
||||||
// (previewed into the real banner) and the site structure tree.
|
// (previewed into the real banner) and the site structure tree.
|
||||||
// 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) {
|
if (import.meta.env.DEV) {
|
||||||
import("./assets/pagerite.css");
|
import("./assets/pagerite.css");
|
||||||
import("./assets/themes/purple/theme.css");
|
import("./assets/themes/purple/theme.css");
|
||||||
@@ -17,22 +15,18 @@ import SiteEditor from './SiteEditor.vue'
|
|||||||
|
|
||||||
let host = null
|
let host = null
|
||||||
|
|
||||||
export function openEditor(path, { standalone = false, mode = 'page' } = {}) {
|
export function openEditor(path, { mode = 'page' } = {}) {
|
||||||
closeEditor()
|
closeEditor()
|
||||||
host = document.createElement('div')
|
host = document.createElement('div')
|
||||||
host.className = 'editor-host'
|
host.className = 'editor-host'
|
||||||
// Docked inside #content: below the banner, next to the article only.
|
// Docked inside #content: below the banner, next to the article only.
|
||||||
const container = (!standalone && document.getElementById('content')) || document.body
|
document.getElementById('content').prepend(host)
|
||||||
container.prepend(host)
|
document.body.classList.add('editing')
|
||||||
if (!standalone) {
|
// Which kind of editor is open; pagerite.js uses this to decide
|
||||||
document.body.classList.add('editing')
|
// whether a pen click closes the panel or swaps in the other editor.
|
||||||
// Which kind of editor is open; pagerite.js uses this to decide
|
document.body.dataset.editorMode = mode
|
||||||
// whether a pen click closes the panel or swaps in the other editor.
|
|
||||||
document.body.dataset.editorMode = mode
|
|
||||||
}
|
|
||||||
createApp(mode === 'site' ? SiteEditor : PageEditor, {
|
createApp(mode === 'site' ? SiteEditor : PageEditor, {
|
||||||
pagePath: path,
|
pagePath: path,
|
||||||
standalone,
|
|
||||||
onClose: closeEditor,
|
onClose: closeEditor,
|
||||||
}).mount(host)
|
}).mount(host)
|
||||||
}
|
}
|
||||||
@@ -47,14 +41,3 @@ export function closeEditor() {
|
|||||||
host = null
|
host = null
|
||||||
setTimeout(() => old.remove(), 250)
|
setTimeout(() => old.remove(), 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
const shell = document.getElementById('app')
|
|
||||||
if (shell) {
|
|
||||||
// Standalone /_admin shell: mount into it and follow the location hash.
|
|
||||||
host = shell
|
|
||||||
createApp(PageEditor, {
|
|
||||||
pagePath: location.hash.replace(/^#\/?/, '').replace(/\/$/, ''),
|
|
||||||
standalone: true,
|
|
||||||
onClose: () => {},
|
|
||||||
}).mount(shell)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
function preload() {
|
function preload() {
|
||||||
const urls = new Set();
|
const urls = new Set();
|
||||||
for (const a of document.querySelectorAll('#nav a[href^="/"], #main a[href^="/"]')) {
|
for (const a of document.querySelectorAll('#nav a[href^="/"], #main a[href^="/"]')) {
|
||||||
if (!a.pathname.startsWith("/_admin")) urls.add(a.pathname);
|
urls.add(a.pathname);
|
||||||
}
|
}
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (url === location.pathname) continue;
|
if (url === location.pathname) continue;
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const backendUrl = process.env.PAGERITE_BACKEND_URL || 'http://localhost:3200'
|
|||||||
|
|
||||||
// Proxy content pages (/slug, /path/to/slug) to the FastAPI backend in dev.
|
// Proxy content pages (/slug, /path/to/slug) to the FastAPI backend in dev.
|
||||||
// Excludes Vite internals (/@..., /src, /node_modules, /__...) and the
|
// Excludes Vite internals (/@..., /src, /node_modules, /__...) and the
|
||||||
// backend's /_ prefix. /_api and /_f are handled by the fastapi-vue plugin;
|
// backend's /_ prefix. /_api and /_f are handled by the fastapi-vue plugin.
|
||||||
// /_admin is proxied explicitly below.
|
|
||||||
const CONTENT_PROXY = '^\\/(?!_|@|src|node_modules|__)(?:[^./?]+(?:\\/[^./?]+)*)?(?:\\?.*)?$'
|
const CONTENT_PROXY = '^\\/(?!_|@|src|node_modules|__)(?:[^./?]+(?:\\/[^./?]+)*)?(?:\\?.*)?$'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
@@ -22,10 +21,10 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/_admin": { target: backendUrl, changeOrigin: false },
|
|
||||||
[CONTENT_PROXY]: { target: backendUrl, changeOrigin: false },
|
[CONTENT_PROXY]: { target: backendUrl, changeOrigin: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appType: 'mpa', // no SPA fallback; every HTML page is served by FastAPI
|
||||||
build: {
|
build: {
|
||||||
// Mirror the URL space in the build output: hashed files land under
|
// 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
|
||||||
|
|||||||
@@ -549,12 +549,6 @@ async def editor_ws(ws: WebSocket) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@app.get("/_admin", response_class=HTMLResponse)
|
|
||||||
async def admin() -> HTMLResponse:
|
|
||||||
"""Serve the editor app shell (Vue mounts into #app)."""
|
|
||||||
return HTMLResponse(views.render_editor())
|
|
||||||
|
|
||||||
|
|
||||||
@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 "")."""
|
||||||
|
|||||||
+8
-38
@@ -72,15 +72,6 @@ def _editor_css_url(vite_url: str | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _editor_page_css_urls(vite_url: str | None) -> list[str]:
|
|
||||||
"""All CSS URLs for the standalone admin editor page."""
|
|
||||||
if vite_url:
|
|
||||||
return []
|
|
||||||
shared = _shared_css_urls(None)
|
|
||||||
editor_css = _editor_css_url(None)
|
|
||||||
return [*shared, editor_css] if editor_css else shared
|
|
||||||
|
|
||||||
|
|
||||||
def _layout(urls: list[str], modules: list[str] = ()) -> Template:
|
def _layout(urls: list[str], modules: list[str] = ()) -> Template:
|
||||||
"""Page layout template with standard asset URLs and ES-module scripts.
|
"""Page layout template with standard asset URLs and ES-module scripts.
|
||||||
|
|
||||||
@@ -230,12 +221,12 @@ def _edit_attrs(path: str, mode: str = "page") -> dict:
|
|||||||
(the pen on the banner) edits the banner and site structure. They are
|
(the pen on the banner) edits the banner and site structure. They are
|
||||||
buttons, not links: editing is an action, not a navigation.
|
buttons, not links: editing is an action, not a navigation.
|
||||||
"""
|
"""
|
||||||
scripts, _styles, editor_css = _editor_assets()
|
script, editor_css = _editor_assets()
|
||||||
return {
|
return {
|
||||||
"type": "button",
|
"type": "button",
|
||||||
"class": "edit-link" if mode == "page" else "edit-link banner-edit-link",
|
"class": "edit-link" if mode == "page" else "edit-link banner-edit-link",
|
||||||
"title": "edit",
|
"title": "edit",
|
||||||
"data-editor-src": scripts[-1],
|
"data-editor-src": script[-1],
|
||||||
"data-editor-css": editor_css or "",
|
"data-editor-css": editor_css or "",
|
||||||
"data-editor-mode": mode,
|
"data-editor-mode": mode,
|
||||||
}
|
}
|
||||||
@@ -346,38 +337,17 @@ def _page_assets() -> tuple[list[str], list[str]]:
|
|||||||
return _asset_cache["page"]
|
return _asset_cache["page"]
|
||||||
|
|
||||||
|
|
||||||
def _editor_assets() -> tuple[list[str], list[str], str | None]:
|
def _editor_assets() -> tuple[list[str], str | None]:
|
||||||
"""Script URLs, page CSS URLs, and editor-specific CSS URL.
|
"""Script URL and editor-specific CSS URL for the public-page edit pen.
|
||||||
|
|
||||||
The standalone admin page uses all CSS URLs; the public-page edit pen
|
The shared CSS is already linked on the page, so the pen only needs the
|
||||||
only needs the editor-specific URL because the shared CSS is already
|
editor-specific stylesheet.
|
||||||
linked on the page.
|
|
||||||
"""
|
"""
|
||||||
vite_url = os.environ.get("PAGERITE_VITE_URL")
|
vite_url = os.environ.get("PAGERITE_VITE_URL")
|
||||||
if vite_url:
|
if vite_url:
|
||||||
return (
|
return [f"{vite_url}/@vite/client", f"{vite_url}/src/main.js"], None
|
||||||
[f"{vite_url}/@vite/client", f"{vite_url}/src/main.js"],
|
|
||||||
_shared_css_urls(vite_url),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if "editor" not in _asset_cache:
|
if "editor" not in _asset_cache:
|
||||||
manifest = _manifest()
|
manifest = _manifest()
|
||||||
entry = manifest["src/main.js"]
|
entry = manifest["src/main.js"]
|
||||||
_asset_cache["editor"] = (
|
_asset_cache["editor"] = [f"/{entry['file']}"], _editor_css_url(None)
|
||||||
[f"/{entry['file']}"],
|
|
||||||
_editor_page_css_urls(None),
|
|
||||||
_editor_css_url(None),
|
|
||||||
)
|
|
||||||
return _asset_cache["editor"]
|
return _asset_cache["editor"]
|
||||||
|
|
||||||
|
|
||||||
def render_editor() -> str:
|
|
||||||
"""Render the admin editor shell: a mount point for the Vue app."""
|
|
||||||
scripts, styles, _editor_css = _editor_assets()
|
|
||||||
doc = Document(f"Admin – {SITE_NAME}", lang="en")
|
|
||||||
for url in styles:
|
|
||||||
doc.link(rel="stylesheet", href=url, blocking="render")
|
|
||||||
for src in scripts:
|
|
||||||
doc.script(src=src, type="module")
|
|
||||||
doc.div(None, id="app")
|
|
||||||
return str(doc)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user