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.
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
_revdespite 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
Environment
4c5aa726a2d858f125109d7727af34d645c5b874. Main was inspected, not separately executed in this reproduction.Screenshots
Logs / error output
Sanitized sequence from the isolated reproduction:
Root cause and proposed fix direction
In
packages/admin/src/router.tsx,ContentEditPageinitializesrevisionTokensRefonly while the entry id is absent from the map. Subsequent successful query refreshes do not synchronize the token.updateMutationsupplies the token from that map, andhandleSeoChangeuses 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:
Related but different conflict recovery changes: #2902 and #3118. This report concerns the token retained after a successful clean refetch, before any conflict occurs.