Skip to content

feat(cms): restructure blog into Context, a lifecycle board, and a list - #6622

Open
danielfurt wants to merge 5 commits into
blogpost-generation-cmsfrom
blog-cms-context-ideas-posts
Open

feat(cms): restructure blog into Context, a lifecycle board, and a list#6622
danielfurt wants to merge 5 commits into
blogpost-generation-cmsfrom
blog-cms-context-ideas-posts

Conversation

@danielfurt

@danielfurt danielfurt commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Restructures the blog CMS around a cleaner mental model. Targets blogpost-generation-cms, not main.

  • Context (renamed from Autonomous content): a 5-tab screen — Brand · Formats · Content pillars · Authors · Categories. Pillars are the reconceived "themes" (recurring communication territories); Authors/Categories moved in from their own sidebar rows. Blog sidebar is now Context · Posts · Scheduling.
  • Posts is one lifecycle the same card travels: Idea → Generating → In review → Scheduled → Published. Two views of the same data:
    • Board — Kanban with drag-to-advance; clicking a card opens a floating right-anchored panel with an expand → full page toggle (expand + close live in a top cluster).
    • Listmaster-detail (narrow grouped list + detail pane, auto-selects the first post), groupable by status / format / pillar.
  • Lifecycle storage invariant: Idea/Generating/In-review posts are planning-only blocks (no __resolveType), so the live site never renders unfinished work. Scheduling promotes them to real collections/blog/posts/* blocks (id + slug preserved); editing preserves the block's form.
  • AI: new BLOG_PILLAR_SUGGEST tool; the idea suggester now takes pillar/format/seed context. Generate ideas is a modal.
  • Status/lane labels stay English in every locale (product terms).

Testing

  • bun run check (all workspaces) ✅
  • bun test blog suite — 278 pass (5-status postStatus/setPostStatus/blocksPostStatus, movePostToStatus promote/demote, scanPillars legacy-themes union, lifecycle scans) ✅
  • bun run lint — 0 errors ✅ · knip clean 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, not main.

Storage

  • Draft, Generating, Awaiting-review, and Archived posts are planning-only blocks without __resolveType, so the live site never renders unfinished work.
  • Scheduling promotes them to real collections/blog/posts/* blocks; demotion preserves the post id, slug, and block form.
  • Statuses use the blog app's own PostStatus union; legacy idea/in_review values map onto their equivalents and are rewritten on save.
  • A status move is one atomic rename, so a post never renders in two lanes at once.
  • Moving to a live state is gated on the site's blog app version; an unsupported lane dims and refuses the drop with the version it needs.
  • An idea is its own block with an optional 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

  • Adds the BLOG_PILLAR_SUGGEST tool; the idea suggester takes pillar, format, and seed context, and the idea generation wizard drops its separate pillar step.
  • BLOG_POST_DRAFT receives the chosen pillar and authors. Status and lane labels stay English in every locale.

Written for commit 6bd8cb9. Summary will update on new commits.

Review in cubic

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>
@github-actions github-actions Bot added the claude PR authored by a coding agent label Aug 26, 2026
decobot and others added 4 commits August 27, 2026 13:46
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant