Skip to content

Revive blog posts as a content-typed gitsheets sheet (revises the deferred decision) #45

@themightychris

Description

@themightychris

specs/deferred.md currently says blog posts get replaced by "staff-authored markdown files in the code repo at apps/web/src/content/blog/<slug>.md, shipped via PR." That decision predates gitsheets v1.2's content-typed records.

With v1.2 we can give blog posts their own gitsheets sheet — markdown bodies + TOML frontmatter — and get a better outcome than files-in-code-repo:

Why this beats the original deferral

Concern Files-in-code-repo Content-typed sheet
PR-reviewable
Publish cadence Tied to web deploys Immediate on data-repo merge
Tags / cross-links Ad-hoc frontmatter Native TagAssignment
Author attribution Hand-stamp in frontmatter Native Person reference
Snapshot inclusion Not in data snapshot In the snapshot (pseudonymized)
API serving Bespoke Vite handler Existing read API pipeline
/blog index cost Bundle every post into web build queryAll({ withBody: false })
laddr-import revival Out of scope Resurrect blog_posts table on the existing one-shot import

Sheet shape

# .gitsheets/blog-posts.toml
[gitsheet]
root = 'blog-posts'
path = '${{ slug }}'

[gitsheet.format]
type = 'markdown'
body = 'body'

[gitsheet.schema]
$ref = './schemas/BlogPost.schema.json'

BlogPost entity (in packages/shared/src/schemas/blog-post.ts):

  • id UUIDv7
  • legacyId (laddr's BlogPost.ID, for the importer's idempotence)
  • slug (kebab-case, slug-handle conventions)
  • title
  • summary (short markdown — stays in frontmatter)
  • authorId → Person
  • postedAt (iso8601)
  • editedAt nullable
  • featuredImageKey nullable (attachment via gitsheets)
  • deletedAt nullable (soft-delete)
  • body (the markdown body — the designated content field)
  • standard createdAt / updatedAt

Routing

Add to the SPA:

  • /blog — index (paginated, optional tag filter)
  • /blog/:slug — detail
  • /blog/tag/:namespace/:slug — tag-filtered (reuse TagsNamespace pattern)

API:

  • GET /api/blog-posts (list with facets, q, sort, page)
  • GET /api/blog-posts/:slug (detail)
  • POST/PATCH/DELETE — staff-only (per the original spec, blog wasn't a per-user-role CMS)

laddr-import revival

The existing one-shot importer at apps/api/scripts/import-laddr.ts currently skips blog_posts. Re-add it as another translator in apps/api/scripts/import-laddr/translators.ts:

  • Map BlogPost.Slugslug (slugify-with-dedupe if invalid)
  • Map BlogPost.Titletitle
  • Map BlogPost.Bodybody
  • Map BlogPost.AuthorID → resolve via the existing idMaps.personByLegacy
  • Map BlogPost.Published (and similar) → postedAt
  • Preserve legacyId so re-runs are idempotent

Sequencing

  • Depends on #44 (content-typed gitsheets is the substrate) — or stand on its own as the first content-typed sheet in the project. Either order works since blog-posts is a brand-new sheet that doesn't conflict with the existing TOML-only ones.
  • Sequenced after cutover-prep so existing migration paths stay valid through cutover.

Spec updates needed

  • specs/deferred.md — update the "Blog (/blog) as a user-facing CMS" entry from "files in code repo" to "content-typed sheet, see this issue."
  • New spec files: specs/api/blog.md, specs/screens/blog-index.md, specs/screens/blog-detail.md.
  • specs/data-model.md — add BlogPost entity.
  • specs/behaviors/legacy-id-mapping.md — note the new BlogPost.legacyId axis.

Out of scope: comments, reactions, the multi-author "posts under a topic" workflow — keep it as simple as the original deferral imagined.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions