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
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions