# 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.

### recipes/image.ts

```ts
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 `![alt](<address>)` where they landed. A source anchor holds the landing position during the upload; read-only editors are left alone. `upload` decides where images go:

~~~ts
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 inside `upload`. `source` distinguishes paste, drop, dialog, Word import and AI, for quotas or naming.
- Without `upload`: PNG, JPEG, GIF and WebP are inlined as `data:` 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

~~~ts
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](/en/guide/selection) 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](/en/extensions/resources) and [UI and themes](/en/customization/theme).
