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
+