# Images

> Images, figures and uploads.

## In Markdown

Plain Markdown, no block needed: `![Alt text](/cat.png)`. Alone in its paragraph with a title, it becomes a figure:

```md
![A cat on a sofa](/cat.png "Our cat, 2026")
```

```html
<figure>
  <img src="/cat.png" alt="A cat on a sofa" loading="lazy" decoding="async" />
  <figcaption>Our cat, 2026</figcaption>
</figure>
```

Size it with attributes so the page doesn't shift: `![Alt](/cat.png){width="800" height="600"}`. Uploads add them for you.

- `data:image/…` URLs work, for images only.
- An image without a URL (`![]()`) doesn't render. In the editor it's an empty card to fill.
- For responsive images or a CDN, replace `img` through `components`, e.g. `{ img: "NuxtImg" }`.

## In the editor

Images on their own line are cards: the picture, its caption and its alt text, edited in place. Arrowing onto a card selects it; Backspace or Delete removes it, Enter adds a paragraph after it. A card never turns back into Markdown; switch to source mode for that.

## Uploads

Give the editor an `upload` function: it gets the `File` and resolves to its URL.

```ts
async function upload(file: File) {
  const body = new FormData();
  body.append("file", file);
  const { url } = await $fetch<{ url: string }>("/api/uploads", { method: "POST", body });
  return url;
}
```

While it uploads, the card shows the local file at its real size, so nothing moves when the URL arrives. Several files upload side by side, in order. A failed upload fires `upload-error` and removes its placeholder, or puts a replaced card back the way it was.

On the server, check what you're given: the file type by its first bytes, not its name, and a size limit. Don't accept SVG on your own origin: it can run script.

```vue
<script setup lang="ts">
const body = ref("Drop, paste or choose an image:\n\n![]()\n");
const editor = useTemplateRef("editor");
const error = ref("");

// Your endpoint stores the file and answers with its URL. Here: the image inlined,
// after a moment, so you can see it upload.
async function upload(file: File) {
  await new Promise((resolve) => setTimeout(resolve, 1500));
  return new Promise<string>((resolve) => {
    const reader = new FileReader();
    reader.addEventListener("load", () => resolve(String(reader.result)));
    reader.readAsDataURL(file);
  });
}
</script>

<template>
  <div class="writer-demo">
    <WriterToolbar :editor="editor" :items="['bold', 'italic', 'image']" />
    <WriterEditor
      ref="editor"
      v-model="body"
      label="Body"
      :upload="upload"
      @upload-error="(_, file) => (error = `Couldn't upload ${file.name}.`)"
    />
    <p class="writer-demo-stats">
      {{ error || (editor?.uploading ? "Uploading…" : "Ready to save") }}
    </p>
  </div>
</template>
```
