# Introduction

> A Markdown editor and renderer for Vue 3 and Nuxt.

Authors write Markdown, but it reads like the published page: formatting marks tuck away, blocks render as your own components, and images are cards with their caption and alt text. Readers get the same Markdown rendered by the same components, server-side.

```vue
<script setup lang="ts">
import { countWords, readingMinutes } from "@humanonlyweb/writer";

const body =
  ref(`City council voted 7–2 on Tuesday to approve this winter's snow-clearing budget, which officials describe as "ambitious, in a spiritual sense."

::callout{type="note"}
The **full budget** is available at city hall, between 2:00 and 2:05 p.m.
::

## What residents can do

- Shovel early, shovel often
- [x] Buy salt
- [ ] Lower expectations

> "It's not a plan, it's a vibe," said one resident.

![A snowy street](https://picsum.photos/id/1036/900/360 "Cul-de-sac, February. Artist's impression."){width="900" height="360"}

Drop a photo of your street below, or choose one:

![]()
`);

const editor = useTemplateRef("editor");
const mode = ref<"live" | "source">("live");
const icons = {
  heading2: "heading",
  bold: "bold",
  italic: "italic",
  link: "link",
  quote: "quote",
  bulletList: "list",
  image: "image",
} as const;
const words = computed(() => countWords(body.value));

// A stand-in for your upload endpoint: the image, inlined, after a moment.
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">
    <div class="writer-demo-bar">
      <WriterToolbar :editor="editor" :items="Object.keys(icons) as (keyof typeof icons)[]">
        <template #item="{ command }">
          <Icon :name="icons[command as keyof typeof icons]" />
        </template>
      </WriterToolbar>
      <button
        type="button"
        class="writer-demo-toggle"
        :aria-pressed="mode === 'source'"
        @click="mode = mode === 'source' ? 'live' : 'source'"
      >
        Markdown
      </button>
    </div>
    <WriterEditor ref="editor" v-model="body" :mode="mode" label="Article" :upload="upload" />
    <p class="writer-demo-stats">{{ words }} words · {{ readingMinutes(words) }} min read</p>
  </div>
</template>
```

- **Markdown is the source of truth.** Store it, diff it, edit it anywhere.
- **Blocks are your Vue components.** `::callout` renders `Callout.vue`, in the editor and on the page.
- **Fast pages.** Parse on save and render the stored document: visitors download neither the parser nor the editor.
- **Safe by default.** Raw HTML is text, and only Markdown's own elements and your blocks render.
- **SSR, accessible, unstyled.** Works with server rendering, follows the ARIA patterns, and takes your site's CSS.

Built on [CodeMirror 6](https://codemirror.net) and [Comark](https://comark.dev).
