Images and external resources
Resolve private assets, release resources and preserve durable source identifiers.
Separate source fields from browser resources
Markdown stores a durable asset reference, while the browser may need signed URLs, blobs or authenticated endpoints. Image.configure({ resolve }) maps source to { url, release? } without persisting credentials.
This complete same-origin adapter accepts /assets/<id>. The host must implement GET /api/assets/:id; this documentation site does not send requests to that illustrative endpoint.
import { Image } from "@quirekit/msfe-image";
import { StarterKit } from "@quirekit/msfe-starter-kit";
/** A same-origin asset resolver; authentication remains in the host fetch policy. */
export const PrivateImages = Image.configure({
/** @param source - Canonical asset reference. @param signal - This image's lifetime. @returns One browser resource lease. */
async resolve(source, signal) {
const match = /^\/assets\/([a-zA-Z0-9_-]+)$/.exec(source);
if (!match) throw new Error("unsupported-asset-reference");
const response = await fetch(`/api/assets/${encodeURIComponent(match[1]!)}`, { signal, credentials: "same-origin" });
if (!response.ok) throw new Error(`asset-http-${response.status}`);
const blob = await response.blob();
if (!/^image\/(png|jpeg|webp|gif)$/.test(blob.type) || blob.size > 5_000_000) throw new Error("unsupported-image");
const url = URL.createObjectURL(blob);
return { url, release() { URL.revokeObjectURL(url); } };
},
});
/** Replace the preset's image owner; the host must implement GET /api/assets/:id. */
export const privateImageKit = StarterKit.configure({ image: PrivateImages });
The recipe replaces StarterKit's image entry to avoid duplicate ownership. It resolves durable identifiers to Blob URLs and revokes each URL on release. Its raster-type and 5 MB limits are application policy, adjustable for your storage contract.
Paste, drop and upload
The image feature handles paste and drop: when the clipboard or drop carries only image files it stores them and inserts  where they landed. A source anchor holds the landing position during the upload; read-only editors are left alone. upload decides where images go:
import { Image, type ImageUploader } from '@quirekit/msfe-image'
const uploader: ImageUploader = {
async upload(file, { source, name, signal }) {
const body = new FormData()
body.append('file', file, name ?? 'image')
const response = await fetch('/api/assets', { method: 'POST', body, signal })
if (!response.ok) throw new Error('upload-failed')
return (await response.json()).url // durable address written to Markdown
},
}
Image.configure({ upload: uploader })
- With
upload: the returned address is written after the upload; a failed image is skipped and the host reports the failure insideupload.sourcedistinguishes paste, drop, dialog, Word import and AI, for quotas or naming. - Without
upload: PNG, JPEG, GIF and WebP are inlined asdata:URLs, keeping the document self-contained but larger; SVG is never inlined and needs a host upload. upload: false: paste and drop are left to your own extension.
The optional resolve is a reverse lookup that fetches stored addresses back as data URIs for Word and PDF export.
Insert command
const inserted = editor.commands.insertImage({
url: '/assets/diagram_42',
alt: 'System diagram',
title: 'Architecture review',
})
The command validates fields and encodes Markdown. When you build your own upload entry (such as a dialog), capture intent, upload, obtain a durable address and issue the command. Use a mapped anchor for long waits. Cancellation or document replacement must not redirect the insertion to a new cursor position.
Built-in Image validates source destinations before invoking the resolver and rejects schemes such as asset:. Use safe relative paths or HTTPS identifiers. Supporting a new scheme requires your own field validation and codec, not just a resolver.
Replace the image component
Image.extend({ addNodeView() { return VueNodeViewRenderer(ImageView) } }) retains the codec and resource protocol. Read nodeView.status for loading/ready/error and use the resolved nodeView.resource. Mutate source through guarded attribute actions.
Add design-system controls, title forms and error UI without moving content out of CodeMirror or changing only a DOM src and treating that as saved data. Resource display and persistent source edits have different owners.
Links and external navigation
Link.configure({ openLink }) controls safe link opening after an explicit user action. Projection building and resource loading must not navigate. Reference-style syntax, relative paths and special schemes need codec-specific handling; successful display does not prove every spelling supports exact field writes.
Verification
Cover success, HTTP failure, non-image responses, oversized data, late responses, deletion, readonly and undo; verify paste and drop both with an uploader and inline. Ensure object URLs are released and Markdown retains durable identifiers. Continue with Resources and UI and themes.