Repository navigation
Admin: selecting an OG image sends a cached revision after a clean entry refetch #3829
Description
Description
Selecting an OG image from the media library can fail with HTTP 409 CONFLICT even when the content editor shows Saved and there are no unsaved content edits.
The editor can mount with a cached entry, then fetch and display the newer server entry. Its separate revision token remains the cached older token. The immediately triggered SEO save therefore sends an outdated _rev despite having already read the current record.
Expected: when a clean editor adopts the refreshed entry, the next SEO save uses the revision associated with that entry. Genuine conflicts involving unsaved edits must remain protected.
Observed: the OG save fails and the editor displays the generic conflict banner. A full browser page reload followed by selecting the same image succeeds.
Steps to reproduce
- Open an existing entry in a collection with SEO enabled, without changing content fields. This seeds the admin query cache.
- Navigate back to the collection list using the SPA navigation.
- Update the same entry through another authorized context, for example changing only the SEO noIndex setting. Keep the first admin SPA open so its cached older entry remains available.
- Allow the cached query to become stale, then reopen the entry from that same admin SPA. Wait for the content GET to return the newer revision. The editor still shows Saved.
- Select a different OG image from the media library.
- Inspect the content PUT: it sends the older cached revision rather than the revision from the completed GET and receives 409 CONFLICT.
- Fully reload the entry page and select the same image again: the correct revision is sent and the image is saved.
Environment
- emdash: 1.1.0
- @emdash-cms/admin: 1.1.0
- OS: macOS
- Reproduction runtime: Node 24.15.0 and SQLite
- Original reported runtime: Cloudflare Workers / D1
- Reproduction used the unchanged installed admin frontend and real EmDash content get/update handlers against a separate local content copy. Authentication and unrelated endpoints were supplied by a local harness; edit-lock endpoint behavior and background media maintenance were excluded.
- The same initialization condition remains in main at
4c5aa726a2d858f125109d7727af34d645c5b874. Main was inspected, not separately executed in this reproduction.
Screenshots
Logs / error output
Sanitized sequence from the isolated reproduction:
GET content/<collection>/<entry> -> version 166
Editor: Saved; no user content edits
PUT content/<collection>/<entry> with seo.image and _rev encoding version 165
HTTP 409 CONFLICT: Content has been modified since last read (version conflict)
After full browser page reload:
GET -> version 166
PUT seo.image with revision 166 -> HTTP 200, version 167
Read-back: selected OG image persisted; content fields unchanged; no draft created
Root cause and proposed fix direction
In packages/admin/src/router.tsx, ContentEditPage initializes revisionTokensRef only while the entry id is absent from the map. Subsequent successful query refreshes do not synchronize the token. updateMutation supplies the token from that map, and handleSeoChange uses that mutation immediately.
Source references on current main:
Suggested approach: associate the write revision with the entry snapshot actually adopted by the form. Synchronize it when a clean editor adopts a newer snapshot, while preserving the base revision for unsaved content. Do not blindly overwrite the token on every refetch: that could permit unsaved stale content to overwrite a newer entry or let a delayed read replace a newer successful-write token.
Suggested regression coverage:
- Cached old entry followed by a fresh read in a clean editor: OG save sends the fresh revision and succeeds.
- The same refresh while content is dirty: retain the existing conflict protection and the user's edits.
- A delayed old GET must not replace the token returned by a newer successful write.
- Preserve conflict recovery, Save anyway, publish/unpublish/discard/restore behavior, and entry/locale identity.
Related but different conflict recovery changes: #2902 and #3118. This report concerns the token retained after a successful clean refetch, before any conflict occurs.

