A new "Site URL" setting (validated, trailing-slash-normalized, with
SITE_URL env fallback) anchors every absolute URL. On top of it:
- /feed.xml — RSS 2.0 with the 20 newest published posts: excerpt
description, full sanitized HTML in content:encoded (relative image
and link URLs rewritten to absolute, since readers resolve nothing),
categories from tags, dc:creator, and a self atom:link. Autodiscovery
<link> on every public page and a footer link.
- /sitemap.xml — home, post list, every published post and page
(lastmod from updatedAt), and publicly visible tags. Rendered per
request like the rest of the site so it never goes stale; drafts
never appear.
- /robots.txt — allow all, disallow /admin/ and /api/, sitemap pointer.
Admin pages also carry noindex meta as a second layer.
- Page metadata: metadataBase + canonical URLs everywhere, Open Graph
(article type with published/modified times, author, and tags on
posts; og:image + summary_large_image card when there's a featured
image), and BlogPosting JSON-LD on post pages.
Gotcha encoded in lib/seo.ts: Next merges metadata shallowly, so pages
setting alternates.canonical alone would wipe the layout's RSS
autodiscovery entry — pageAlternates() always sets both.
Backups gain settings.siteUrl (export v4; older files still import).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seeded account is the single admin; it can create author accounts
on the new /admin/users page (username + password) and grant each one
access to specific tags. Authors sign in to a Posts-only panel where
they can write, edit, publish, and unpublish their own posts — every
post must carry at least one granted tag, tags outside the grants are
rejected server-side, and only the admin can create tags or delete
posts (or anything else: pages, comments, settings, and backups stay
admin-only). Admin-only URLs bounce authors to their post list, and
foreign post editors 404.
posts.author_id records ownership; deleting an account keeps its posts
as unowned, admin-managed rows and signs the account out everywhere.
Backups (export v3) store the owner's username per post and re-attach
ownership on import when the account still exists.
Also fixes a latent form bug: a missing newTags field (author forms
don't render it) failed zod validation with an invisible error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Visitors comment with just a name and email; a checkbox controls
whether the email is shown publicly (default private — only the admin
sees it). Every comment lands as pending and is invisible until
approved on the new /admin/comments page (approve / unapprove /
delete, with a pending-count badge in the admin nav and a dashboard
stat). Replies nest under their parent; a reply is only accepted on an
approved comment of the same post, and replies stay hidden while their
parent is unapproved so threads never render out of context. A hidden
honeypot field silently drops naive bots. Comment bodies are plain
text, rendered escaped.
The backup format gains a comments section (export version 2; v1 files
still import) with parent links remapped through file-local ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Export downloads a versioned JSON snapshot of all posts, pages, tags,
navigation, and settings from GET /api/admin/export. Cross-references
are keyed by slug rather than database id, so a backup restores cleanly
into any database. Import (a new settings-page section) validates the
file and atomically replaces all content in one transaction, sanitizing
bodies at the trust boundary; users, sessions, and uploaded files are
untouched. Server-action body limit raised so backups fit in the
import upload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>