Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-mdx-external-image-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystatic/core': patch
---

Fix `fields.mdx()` and `fields.markdoc()` handling of external `http://` / `https://` image URLs so remote images are preserved on save without corrupting document content or registering phantom local file deletions in `TrackedMap`.
4 changes: 3 additions & 1 deletion packages/keystatic/src/app/useItemData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class TrackedMap<K, V> extends Map<K, V> {
this.#onGet = onGet;
}
get(key: K) {
this.#onGet(key);
if (super.has(key)) {
this.#onGet(key);
}
return super.get(key);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,15 +384,29 @@ function markdocNodeToProseMirrorNode(
return childrenToProseMirrorNodes(node.children, parentType);
}
if (node.type === 'image') {
if (
typeof node.attributes.src === 'string' &&
/^(?:https?:)?\/\//i.test(node.attributes.src) &&
schema.nodes.image
) {
return schema.nodes.image.createChecked({
src: node.attributes.src,
alt: node.attributes.alt,
title: node.attributes.title,
filename: '',
});
}
const prefix = getSrcPrefixForImageBlock(schema.config, getState().slug);
const fullFilename = decodeURIComponent(node.attributes.src);
const filename = fullFilename.slice(prefix.length);
const content = (
typeof schema.config.image === 'object' &&
typeof schema.config.image.directory === 'string'
? getState().otherFiles.get(fixPath(schema.config.image.directory))
: getState().extraFiles
)?.get(filename);
const content = fullFilename.startsWith(prefix)
? (
typeof schema.config.image === 'object' &&
typeof schema.config.image.directory === 'string'
? getState().otherFiles.get(fixPath(schema.config.image.directory))
: getState().extraFiles
)?.get(filename)
: undefined;

if (content && schema.nodes.image) {
return schema.nodes.image.createChecked({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ function getLeafContent(
}
if (node.type === schema.nodes.image) {
const { src, filename } = node.attrs;
if (typeof src === 'string') {
return new Ast.Node('image', {
src,
alt: node.attrs.alt,
title: node.attrs.title,
});
}

if (
typeof state.schema.config.image === 'object' &&
Expand Down
22 changes: 16 additions & 6 deletions packages/keystatic/src/form/fields/markdoc/editor/mdx/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,25 @@ function markdocNodeToProseMirrorNode(
);
return null;
}
if (/^(?:https?:)?\/\//i.test(data.url) && schema.nodes.image) {
return schema.nodes.image.createChecked({
src: data.url,
alt: node.alt,
title: data.title,
filename: '',
});
}
const prefix = getSrcPrefixForImageBlock(schema.config, getState().slug);
const fullFilename = decodeURI(data.url);
const filename = fullFilename.slice(prefix.length);
const content = (
typeof schema.config.image === 'object' &&
typeof schema.config.image.directory === 'string'
? getState().otherFiles.get(fixPath(schema.config.image.directory))
: getState().extraFiles
)?.get(filename);
const content = fullFilename.startsWith(prefix)
? (
typeof schema.config.image === 'object' &&
typeof schema.config.image.directory === 'string'
? getState().otherFiles.get(fixPath(schema.config.image.directory))
: getState().extraFiles
)?.get(filename)
: undefined;

if (content && schema.nodes.image) {
return schema.nodes.image.createChecked({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ function getLeafContent(
}
if (node.type === schema.nodes.image) {
const { src, filename } = node.attrs;
if (typeof src === 'string') {
return {
type: 'image',
url: src,
alt: node.attrs.alt,
title: node.attrs.title,
};
}

if (
typeof state.schema.config.image === 'object' &&
Expand Down
29 changes: 22 additions & 7 deletions packages/keystatic/src/form/fields/markdoc/editor/schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,16 @@ const nodeSpecs = {
},
},
nodeView(node) {
const blob = new Blob([node.attrs.src], {
type: node.attrs.filename.endsWith('.svg')
? 'image/svg+xml'
: undefined,
});
const isExternalUrl = typeof node.attrs.src === 'string';
const blob = isExternalUrl
? undefined
: new Blob([node.attrs.src], {
type: node.attrs.filename.endsWith('.svg')
? 'image/svg+xml'
: undefined,
});
const dom = document.createElement('img');
dom.src = URL.createObjectURL(blob);
dom.src = isExternalUrl ? node.attrs.src : URL.createObjectURL(blob!);
dom.alt = node.attrs.alt;
dom.title = node.attrs.title;
dom.dataset.filename = node.attrs.filename;
Expand All @@ -395,11 +398,23 @@ const nodeSpecs = {
return {
dom,
destroy() {
URL.revokeObjectURL(dom.src);
if (!isExternalUrl) {
URL.revokeObjectURL(dom.src);
}
},
};
},
toDOM(node) {
if (typeof node.attrs.src === 'string') {
return [
'img',
{
src: node.attrs.src,
alt: node.attrs.alt,
title: node.attrs.title,
},
];
}
return [
'img',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1067,3 +1067,9 @@ test('ordered list with start', () => {
"
`);
});

test('external https:// image URL is preserved on round-trip without escaping or phantom local file registration', () => {
const mdx = `![External diagram](https://example.com/assets/diagram.png)\n`;
const editor = fromMDX(mdx);
expect(toMDX(editor)).toBe(mdx);
});