diff --git a/AGENTS.md b/AGENTS.md index 9c6b64c..860a4e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,11 @@ not for the public pages. See `docs/design-principles.md` for the design. referencing the per-family variables (`--font-source-sans` etc.) from pagerite.css; the base stylesheet's `--font-brand` defaults to `var(--font-heading)`. + `Data.favicon` names a file in the content-addressed `files` store, + uploaded/cleared in the site editor via `PUT`/`DELETE + /_api/settings/favicon`; when set it is linked as `` + on every page, otherwise browsers fall back to the build's + `/favicon.ico` by convention. - `markdown.py` — markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists plugins). Custom image rule: relative srcs resolve against the page path, titled images become figures. @@ -141,7 +146,8 @@ not for the public pages. See `docs/design-principles.md` for the design. 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 + theme selector + - site-wide custom CSS + banner HTML edited in small CodeMirror windows; + favicon upload/remove + site-wide custom CSS + banner HTML edited in + small CodeMirror windows; banner previewed into `#page-banner`, CSS injected into ``) + vue-draggable structure tree with always-editable title/slug inputs per row, opened by the banner pen — diff --git a/frontend/src/SiteEditor.vue b/frontend/src/SiteEditor.vue index 324066e..d72135c 100644 --- a/frontend/src/SiteEditor.vue +++ b/frontend/src/SiteEditor.vue @@ -315,9 +315,60 @@ async function loadSettings() { brand.value = s.brand theme.value = s.theme || '' customCss.value = s.custom_css || '' + favicon.value = s.favicon || '' } catch { /* keep default */ } } +// --- Favicon --------------------------------------------------------------- +// Uploaded into the content-addressed file store (PUT /_api/settings/favicon) +// and linked on every page as ; empty falls back to the +// build's /favicon.ico. Applies to the live page immediately. +const favicon = ref('') +const faviconInput = ref(null) + +function applyFavicon(url) { + let link = document.querySelector('link[rel="icon"]') + if (url) { + if (!link) { + link = document.createElement('link') + link.rel = 'icon' + link.id = 'pagerite-favicon' + document.head.append(link) + } + link.href = url + } else if (link?.id === 'pagerite-favicon') { + link.remove() + } +} + +async function uploadFavicon(file) { + if (!file || !file.type.startsWith('image/')) return + const res = await fetch('/_api/settings/favicon', { + method: 'PUT', + headers: { 'x-filename': file.name.replace(/[^\w.-]/g, '-') }, + body: file, + }) + if (res.ok) { + saveError.value = '' + const { path: url } = await res.json() + favicon.value = url + applyFavicon(url) + } else { + saveError.value = `⚠️ ${await errorDetail(res)}` + } +} + +async function removeFavicon() { + const res = await fetch('/_api/settings/favicon', { method: 'DELETE' }) + if (res.ok) { + saveError.value = '' + favicon.value = '' + applyFavicon('') + } else { + saveError.value = `⚠️ ${await errorDetail(res)}` + } +} + function currentTitle() { return flatMap.value[path.value]?.title || document.title.replace(/ – [^–]*$/, '') @@ -887,6 +938,28 @@ onUnmounted(() => { A +
+ + ? + + + +