diff --git a/app/pages/admin/site-content.vue b/app/pages/admin/site-content.vue new file mode 100644 index 0000000..077cbaf --- /dev/null +++ b/app/pages/admin/site-content.vue @@ -0,0 +1,225 @@ + + + + Site Content + Editable copy rendered on the public site. Leave fields blank to use defaults. + + + Loading… + + + + + {{ entry.key }} + {{ KEY_LABELS[entry.key] || entry.key }} + + + Updated {{ formatTime(entry.updatedAt) }} + + + + + Title + + + + + Body + + Paragraphs separated by blank lines. Plain text only. + + + + + {{ entry.saving ? 'Saving…' : 'Save' }} + + + + + + + + + + diff --git a/server/api/admin/site-content/[key].put.js b/server/api/admin/site-content/[key].put.js new file mode 100644 index 0000000..e4fdec0 --- /dev/null +++ b/server/api/admin/site-content/[key].put.js @@ -0,0 +1,28 @@ +import SiteContent from '../../../models/siteContent.js' +import { requireAdmin } from '../../../utils/auth.js' +import { validateBody } from '../../../utils/validateBody.js' +import { SITE_CONTENT_KEYS, siteContentUpsertSchema } from '../../../utils/schemas.js' + +export default defineEventHandler(async (event) => { + await requireAdmin(event) + + const key = getRouterParam(event, 'key') + if (!SITE_CONTENT_KEYS.includes(key)) { + throw createError({ statusCode: 404, statusMessage: 'Unknown content key' }) + } + + const body = await validateBody(event, siteContentUpsertSchema) + + const doc = await SiteContent.findOneAndUpdate( + { key }, + { $set: { title: body.title || '', body: body.body || '' } }, + { upsert: true, new: true, runValidators: true } + ).lean() + + return { + key: doc.key, + title: doc.title || '', + body: doc.body || '', + updatedAt: doc.updatedAt || null + } +}) diff --git a/server/api/site-content/[key].get.js b/server/api/site-content/[key].get.js new file mode 100644 index 0000000..994cb6a --- /dev/null +++ b/server/api/site-content/[key].get.js @@ -0,0 +1,18 @@ +import SiteContent from '../../models/siteContent.js' +import { SITE_CONTENT_KEYS } from '../../utils/schemas.js' + +export default defineEventHandler(async (event) => { + const key = getRouterParam(event, 'key') + + if (!SITE_CONTENT_KEYS.includes(key)) { + throw createError({ statusCode: 404, statusMessage: 'Unknown content key' }) + } + + const doc = await SiteContent.findOne({ key }).lean() + return { + key, + title: doc?.title || '', + body: doc?.body || '', + updatedAt: doc?.updatedAt || null + } +}) diff --git a/server/api/site-content/keys.get.js b/server/api/site-content/keys.get.js new file mode 100644 index 0000000..017fff8 --- /dev/null +++ b/server/api/site-content/keys.get.js @@ -0,0 +1,5 @@ +import { SITE_CONTENT_KEYS } from '../../utils/schemas.js' + +export default defineEventHandler(() => { + return { keys: [...SITE_CONTENT_KEYS] } +}) diff --git a/server/models/siteContent.js b/server/models/siteContent.js new file mode 100644 index 0000000..50bd5ca --- /dev/null +++ b/server/models/siteContent.js @@ -0,0 +1,9 @@ +import mongoose from 'mongoose' + +const siteContentSchema = new mongoose.Schema({ + key: { type: String, required: true, unique: true, index: true }, + title: { type: String, default: '' }, + body: { type: String, default: '' }, +}, { timestamps: true }) + +export default mongoose.models.SiteContent || mongoose.model('SiteContent', siteContentSchema)
Editable copy rendered on the public site. Leave fields blank to use defaults.
Paragraphs separated by blank lines. Plain text only.