Rework category labels, slug validation, and task-list checkboxes

- Remove the section-header  "add landing page" button; landing pages are
  now created via the page editor pen.
- Treat saving a page with empty (after stripping) Markdown as deleting it:
  childless nodes are removed, nodes with children become content-less labels.
- Restrict server-side slugs to [a-z0-9_-] with no leading underscore or dot;
  drop the reserved-filename blacklist.
- Render placeholder pages for content-less category labels and link them to
  their first published child in the navigation.
- Make task-list checkboxes live, non-disabled inputs: toggling updates the
  Markdown source, updates the open CodeMirror document when the page editor is
  open, and persists to the server when no editor is open. Errors revert the UI.
- Update docs and AGENTS.md conventions accordingly.
This commit is contained in:
2026-08-16 19:04:55 +00:00
parent 3dc438f8df
commit a8cf5a6a82
12 changed files with 304 additions and 131 deletions
+6 -23
View File
@@ -8,8 +8,8 @@
//
// 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 redirecting to its first child). The front page is a
// top-level row with an empty slug, not the parent of the others.
// 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.
import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
import StructureTree from './StructureTree.vue'
import { EditorView, basicSetup } from 'codemirror'
@@ -125,8 +125,8 @@ async function loadPlain(p) {
const res = await fetch(finalUrl)
const type = res.headers.get('content-type') || ''
if (!type.includes('text/html')) return
// Category URLs redirect to their first child; reflect that. A 404
// layout is fine too (new pages are created by editing them).
// Category and missing URLs render a placeholder 404 page — fine to
// swap in (new pages are created by editing them).
if (res.redirected) finalUrl = res.url
doc = new DOMParser().parseFromString(await res.text(), 'text/html')
} catch { return }
@@ -239,22 +239,6 @@ async function commitPending() {
navigate(newPath)
}
// Give a content-less category a landing page (empty page at its path).
async function addContent(node) {
const res = await fetch(`/_/api/pages/${node.path}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ title: node.title, markdown: '', published: node.published }),
})
if (res.ok) {
saveError.value = ''
await refreshPages()
navigate(node.path)
} else {
saveError.value = '⚠️ changes could not be saved'
}
}
// --- Site-wide brand (header link + <title> suffix) ----------------------
// Edits apply to the live page immediately and save while typing. An
// empty brand removes the header link and the title suffix entirely.
@@ -330,8 +314,8 @@ async function removePage(node) {
refreshPages()
const p = node.path
if (p === path.value || (p && path.value.startsWith(`${p}/`))) {
// The current page was deleted — or reduced to a category that now
// redirects to its first child. Either way, re-render from the server.
// The current page was deleted — or reduced to a category, which now
// renders a placeholder page. Either way, re-render from the server.
if (node.children.length) loadPlain(path.value)
else navigate('')
} else {
@@ -429,7 +413,6 @@ provide('structureHandlers', {
reorder: onReorder,
titleInput: onTitleInput,
commitSlug,
addContent,
commitPending,
discardPending,
newPage,