Migrate Posts to v2 Frontmatter
Concept of the skill
What it is: The rollout procedure for migrating all existing markdown posts to a new frontmatter contract. Mental model: Treat the migration as a staged data change: add support, backfill content, validate everything, then enforce the new schema. Why it exists: Required metadata changes can break every existing post unless compatibility and validation are sequenced deliberately. What it is NOT: It is not a generic schema-design exercise or a debugging guide for a failed migration run. Adjacent concepts: Content backfills, schema compatibility windows, tag normalization, date conversion. One-line analogy: It is a bridge that lets old posts cross safely into the new metadata format. Common misconception: Updating the validator first is harmless; populated content needs a compatibility window before enforcement.
Coverage
- The four-phase pattern for adding a required field to a populated content tree — add as nullable → backfill from existing data → verify → flip the validator to require it — and why collapsing any two phases into one is unsafe
- The backfill query — generating a
summaryfrom each post's first paragraph, with a per-post manual-override fallback for cases where the auto-summary is wrong - The verification gate between backfill and validator-flip — running
validate-posts.tsagainst the entirecontent/posts/**/*.mdtree must return zero errors before the schema is updated - The tag-normalization step — mapping every tag to its canonical form in
tag-vocabulary.ts, with a deny-list for tags that should be removed entirely (e.g., legacy synonyms now folded into a canonical tag) - The dry-run gate — the migration script always runs in dry-run by default, printing the diff per post; the
--applyflag is opt-in and never the CI default - The rollback path — what
ROLLBACK.mdfor this migration looks like and why "regenerate every summary" is wrong (overwrites authored summaries); the correct rollback restores the per-post.bakfile the migration writes alongside each edit
Philosophy of the skill
A content-schema migration is a rare migration where being careful is cheaper than being clever. The temptation to combine the four phases into one "atomic" pass fails because the backfill produces some surprising auto-summaries, the human reviewer needs time to override them, and flipping the validator before the human pass is done means every build between then and the override fails. The four-phase pattern is verbose but unambiguous: each phase has a clear success criterion, each phase is re-runnable, and the rollback at any phase is well-defined. Pay the verbosity cost; the alternative is a build outage on a non-emergency migration.
Workflow
Each step has a clear precondition and a clear success criterion. Do not skip steps; the steps exist because skipping them is how content migrations corrupt authored data.
| Step | Precondition | Action | Success criterion |
|---|---|---|---|
1. Add nullable summary |
The schema has no summary field |
lib/content/schema.ts: add summary: z.string().optional() (no required). Deploy. |
The schema accepts posts both with and without summary; the build does not fail on existing posts. |
| 2. Backfill | Step 1 deployed | Run scripts/migrate-frontmatter-v2.ts --apply --field summary which generates a draft summary per post from the first paragraph and writes a .bak for each modified file. |
Verification query reports 0 posts where summary is null or empty. |
| 3. Human review of auto-summaries | Step 2 success | Each post author reviews their auto-summary. Override by editing the post's frontmatter manually; the migration script will not re-run on a post whose summary was edited after step 2's .bak was written. |
Author sign-off recorded in audits/0007-frontmatter-v2/sign-off.md. |
| 4. Flip the validator | Step 3 sign-off committed | lib/content/schema.ts: change summary: z.string().optional() to summary: z.string().min(40) (required, with a minimum length). Deploy. |
Builds fail on any post that doesn't pass the v2 schema; the failure surface is the build log, not user-facing pages. |
When to back out
- Step 2 backfill produces too many surprising auto-summaries → reduce the auto-summary rule (e.g., first-sentence-only); the migration is still safe to resume from the same
.bakset. - Step 3 reveals that some posts genuinely have no extractable summary → those posts need authored summaries before step 4; do NOT flip the validator with placeholder summaries in place.
- Step 4 is flipped and the build fails on a post whose summary was edited but didn't trip the
min(40)floor → revert step 4 (.optional()again), have the author rewrite the summary, re-flip.
Verification
- Step 1 added the field as
.optional(), not as required - Step 2 was run with
--applyonly after a--dry-runwas reviewed (the dry-run output is committed underaudits/0007-frontmatter-v2/dry-run.md) - Step 2's
.bakfiles exist for every modified post and are committed (so rollback is a one-command restore) - Step 3 sign-off is recorded for every post in
content/posts/**/*.md— no post moved past step 3 without explicit author confirmation - Step 4 was applied AFTER step 3 sign-off — never before, even if the author backlog is taking longer than expected
- The rollback path in
ROLLBACK.mddoes NOT include "regenerate every summary" — that overwrites authored content. Rollback ismv <post>.md.bak <post>.mdper file, with the.baks already committed. - An end-to-end CI test runs the migration in dry-run mode against the live
content/posts/set and reports the diff in the PR description before any human merges step 4
Do NOT Use When
| Use instead | When |
|---|---|
markdown-post-frontmatter-validation |
The task is reviewing or authoring a single post's frontmatter, not the migration that adds a new required field |
debugging |
A specific migration step is failing in CI and you need to reproduce |
documentation |
The task is writing a runbook or contributor doc about the migration |
| (a generic migration skill) | The task is a different content migration with no relation to the v2 frontmatter rollout |