feat(cms): restructure blog into Context, a lifecycle board, and a list - #6622
Open
danielfurt wants to merge 5 commits into
Open
feat(cms): restructure blog into Context, a lifecycle board, and a list#6622danielfurt wants to merge 5 commits into
danielfurt wants to merge 5 commits into
Conversation
Reorganize the blog CMS around the user's mental model: - Context (was "Autonomous content"): Brand, Formats, Content pillars (reconceived themes), plus Authors and Categories folded in as tabs. Sidebar Blog group is now Context, Posts, Scheduling. - Posts is one lifecycle the same card travels: Idea → Generating → In review → Scheduled → Published. Board (drag to advance) and a grouped List (by status / format / pillar) are two views; opening a card floats a right-anchored panel with an expand-to-full-page toggle, list mode is master-detail. - Idea/Generating/In-review posts are planning-only blocks (no __resolveType); scheduling promotes them to real collections/blog/posts blocks so the site never renders unfinished work. Editing preserves the block's form. - New BLOG_PILLAR_SUGGEST tool; the idea suggester takes pillar/format/seed context. Generate ideas is a modal. Status labels stay English in every locale. Data helpers + 5-status lifecycle are unit-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Archived Studio was writing `status: "idea"` / `"in_review"` into `payload.status` — values the blog app's own `PostStatus` union does not have, so the app could not read back what the CMS wrote. A test even documented `awaiting_review` as an *unrecognized* value. `PostStatus` is now the app's union, in lifecycle order: draft, generating, awaiting_review, scheduled, published, archived. `postStatus()` reads all six and maps the legacy Studio-only `idea` / `in_review` onto their app equivalents; `stampPostModified()` retires those two names on any save, so decofiles written by earlier commits on this branch heal themselves. Nothing in the app was touched. Deleting a post is a soft delete: it moves to Archived, which is a planning-form block with no `__resolveType`, so the site stops rendering it without depending on the app to filter by status. The delete affordance is on both the board card and the list row, and is hidden for a post already archived. Archived is a sixth board lane that collapses to a rail (still a drop target), with the toggle persisted in localStorage. Also cleans up the i18n this branch left behind: 19 pt-BR entries that were still English (including a regression on statusScheduled/statusPublished), 68 keys orphaned when generate.tsx / themes.tsx / autonomous.tsx were deleted, new keys reordered into place, and the two hardcoded toasts in content-browser.tsx routed through `t()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cally
A post's status could be changed from four places — dragging a board lane, the
delete-into-Archived action, the drawer opened from the board, and the same
drawer as the list's right pane — and they did not agree with each other.
A status move is special: it renames the block. Planning states live at
`blog-manager/posts/<id>` with no `__resolveType`, live states at
`collections/blog/posts/<id>`. Crossing that boundary is a write AND a delete,
and it was done as two sequential requests by two divergent copies of the same
code. Between the two awaits both keys existed, so `listAllPostsWithMeta`
rendered the post in two lanes at once — and a failed delete left that
duplicate committed with nothing to roll it back.
`movePostToStatus` already returned a `PostMove { writes, deletes }` and its
own doc said the caller should apply it "as one atomic patchDecofile". That API
existed and commits set+delete in a single commit; nothing consumed both
fields. `useMoveBlocks` now does, and patches the cache synchronously before
dispatching rather than from `onMutate` — so a caller that follows the post to
its new key re-points in the same tick, and no render sees the old key already
gone and the new one not yet there. That was the "Untitled post" flash.
`usePostStatusMove` is now the single path. Every surface asks the same
`refuse` before offering a move and applies it through the same mutation, so
none of them can disagree or leave a post half-moved. It reports a rename as
(from, to), so the board — which drags posts it has not opened — only re-points
a selection that was already on that post instead of popping the drawer.
The editor hands its live draft to the move and cancels the pending autosave,
which would otherwise land on the key the move just retired and resurrect the
post's old form. It is keyed by post id rather than block key, so a move no
longer remounts it and resets the open tab.
The capability gate applies to the board too: a lane the site's blog app cannot
honour is dimmed and refuses the drop with the version it needs, the same
answer the editor's control gives. That control now offers the full lifecycle
instead of two switches, with an exhaustive refusal reason.
Also deletes the posts surface in `ItemList` — filter bar, sort, bulk selection
and the bulk category panel — unreachable since ff990f3, along with the
helpers, tests and 49 translation keys it owned. That retires the second set of
status labels, leaving one map shared by the board and the editor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An idea and a content pillar were the same shape in code — a title and a
markdown body, both suggested the same way, both deduped by the same helper —
so nothing could tell them apart, and the pillar the wizard collected never
even reached the generator.
They are different things. A pillar is a territory the brand returns to across
many posts ("Electrolux product advantages"); an idea is one angle inside it
("why Inverter is worth it"). The test that separates them: can you write ten
posts from it? Then it is a pillar.
That distinction had an origin. `blog-manager/themes/*` was the ideas queue;
ff990f3 renamed themes to pillars and had `scanPillars` union the old prefix,
which turned every stored idea into a pillar. This restores the split: ideas
own that prefix again — it is what they always were — and pillars stop reading
it, so old sites get their ideas back with no migration.
An idea is now its own block with an optional `pillarKey`, and it is NOT a
post: it has no status, never moves through the lifecycle, and one idea is
worth several posts in several formats. On the board it sits in a tray beside
the lanes rather than in them, and writing from it produces a new post while
the idea stays put. Generating ideas takes a saved pillar, so ideas are born
inside a territory and carry the link.
The wizard drops its pillar step: the chosen idea carries the pillar, and
asking twice would let the two disagree. `BLOG_POST_DRAFT` gains that pillar
plus the authors it never had, and the system prompt now separates the ground
from the angle — a post that restates the pillar is the article the brand
already published.
Also: every board column collapses, not just Archived, keyed by status so a
reordered board cannot reopen the wrong one; and the Brand tab's "Blog
categories" field is gone — it duplicated the Categories collection and nothing
but its own input ever read it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…og-cms-context-ideas-posts # Conflicts: # apps/web/src/i18n/en/sandbox.ts # apps/web/src/i18n/pt-br/sandbox.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Restructures the blog CMS around a cleaner mental model. Targets
blogpost-generation-cms, notmain.__resolveType), so the live site never renders unfinished work. Scheduling promotes them to realcollections/blog/posts/*blocks (id + slug preserved); editing preserves the block's form.BLOG_PILLAR_SUGGESTtool; the idea suggester now takes pillar/format/seed context. Generate ideas is a modal.Testing
bun run check(all workspaces) ✅bun testblog suite — 278 pass (5-statuspostStatus/setPostStatus/blocksPostStatus,movePostToStatuspromote/demote,scanPillarslegacy-themes union, lifecycle scans) ✅bun run lint— 0 errors ✅ ·knipclean on touched files ✅Follow-ups (tracked, not in this PR)
Generate-full-post-from-idea (
BLOG_POST_DRAFT→ In review), Board/Calendar unified toggle, dedicated credit-consumption pill, cms-tour coach-marks, and cleanup of the now-unreachable authors/categories content-browser branches + orphaned i18n keys.🤖 Generated with Claude Code
Summary by cubic
Restructures the blog CMS around a single post lifecycle — Draft → Generating → Awaiting review → Scheduled → Published — and makes ideas separate objects that never enter it. "Autonomous content" becomes Context, a five-tab screen (Brand, Formats, Content pillars, Authors, Categories); the sidebar's Blog group is now Context · Posts · Scheduling. Posts offers two views of the same data: a drag-to-advance board and a master-detail list groupable by status, format, or pillar. Deletes are soft — a post moves to Archived. Targets
blogpost-generation-cms, notmain.Storage
__resolveType, so the live site never renders unfinished work.collections/blog/posts/*blocks; demotion preserves the post id, slug, and block form.PostStatusunion; legacyidea/in_reviewvalues map onto their equivalents and are rewritten on save.pillarKey— no status, and writing from it produces a new post while the idea stays put.blog-manager/themes/*holds ideas again, so sites whose stored ideas were read as pillars get them back with no migration.AI
BLOG_PILLAR_SUGGESTtool; the idea suggester takes pillar, format, and seed context, and the idea generation wizard drops its separate pillar step.BLOG_POST_DRAFTreceives the chosen pillar and authors. Status and lane labels stay English in every locale.Written for commit 6bd8cb9. Summary will update on new commits.