Thinkmill / Thinkmill/keystatic
fields.mdx(): external https:// image URL treated as local asset, corrupts body on save, causes GraphQL deletion error (v0.6.9)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 159
- Avg merge
- 21h 41m
- Merged PRs (30d)
- 2
Description
Title
fields.mdx() treats a fully-external https:// image URL as a local asset, corrupts body content on save, and causes [GraphQL] A path was requested for deletion which does not exist
Body
Environment
@keystatic/core: 0.6.9 (latest at time of writing)- Storage:
kind: 'github', committing directly to a branch - Collection field affected:
fields.mdx()for the content field,options.imagenot configured (nodirectory/publicPath)
What we did
We have a collection whose body field is fields.mdx() with no options.image set:
body: fields.mdx({
label: 'Body',
components: { /* a few custom block/inline components, none related to images */ },
})
The markdown body references images by a fully-qualified external URL, e.g.:

These images are not managed by Keystatic at all — they live in a completely separate storage/CDN outside the repository. We intentionally used plain markdown image syntax with an absolute https:// URL specifically so Keystatic would treat them as external and leave them alone.
Expected behavior
Since the image src is a fully-qualified external URL (not a path relative to any configured options.image.directory), we expected Keystatic to either:
- render/preserve it as a normal image reference without trying to resolve local file content for it, or
- at minimum, round-trip the markdown unchanged and not attempt to register/delete any file for it.
Actual behavior
-
Content corruption on every save. On load, since no local file matches the URL, the editor falls back to treating the entire
as a plain text node (not an image node). On save, the markdown serializer then backslash-escapes the markdown-special characters in that text run (to avoid re-triggering markdown/autolink parsing), producing:!\[some alt text\]\(https\://assets.example.com/posts/my-post/01.abcdef12.png\)Every save further mangles the content this way.
-
Save fails outright with a GraphQL error. More critically, saving the entry (via the GitHub storage mode's commit mutation) fails with:
[GraphQL] A path was requested for deletion which does not exist as of commit oid `<current head oid>`Inspecting the mutation's
fileChanges.deletions, it lists paths shaped like:<collection-path>/<entry>/<content-field-name>/https://assets.example.com/posts/my-post/01.abcdef12.pngi.e. the full external URL string is being concatenated as if it were a relative filename under an implicit
<entry>/<contentFieldName>/asset directory, and Keystatic then asks GitHub to delete that path. That path has never existed in the repository (verified viagit ls-treeagainst the exact commit oid referenced in the mutation) — there is no<content-field-name>/subdirectory for this entry at all, sinceoptions.image.directorywas never configured.This makes it impossible to save any entry whose body contains at least one markdown image with an external URL, regardless of which field is being edited (title, a plain text field, etc. all trigger it, since the whole body gets re-serialized on every save). Entries with zero images in the body save fine.
What we tried
- Cleared browser
localStorageand re-authenticated — same error. - Confirmed (via
git ls-tree -r <commit-oid>) that the paths listed infileChanges.deletionsdo not and have never existed in the repository, at the exact commit oid the mutation targets. - Reproduced the escaping behavior in isolation by calling the published package's own
fields.mdx()factory directly (importing the built browser bundle in Node, bypassing the UI) and round-tripping a body containing only a markdown image pointing at an external URL: parsing produces a plain-text fallback node (no image node), and serializing it back out reproduces the exact backslash-escaped string seen in the real save payload. This confirms the escaping/mis-handling originates in thefields.mdx()parse/serialize round-trip itself, for images without a resolvable local asset. - However, that same isolated round-trip on the field alone returns empty
other/externalmaps for this content — i.e. the field's ownserialize()doesn't appear to register any file changes for the external image. So the<content-field-name>/<url>deletion path visible in the real save request must be computed elsewhere (some other diffing step between the loaded entry state and the outgoing mutation), which we haven't been able to pin down from outside the app. - Workaround that avoids the bug entirely: define a custom block/inline component (e.g.
ImageBlockwith asrc: fields.url()prop) and use that JSX component instead of bare markdown image syntax for any image with a fully-external URL. Since thesrcis an ordinary text/url form field (not an asset-kind field), it's never asset-tracked, and saving works normally.
Question
Is there a supported way to reference a fully-external image URL in a fields.mdx() body without it being mis-treated as a local asset (and without resorting to a custom component as a workaround)? If not, this seems like it should either:
- skip asset resolution entirely for
image/imageReferencenodes whoseurlis an absolute URL (http:///https://), or - at minimum, not silently corrupt the markdown via escaping, and not emit spurious deletions for paths that were never real files.
Happy to put together a minimal reproduction repo if useful.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the fields.mdx() parse/serialize round-trip for a markdown image using an absolute https:// URL, then trace how the GitHub storage commit mutation builds fileChanges.deletions. Reproduce the escaped markdown and spurious deletion path described here; done means external URLs round-trip unchanged and no nonexistent asset deletion is emitted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- content
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100