Activity
emdashbot commentedon Oct 3, 2026
Completed
Triage issue #3829: cached revision token after clean refetch causes OG image save 409.
Outcome: Confirmed bug in `packages/admin/src/router.tsx`. `ContentEditPage` initializes `revisionTokensRef` only when the entry id is absent from the map (`if (rawItem && !revisionTokensRef.current.has(rawItem.id))`), so a cached older `_rev` is retained after TanStack Query refetches a newer entry in a clean editor. `updateMutation` then sends the stale `_rev`, and `handleSeoChange` triggers it immediately, producing the reported 409 CONFLICT.
Scope is bounded: synchronize the token when a clean editor adopts a fresher `rawItem` snapshot, while preserving the base token when the editor is dirty and guarding against delayed old GETs overwriting a newer successful-write token (`_rev` encodes `version:updatedAt`, so version comparison is available). Likely needs a dirty-state callback from `ContentEditor` to `ContentEditPage` plus the existing token map. Existing `packages/admin/tests/editor-save-conflict.test.tsx` and `packages/admin/tests/router.test.tsx` provide regression scaffolding; add tests for clean-refetch OG save success, dirty-refetch conflict protection, and non-downgrade after write.
Mode: triage
View live dashboard · Run:
7afd0c2b-0f97-4930-b00e-88f8fcabbaaeemdashbot commentedon Oct 3, 2026
Completed
Fix cached revision token bug in admin ContentEditPage
Outcome: Fixed the cached revision token bug in the admin ContentEditPage (#3829).
Root cause: `ContentEditPage` only stored `rawItem._rev` on initial load. After TanStack Query refetched a newer clean entry, the stale cached token remained, so SEO/auxiliary saves sent the old `_rev` and received 409 CONFLICT.
Fix:
Verification: 44 tests passed (router + editor-save-conflict), admin typecheck passed, root build passed, format applied, lint:quick zero diagnostics. `lint:json` passed standalone but the chained jq invocation was killed by SIGKILL (likely OOM/environment). Published to `bot/fix-3829`.
Mode: work
View live dashboard · Run:
18774c42-2136-4f5f-9cc1-0fc53e521f9cemdashbot commentedon Oct 3, 2026
I opened PR #3831 with a candidate change.
Fixed the cached revision token bug in the admin ContentEditPage (#3829).
Root cause:
ContentEditPageonly storedrawItem._revon initial load. After TanStack Query refetched a newer clean entry, the stale cached token remained, so SEO/auxiliary saves sent the old_revand received 409 CONFLICT.Fix:
onDirtyChangetoContentEditorso the page knows when the form is dirty.ContentEditPagenow decodes_rev(formatbase64("version:updatedAt")) and ratchets the stored write token forward only while the editor is clean and the fetched version is newer. This preserves conflict protection for unsaved edits and prevents a delayed old GET from overwriting a newer successful-write token.router.test.tsxfor clean-refetch adoption, dirty retention, and non-downgrade after write.Verification: 44 tests passed (router + editor-save-conflict), admin typecheck passed, root build passed, format applied, lint:quick zero diagnostics.
lint:jsonpassed standalone but the chained jq invocation was killed by SIGKILL (likely OOM/environment). Published tobot/fix-3829.Try the change against your own site:
Or try the candidate in a ready-to-use playground:
Open the playground preview
@onvarga could you try this? Reply naturally to say whether it works or explain what still needs to change.
A maintainer reviews the pull request before merge; requested changes start another candidate revision.
Fix branch:
bot/fix-3829· Artifacts branch:bot/artifacts-3829onvarga commentedon Oct 3, 2026
Confirmed: the candidate fix resolves the originally reported OG image selection issue on our site.
We installed the preview from PR #3831, pinned to commit
2e1f2310f15b20966181d03e1a511906db5a9092, on both localhost and our Cloudflare Workers/D1 DEV environment. After testing the original workflow, I can confirm that selecting an OG image from the media library now works without the false conflict warning.This confirms the original reported workflow; it does not constitute a separate verification of all concurrent-edit scenarios.
Thank you for the quick diagnosis and fix!
emdashbot commentedon Oct 3, 2026
Thanks for confirming the change works! I've marked the issue as verified, and a maintainer will review PR #3831.
fix(admin): synchronize write revision with clean entry snapshot afte…
fix(admin): synchronize write revision with clean refetches while pre…
fix(admin): synchronize write revision with clean refetched snapshots (…
fix(admin): preserve edits and synchronize clean editor revisions (#3831
fix(emdash-bot): fix the failures found in an audit of 10 days of bot…