Note

    blog

    Full-stack personal blog — authoring, admin workflow, public pages, feeds (RSS/Atom/JSON), SEO, structured data, cros...

    Dates

    Created
    Not recorded
    Last updated
    Not recorded

    Document Metadata

    • title: Blog
    • description: Full-stack personal blog — authoring, admin workflow, public pages, feeds (RSS/Atom/JSON), SEO, structured data, cross-content-type linking
    • status: stable
    • lastUpdated: "2026-03-25 11:18 ET (America/New_York)"
    • owner: Product/Engineering

    Blog The blog is a branded publishing system integrated into the SMC Directory. It provi

    Blog

    The blog is a branded publishing system integrated into the SMC Directory. It provides the full authoring and publishing workflow for Markdown posts, public feeds, SEO/structured-data support, and cross-content-type linking with the resource directory. It also now includes a first editorial-governance slice for review-gated posts, selective byline mode, and publish-time review enforcement in admin flows.

    URL Structure

    RoutePurpose
    /blogBlog index — paginated grid of published posts with tag sidebar
    /blog/[slug]Individual post detail — full article with cover image, ToC, prose body, related resources, prev/next nav
    /blog/tag/[slug]Tag browse — filtered posts for a specific tag
    /blog/feed.xmlRSS 2.0 feed — all published posts, WebSub hub
    /blog/atom.xmlAtom 1.0 feed — all published posts, WebSub hub
    /blog/feed.jsonJSON Feed 1.1 — all published posts
    /blog/sitemap.xmlBlog sitemap — index, post, and tag URLs
    /admin/blogAdmin post list — all statuses with tabs and search
    /admin/blog/reviewsAdmin editorial drafting and review queue — review briefs, generate AI drafts, then capture approval/revision/rejection decisions
    /admin/blog/reviews/[id]Brief review detail — inspect brief contents, evidence, and draft scaffold before generating a blog draft
    /admin/blog/newAdmin new post editor
    /admin/blog/[id]Admin post editor (edit existing)
    /admin/blog/tagsAdmin tag management

    Database Schema

    The core blog shape is defined in db/neon/schema.ts and was introduced by db/migrations/neon/0116_blog_tables.sql, then extended by the 3041 review-gate tranche in 0121_editorial_review_controls_phase1.sql and 0122_blog_publish_review_gate_phase1.sql.

    blog_posts

    Primary content table. Stores Markdown source and pre-rendered HTML.

    ColumnTypeNotes
    iduuidPK, auto-generated
    slugtextUnique, URL-safe identifier
    titletextRequired
    subtitletextOptional deck line
    bodytextMarkdown source
    body_htmltextPre-rendered HTML (generated at save time)
    excerpttextAuto-derived from body if not provided
    cover_image_urltextHero/OG image URL
    cover_image_alttextAlt text for cover image
    statustextdraft · published · scheduled · archived
    published_attimestamptzSet when first published
    scheduled_fortimestamptzFuture publish timestamp
    reading_time_minutesintegerComputed from word count
    meta_titletextSEO title override
    meta_descriptiontextSEO description override
    og_image_urltextOpenGraph image override
    sort_orderintegerManual ordering (default 0)
    is_featuredbooleanPin to homepage/top of index
    requires_review_gatebooleanEnables publish-time review enforcement
    review_statetextdraft_intake_pending · in_review · needs_revision · approved_for_publish · rejected
    decision_reason_codetextReviewer-provided reason code for gated decisions
    byline_modetextorganization · maggie_named
    review_decision_iduuidOptional link to editorial_review_decisions.id
    created_attimestamptzAuto-set
    updated_attimestamptzAuto-set

    Indexes: blog_posts_slug_key (unique), blog_posts_status_published_idx (status + published_at DESC), blog_posts_featured_idx, blog_posts_review_gate_state_idx, blog_posts_review_decision_idx.

    blog_tags

    ColumnTypeNotes
    iduuidPK
    slugtextUnique
    nametextDisplay name
    descriptiontextOptional
    created_attimestamptzAuto-set

    blog_post_tags (bridge)

    ColumnTypeNotes
    post_iduuidFK → blog_posts, cascade delete
    tag_iduuidFK → blog_tags, cascade delete
    created_attimestamptzAuto-set

    Primary key: composite (post_id, tag_id).

    Linked Review Decisions

    Review-gated blog posts can point at editorial_review_decisions via blog_posts.review_decision_id.

    The decision record stores:

    • review_state
    • reviewer_id
    • review_decision
    • decision_reason_code
    • review_notes
    • revision_request_summary
    • artifact_audit_signature
    • byline_mode

    Post Lifecycle

      ┌─────────┐     publish      ┌───────────┐
      │  DRAFT  │ ───────────────→ │ PUBLISHED │
      └─────────┘                  └───────────┘
           │ ↑                        │      ↑
           │ │ unpublish              │      │
           │ └────────────────────────┘      │
           │                                  │
           │  schedule     ┌───────────┐      │
           └──────────────→│ SCHEDULED │──────┘
                           └───────────┘  (auto-promoted
                                │          at query time)
                                │
                           ┌───────────┐
                           │ ARCHIVED  │
                           └───────────┘
    
    • Draft → default state for new posts. Not publicly visible.
    • Published → visible on /blog, homepage, RSS, sitemap. published_at is set once on first publish and preserved through edits.
    • Scheduledscheduled_for timestamp set. Automatically promoted to published at query time when scheduled_for ≤ now() (lazy promotion in getPublishedBlogPosts).
    • Archived → soft-removed from public view. Still accessible in admin.

    Admin Workflow

    Creating a Post

    1. Navigate to /admin/blog → click "New Post".
    2. Fill in title (required) and body (Markdown). Slug auto-generates from title but is editable.
    3. Optionally add: subtitle, excerpt (auto-derived if blank), cover image via upload or URL, tags, SEO overrides (meta title, meta description), and review-gate metadata.
    4. Click "Save Draft" to create as draft, or "Publish" to go live immediately.

    Editing a Post

    1. From /admin/blog, click a post title to open the editor.
    2. The editor shows the post form plus a preview toggle for rendered Markdown.
    3. Changes to the body auto-update the preview.
    4. Save updates the post, re-renders the Markdown to HTML, recomputes reading time and excerpt.

    Publishing Flow

    • Publish now: Click "Publish" on a draft. Sets status=published and published_at=now().
    • Schedule: Select a future date/time and click "Schedule". Sets status=scheduled and scheduled_for.
    • Unpublish: Reverts a published post to draft. published_at is preserved for re-publish.
    • Archive: Soft-removes from public view. Can be unarchived by publishing again.

    Review-Gated Publishing

    • Posts can opt into requires_review_gate.
    • Review-gated posts expose admin controls for review_state, byline_mode, decision_reason_code, and optional review_decision_id.
    • publishBlogPost, createBlogPost, and updateBlogPost all enforce the same gate whenever a post resolves to published.
    • A review-gated post cannot publish unless review_state = approved_for_publish.
    • byline_mode = maggie_named additionally requires a non-empty decision_reason_code.
    • /admin/blog includes a Review Queue tab for gated posts that are not yet approved for publish.
    • /admin/blog/reviews provides the dedicated reviewer workflow for linked editorial_review_decisions:
      • treat draft_intake_pending rows as brief-review intake first, not human review first
      • review a dedicated brief detail page before generating a blog draft
      • generate an AI-written blog draft from the approved brief instead of treating the brief itself as the editable post body
      • assign or reassign human reviewer ownership once a draft is actually being reviewed
      • capture approved_for_publish, needs_revision, and rejected outcomes
      • keep linked blog_posts.review_state, decision_reason_code, and byline_mode synchronized when the decision row changes
      • separate open queue items from completed history so already-approved decisions remain visible without pretending they are still actionable
      • intake rows now show brief detail metadata inline, including problem statement, recommendation count, outline count, and evidence-reference count
      • queue rows now open /admin/blog/reviews/[id] for dedicated brief review
      • approving the brief can queue a real AI-generated blog_posts draft, and the operator then continues review from the normal /admin/blog drafts list
      • /admin/blog/new?reviewDecisionId=<id> remains available as a manual fallback, but it now opens with an empty article body plus read-only structured draft notes derived from buildEditorialDraftArtifact(...)
    • Blog admin navigation should now come from the admin sidebar rather than page-level shortcut bars. The remaining page header control on /admin/blog is the action-oriented New Post button.

    Tag Management

    Accessible at /admin/blog/tags. Supports:

    • Create tags with name and optional description (slug auto-generated).
    • Edit tag name/description.
    • Delete tags (automatically removed from all posts via cascade).
    • Tags with zero posts are still shown in admin but hidden from the public sidebar.

    Server Actions

    Public Actions (app/actions/blog.ts)

    All public actions only return posts where status = 'published' and published_at ≤ now(). They gracefully return empty results if the database is unavailable.

    ActionPurpose
    getPublishedBlogPosts(page, limit)Paginated published posts with tags. Also runs lazy scheduled-post promotion.
    getBlogPostBySlug(slug)Single post by slug with tags
    getBlogPostsByTag(tagSlug, page, limit)Posts filtered by tag slug
    getFeaturedBlogPosts(limit)Featured published posts for homepage
    getRecentBlogPosts(limit)Most recent posts for widgets
    getAdjacentPosts(publishedAt)Previous/next posts for detail page navigation
    getBlogTags()All tags with post counts (only tags with posts shown publicly)
    getRelatedBlogPosts(postId, tagIds, limit)Posts sharing the same tags as a given post (for "Related Posts" section)
    getBlogPostsForResource(category, tags, limit)Blog posts whose tags match a resource's category/tags (for resource detail page)
    searchBlogPosts(query)Full-text search across title, subtitle, excerpt, and tag names

    Admin Actions (app/actions/blog-admin.ts)

    All admin actions require authenticated admin session (assertAdmin()). Redirects to login/home if unauthorized.

    ActionPurpose
    createBlogPost(data)Create new post (defaults to draft); enforces review gate if created directly as published
    updateBlogPost(id, data)Update post fields, re-render HTML, and enforce review gate when status resolves to published
    publishBlogPost(id)Set published, preserve original published_at, and block publish when review-gate invariants fail
    unpublishBlogPost(id)Revert to draft
    archiveBlogPost(id)Soft-archive
    scheduleBlogPost(id, scheduledFor)Set scheduled status with future timestamp
    deleteBlogPost(id)Hard delete
    createBlogTag(name, description?)Create tag
    updateBlogTag(id, data)Edit tag name/description
    deleteBlogTag(id)Delete tag (cascades from posts)
    getAdminBlogPosts(status?)List posts for admin (any status, optional filter)
    getAdminBlogPost(id)Single post for admin editing (includes tag IDs)
    getAdminBlogTags()All tags for admin UI

    Review Admin Actions (`app

    ...[truncated for intake]

    Provenance