fix(plugins): route ctx.content seo writes to the SEO panel#453
Merged
ascorbic merged 1 commit intoemdash-cms:mainfrom Apr 11, 2026
Merged
fix(plugins): route ctx.content seo writes to the SEO panel#453ascorbic merged 1 commit intoemdash-cms:mainfrom
ascorbic merged 1 commit intoemdash-cms:mainfrom
Conversation
Fixes emdash-cms#374 ctx.content.create/update previously failed with "SQLite error: no such column: seo" when the input data included a seo key, because SEO fields live in the separate _emdash_seo table, not on the content row. Plugins could not programmatically populate the core SEO panel, blocking WordPress/Yoast importers and AI-assisted SEO tools. The plugin content adapter now extracts the reserved seo key from the input, runs the content update and the SeoRepository.upsert inside the same withTransaction, and hydrates ContentItem.seo on reads for SEO-enabled collections. Writing seo to a collection without has_seo throws a validation error, matching the REST API semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 5d016a5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Open
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.
What does this PR do?
Fixes
ctx.content.create()/ctx.content.update()so plugins can write to the core SEO panel. Previously, passing aseokey in the input failed withSQLite error: no such column: seobecause SEO fields live in the separate_emdash_seotable, not on the content row. Plugins could not programmatically populate the SEO panel, which blocked WordPress/Yoast importers, AI-assisted SEO tools, and content automation plugins.The plugin content adapter now mirrors the REST API behavior:
createContentAccessWithWrite.create/updateextract the reservedseokey from the input and route it toSeoRepository.upsert, running content + SEO writes inside a singlewithTransactionso either both succeed or neither does.seoto a collection withouthas_seo = 1throws a validation error matching the REST API (Collection "<x>" does not have SEO enabled).createContentAccess.get/listhydrateContentItem.seofor SEO-enabled collections, so plugins can read current SEO state before doing non-destructive updates (also requested in the issue).SeoRepository.upsertCOALESCElogic).SeoRepository.isEnabled(collection)helper encapsulates thehas_seolookup used by both the plugin context and the new tests.ContentItem,ContentItemSeo,ContentItemSeoInput, andContentWriteInputare exported fromplugins/types.tsso plugin authors get full typings.Closes #374
Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0 (for files touched by this PR)pnpm testpasses (2257 tests; 8 new regression tests for [plugins] ctx.content.update() cannot write to seo.* fields (no such column: seo) #374)pnpm formathas been runAI-generated code disclosure
Implemented with Claude Code (Opus 4.6). A human reviewed every change, ran the test suite, and wrote the regression tests against the exact repro from the issue. Reviewers may want to pay extra attention to: the transaction boundary in
createContentAccessWithWrite.update(the seo-only path intentionally skipsContentRepository.updateso it does not bumpupdated_at/versionwhen only SEO changed — open to flipping this if the project prefers always bumping on any write), and the decision to reserveseoas a magic key inside the flatdatamap rather than introducing a new envelope shape.Screenshots / test output
New tests added in
packages/core/tests/integration/plugins/capabilities.test.tsunderContent Access > SEO panel integration:get()returns SEO defaults for SEO-enabled collectionsget()omits seo for non-SEO collectionsupdate()routesseoto the SEO panel (direct regression test for [plugins] ctx.content.update() cannot write to seo.* fields (no such column: seo) #374)update()accepts field updates alongside seo in one callupdate()partial seo updates do not clobber existing fieldscreate()routesseoto the SEO panelupdate()throws when seo is provided on a non-SEO collectionlist()hydrates seo for each item in SEO-enabled collections