From d9cbc7d5114631fc850f7ac3400afc57e5bb1367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Tue, 6 Oct 2026 10:58:42 +0200 Subject: [PATCH 01/19] docs(seo): canonical description, query titles, llms.txt decision rule and topic files - One description everywhere (README, npm, llms.txt, schema, homepage meta and hero) - Homepage H1 "The Markdown parser and renderer for everything." and demo heading as h2 - Query titles for renderer, streaming, intro, syntax, security, why and example pages, with short sidebar labels kept in navigation.title; unique example titles - llms.txt: "When to use Comark" first, plus llms-{core,streaming,vue,react,svelte,angular,plugins}.txt - Sitemap lists /play and /stream; drop the unused /compare Vue page so /compare/** gets .md twins - Organization sameAs, npm keywords, comark-docs layer bumped to comarkdown/comark-docs#54 --- docs/app/app.config.ts | 12 +- docs/app/pages/compare.vue | 105 ---------------- docs/app/pages/play/index.vue | 4 +- docs/app/pages/stream.vue | 2 +- .../1.getting-started/0.introduction.md | 10 +- docs/content/2.syntax/2.components.md | 2 +- docs/content/3.rendering/2.html.md | 4 +- docs/content/3.rendering/3.vue.md | 4 +- docs/content/3.rendering/4.nuxt.md | 4 +- docs/content/3.rendering/5.react.md | 4 +- docs/content/3.rendering/6.svelte.md | 4 +- docs/content/3.rendering/7.angular.md | 4 +- docs/content/3.rendering/8.ansi.md | 4 +- docs/content/4.plugins/1.built-in/security.md | 5 +- docs/content/5.reference/2.auto-close.md | 6 +- docs/content/7.kb/0.why-comark.md | 4 +- docs/content/8.examples/1.frameworks/astro.md | 3 +- .../content/8.examples/1.frameworks/nextjs.md | 3 +- docs/content/8.examples/1.frameworks/nuxt.md | 5 +- .../8.examples/1.frameworks/sveltekit.md | 3 +- .../8.examples/1.frameworks/vitepress.md | 3 +- docs/content/8.examples/2.vite/angular.md | 5 +- docs/content/8.examples/2.vite/html.md | 3 +- docs/content/8.examples/2.vite/react.md | 5 +- docs/content/8.examples/2.vite/svelte.md | 5 +- docs/content/8.examples/2.vite/vue.md | 5 +- .../8.examples/3.plugins/react-vite-math.md | 5 +- .../3.plugins/react-vite-mermaid.md | 5 +- .../8.examples/3.plugins/vue-vite-binding.md | 3 +- .../3.plugins/vue-vite-highlight.md | 3 +- .../3.plugins/vue-vite-json-render.md | 3 +- .../8.examples/3.plugins/vue-vite-math.md | 5 +- .../8.examples/3.plugins/vue-vite-mermaid.md | 5 +- .../3.plugins/vue-vite-punctuation.md | 3 +- docs/content/8.examples/4.ai/nuxt-ai-sdk.md | 5 +- docs/content/index.md | 14 +-- docs/nuxt.config.ts | 19 ++- docs/package.json | 4 +- docs/server/plugins/llms.ts | 56 +++++++++ docs/server/routes/llms-angular.txt.get.ts | 1 + docs/server/routes/llms-core.txt.get.ts | 1 + docs/server/routes/llms-plugins.txt.get.ts | 1 + docs/server/routes/llms-react.txt.get.ts | 1 + docs/server/routes/llms-streaming.txt.get.ts | 1 + docs/server/routes/llms-svelte.txt.get.ts | 1 + docs/server/routes/llms-vue.txt.get.ts | 1 + docs/server/utils/llms.ts | 118 ++++++++++++++++++ packages/comark-angular/package.json | 6 +- packages/comark-ansi/package.json | 2 + packages/comark-html/package.json | 2 + packages/comark-nuxt/package.json | 4 + packages/comark-react/package.json | 6 +- packages/comark-svelte/package.json | 4 + packages/comark-vue/package.json | 4 + packages/comark/README.md | 2 +- packages/comark/package.json | 9 +- pnpm-lock.yaml | 10 +- 57 files changed, 336 insertions(+), 186 deletions(-) delete mode 100644 docs/app/pages/compare.vue create mode 100644 docs/server/plugins/llms.ts create mode 100644 docs/server/routes/llms-angular.txt.get.ts create mode 100644 docs/server/routes/llms-core.txt.get.ts create mode 100644 docs/server/routes/llms-plugins.txt.get.ts create mode 100644 docs/server/routes/llms-react.txt.get.ts create mode 100644 docs/server/routes/llms-streaming.txt.get.ts create mode 100644 docs/server/routes/llms-svelte.txt.get.ts create mode 100644 docs/server/routes/llms-vue.txt.get.ts create mode 100644 docs/server/utils/llms.ts diff --git a/docs/app/app.config.ts b/docs/app/app.config.ts index cc618e30..92ad65f0 100644 --- a/docs/app/app.config.ts +++ b/docs/app/app.config.ts @@ -51,21 +51,25 @@ export default defineAppConfig({ docs: { ogImage: { mark: 'comark' as const, - tagline: 'Parse and render Markdown anywhere', + tagline: 'The Markdown parser and renderer for everything', }, llms: { description: - 'Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming.', + 'Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.', }, schemaOrg: { description: - 'Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming.', + 'Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.', applicationCategory: 'DeveloperApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, license: 'https://github.com/comarkdown/comark/blob/main/LICENSE', - sameAs: ['https://github.com/comarkdown/comark', 'https://comark.dev'], + sameAs: ['https://github.com/comarkdown/comark', 'https://www.npmjs.com/package/comark'], programmingLanguage: 'TypeScript', + organization: { + url: 'https://comark.dev', + sameAs: ['https://github.com/comarkdown', 'https://www.npmjs.com/org/comark'], + }, }, }, diff --git a/docs/app/pages/compare.vue b/docs/app/pages/compare.vue deleted file mode 100644 index 009e2104..00000000 --- a/docs/app/pages/compare.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - diff --git a/docs/app/pages/play/index.vue b/docs/app/pages/play/index.vue index e7caf8f6..2b3e396d 100644 --- a/docs/app/pages/play/index.vue +++ b/docs/app/pages/play/index.vue @@ -2,9 +2,9 @@ definePageMeta({ footer: false, }) -const title = 'Playground - Comark' +const title = 'Markdown Playground' const description = - 'Try Comark live in the browser. Edit markdown with component syntax and see the rendered output in real-time.' + 'Try Comark live in the browser. Edit Markdown with component syntax and see the rendered output as you type.' useSeoMeta({ title, description, diff --git a/docs/app/pages/stream.vue b/docs/app/pages/stream.vue index a4af2f6d..ae15de17 100644 --- a/docs/app/pages/stream.vue +++ b/docs/app/pages/stream.vue @@ -8,7 +8,7 @@ import landingMD from '~~/content/index.md?raw' definePageMeta({ footer: false, }) -const title = 'Streaming Demo - Comark' +const title = 'Streaming Markdown Demo' const description = 'See Comark streaming in action. Watch markdown content parse and render in real-time as it arrives.' useSeoMeta({ diff --git a/docs/content/1.getting-started/0.introduction.md b/docs/content/1.getting-started/0.introduction.md index 2b793466..f06ce074 100644 --- a/docs/content/1.getting-started/0.introduction.md +++ b/docs/content/1.getting-started/0.introduction.md @@ -1,6 +1,8 @@ --- -title: Introduction -description: Parse and render CommonMark and GFM anywhere with Comark, a JavaScript library for HTML, ANSI, Vue, React, Svelte, Angular, plugins, and streaming. +title: 'What Is Comark? Markdown Parser and Renderer' +description: 'Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.' +navigation: + title: Introduction links: - label: Installation to: /getting-started/installation @@ -15,9 +17,7 @@ links: variant: soft --- -## What is Comark? - -**Comark** is a JavaScript library for parsing and rendering Markdown anywhere. Give it standard CommonMark or GitHub Flavored Markdown (GFM), then render the same source to HTML, ANSI, Vue, React, Svelte, or Angular. +**Comark** is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. Give it standard CommonMark or GitHub Flavored Markdown (GFM), and render the same source to any of these targets. Parsing and rendering are decoupled through a compact, serializable [`MarkdownDocument`](/getting-started/document-model). Parse on the server, in the browser, in a worker, during a build, or as content streams in. You can then inspect, transform, cache, serialize, or render the document without tying your content to one output target. diff --git a/docs/content/2.syntax/2.components.md b/docs/content/2.syntax/2.components.md index d5e9d41a..1d9abd5a 100644 --- a/docs/content/2.syntax/2.components.md +++ b/docs/content/2.syntax/2.components.md @@ -1,5 +1,5 @@ --- -title: Components +title: 'Components in Markdown: Block and Inline Syntax' description: Embed custom components directly in your Markdown with block and inline syntax, props, slots, and nested children. links: - label: Attributes diff --git a/docs/content/3.rendering/2.html.md b/docs/content/3.rendering/2.html.md index 54e90ab7..a5d12f86 100644 --- a/docs/content/3.rendering/2.html.md +++ b/docs/content/3.rendering/2.html.md @@ -1,6 +1,6 @@ --- -title: Render Comark to HTML -description: Parse Markdown or render existing documents as HTML strings without any framework dependency. +title: 'Markdown to HTML in JavaScript, No Framework' +description: 'Convert Markdown to HTML strings in JavaScript without a framework, for server rendering, static sites, RSS feeds, and emails.' navigation: title: HTML icon: i-lucide-file-code diff --git a/docs/content/3.rendering/3.vue.md b/docs/content/3.rendering/3.vue.md index c8426cd9..118d7b1f 100644 --- a/docs/content/3.rendering/3.vue.md +++ b/docs/content/3.rendering/3.vue.md @@ -1,6 +1,6 @@ --- -title: Render Comark in Vue -description: Learn how to render Comark in a Vue.js application with custom components, plugins, and streaming support. +title: 'Vue Markdown Component with Streaming and Custom Components' +description: 'Render Markdown in Vue 3 with the Markdown component: map elements to your own components, add plugins, and stream AI output as it arrives.' navigation: title: Vue icon: i-simple-icons-vuedotjs diff --git a/docs/content/3.rendering/4.nuxt.md b/docs/content/3.rendering/4.nuxt.md index c86cb635..11aae3e0 100644 --- a/docs/content/3.rendering/4.nuxt.md +++ b/docs/content/3.rendering/4.nuxt.md @@ -1,6 +1,6 @@ --- -title: Render Comark in Nuxt -description: Learn how to use Comark in a Nuxt application with auto-imported components and Nuxt UI integration. +title: 'Nuxt Markdown Rendering with Components' +description: 'Render Markdown in Nuxt with the @comark/nuxt module: auto-imported components, prose overrides, Nuxt UI integration, and streaming.' navigation: title: Nuxt icon: i-simple-icons-nuxt diff --git a/docs/content/3.rendering/5.react.md b/docs/content/3.rendering/5.react.md index 9ec2deb1..ceeb510a 100644 --- a/docs/content/3.rendering/5.react.md +++ b/docs/content/3.rendering/5.react.md @@ -1,6 +1,6 @@ --- -title: Render Comark in React -description: Learn how to render Comark in a React application with custom components, plugins, and Next.js support. +title: 'React Markdown Renderer with Streaming and Components' +description: 'Render Markdown in React and Next.js with custom components, plugins, React Server Components, and streaming for AI chat output.' navigation: title: React icon: i-simple-icons-react diff --git a/docs/content/3.rendering/6.svelte.md b/docs/content/3.rendering/6.svelte.md index 01e508f2..f1f6207d 100644 --- a/docs/content/3.rendering/6.svelte.md +++ b/docs/content/3.rendering/6.svelte.md @@ -1,6 +1,6 @@ --- -title: Render Comark in Svelte -description: Learn how to render Comark in a Svelte 5 application with custom components, plugins, and streaming support. +title: 'Svelte 5 Markdown Renderer with Streaming' +description: 'Render Markdown in Svelte 5 with native components, custom components, plugins, and streaming for AI output, including SvelteKit SSR.' navigation: title: Svelte icon: i-simple-icons-svelte diff --git a/docs/content/3.rendering/7.angular.md b/docs/content/3.rendering/7.angular.md index 5ffa3d4e..b5d3c63c 100644 --- a/docs/content/3.rendering/7.angular.md +++ b/docs/content/3.rendering/7.angular.md @@ -1,6 +1,6 @@ --- -title: Render Comark in Angular -description: Learn how to render Comark in an Angular 17+ application with standalone components, custom components, plugins, and streaming support. +title: 'Angular Markdown Component with Streaming (v17+)' +description: 'Render Markdown in Angular 17+ with standalone components, custom components, plugins, and streaming for AI output.' navigation: title: Angular icon: i-simple-icons-angular diff --git a/docs/content/3.rendering/8.ansi.md b/docs/content/3.rendering/8.ansi.md index 2b63de4c..ba3b9ea8 100644 --- a/docs/content/3.rendering/8.ansi.md +++ b/docs/content/3.rendering/8.ansi.md @@ -1,6 +1,6 @@ --- -title: ANSI Terminal Rendering -description: Render Markdown as styled terminal output using ANSI escape codes, perfect for CLIs, scripts, and developer tooling. +title: 'Render Markdown in the Terminal (ANSI) for Node CLIs' +description: 'Render Markdown as styled ANSI output in the terminal for Node.js CLIs, scripts, and coding agents, with syntax highlighting.' navigation: title: ANSI (Terminal) icon: i-lucide-terminal diff --git a/docs/content/4.plugins/1.built-in/security.md b/docs/content/4.plugins/1.built-in/security.md index 1de68551..e6a06672 100644 --- a/docs/content/4.plugins/1.built-in/security.md +++ b/docs/content/4.plugins/1.built-in/security.md @@ -1,9 +1,8 @@ --- -title: Security +title: 'Sanitize Markdown and Prevent XSS' description: Sanitize the parsed AST by removing dangerous elements, blocking malicious protocols, and restricting link destinations. -seo: - title: Security Sanitization Plugin navigation: + title: Security icon: i-lucide-shield-check links: - label: Parse API diff --git a/docs/content/5.reference/2.auto-close.md b/docs/content/5.reference/2.auto-close.md index cea804c1..8181b0bc 100644 --- a/docs/content/5.reference/2.auto-close.md +++ b/docs/content/5.reference/2.auto-close.md @@ -1,6 +1,8 @@ --- -title: Streaming API +title: 'Streaming Markdown: Auto-Close Incomplete LLM Output' description: Render incomplete Markdown correctly during streaming. autoCloseMarkdown() closes unterminated syntax so AI output displays at every frame. +navigation: + title: Streaming API links: - label: Parse API icon: i-lucide-code @@ -14,6 +16,8 @@ links: variant: soft --- +A language model streams Markdown a few characters at a time, so most frames end inside a bold span, a code fence, or a component. `autoCloseMarkdown()` closes the syntax that is still open, so every frame renders as finished markup. `parseMarkdown()` and every renderer call it by default (the `autoClose` option), so you only call it yourself on raw Markdown strings. + ## `autoCloseMarkdown(source, options?)`{lang="ts"} Automatically closes unclosed markdown inline syntax and Comark components. Built for streaming scenarios where content arrives incrementally and may be incomplete at any point. CommonMark/GFM healing follows the behavioral SPEC in `packages/comark/SPEC/auto-close.md`. diff --git a/docs/content/7.kb/0.why-comark.md b/docs/content/7.kb/0.why-comark.md index d8328f8e..41156085 100644 --- a/docs/content/7.kb/0.why-comark.md +++ b/docs/content/7.kb/0.why-comark.md @@ -1,5 +1,5 @@ --- -title: 'Comark: Parse and Render Markdown Anywhere' +title: 'Why Comark? One Markdown Pipeline for Every Renderer' description: Comark gives JavaScript applications one Markdown pipeline for standards-based parsing, serializable documents, multiple renderers, plugins, and streaming. navigation: title: Why Comark? @@ -18,7 +18,7 @@ links: Markdown won the authoring war. It is readable, portable, widely supported, and understood by both humans and machines. The remaining challenge is everything that happens after authoring: parsing it reliably, transforming it as data, rendering it to different targets, and displaying it while content is still arriving. -Comark provides one JavaScript library for that complete Markdown pipeline. +Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal, so one library covers that complete Markdown pipeline. ## Why Markdown? diff --git a/docs/content/8.examples/1.frameworks/astro.md b/docs/content/8.examples/1.frameworks/astro.md index b5e7dc43..2078e979 100644 --- a/docs/content/8.examples/1.frameworks/astro.md +++ b/docs/content/8.examples/1.frameworks/astro.md @@ -1,7 +1,8 @@ --- -title: Astro +title: 'Astro Markdown Blog with Comark' description: A blog example using Comark with Astro content collections and React components. navigation: + title: 'Astro' icon: i-simple-icons:astro --- diff --git a/docs/content/8.examples/1.frameworks/nextjs.md b/docs/content/8.examples/1.frameworks/nextjs.md index 08bc4ce6..a66633d5 100644 --- a/docs/content/8.examples/1.frameworks/nextjs.md +++ b/docs/content/8.examples/1.frameworks/nextjs.md @@ -1,7 +1,8 @@ --- -title: Next.js +title: 'Next.js Markdown Blog with React Server Components' description: A blog example using Comark with Next.js App Router and React Server Components. navigation: + title: 'Next.js' icon: i-simple-icons:nextdotjs --- diff --git a/docs/content/8.examples/1.frameworks/nuxt.md b/docs/content/8.examples/1.frameworks/nuxt.md index 0b9ae9f9..b1e7e151 100644 --- a/docs/content/8.examples/1.frameworks/nuxt.md +++ b/docs/content/8.examples/1.frameworks/nuxt.md @@ -1,7 +1,8 @@ --- -title: Nuxt -description: A minimal example showing how to use Comark Syntax with Nuxt UI. +title: 'Nuxt UI Markdown Example with Comark' +description: 'A minimal Nuxt app that renders Comark component syntax with Nuxt UI components through the @comark/nuxt module.' navigation: + title: 'Nuxt' icon: i-simple-icons-nuxt --- diff --git a/docs/content/8.examples/1.frameworks/sveltekit.md b/docs/content/8.examples/1.frameworks/sveltekit.md index 47d3d8b6..590d7c00 100644 --- a/docs/content/8.examples/1.frameworks/sveltekit.md +++ b/docs/content/8.examples/1.frameworks/sveltekit.md @@ -1,7 +1,8 @@ --- -title: SvelteKit +title: 'SvelteKit Markdown with SSR and Lazy Components' description: A SvelteKit example showing Comark with lazy components, MarkdownAsync, and SSR. navigation: + title: 'SvelteKit' icon: i-simple-icons-svelte --- diff --git a/docs/content/8.examples/1.frameworks/vitepress.md b/docs/content/8.examples/1.frameworks/vitepress.md index 2892095b..3896a599 100644 --- a/docs/content/8.examples/1.frameworks/vitepress.md +++ b/docs/content/8.examples/1.frameworks/vitepress.md @@ -1,7 +1,8 @@ --- -title: VitePress +title: 'Comark Components in VitePress' description: Using Comark component syntax natively in VitePress via the comark syntax plugin. navigation: + title: 'VitePress' icon: i-simple-icons-vitepress --- diff --git a/docs/content/8.examples/2.vite/angular.md b/docs/content/8.examples/2.vite/angular.md index d778595d..4b9faba0 100644 --- a/docs/content/8.examples/2.vite/angular.md +++ b/docs/content/8.examples/2.vite/angular.md @@ -1,7 +1,8 @@ --- -title: Angular -description: A minimal example showing how to use Comark with Angular and Vite. +title: 'Angular Markdown Example with Vite' +description: 'An Angular and Vite app that renders a Markdown blog, a syntax showcase, and a live editor with Comark and a custom alert component.' navigation: + title: 'Angular' icon: i-simple-icons-angular --- diff --git a/docs/content/8.examples/2.vite/html.md b/docs/content/8.examples/2.vite/html.md index beebcc43..832753d3 100644 --- a/docs/content/8.examples/2.vite/html.md +++ b/docs/content/8.examples/2.vite/html.md @@ -1,7 +1,8 @@ --- -title: HTML preview +title: 'Live Markdown to HTML Preview' description: A live markdown editor that renders Comark content to HTML and displays it in a sandboxed iframe preview, with syntax highlighting support. navigation: + title: 'HTML preview' icon: i-lucide-file-code --- diff --git a/docs/content/8.examples/2.vite/react.md b/docs/content/8.examples/2.vite/react.md index bde7839f..ab405f2f 100644 --- a/docs/content/8.examples/2.vite/react.md +++ b/docs/content/8.examples/2.vite/react.md @@ -1,7 +1,8 @@ --- -title: React -description: A minimal example showing how to use Comark with React and Vite. +title: 'React Markdown Example with Vite' +description: 'A React and Vite app that renders a Markdown blog, a syntax showcase, and a live editor with Comark and a custom alert component.' navigation: + title: 'React' icon: i-simple-icons-react --- diff --git a/docs/content/8.examples/2.vite/svelte.md b/docs/content/8.examples/2.vite/svelte.md index 0ca9d79b..5b93ffdf 100644 --- a/docs/content/8.examples/2.vite/svelte.md +++ b/docs/content/8.examples/2.vite/svelte.md @@ -1,7 +1,8 @@ --- -title: Svelte -description: A minimal example showing how to use Comark with Svelte and Vite. +title: 'Svelte Markdown Example with Vite' +description: 'A Svelte 5 and Vite app that renders a Markdown blog, a syntax showcase, and a live editor with Comark, a custom alert, and a lazy component.' navigation: + title: 'Svelte' icon: i-simple-icons-svelte --- diff --git a/docs/content/8.examples/2.vite/vue.md b/docs/content/8.examples/2.vite/vue.md index f0c18eab..869f5171 100644 --- a/docs/content/8.examples/2.vite/vue.md +++ b/docs/content/8.examples/2.vite/vue.md @@ -1,7 +1,8 @@ --- -title: Vue -description: A minimal example showing how to use Comark with Vue and Vite. +title: 'Vue Markdown Example with Vite' +description: 'A Vue and Vite app that renders a Markdown blog, a syntax showcase, and a live editor with Comark and a custom alert component.' navigation: + title: 'Vue' icon: i-simple-icons-vuedotjs --- diff --git a/docs/content/8.examples/3.plugins/react-vite-math.md b/docs/content/8.examples/3.plugins/react-vite-math.md index 9d9138da..6fa4d446 100644 --- a/docs/content/8.examples/3.plugins/react-vite-math.md +++ b/docs/content/8.examples/3.plugins/react-vite-math.md @@ -1,7 +1,8 @@ --- -title: Math formulas -description: Example showing how to use Comark with LaTeX math formulas in React and Vite. +title: 'LaTeX Math Formulas in React with Comark and KaTeX' +description: 'A React and Vite app that renders inline and block LaTeX math in Markdown with the Comark math plugin and KaTeX.' navigation: + title: 'Math formulas (React)' icon: i-lucide-calculator --- diff --git a/docs/content/8.examples/3.plugins/react-vite-mermaid.md b/docs/content/8.examples/3.plugins/react-vite-mermaid.md index 04e4423d..ba13d828 100644 --- a/docs/content/8.examples/3.plugins/react-vite-mermaid.md +++ b/docs/content/8.examples/3.plugins/react-vite-mermaid.md @@ -1,7 +1,8 @@ --- -title: Mermaid diagrams -description: Example showing how to use Comark with Mermaid diagrams in React and Vite. +title: 'Mermaid Diagrams in React Markdown' +description: 'A React and Vite app that turns mermaid code blocks in Markdown into diagrams with the Comark mermaid plugin.' navigation: + title: 'Mermaid diagrams (React)' icon: i-simple-icons-mermaid --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-binding.md b/docs/content/8.examples/3.plugins/vue-vite-binding.md index b3790549..0877ffc1 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-binding.md +++ b/docs/content/8.examples/3.plugins/vue-vite-binding.md @@ -1,7 +1,8 @@ --- -title: Binding (frontmatter + data) +title: 'Frontmatter and Data Binding in Vue Markdown' description: Example showing how to interpolate frontmatter and runtime data into Markdown using the Comark `binding` plugin in Vue and Vite. navigation: + title: 'Binding (frontmatter + data)' icon: i-lucide-replace --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-highlight.md b/docs/content/8.examples/3.plugins/vue-vite-highlight.md index b79c2cd0..3ddfc366 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-highlight.md +++ b/docs/content/8.examples/3.plugins/vue-vite-highlight.md @@ -1,7 +1,8 @@ --- -title: Syntax highlighting +title: 'Syntax Highlighting in Vue Markdown with Shiki' description: Example showing how to use Comark with syntax highlighting using Shiki in Vue and Vite. navigation: + title: 'Syntax highlighting' icon: i-lucide-code --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-json-render.md b/docs/content/8.examples/3.plugins/vue-vite-json-render.md index 0dc93ce4..4107e685 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-json-render.md +++ b/docs/content/8.examples/3.plugins/vue-vite-json-render.md @@ -1,7 +1,8 @@ --- -title: JSON Render +title: 'JSON Render: UI Components from JSON Specs in Vue Markdown' description: Example showing how to use Comark with JSON Render and YAML Render in Vue and Vite. navigation: + title: 'JSON Render' icon: i-lucide-braces --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-math.md b/docs/content/8.examples/3.plugins/vue-vite-math.md index 97642481..2b3c70f0 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-math.md +++ b/docs/content/8.examples/3.plugins/vue-vite-math.md @@ -1,7 +1,8 @@ --- -title: Math formulas -description: Example showing how to use Comark with LaTeX math formulas in Vue and Vite. +title: 'LaTeX Math Formulas in Vue with Comark and KaTeX' +description: 'A Vue and Vite app that renders inline and block LaTeX math in Markdown with the Comark math plugin and KaTeX.' navigation: + title: 'Math formulas (Vue)' icon: i-lucide-calculator --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-mermaid.md b/docs/content/8.examples/3.plugins/vue-vite-mermaid.md index da67a31c..00f2bcf4 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-mermaid.md +++ b/docs/content/8.examples/3.plugins/vue-vite-mermaid.md @@ -1,7 +1,8 @@ --- -title: Mermaid diagrams -description: Example showing how to use Comark with Mermaid diagrams in Vue and Vite. +title: 'Mermaid Diagrams in Vue Markdown' +description: 'A Vue and Vite app that turns mermaid code blocks in Markdown into diagrams with the Comark mermaid plugin.' navigation: + title: 'Mermaid diagrams (Vue)' icon: i-simple-icons-mermaid --- diff --git a/docs/content/8.examples/3.plugins/vue-vite-punctuation.md b/docs/content/8.examples/3.plugins/vue-vite-punctuation.md index 3ae3da80..cc692227 100644 --- a/docs/content/8.examples/3.plugins/vue-vite-punctuation.md +++ b/docs/content/8.examples/3.plugins/vue-vite-punctuation.md @@ -1,7 +1,8 @@ --- -title: Punctuation +title: 'Smart Quotes and Dashes in Vue Markdown' description: Example showing how to use Comark with the punctuation plugin for smart quotes, dashes, and symbols in Vue and Vite. navigation: + title: 'Punctuation' icon: i-lucide-quote --- diff --git a/docs/content/8.examples/4.ai/nuxt-ai-sdk.md b/docs/content/8.examples/4.ai/nuxt-ai-sdk.md index 037d972e..4b359ca2 100644 --- a/docs/content/8.examples/4.ai/nuxt-ai-sdk.md +++ b/docs/content/8.examples/4.ai/nuxt-ai-sdk.md @@ -1,7 +1,8 @@ --- -title: AI SDK -description: Streaming AI chat with live Comark rendering +title: 'Streaming AI Chat with the Vercel AI SDK and Nuxt' +description: 'A Nuxt chat app that streams Vercel AI SDK responses and renders the Markdown with Comark while each message is still arriving.' navigation: + title: AI SDK icon: i-simple-icons-vercel --- diff --git a/docs/content/index.md b/docs/content/index.md index 4d17d037..7b61db64 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -1,10 +1,10 @@ --- navigation: false -title: Parse and render Markdown anywhere with Comark -description: 'A JavaScript library to parse and stream Markdown, with renderers for HTML, ANSI, Vue, React, Svelte and Angular, plus components, attributes, and plugins.' +title: 'Comark: Markdown Parser and Renderer for Any Framework' +description: 'Open-source Markdown parser and renderer with components, attributes and plugins. Streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.' seo: - title: Parse and render Markdown anywhere with Comark - description: 'Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming.' + title: 'Comark: Markdown Parser and Renderer for Any Framework' + description: 'Open-source Markdown parser and renderer with components, attributes and plugins. Streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.' ogImage: /social-card.jpg --- @@ -16,7 +16,7 @@ orientation: horizontal --- playground: /play source: |- - # Hello World + ## Hello World A JavaScript library to **parse and render Markdown** anywhere. @@ -38,10 +38,10 @@ orientation: horizontal --- ::: #title -Parse and render Markdown anywhere. +The Markdown parser and renderer for everything. #description -A JavaScript library to parse and stream Markdown, with renderers for HTML, ANSI, Vue, React, Svelte and Angular, plus components, attributes, and plugins. +Open-source Markdown parser and renderer with components, attributes and plugins. Streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. #links :::u-button diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts index 6c26d4d8..a7a990cd 100644 --- a/docs/nuxt.config.ts +++ b/docs/nuxt.config.ts @@ -16,7 +16,7 @@ export default defineNuxtConfig({ domain: 'https://comark.dev', title: 'Comark', description: - 'Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming.', + 'Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.', full: { title: 'Comark Documentation', description: @@ -24,6 +24,11 @@ export default defineNuxtConfig({ }, }, + sitemap: { + // Vue pages of this app; the layer's sitemap source only knows the content pages. + urls: ['/play', '/stream'], + }, + app: { head: { link: [ @@ -85,6 +90,18 @@ export default defineNuxtConfig({ '/kb': { isr: 300 }, '/kb/**': { isr: 300 }, '/ecosystem': { isr: 300 }, + '/use-cases': { isr: 300 }, + '/use-cases/**': { isr: 300 }, + '/blog': { isr: 300 }, + '/blog/**': { isr: 300 }, + // Topic files next to llms-full.txt (server/routes/llms-*.txt.get.ts). + '/llms-core.txt': { isr: 300 }, + '/llms-streaming.txt': { isr: 300 }, + '/llms-vue.txt': { isr: 300 }, + '/llms-react.txt': { isr: 300 }, + '/llms-svelte.txt': { isr: 300 }, + '/llms-angular.txt': { isr: 300 }, + '/llms-plugins.txt': { isr: 300 }, }, }, }) diff --git a/docs/package.json b/docs/package.json index 7d1f97f1..ec92eb61 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "comark", - "description": "Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming.", + "description": "Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.", "type": "module", "scripts": { "dev": "nuxt dev", @@ -22,7 +22,7 @@ "ai": "catalog:", "beautiful-mermaid": "catalog:", "comark": "workspace:*", - "comark-docs": "github:comarkdown/comark-docs#61ea1128b6e873fe06dd8853d6361ba53d08b0f2", + "comark-docs": "github:comarkdown/comark-docs#98b83bf148b0333a9c103ce3db2db88cbb560dd4", "katex": "catalog:", "nuxt": "catalog:", "splitpanes": "catalog:", diff --git a/docs/server/plugins/llms.ts b/docs/server/plugins/llms.ts new file mode 100644 index 00000000..5727bc2f --- /dev/null +++ b/docs/server/plugins/llms.ts @@ -0,0 +1,56 @@ +import type { LLMsSection } from 'nuxt-llms' + +/** + * The decision rule goes first in `llms.txt`, so an agent knows when Comark fits before it reads the + * page index. Keep it factual: agents repeat it. + */ +const WHEN_TO_USE: LLMsSection = { + title: 'When to use Comark', + description: [ + 'Use Comark when:', + '', + '- The same Markdown renders in more than one place: React, Vue, Svelte, Angular, HTML strings, or the terminal (ANSI).', + '- You render streaming LLM output. Comark closes unterminated syntax (bold, code fences, components) on every frame, in every renderer.', + '- Content needs components without a build step or code execution, for example `::alert{type="info"}` written by an author, a CMS, a database, or a model.', + '- You want to parse once and keep the result: `parseMarkdown()` returns a serializable `MarkdownDocument` you can cache, store, and render later on any client.', + '', + 'Use something else when:', + '', + '- You only convert Markdown to HTML, with no components and no streaming: marked or markdown-it are smaller.', + '- Your content imports and runs JavaScript (JSX expressions, ESM imports): use MDX.', + '- You depend on remark or rehype plugins: Comark runs markdown-it plugins, not unified plugins.', + '- You build a React-only chat UI and need no component syntax in the output: Streamdown also fits.', + ].join('\n'), +} + +/** Pages that back the decision rule, linked on their raw Markdown like the rest of `llms.txt`. */ +const WHEN_TO_USE_LINKS = [ + { title: 'Choosing a Markdown library for AI chat', path: '/compare/markdown-libraries-for-ai-chat' }, + { title: 'Comark vs MDX', path: '/compare/comark-vs-mdx' }, + { title: 'Comark vs react-markdown', path: '/compare/comark-vs-react-markdown' }, + { title: 'Comark vs Streamdown', path: '/compare/comark-vs-streamdown' }, + { title: 'Comark vs Markdoc', path: '/compare/comark-vs-markdoc' }, +] + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('llms:generate', (event, options) => { + // The layer adds its "Documentation" section in the same hook. Whichever runs first, the decision + // rule ends up right before it, or first when the layer hasn't added it yet. + const documentation = options.sections.findIndex((section) => section.title === 'Documentation') + const site = getSiteConfig(event) + const links = WHEN_TO_USE_LINKS.map((link) => ({ title: link.title, href: `${site.url}/raw${link.path}.md` })) + options.sections.splice(Math.max(documentation, 0), 0, { ...WHEN_TO_USE, links }) + + const topics = LLMS_BUNDLES.map((bundle) => ({ + title: bundle.title, + description: bundle.description, + href: `${site.url}/llms-${bundle.slug}.txt`, + })) + const sets = options.sections.find((section) => section.title === 'Documentation Sets') + if (sets) { + sets.links = [...(sets.links ?? []), ...topics] + } else { + options.sections.push({ title: 'Documentation Sets', links: topics }) + } + }) +}) diff --git a/docs/server/routes/llms-angular.txt.get.ts b/docs/server/routes/llms-angular.txt.get.ts new file mode 100644 index 00000000..0e0a73e1 --- /dev/null +++ b/docs/server/routes/llms-angular.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('angular') diff --git a/docs/server/routes/llms-core.txt.get.ts b/docs/server/routes/llms-core.txt.get.ts new file mode 100644 index 00000000..d43e6e08 --- /dev/null +++ b/docs/server/routes/llms-core.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('core') diff --git a/docs/server/routes/llms-plugins.txt.get.ts b/docs/server/routes/llms-plugins.txt.get.ts new file mode 100644 index 00000000..5dda5018 --- /dev/null +++ b/docs/server/routes/llms-plugins.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('plugins') diff --git a/docs/server/routes/llms-react.txt.get.ts b/docs/server/routes/llms-react.txt.get.ts new file mode 100644 index 00000000..08e5bca0 --- /dev/null +++ b/docs/server/routes/llms-react.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('react') diff --git a/docs/server/routes/llms-streaming.txt.get.ts b/docs/server/routes/llms-streaming.txt.get.ts new file mode 100644 index 00000000..10bd527b --- /dev/null +++ b/docs/server/routes/llms-streaming.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('streaming') diff --git a/docs/server/routes/llms-svelte.txt.get.ts b/docs/server/routes/llms-svelte.txt.get.ts new file mode 100644 index 00000000..8850bc4e --- /dev/null +++ b/docs/server/routes/llms-svelte.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('svelte') diff --git a/docs/server/routes/llms-vue.txt.get.ts b/docs/server/routes/llms-vue.txt.get.ts new file mode 100644 index 00000000..6bc686c8 --- /dev/null +++ b/docs/server/routes/llms-vue.txt.get.ts @@ -0,0 +1 @@ +export default defineLlmsBundle('vue') diff --git a/docs/server/utils/llms.ts b/docs/server/utils/llms.ts new file mode 100644 index 00000000..1b42671a --- /dev/null +++ b/docs/server/utils/llms.ts @@ -0,0 +1,118 @@ +import type { H3Event } from 'h3' + +/** A topic file served next to `llms-full.txt`, for agents that can't read the whole documentation. */ +export interface LlmsBundle { + /** Served at `/llms-.txt`. */ + slug: string + title: string + description: string + /** Pages in reading order. A path ending in `/**` adds every page under it, in sidebar order. */ + pages: string[] +} + +export const LLMS_BUNDLES: LlmsBundle[] = [ + { + slug: 'core', + title: 'Comark core', + description: 'Parse Markdown into a serializable document, the Comark syntax, and the core APIs.', + pages: [ + '/getting-started/introduction', + '/getting-started/installation', + '/getting-started/document-model', + '/syntax/**', + '/reference/parse', + '/reference/render', + '/reference/reference', + ], + }, + { + slug: 'streaming', + title: 'Streaming Markdown with Comark', + description: 'Render streaming LLM output: auto-close, AI chat in every framework, and components in model output.', + pages: [ + '/reference/auto-close', + '/use-cases/ai-chat-streaming', + '/use-cases/vue-ai-chat', + '/use-cases/svelte-ai-chat', + '/use-cases/angular-ai-chat', + '/use-cases/generative-ui', + '/plugins/built-in/security', + ], + }, + { + slug: 'vue', + title: 'Comark for Vue and Nuxt', + description: 'Render Markdown in Vue 3 and Nuxt, with custom components, plugins, and streaming.', + pages: ['/rendering/vue', '/rendering/nuxt', '/use-cases/vue-ai-chat', '/reference/auto-close'], + }, + { + slug: 'react', + title: 'Comark for React', + description: 'Render Markdown in React and Next.js, with custom components, plugins, and streaming.', + pages: [ + '/rendering/react', + '/use-cases/ai-chat-streaming', + '/reference/auto-close', + '/compare/comark-vs-react-markdown', + '/compare/comark-vs-streamdown', + ], + }, + { + slug: 'svelte', + title: 'Comark for Svelte', + description: 'Render Markdown in Svelte 5 and SvelteKit, with custom components, plugins, and streaming.', + pages: ['/rendering/svelte', '/use-cases/svelte-ai-chat', '/reference/auto-close'], + }, + { + slug: 'angular', + title: 'Comark for Angular', + description: 'Render Markdown in Angular 17+, with standalone components, plugins, and streaming.', + pages: ['/rendering/angular', '/use-cases/angular-ai-chat', '/reference/auto-close'], + }, + { + slug: 'plugins', + title: 'Comark plugins', + description: 'Every built-in plugin, and the APIs to write your own.', + pages: ['/plugins', '/plugins/**'], + }, +] + +/** + * Concatenates the raw Markdown of a bundle's pages, the same documents `/raw/**` serves, so a topic file + * never drifts from the pages it is built from. A page that doesn't exist (yet) is skipped. + */ +export async function renderLlmsBundle(event: H3Event, bundle: LlmsBundle): Promise { + const pages = await listDocsPages(await getProdContent()) + const paths: string[] = [] + for (const entry of bundle.pages) { + const prefix = entry.endsWith('/**') ? entry.slice(0, -3) : undefined + const matches = prefix + ? pages.filter((page) => page.path.startsWith(`${prefix}/`)).map((page) => page.path) + : [entry] + for (const path of matches) { + if (!paths.includes(path)) paths.push(path) + } + } + + const documents = await Promise.all( + paths.map((path) => event.$fetch(`/raw${path}.md`, { responseType: 'text' }).catch(() => undefined)) + ) + + const site = getSiteConfig(event) + const header = [ + `# ${bundle.title}`, + `> ${bundle.description} Part of the ${site.name} documentation. The index of every page is ${site.url}/llms.txt.`, + ].join('\n\n') + const bodies = documents.filter((document): document is string => Boolean(document?.trim())) + return [header, ...bodies.map((document) => document.trim())].join('\n\n') +} + +/** Event handler for one topic file. */ +export function defineLlmsBundle(slug: string) { + const bundle = LLMS_BUNDLES.find((entry) => entry.slug === slug) + if (!bundle) throw new Error(`Unknown llms bundle: ${slug}`) + return defineEventHandler(async (event) => { + setHeader(event, 'Content-Type', 'text/plain; charset=utf-8') + return renderLlmsBundle(event, bundle) + }) +} diff --git a/packages/comark-angular/package.json b/packages/comark-angular/package.json index 6c31af7f..f7206357 100644 --- a/packages/comark-angular/package.json +++ b/packages/comark-angular/package.json @@ -4,13 +4,17 @@ "description": "Angular renderer for Comark. Render Markdown with components at runtime, with streaming support for AI output.", "keywords": [ "ai", + "ai-chat", "angular", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "renderer", - "streaming" + "streaming", + "streaming-markdown" ], "homepage": "https://comark.dev/rendering/angular", "bugs": { diff --git a/packages/comark-ansi/package.json b/packages/comark-ansi/package.json index c8f9671a..10bbda0c 100644 --- a/packages/comark-ansi/package.json +++ b/packages/comark-ansi/package.json @@ -7,7 +7,9 @@ "cli", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "renderer", "streaming", diff --git a/packages/comark-html/package.json b/packages/comark-html/package.json index a4cac460..6a51dc86 100644 --- a/packages/comark-html/package.json +++ b/packages/comark-html/package.json @@ -7,6 +7,8 @@ "components", "html", "markdown", + "markdown-renderer", + "markdown-to-html", "mdc", "renderer", "ssr", diff --git a/packages/comark-nuxt/package.json b/packages/comark-nuxt/package.json index f7cfd037..c4f12d4d 100644 --- a/packages/comark-nuxt/package.json +++ b/packages/comark-nuxt/package.json @@ -4,14 +4,18 @@ "description": "Nuxt module for Comark. Render Markdown with components at runtime, with auto-imported components and streaming support for AI output.", "keywords": [ "ai", + "ai-chat", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "nuxt", "nuxt-module", "renderer", "streaming", + "streaming-markdown", "vue" ], "homepage": "https://comark.dev/rendering/nuxt", diff --git a/packages/comark-react/package.json b/packages/comark-react/package.json index efae383e..e0f2f539 100644 --- a/packages/comark-react/package.json +++ b/packages/comark-react/package.json @@ -4,15 +4,19 @@ "description": "React renderer for Comark. Render Markdown with components at runtime, with streaming support for AI output. Works with Next.js and Server Components.", "keywords": [ "ai", + "ai-chat", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "mdx", "nextjs", "react", "renderer", - "streaming" + "streaming", + "streaming-markdown" ], "homepage": "https://comark.dev/rendering/react", "bugs": { diff --git a/packages/comark-svelte/package.json b/packages/comark-svelte/package.json index 76339b5a..8555d6e9 100644 --- a/packages/comark-svelte/package.json +++ b/packages/comark-svelte/package.json @@ -4,12 +4,16 @@ "description": "Svelte 5 renderer for Comark. Render Markdown with components at runtime, with streaming support for AI output.", "keywords": [ "ai", + "ai-chat", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "renderer", "streaming", + "streaming-markdown", "svelte", "sveltekit" ], diff --git a/packages/comark-vue/package.json b/packages/comark-vue/package.json index 7b0cd833..84edccb9 100644 --- a/packages/comark-vue/package.json +++ b/packages/comark-vue/package.json @@ -4,12 +4,16 @@ "description": "Vue 3 renderer for Comark. Render Markdown with components at runtime, with streaming support for AI output.", "keywords": [ "ai", + "ai-chat", "comark", "components", + "llm", "markdown", + "markdown-renderer", "mdc", "renderer", "streaming", + "streaming-markdown", "vue" ], "homepage": "https://comark.dev/rendering/vue", diff --git a/packages/comark/README.md b/packages/comark/README.md index d88cc4a6..240303f0 100644 --- a/packages/comark/README.md +++ b/packages/comark/README.md @@ -8,7 +8,7 @@ [![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://comark.dev) [![license](https://img.shields.io/github/license/comarkdown/comark?color=black)](https://github.com/comarkdown/comark/blob/main/LICENSE) -Parse and render Markdown anywhere with one JavaScript library for HTML, ANSI, Vue, React, Svelte and Angular, plus plugins and streaming. +Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. Comark supports CommonMark and GFM, then parses them into a compact, serializable document at build time, runtime, or during a stream. Use the same document across renderers, or extend the syntax with readable components and attributes when you need richer content. diff --git a/packages/comark/package.json b/packages/comark/package.json index dcc150d5..c49e45de 100644 --- a/packages/comark/package.json +++ b/packages/comark/package.json @@ -1,22 +1,29 @@ { "name": "comark", "version": "0.7.0", - "description": "Parse CommonMark and GFM into a compact, serializable document at build time, runtime, or during a stream, then render to HTML, ANSI, Vue, React, Svelte or Angular.", + "description": "Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.", "keywords": [ "ai", + "ai-chat", "angular", "ast", "commonmark", "components", "gfm", + "llm", "markdown", "markdown-it", + "markdown-parser", + "markdown-renderer", "mdc", "mdx", "parser", "react", + "renderer", "streaming", + "streaming-markdown", "svelte", + "terminal", "vue" ], "homepage": "https://comark.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0317683..1277fa43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,8 +575,8 @@ importers: specifier: workspace:* version: link:../packages/comark comark-docs: - specifier: github:comarkdown/comark-docs#61ea1128b6e873fe06dd8853d6361ba53d08b0f2 - version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/61ea1128b6e873fe06dd8853d6361ba53d08b0f2(3ff7737281dfbf12c4fa8742bd038b22) + specifier: github:comarkdown/comark-docs#98b83bf148b0333a9c103ce3db2db88cbb560dd4 + version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4(3ff7737281dfbf12c4fa8742bd038b22) katex: specifier: 'catalog:' version: 0.18.7 @@ -6843,8 +6843,8 @@ packages: resolution: {integrity: sha512-z0nuo3PM53FwBYbxP4rNrUo1QW7jv8YO44HDYaGm2Wx4pmHsLqKdsA6CiHcSOaURze6m7my0Z0YFaLL77LtDUA==} hasBin: true - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/61ea1128b6e873fe06dd8853d6361ba53d08b0f2: - resolution: {gitHosted: true, integrity: sha512-ol9HC3u1Qh/2CqrEtj3FCMXKjD2cXIecSlms0OmSLtKZKo0rG8jtXKJykNqJarFoiOd+lA8ulDf7whal7tFwOg==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/61ea1128b6e873fe06dd8853d6361ba53d08b0f2} + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4: + resolution: {gitHosted: true, integrity: sha512-Zz+/o4/zgqIcK3Yme/gw/pkJOasKIzmJxeIIt/q+DAsg1vmMY8xTgVxUydXY7py2RSmRc7Vn1TzBIQITDbvdRw==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4} version: 0.0.1 engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} peerDependencies: @@ -16132,7 +16132,7 @@ snapshots: - shiki - uploadthing - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/61ea1128b6e873fe06dd8853d6361ba53d08b0f2(3ff7737281dfbf12c4fa8742bd038b22): + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4(3ff7737281dfbf12c4fa8742bd038b22): dependencies: '@ai-sdk/gateway': 4.0.90(zod@4.6.5) '@ai-sdk/vue': 4.0.112(vue@3.5.43(typescript@6.0.3))(zod@4.6.5) From 243916f2c16f2048a29402ca6c718f08ee3d3ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 7 Oct 2026 14:09:13 +0200 Subject: [PATCH 02/19] docs: pin comark-docs#54 head and list /plugins in the sitemap --- docs/nuxt.config.ts | 5 +++-- docs/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/nuxt.config.ts b/docs/nuxt.config.ts index a7a990cd..aa5702d8 100644 --- a/docs/nuxt.config.ts +++ b/docs/nuxt.config.ts @@ -25,8 +25,9 @@ export default defineNuxtConfig({ }, sitemap: { - // Vue pages of this app; the layer's sitemap source only knows the content pages. - urls: ['/play', '/stream'], + // The layer's sitemap source lists the navigation pages only: add the Vue pages of this app and + // `/plugins`, whose `index.md` has `navigation: false`. + urls: ['/plugins', '/play', '/stream'], }, app: { diff --git a/docs/package.json b/docs/package.json index ec92eb61..5a460a61 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "ai": "catalog:", "beautiful-mermaid": "catalog:", "comark": "workspace:*", - "comark-docs": "github:comarkdown/comark-docs#98b83bf148b0333a9c103ce3db2db88cbb560dd4", + "comark-docs": "github:comarkdown/comark-docs#3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1", "katex": "catalog:", "nuxt": "catalog:", "splitpanes": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1277fa43..0cdfec6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,8 +575,8 @@ importers: specifier: workspace:* version: link:../packages/comark comark-docs: - specifier: github:comarkdown/comark-docs#98b83bf148b0333a9c103ce3db2db88cbb560dd4 - version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4(3ff7737281dfbf12c4fa8742bd038b22) + specifier: github:comarkdown/comark-docs#3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1 + version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1(3ff7737281dfbf12c4fa8742bd038b22) katex: specifier: 'catalog:' version: 0.18.7 @@ -6843,8 +6843,8 @@ packages: resolution: {integrity: sha512-z0nuo3PM53FwBYbxP4rNrUo1QW7jv8YO44HDYaGm2Wx4pmHsLqKdsA6CiHcSOaURze6m7my0Z0YFaLL77LtDUA==} hasBin: true - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4: - resolution: {gitHosted: true, integrity: sha512-Zz+/o4/zgqIcK3Yme/gw/pkJOasKIzmJxeIIt/q+DAsg1vmMY8xTgVxUydXY7py2RSmRc7Vn1TzBIQITDbvdRw==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4} + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1: + resolution: {gitHosted: true, integrity: sha512-crl8Lj0qBTcheg22stXwUH893JpyHTH9ZxYAds97jPdLZJ0xOsliVSUX0eW70a4Hypou96qDqwUsS6M4QKSs1Q==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1} version: 0.0.1 engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} peerDependencies: @@ -16132,7 +16132,7 @@ snapshots: - shiki - uploadthing - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/98b83bf148b0333a9c103ce3db2db88cbb560dd4(3ff7737281dfbf12c4fa8742bd038b22): + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1(3ff7737281dfbf12c4fa8742bd038b22): dependencies: '@ai-sdk/gateway': 4.0.90(zod@4.6.5) '@ai-sdk/vue': 4.0.112(vue@3.5.43(typescript@6.0.3))(zod@4.6.5) From a7a982694ca7936f9994ab8e5d8d081e75ad55ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 7 Oct 2026 14:13:33 +0200 Subject: [PATCH 03/19] docs: pin comark-docs#54 head --- docs/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index 5a460a61..f5eed899 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "ai": "catalog:", "beautiful-mermaid": "catalog:", "comark": "workspace:*", - "comark-docs": "github:comarkdown/comark-docs#3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1", + "comark-docs": "github:comarkdown/comark-docs#5f15eaf90776447e0be2b6490ab3f60a66c9c8d6", "katex": "catalog:", "nuxt": "catalog:", "splitpanes": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cdfec6d..767a5421 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,8 +575,8 @@ importers: specifier: workspace:* version: link:../packages/comark comark-docs: - specifier: github:comarkdown/comark-docs#3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1 - version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1(3ff7737281dfbf12c4fa8742bd038b22) + specifier: github:comarkdown/comark-docs#5f15eaf90776447e0be2b6490ab3f60a66c9c8d6 + version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6(3ff7737281dfbf12c4fa8742bd038b22) katex: specifier: 'catalog:' version: 0.18.7 @@ -6843,8 +6843,8 @@ packages: resolution: {integrity: sha512-z0nuo3PM53FwBYbxP4rNrUo1QW7jv8YO44HDYaGm2Wx4pmHsLqKdsA6CiHcSOaURze6m7my0Z0YFaLL77LtDUA==} hasBin: true - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1: - resolution: {gitHosted: true, integrity: sha512-crl8Lj0qBTcheg22stXwUH893JpyHTH9ZxYAds97jPdLZJ0xOsliVSUX0eW70a4Hypou96qDqwUsS6M4QKSs1Q==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1} + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6: + resolution: {gitHosted: true, integrity: sha512-Yn9jzmP2NmhGJgnM4uuW2L1PSvSEjBpY5tB1s7CMnBHyuqx5wED/HvgT0hqkFT0QxJSV6pMN7i9RnLOyl7vvTQ==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6} version: 0.0.1 engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} peerDependencies: @@ -16132,7 +16132,7 @@ snapshots: - shiki - uploadthing - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/3dd6e3eaaf66afc418eef27b38bdb91b2c15abc1(3ff7737281dfbf12c4fa8742bd038b22): + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6(3ff7737281dfbf12c4fa8742bd038b22): dependencies: '@ai-sdk/gateway': 4.0.90(zod@4.6.5) '@ai-sdk/vue': 4.0.112(vue@3.5.43(typescript@6.0.3))(zod@4.6.5) From 9785b58f1581948dbebc0eea54966559fe6c0193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 7 Oct 2026 14:20:34 +0200 Subject: [PATCH 04/19] docs: pin comark-docs to main (#54 merged) --- docs/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index f5eed899..a97ff703 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "ai": "catalog:", "beautiful-mermaid": "catalog:", "comark": "workspace:*", - "comark-docs": "github:comarkdown/comark-docs#5f15eaf90776447e0be2b6490ab3f60a66c9c8d6", + "comark-docs": "github:comarkdown/comark-docs#4c0ac9e471b3ac78cb7b497908d93db7c1fd0613", "katex": "catalog:", "nuxt": "catalog:", "splitpanes": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 767a5421..f5e8e03c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,8 +575,8 @@ importers: specifier: workspace:* version: link:../packages/comark comark-docs: - specifier: github:comarkdown/comark-docs#5f15eaf90776447e0be2b6490ab3f60a66c9c8d6 - version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6(3ff7737281dfbf12c4fa8742bd038b22) + specifier: github:comarkdown/comark-docs#4c0ac9e471b3ac78cb7b497908d93db7c1fd0613 + version: https://codeload.github.com/comarkdown/comark-docs/tar.gz/4c0ac9e471b3ac78cb7b497908d93db7c1fd0613(3ff7737281dfbf12c4fa8742bd038b22) katex: specifier: 'catalog:' version: 0.18.7 @@ -6843,8 +6843,8 @@ packages: resolution: {integrity: sha512-z0nuo3PM53FwBYbxP4rNrUo1QW7jv8YO44HDYaGm2Wx4pmHsLqKdsA6CiHcSOaURze6m7my0Z0YFaLL77LtDUA==} hasBin: true - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6: - resolution: {gitHosted: true, integrity: sha512-Yn9jzmP2NmhGJgnM4uuW2L1PSvSEjBpY5tB1s7CMnBHyuqx5wED/HvgT0hqkFT0QxJSV6pMN7i9RnLOyl7vvTQ==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6} + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/4c0ac9e471b3ac78cb7b497908d93db7c1fd0613: + resolution: {gitHosted: true, integrity: sha512-KazWobNR1CmJdP4uNTvSHAwVFx7rz7BOLsDmjhxWMgCrO1u2su0sICpHvO84XKBYzqUioqNqHbop06H6jotVDA==, tarball: https://codeload.github.com/comarkdown/comark-docs/tar.gz/4c0ac9e471b3ac78cb7b497908d93db7c1fd0613} version: 0.0.1 engines: {node: ^22.19.0 || ^24.11.0 || >=26.0.0} peerDependencies: @@ -16132,7 +16132,7 @@ snapshots: - shiki - uploadthing - comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/5f15eaf90776447e0be2b6490ab3f60a66c9c8d6(3ff7737281dfbf12c4fa8742bd038b22): + comark-docs@https://codeload.github.com/comarkdown/comark-docs/tar.gz/4c0ac9e471b3ac78cb7b497908d93db7c1fd0613(3ff7737281dfbf12c4fa8742bd038b22): dependencies: '@ai-sdk/gateway': 4.0.90(zod@4.6.5) '@ai-sdk/vue': 4.0.112(vue@3.5.43(typescript@6.0.3))(zod@4.6.5) From 663115580fa6075bc441dbc3633940b6eb012661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 7 Oct 2026 14:21:39 +0200 Subject: [PATCH 05/19] docs(syntax): document the differences from CommonMark and GFM --- .../1.getting-started/0.introduction.md | 2 +- docs/content/2.syntax/1.markdown.md | 32 +++++++++++++++++-- docs/content/index.md | 4 +-- .../comark/references/markdown-syntax.md | 4 ++- packages/comark/README.md | 2 +- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/content/1.getting-started/0.introduction.md b/docs/content/1.getting-started/0.introduction.md index f06ce074..4b865187 100644 --- a/docs/content/1.getting-started/0.introduction.md +++ b/docs/content/1.getting-started/0.introduction.md @@ -214,6 +214,6 @@ For detailed, honest comparisons, see [Comark vs MDX](/compare/comark-vs-mdx), [ ::: :::accordion-item{label="Do I have to use components?"} - No. Comark is a superset of CommonMark and GFM, so plain Markdown parses unchanged. Components and [attributes](/syntax/attributes) are opt-in syntax. + No. Plain CommonMark and GFM parse as standard Markdown, with a few [documented differences](/syntax/markdown#differences-from-commonmark-and-gfm). Components and [attributes](/syntax/attributes) are opt-in syntax. ::: :: diff --git a/docs/content/2.syntax/1.markdown.md b/docs/content/2.syntax/1.markdown.md index 618ef035..df7f23f0 100644 --- a/docs/content/2.syntax/1.markdown.md +++ b/docs/content/2.syntax/1.markdown.md @@ -1,6 +1,6 @@ --- title: Markdown -description: Comark supports all standard CommonMark and GitHub Flavored Markdown (GFM) features including headings, formatting, lists, tables, code blocks, and more. +description: Comark parses CommonMark and GitHub Flavored Markdown (GFM), including headings, formatting, lists, tables, and code blocks, with a few documented differences. navigation: title: Markdown links: @@ -16,7 +16,7 @@ links: variant: soft --- -Comark supports all standard CommonMark and GitHub Flavored Markdown (GFM) features. +Comark parses CommonMark and GitHub Flavored Markdown (GFM). The parser is built on markdown-exit, which follows the CommonMark spec. Comark then converts the tokens into its own [document model](/getting-started/document-model), and a few constructs render differently. See [Differences from CommonMark and GFM](#differences-from-commonmark-and-gfm). ## Headings @@ -415,3 +415,31 @@ Hello :wave: Welcome to our docs! :rocket: :thinking: :eyes: :100: :star: :zap: :bulb: :warning: ``` :: + +## Differences from CommonMark and GFM + +Comark renders most CommonMark and GFM documents the same way as a spec-compliant parser. The differences below come from Comark syntax, from the default options, or from the conversion to the document model. + +### Comark syntax + +- **Shortcut reference links.** `[text]` without a destination is a [span](/syntax/attributes#span-attributes), so `[foo]` doesn't resolve a `[foo]: /url` definition. Full (`[foo][ref]`) and collapsed (`[foo][]`) reference links work. +- **Component and attribute markers.** Text such as `::name`, `:name[...]`, and `{...}` after an element is Comark syntax. Set `registerDefaultPlugins: false` to parse it as plain text. + +### Default options + +- **`autoClose`** is on by default, so unterminated syntax is closed: `*foo bar` renders as emphasis, and an unclosed `` ` `` becomes inline code. Set `autoClose: false` for static content that must follow the spec. See the [Streaming API](/reference/auto-close). +- **`autoUnwrap`** removes the `

` inside a list item or a blockquote that holds a single paragraph. +- **`headingIds`** adds an `id` to every heading. + +### Document model + +- **Tight and loose lists** aren't distinguished. List items render the same way with or without blank lines between them. +- **Empty link text** removes the link: `[](/url)` renders nothing. +- **Image alt text** keeps the raw Markdown: `![foo *bar*](/img.png)` gives the alt text `foo *bar*`. +- **Markdown inside an HTML block**, such as a `

` followed by a blank line, renders after the HTML element instead of inside it. +- **Fenced code blocks** add `language` and `meta` attributes to the `
` element.
+
+### GFM extensions
+
+- **Email autolinks** such as `foo@bar.baz` stay plain text. URL autolinks such as `www.example.com` work.
+- **The tag filter** isn't applied: raw ``, `<style>`, `<xmp>` and the other filtered tags aren't escaped. Set `blockedTags` in the [security plugin](/plugins/built-in/security) to remove them.
diff --git a/docs/content/index.md b/docs/content/index.md
index 7b61db64..7435c333 100644
--- a/docs/content/index.md
+++ b/docs/content/index.md
@@ -253,7 +253,7 @@ Syntax
 [Co]{.font-bold}[mponents in ]{.text-muted} [Mark]{.font-bold}[down]{.text-muted}
 
 #description
-Discover our opt-in syntax for components, attributes, and frontmatter, handled by default plugins you can turn off. CommonMark and GFM  are supported by default, so every Markdown file you already have keeps working. 
+Discover our opt-in syntax for components, attributes, and frontmatter, handled by default plugins you can turn off. CommonMark and GFM are supported by default, so the Markdown files you already have keep working.
 
 #code-0
   ```mdc [components.md]
@@ -306,7 +306,7 @@ items:
   - label: How does Comark handle streaming AI output?
     content: The auto-close parser completes unterminated syntax (bold, code fences, components) so every incomplete frame renders correctly. Framework renderers expose this as a streaming prop.
   - label: Do I have to use components?
-    content: No. Comark is a superset of CommonMark and GFM, so plain Markdown parses unchanged. Components and attributes are opt-in syntax.
+    content: No. Plain CommonMark and GFM parse as standard Markdown, with a few documented differences. Components and attributes are opt-in syntax.
   - label: Is it free?
     content: Yes. MIT-licensed open source, maintained by Vercel. Your only costs are your own hosting.
 ---
diff --git a/docs/skills/comark/references/markdown-syntax.md b/docs/skills/comark/references/markdown-syntax.md
index c834be62..ac7f07c3 100644
--- a/docs/skills/comark/references/markdown-syntax.md
+++ b/docs/skills/comark/references/markdown-syntax.md
@@ -16,7 +16,9 @@ Complete guide for writing Comark (Components in Markdown) documents.
 
 ## Standard Markdown
 
-Comark supports all standard CommonMark and GitHub Flavored Markdown (GFM) features:
+Comark parses CommonMark and GitHub Flavored Markdown (GFM). A few constructs differ, for example `[text]` without a link destination is a span, not a shortcut reference link. See https://comark.dev/syntax/markdown#differences-from-commonmark-and-gfm.
+
+The standard features:
 
 ### Headings
 
diff --git a/packages/comark/README.md b/packages/comark/README.md
index 240303f0..a829f038 100644
--- a/packages/comark/README.md
+++ b/packages/comark/README.md
@@ -27,7 +27,7 @@ This is **Markdown** inside your own component.
 - **Runtime parsing**: `parseMarkdown(markdown)` is a pure function returning a compact, serializable Markdown document. Content from a database, CMS, or LLM is live the moment it is saved. No rebuild, no redeploy. ([Comark vs MDX](https://comark.dev/compare/comark-vs-mdx))
 - **Streaming built in**: auto-close completes unterminated syntax (`**bold`, open code fences, half-open components) so AI output renders correctly at every frame.
 - **One parser, every renderer**: the same source renders to HTML, ANSI, Vue, React, Svelte, and Angular. Your content outlasts your framework.
-- **Still just Markdown**: full CommonMark + GFM, frontmatter, and `{.class}` attributes on native elements. Components are opt-in syntax, not a new language.
+- **Still just Markdown**: CommonMark + GFM ([with a few differences](https://comark.dev/syntax/markdown#differences-from-commonmark-and-gfm)), frontmatter, and `{.class}` attributes on native elements. Components are opt-in syntax, not a new language.
 - **Plugin ecosystem**: Shiki highlighting, KaTeX math, Mermaid diagrams, TOC, alerts, footnotes and more, plus compatibility with existing markdown-it plugins.
 - **Decoupled parse & render**: parse once on the server, send the serializable document (`['tag', props, ...children]`) to the client, render without re-parsing.
 - **Fast**: built on [markdown-exit](https://github.com/serkodev/markdown-exit), a TypeScript rewrite of markdown-it, with full TypeScript support.

From b50427398af09a289e24aac03e232e9ffd20191f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= <atinux@gmail.com>
Date: Wed, 7 Oct 2026 15:06:37 +0200
Subject: [PATCH 06/19] docs: add use-case guides (AI chat streaming,
 generative UI, docs, CMS, CLI, email/RSS)

---
 docs/content/10.use-cases/.navigation.yml     |   2 +
 .../10.use-cases/1.ai-chat-streaming.md       | 160 ++++++++++++++++++
 docs/content/10.use-cases/2.vue-ai-chat.md    | 157 +++++++++++++++++
 docs/content/10.use-cases/3.svelte-ai-chat.md | 157 +++++++++++++++++
 .../content/10.use-cases/4.angular-ai-chat.md | 151 +++++++++++++++++
 docs/content/10.use-cases/5.generative-ui.md  | 152 +++++++++++++++++
 docs/content/10.use-cases/6.docs-and-blogs.md | 156 +++++++++++++++++
 .../10.use-cases/7.cms-runtime-content.md     | 137 +++++++++++++++
 docs/content/10.use-cases/8.cli-and-agents.md | 134 +++++++++++++++
 docs/content/10.use-cases/9.email-and-rss.md  | 145 ++++++++++++++++
 docs/content/10.use-cases/index.md            |  55 ++++++
 11 files changed, 1406 insertions(+)
 create mode 100644 docs/content/10.use-cases/.navigation.yml
 create mode 100644 docs/content/10.use-cases/1.ai-chat-streaming.md
 create mode 100644 docs/content/10.use-cases/2.vue-ai-chat.md
 create mode 100644 docs/content/10.use-cases/3.svelte-ai-chat.md
 create mode 100644 docs/content/10.use-cases/4.angular-ai-chat.md
 create mode 100644 docs/content/10.use-cases/5.generative-ui.md
 create mode 100644 docs/content/10.use-cases/6.docs-and-blogs.md
 create mode 100644 docs/content/10.use-cases/7.cms-runtime-content.md
 create mode 100644 docs/content/10.use-cases/8.cli-and-agents.md
 create mode 100644 docs/content/10.use-cases/9.email-and-rss.md
 create mode 100644 docs/content/10.use-cases/index.md

diff --git a/docs/content/10.use-cases/.navigation.yml b/docs/content/10.use-cases/.navigation.yml
new file mode 100644
index 00000000..aea76ac3
--- /dev/null
+++ b/docs/content/10.use-cases/.navigation.yml
@@ -0,0 +1,2 @@
+title: Use cases
+icon: i-lucide-lightbulb
diff --git a/docs/content/10.use-cases/1.ai-chat-streaming.md b/docs/content/10.use-cases/1.ai-chat-streaming.md
new file mode 100644
index 00000000..a55e4e95
--- /dev/null
+++ b/docs/content/10.use-cases/1.ai-chat-streaming.md
@@ -0,0 +1,160 @@
+---
+title: 'Render Streaming Markdown from an LLM'
+description: 'Render LLM output as Markdown while it streams, with no raw asterisks or open code fences, in Vue, React, Svelte, and Angular.'
+navigation:
+  title: AI chat streaming
+  icon: i-lucide-message-square-text
+links:
+  - label: Auto-close API
+    icon: i-lucide-radio
+    to: /reference/auto-close
+    color: neutral
+    variant: soft
+  - label: Streaming demo
+    icon: i-lucide-play
+    to: /stream
+    color: neutral
+    variant: soft
+---
+
+Pass the accumulated text to a Comark renderer component, and set its `streaming` prop while tokens arrive. Comark closes unfinished syntax on every parse, so each frame renders as valid Markdown.
+
+## The problem
+
+An LLM sends Markdown in small chunks. Most chunks end in the middle of a construct, for example `**bold` or a code fence with no closing line. A standard Markdown renderer shows these half-written parts as raw characters. When the closing marker arrives, the block changes shape, and the layout jumps. Comark handles this with two features:
+
+- **`autoClose`** is on by default in every renderer. It closes unterminated bold, italic, inline code, links, tables, and components before each parse. An open code fence renders as a code block that grows until the fence closes. An incomplete link gets the placeholder URL `comark:incomplete-link`.
+- **`streaming`** turns on incremental parsing. Comark reuses the output of completed blocks, drops a half-typed opener at the end, and keeps the previous output if a parse fails.
+
+Set `streaming` back to `false` when the stream ends. Comark then runs a final full parse. The `caret` prop adds a blinking cursor to the last text node while `streaming` is `true`.
+
+## Render a streaming message
+
+Each renderer takes the same three inputs: the text, a `streaming` flag, and an optional `caret`:
+
+::code-group
+
+```vue [Vue]
+<script setup lang="ts">
+import { Markdown } from '@comark/vue'
+
+defineProps<{ text: string, isStreaming: boolean }>()
+</script>
+
+<template>
+  <Suspense>
+    <Markdown :value="text" :streaming="isStreaming" caret />
+  </Suspense>
+</template>
+```
+
+```tsx [React]
+'use client'
+import { MarkdownClient } from '@comark/react'
+
+export function Message({ text, isStreaming }: { text: string, isStreaming: boolean }) {
+  return <MarkdownClient value={text} streaming={isStreaming} caret />
+}
+```
+
+```svelte [Svelte]
+<script lang="ts">
+  import { Markdown } from '@comark/svelte'
+
+  let { text, isStreaming }: { text: string, isStreaming: boolean } = $props()
+</script>
+
+<Markdown value={text} streaming={isStreaming} caret />
+```
+
+```typescript [Angular]
+import { Component, Input } from '@angular/core'
+import { Markdown } from '@comark/angular'
+
+@Component({
+  selector: 'app-message',
+  standalone: true,
+  imports: [Markdown],
+  template: `<comark-markdown [value]="text" [streaming]="isStreaming" [caret]="true" />`,
+})
+export class MessageComponent {
+  @Input() text = ''
+  @Input() isStreaming = false
+}
+```
+
+::
+
+In Vue, `<Markdown>` is async, so wrap it in `<Suspense>`. In React, `<Markdown>` is an async component. It hands off to `MarkdownClient` when `streaming` is `true`. In a client component, such as a chat that uses `useChat`, use `MarkdownClient` directly.
+
+## Set `streaming` from the AI SDK
+
+The [AI SDK](https://ai-sdk.dev/docs) splits each message into `parts`. A text part has `state === 'streaming'` while it receives tokens. Pass that value to the `streaming` prop, so each part stops streaming on its own:
+
+```tsx [app/chat.tsx]
+'use client'
+import { useState } from 'react'
+import { useChat } from '@ai-sdk/react'
+import { MarkdownClient } from '@comark/react'
+
+export default function Chat() {
+  const { messages, sendMessage } = useChat()
+  const [input, setInput] = useState('')
+  return (
+    <div>
+      {messages.map(message => (
+        <div key={message.id}>
+          {message.parts.map((part, index) =>
+            part.type === 'text'
+              ? <MarkdownClient key={index} value={part.text} streaming={part.state === 'streaming'} caret />
+              : null
+          )}
+        </div>
+      ))}
+      <form onSubmit={(event) => { event.preventDefault(); sendMessage({ text: input }); setInput('') }}>
+        <input value={input} onChange={event => setInput(event.target.value)} />
+      </form>
+    </div>
+  )
+}
+```
+
+For complete chat setups, see the [Vue and Nuxt](/use-cases/vue-ai-chat), [Svelte](/use-cases/svelte-ai-chat), and [Angular](/use-cases/angular-ai-chat) guides.
+
+## Sanitize model output
+
+Treat model output as untrusted input. The default `html` plugin parses raw HTML into nodes. Without more configuration, a raw `<script>` element, an `onerror` attribute, or a `javascript:` link stays in the document. A `<script>` element then appears in server-rendered HTML.
+
+Add the [security plugin](/plugins/built-in/security). It always strips `on*` attributes and unsafe protocols such as `javascript:`. Its `blockedTags` option removes tags from raw HTML and from component syntax, because both produce the same nodes:
+
+```tsx [app/chat.tsx]
+import security from '@comark/react/plugins/security'
+
+const plugins = [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })]
+// <MarkdownClient value={part.text} streaming={part.state === 'streaming'} plugins={plugins} caret />
+```
+
+Define `plugins` outside the component, so the reference stays stable between renders. Import the same plugin from `@comark/vue/plugins/security`, `@comark/svelte/plugins/security`, or `@comark/angular/plugins/security`. To turn off raw HTML parsing, set `registerDefaultPlugins: false` and register only the plugins you need, without `html`.
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Do I have to call autoCloseMarkdown myself?"}
+  No. `autoClose` is on by default in `parseMarkdown()` and in every renderer. Call [`autoCloseMarkdown()`](/reference/auto-close) only when you parse with another tool.
+  :::
+
+  :::accordion-item{label="What happens if I leave streaming set to true?"}
+  Comark stays in incremental mode and doesn't run the final full parse. Set `streaming` to `false` when the stream ends, for example from `part.state`.
+  :::
+
+  :::accordion-item{label="Can the model use components while it streams?"}
+  Yes. `autoClose` also closes an open `::component` block, so the component renders before its closing `::` arrives. See [generative UI](/use-cases/generative-ui).
+  :::
+::
+
+## Next steps
+
+- [Streaming Markdown in Vue and Nuxt with the AI SDK](/use-cases/vue-ai-chat)
+- [Let an LLM render UI components](/use-cases/generative-ui)
+- [Auto-close API reference](/reference/auto-close)
+- [Markdown libraries for AI chat](/compare/markdown-libraries-for-ai-chat)
diff --git a/docs/content/10.use-cases/2.vue-ai-chat.md b/docs/content/10.use-cases/2.vue-ai-chat.md
new file mode 100644
index 00000000..dffe0ad6
--- /dev/null
+++ b/docs/content/10.use-cases/2.vue-ai-chat.md
@@ -0,0 +1,157 @@
+---
+title: 'Stream Markdown in Vue and Nuxt with the AI SDK'
+description: 'Build a Nuxt chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives.'
+navigation:
+  title: Vue and Nuxt chat
+  icon: i-simple-icons-nuxt
+links:
+  - label: Full example
+    icon: i-lucide-square-play
+    to: /examples/ai/nuxt-ai-sdk
+    color: neutral
+    variant: soft
+  - label: Nuxt rendering
+    icon: i-simple-icons-nuxt
+    to: /rendering/nuxt
+    color: neutral
+    variant: soft
+---
+
+Use the `Chat` class from `@ai-sdk/vue` to receive messages, and render each text part with Comark's `<Markdown>` component. Bind `streaming` to the part state, so Comark closes unfinished syntax until the part is complete.
+
+This guide uses Nuxt with the `@comark/nuxt` module, which auto-imports `<Markdown>`. In a Vue app without Nuxt, import `Markdown` from `@comark/vue`. The steps follow the [AI SDK example](/examples/ai/nuxt-ai-sdk).
+
+::steps{level="3"}
+
+### Install the packages
+
+```bash [Terminal]
+npx nuxt add comark
+npm install ai @ai-sdk/vue
+```
+
+The AI SDK reads your provider key from the environment. The example uses the Vercel AI Gateway with the `AI_GATEWAY_API_KEY` variable.
+
+### Create the server route
+
+The route streams the model response in the format that `Chat` expects:
+
+```typescript [server/api/chat.post.ts]
+import { convertToModelMessages, streamText } from 'ai'
+
+export default defineEventHandler(async (event) => {
+  const { messages } = await readBody(event)
+
+  const result = streamText({
+    model: 'anthropic/claude-sonnet-4.6',
+    system: 'You are a helpful assistant. Answer in Markdown.',
+    messages: await convertToModelMessages(messages),
+  })
+
+  return result.toUIMessageStreamResponse()
+})
+```
+
+The server API can change between AI SDK versions. Check the [AI SDK docs](https://ai-sdk.dev/docs) for your version.
+
+### Render the messages
+
+`Chat` sends requests to `/api/chat` by default. Render each text part with `<Markdown>`, and wrap it in `<Suspense>` because the Vue component is async:
+
+```vue [app/pages/index.vue]
+<script setup lang="ts">
+import { Chat } from '@ai-sdk/vue'
+
+const chat = new Chat({})
+const input = ref('')
+
+function onSubmit() {
+  chat.sendMessage({ text: input.value })
+  input.value = ''
+}
+</script>
+
+<template>
+  <div v-for="message in chat.messages" :key="message.id">
+    <template v-for="(part, index) in message.parts" :key="`${message.id}-${index}`">
+      <template v-if="part.type === 'text'">
+        <p v-if="message.role === 'user'">{{ part.text }}</p>
+        <Suspense v-else>
+          <Markdown :value="part.text" :streaming="part.state === 'streaming'" caret />
+        </Suspense>
+      </template>
+    </template>
+  </div>
+
+  <form @submit.prevent="onSubmit">
+    <input v-model="input" placeholder="Ask something…">
+  </form>
+</template>
+```
+
+::
+
+User messages render as plain text in this example. Only the assistant output goes through the Markdown parser.
+
+## Use Nuxt UI chat components
+
+The [full example](/examples/ai/nuxt-ai-sdk) uses Nuxt UI chat components, such as `UChatMessages` and `UChatPrompt`. It reads the part state with the `isPartStreaming()` helper:
+
+```vue [app/pages/index.vue]
+<script setup lang="ts">
+import { isPartStreaming } from '@nuxt/ui/utils/ai'
+</script>
+
+<template>
+  <Suspense>
+    <Markdown :value="part.text" :streaming="isPartStreaming(part)" caret />
+  </Suspense>
+</template>
+```
+
+When `@nuxt/ui` is installed, Comark also renders Markdown elements with the Nuxt UI prose components. See [Nuxt UI integration](/rendering/nuxt#nuxt-ui-integration).
+
+## Add components and plugins
+
+Pass components and plugins to `<Markdown>`, for example to highlight code in answers. To keep untrusted output safe, add the [security plugin](/plugins/built-in/security):
+
+```vue [app/pages/index.vue]
+<script setup lang="ts">
+import shiki from '@comark/vue/plugins/shiki'
+import security from '@comark/vue/plugins/security'
+
+const plugins = [
+  shiki(),
+  security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] }),
+]
+</script>
+
+<template>
+  <Suspense>
+    <Markdown :value="part.text" :streaming="part.state === 'streaming'" :plugins="plugins" caret />
+  </Suspense>
+</template>
+```
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Why do I need Suspense?"}
+  The Vue `<Markdown>` component is async, and it must be inside a `<Suspense>` boundary. The example puts one boundary around each assistant part.
+  :::
+
+  :::accordion-item{label="Can I render the answer on the server?"}
+  Chat messages arrive in the browser, so client rendering is enough. For server-rendered content, parse on the server and render with [`<MarkdownDocument>`](/rendering/vue#code-markdowndocument).
+  :::
+
+  :::accordion-item{label="Can the model write Comark components?"}
+  Yes. Register the components in the `components` prop and tell the model which ones exist. See [generative UI](/use-cases/generative-ui).
+  :::
+::
+
+## Next steps
+
+- [Run the full Nuxt AI SDK example](/examples/ai/nuxt-ai-sdk)
+- [Render streaming Markdown from an LLM](/use-cases/ai-chat-streaming)
+- [Let an LLM render UI components](/use-cases/generative-ui)
+- [Vue streaming reference](/rendering/vue#streaming)
diff --git a/docs/content/10.use-cases/3.svelte-ai-chat.md b/docs/content/10.use-cases/3.svelte-ai-chat.md
new file mode 100644
index 00000000..6b64baf1
--- /dev/null
+++ b/docs/content/10.use-cases/3.svelte-ai-chat.md
@@ -0,0 +1,157 @@
+---
+title: 'Stream Markdown in Svelte and SvelteKit with the AI SDK'
+description: 'Build a Svelte 5 chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives.'
+navigation:
+  title: Svelte chat
+  icon: i-simple-icons-svelte
+links:
+  - label: Svelte rendering
+    icon: i-simple-icons-svelte
+    to: /rendering/svelte
+    color: neutral
+    variant: soft
+  - label: SvelteKit example
+    icon: i-lucide-square-play
+    to: /examples/frameworks/sveltekit
+    color: neutral
+    variant: soft
+---
+
+Use the `Chat` class from `@ai-sdk/svelte` to receive messages, and render each text part with `<Markdown value streaming caret />` from `@comark/svelte`. Comark closes unfinished syntax on every update, and runs a full parse when `streaming` becomes `false`.
+
+This guide uses Svelte 5 and SvelteKit. It assumes that you have a SvelteKit app and an AI provider key.
+
+::steps{level="3"}
+
+### Install the packages
+
+```bash [Terminal]
+npm install @comark/svelte ai @ai-sdk/svelte
+```
+
+### Create the server route
+
+`Chat` sends requests to `/api/chat` by default. Create a SvelteKit endpoint at that path:
+
+```typescript [src/routes/api/chat/+server.ts]
+import { convertToModelMessages, createGateway, streamText, type UIMessage } from 'ai'
+import { AI_GATEWAY_API_KEY } from '$env/static/private'
+
+const gateway = createGateway({ apiKey: AI_GATEWAY_API_KEY })
+
+export async function POST({ request }) {
+  const { messages }: { messages: UIMessage[] } = await request.json()
+
+  const result = streamText({
+    model: gateway('anthropic/claude-sonnet-4.6'),
+    messages: await convertToModelMessages(messages),
+  })
+
+  return result.toUIMessageStreamResponse()
+}
+```
+
+Vite doesn't copy environment variables to `process.env`, so the route reads the key from `$env/static/private`. The server API can change between AI SDK versions. Check the [AI SDK Svelte guide](https://ai-sdk.dev/docs/getting-started/svelte) for your version.
+
+### Render the messages
+
+Pass `part.state === 'streaming'` to the `streaming` prop. Each part then runs its final full parse as soon as it is complete:
+
+```svelte [src/routes/+page.svelte]
+<script lang="ts">
+  import { Chat } from '@ai-sdk/svelte'
+  import { Markdown } from '@comark/svelte'
+
+  let input = $state('')
+  const chat = new Chat({})
+
+  function handleSubmit(event: SubmitEvent) {
+    event.preventDefault()
+    chat.sendMessage({ text: input })
+    input = ''
+  }
+</script>
+
+{#each chat.messages as message (message.id)}
+  {#each message.parts as part, index (index)}
+    {#if part.type === 'text'}
+      {#if message.role === 'user'}
+        <p>{part.text}</p>
+      {:else}
+        <Markdown value={part.text} streaming={part.state === 'streaming'} caret />
+      {/if}
+    {/if}
+  {/each}
+{/each}
+
+<form onsubmit={handleSubmit}>
+  <input bind:value={input} placeholder="Ask something…" />
+</form>
+```
+
+::
+
+Don't destructure `chat`. A destructured `messages` value is a copy and doesn't update.
+
+## Server-side rendering
+
+The Svelte `<Markdown>` component uses `$effect`, so it doesn't render during SSR. This is fine for chat, because the messages arrive in the browser.
+
+For content that must be in the server HTML, parse it in a `load` function and render it with `<MarkdownDocument>`:
+
+```typescript [src/routes/docs/+page.server.ts]
+import { parseMarkdown } from 'comark'
+
+export const load = async () => {
+  return { tree: await parseMarkdown('# Release notes\n\nThis page renders on the server.') }
+}
+```
+
+```svelte [src/routes/docs/+page.svelte]
+<script lang="ts">
+  import { MarkdownDocument } from '@comark/svelte'
+
+  let { data } = $props()
+</script>
+
+<MarkdownDocument value={data.tree} />
+```
+
+The [SvelteKit example](/examples/frameworks/sveltekit) shows both routes, including lazy components with `<MarkdownAsync>`.
+
+## Sanitize model output
+
+Raw HTML in model output is parsed into nodes by default. Add the [security plugin](/plugins/built-in/security) and pass it in the `plugins` prop:
+
+```svelte
+<script lang="ts">
+  import security from '@comark/svelte/plugins/security'
+
+  const plugins = [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })]
+</script>
+
+<Markdown value={part.text} streaming={part.state === 'streaming'} {plugins} caret />
+```
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Does @comark/svelte support Svelte 4?"}
+  No. `@comark/svelte` provides Svelte 5 components that use runes such as `$state` and `$effect`.
+  :::
+
+  :::accordion-item{label="Why is my Markdown missing from the server HTML?"}
+  `<Markdown>` renders only in the browser. Parse on the server and use `<MarkdownDocument>`, or use the experimental [`<MarkdownAsync>`](/rendering/svelte#code-markdownasync-code-experimental).
+  :::
+
+  :::accordion-item{label="How do I map a component to a Svelte component?"}
+  Pass a `components` map, for example `components={{ alert: Alert }}`. See [Svelte components](/rendering/svelte#code-markdown-props-code-components).
+  :::
+::
+
+## Next steps
+
+- [Render streaming Markdown from an LLM](/use-cases/ai-chat-streaming)
+- [Let an LLM render UI components](/use-cases/generative-ui)
+- [Svelte streaming reference](/rendering/svelte#streaming)
+- [SvelteKit example](/examples/frameworks/sveltekit)
diff --git a/docs/content/10.use-cases/4.angular-ai-chat.md b/docs/content/10.use-cases/4.angular-ai-chat.md
new file mode 100644
index 00000000..0aa7b36a
--- /dev/null
+++ b/docs/content/10.use-cases/4.angular-ai-chat.md
@@ -0,0 +1,151 @@
+---
+title: 'Stream Markdown in Angular with the AI SDK'
+description: 'Build an Angular 17+ chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives.'
+navigation:
+  title: Angular chat
+  icon: i-simple-icons-angular
+links:
+  - label: Angular rendering
+    icon: i-simple-icons-angular
+    to: /rendering/angular
+    color: neutral
+    variant: soft
+  - label: Vite Angular example
+    icon: i-lucide-square-play
+    to: /examples/vite/angular
+    color: neutral
+    variant: soft
+---
+
+Use the `Chat` class from `@ai-sdk/angular` to receive messages, and render each text part with `<comark-markdown [value] [streaming] [caret]>`. Comark closes unfinished syntax while the part streams, and runs a full parse when it is complete.
+
+`@comark/angular` supports Angular 17 or later. See the [requirements](/rendering/angular#requirements) for the exact range. The `Chat` class from `@ai-sdk/angular` stores its state in Angular signals, so the template updates as tokens arrive.
+
+::steps{level="3"}
+
+### Install the packages
+
+```bash [Terminal]
+npm install @comark/angular ai @ai-sdk/angular
+```
+
+### Create the server route
+
+`Chat` sends requests to `/api/chat` by default. This Express route streams the model response in the format that `Chat` expects:
+
+```typescript [server.ts]
+import express from 'express'
+import { convertToModelMessages, streamText } from 'ai'
+
+const app = express()
+app.use(express.json())
+
+app.post('/api/chat', async (req, res) => {
+  const result = streamText({
+    model: 'anthropic/claude-sonnet-4.6',
+    messages: await convertToModelMessages(req.body.messages),
+  })
+
+  result.pipeUIMessageStreamToResponse(res)
+})
+
+app.listen(3000)
+```
+
+Configure the Angular dev server to proxy `/api` to this server. The server API can change between AI SDK versions. Check the [AI SDK docs](https://ai-sdk.dev/docs) for your version.
+
+### Render the messages
+
+Read `chat.messages` in the template. Pass `part.state === 'streaming'` to the `streaming` input:
+
+```typescript [chat.component.ts]
+import { Component } from '@angular/core'
+import { Chat } from '@ai-sdk/angular'
+import { Markdown } from '@comark/angular'
+
+@Component({
+  selector: 'app-chat',
+  standalone: true,
+  imports: [Markdown],
+  template: `
+    @for (message of chat.messages; track message.id) {
+      @for (part of message.parts; track $index) {
+        @switch (part.type) {
+          @case ('text') {
+            @if (message.role === 'user') {
+              <p>{{ part.text }}</p>
+            } @else {
+              <comark-markdown [value]="part.text" [streaming]="part.state === 'streaming'" [caret]="true" />
+            }
+          }
+        }
+      }
+    }
+
+    <form (submit)="send($event, prompt)">
+      <input #prompt placeholder="Ask something…" />
+    </form>
+  `,
+})
+export class ChatComponent {
+  chat = new Chat({})
+
+  send(event: Event, prompt: HTMLInputElement) {
+    event.preventDefault()
+    this.chat.sendMessage({ text: prompt.value })
+    prompt.value = ''
+  }
+}
+```
+
+::
+
+## Map components and add plugins
+
+Use the `components` input to map Comark tags to Angular components. Use the `plugins` input to add parser plugins, for example the [security plugin](/plugins/built-in/security) for untrusted output. This excerpt adds two properties to the component above:
+
+```typescript [chat.component.ts]
+import security from '@comark/angular/plugins/security'
+import { AlertComponent } from './alert.component'
+
+export class ChatComponent {
+  chat = new Chat({})
+  components = { alert: AlertComponent }
+  plugins = [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })]
+}
+```
+
+```html
+<comark-markdown
+  [value]="part.text"
+  [streaming]="part.state === 'streaming'"
+  [caret]="true"
+  [components]="components"
+  [plugins]="plugins"
+/>
+```
+
+Comark passes component props as `@Input()` values. The default slot maps to `<ng-content />`. See [component bindings](/rendering/angular#component-bindings).
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Does it work without the Angular CLI?"}
+  Yes. `@comark/angular` doesn't depend on a bundler. It works with the Angular CLI, Vite with AnalogJS, webpack, and esbuild.
+  :::
+
+  :::accordion-item{label="How do I style the caret?"}
+  Pass `[caret]="{ class: 'my-caret' }"` and style that class. See [caret](/rendering/angular#caret).
+  :::
+
+  :::accordion-item{label="How do I render content that is already parsed?"}
+  Use `<comark-markdown-document [value]="document" />` with a document from `parseMarkdown()`. It renders the document without a parse step. See [`<comark-markdown-document>`](/rendering/angular#code-comark-markdown-document).
+  :::
+::
+
+## Next steps
+
+- [Render streaming Markdown from an LLM](/use-cases/ai-chat-streaming)
+- [Let an LLM render UI components](/use-cases/generative-ui)
+- [Angular streaming reference](/rendering/angular#streaming)
+- [Vite Angular example](/examples/vite/angular)
diff --git a/docs/content/10.use-cases/5.generative-ui.md b/docs/content/10.use-cases/5.generative-ui.md
new file mode 100644
index 00000000..5fb58105
--- /dev/null
+++ b/docs/content/10.use-cases/5.generative-ui.md
@@ -0,0 +1,152 @@
+---
+title: 'Let an LLM Render UI Components Inside Markdown Safely'
+description: 'Let a model write Comark component syntax such as ::flight-card and render real Vue, React, Svelte, or Angular components, with no code execution.'
+navigation:
+  title: Generative UI
+  icon: i-lucide-sparkles
+links:
+  - label: Component syntax
+    icon: i-lucide-component
+    to: /syntax/components
+    color: neutral
+    variant: soft
+  - label: Security plugin
+    icon: i-lucide-shield-check
+    to: /plugins/built-in/security
+    color: neutral
+    variant: soft
+---
+
+The model writes component syntax, for example `::flight-card{from="CDG" to="JFK" price="342"}`, and your app maps the tag to a real component. The Markdown is data: nothing in it executes, and only the components that you register render as components.
+
+## How it works
+
+Comark parses component syntax into a node, for example `['flight-card', { from: 'CDG', to: 'JFK', price: '342' }]`. The renderer looks up `flight-card` in the `components` prop and renders your component with those props. A tag with no registered component renders as a plain element with the same name, for example `<flight-card from="CDG">`.
+
+The following diagram shows the path from model output to the screen:
+
+```mermaid
+flowchart LR
+  Model[LLM output] --> Parser[Comark parser]
+  Parser --> Node["flight-card node"]
+  Node --> Registered{Registered?}
+  Registered -- Yes --> Component[Your component]
+  Registered -- No --> Element["Plain flight-card element"]
+```
+
+## Register the component
+
+Write the component with optional props. During streaming, a prop can be missing until the model writes it:
+
+::code-group
+
+```vue [FlightCard.vue]
+<script setup lang="ts">
+defineProps<{ from?: string, to?: string, price?: number }>()
+</script>
+
+<template>
+  <div class="flight-card">
+    <strong>{{ from }} → {{ to }}</strong>
+    <span v-if="price">${{ price }}</span>
+    <slot />
+  </div>
+</template>
+```
+
+```vue [Chat.vue]
+<script setup lang="ts">
+import { Markdown } from '@comark/vue'
+import FlightCard from './FlightCard.vue'
+
+defineProps<{ text: string, isStreaming: boolean }>()
+
+const components = { 'flight-card': FlightCard }
+</script>
+
+<template>
+  <Suspense>
+    <Markdown :value="text" :streaming="isStreaming" :components="components" caret />
+  </Suspense>
+</template>
+```
+
+::
+
+React, Svelte, and Angular use the same `components` map. See [components](/syntax/components) for props, slots, and nesting.
+
+Attribute values are strings. Ask the model for a `:` prefix to get a typed value: `:price="342"` passes the number `342`.
+
+## Tell the model which components exist
+
+A model can't guess your components. List each component, its props, and one example in the system prompt:
+
+```typescript [server/api/chat.post.ts]
+const system = `Answer in Markdown. You can use these Comark components:
+
+- ::flight-card — one flight offer. Props: from and to (IATA airport codes), :price (number, USD).
+  Example: ::flight-card{from="CDG" to="JFK" :price="342"}
+  ::
+
+Don't use other components or raw HTML.`
+```
+
+For the full syntax, give the model the Comark syntax guide for agents at [comark.dev/.well-known/skills/comark/references/markdown-syntax.md](https://comark.dev/.well-known/skills/comark/references/markdown-syntax.md).
+
+## Validate the output
+
+Component syntax can name any tag, for example `::script` or `::iframe`. Because unregistered tags render as plain elements, block dangerous tag names with the [security plugin](/plugins/built-in/security):
+
+```typescript
+import security from '@comark/vue/plugins/security'
+
+const plugins = [
+  security({
+    blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'],
+    allowedProtocols: ['https', 'mailto'],
+  }),
+]
+```
+
+The plugin removes blocked tags from raw HTML and from component syntax. It also strips `on*` attributes and unsafe URLs such as `javascript:`. To restrict links to your own domains, add `allowedLinkPrefixes`. Your components also receive props from the model, so validate those values in the component.
+
+A `:`-prefixed prop can also read a path from the render context, for example `:label="data.user.email"`. Don't put secrets in the `data` prop when the content comes from a model.
+
+## Stream components
+
+`autoClose` closes an open component while the model writes it. A frame that ends with `::flight-card{from="CDG"` still parses as a `flight-card` node. Your component renders at once, and gets new props on each frame. Set `streaming` from the part state, as described in [AI chat streaming](/use-cases/ai-chat-streaming).
+
+## Use JSON specs instead
+
+Some models produce structured JSON more reliably than Markdown syntax. The [json-render plugin](/plugins/built-in/json-render) reads a `json-render` or `yaml-render` code block and replaces it with element nodes:
+
+````mdc
+```json-render
+{ "type": "FlightCard", "props": { "from": "CDG", "to": "JFK", "price": 342 } }
+```
+````
+
+Each element `type` becomes the tag name, so map `FlightCard` in the `components` prop. JSON values keep their types. Pass `jsonRender()` in `plugins`, from `@comark/vue/plugins/json-render` or the path for your framework.
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Can the model run JavaScript through a component?"}
+  No. Component syntax produces data, and Comark passes it to the components that you register. Block `script` and other risky tags, because unregistered tags render as plain elements.
+  :::
+
+  :::accordion-item{label="What if the model uses a component I didn't register?"}
+  It renders as a plain element with the same tag name and its children. To keep only the tags you expect, set the `allowedTags` option of the security plugin. List your components and the Markdown tags that you allow.
+  :::
+
+  :::accordion-item{label="When do I use json-render instead of component syntax?"}
+  Use a `json-render` block when your model writes structured JSON more reliably than component syntax. Component syntax mixes text and UI in one answer. Both produce the same kind of nodes.
+  :::
+::
+
+## Next steps
+
+- [Render streaming Markdown from an LLM](/use-cases/ai-chat-streaming)
+- [Component syntax](/syntax/components)
+- [Security plugin options](/plugins/built-in/security#options)
+- [json-render example](/examples/plugins/vue-vite-json-render)
diff --git a/docs/content/10.use-cases/6.docs-and-blogs.md b/docs/content/10.use-cases/6.docs-and-blogs.md
new file mode 100644
index 00000000..87272967
--- /dev/null
+++ b/docs/content/10.use-cases/6.docs-and-blogs.md
@@ -0,0 +1,156 @@
+---
+title: 'Build a Markdown Docs Site or Blog in Nuxt, Next.js, Astro, or SvelteKit'
+description: 'Parse Markdown files with frontmatter, a table of contents, and highlighted code, then render them with your own components in Nuxt, Next.js, Astro, or SvelteKit.'
+navigation:
+  title: Docs and blogs
+  icon: i-lucide-book-open
+links:
+  - label: Examples
+    icon: i-lucide-square-play
+    to: /examples/frameworks/nuxt
+    color: neutral
+    variant: soft
+  - label: Parse API
+    icon: i-lucide-file-code
+    to: /reference/parse
+    color: neutral
+    variant: soft
+---
+
+Parse each Markdown file on the server or at build time with `parseMarkdown()`, then render the result with `<MarkdownDocument>`. The browser receives the rendered page, and it doesn't need the parser.
+
+A Markdown file for a post holds YAML frontmatter, Markdown, and components:
+
+```mdc [content/posts/hello-world.md]
+---
+title: Hello World
+pubDate: 2026-10-07
+---
+
+## Getting started
+
+::alert{type="info"}
+Components render with your own design system.
+::
+```
+
+## Parse with frontmatter, a table of contents, and code highlighting
+
+One parse returns the content, the frontmatter, and plugin metadata. The [shiki plugin](/plugins/built-in/shiki) highlights code blocks. The [toc plugin](/plugins/built-in/toc) builds a table of contents from the `h2` to `h6` headings:
+
+```typescript [lib/posts.ts]
+import { readFile } from 'node:fs/promises'
+import { createMarkdownParser } from 'comark'
+import shiki from 'comark/plugins/shiki'
+import toc from 'comark/plugins/toc'
+
+const parse = createMarkdownParser({ plugins: [shiki(), toc({ depth: 3 })] })
+
+export async function getPost(slug: string) {
+  const tree = await parse(await readFile(`content/posts/${slug}.md`, 'utf-8'))
+
+  tree.frontmatter.title // 'Hello World'
+  tree.meta.toc.links // [{ id: 'getting-started', text: 'Getting started', depth: 2 }]
+  return tree
+}
+```
+
+`createMarkdownParser()` sets up the parser and its plugins once. Reuse the returned function for every file.
+
+## Render in your framework
+
+Each framework calls `getPost()` from the previous section on the server. It then renders the document with `<MarkdownDocument>`.
+
+### Nuxt
+
+Move `getPost()` to `server/utils/posts.ts`, so server routes can use it. With the `@comark/nuxt` module, `<MarkdownDocument>` is auto-imported:
+
+```typescript [server/api/posts/[slug].get.ts]
+export default defineEventHandler(event => getPost(getRouterParam(event, 'slug')!))
+```
+
+```vue [app/pages/blog/[slug].vue]
+<script setup lang="ts">
+const route = useRoute()
+const { data: tree } = await useFetch(`/api/posts/${route.params.slug}`)
+</script>
+
+<template>
+  <MarkdownDocument v-if="tree" :value="tree" />
+</template>
+```
+
+### Next.js
+
+A React Server Component parses and renders the post. No client JavaScript ships for the content:
+
+```tsx [app/blog/[slug]/page.tsx]
+import { MarkdownDocument } from '@comark/react'
+import { getPost } from '@/lib/posts'
+import Alert from '@/components/Alert'
+
+export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
+  const { slug } = await params
+  return <MarkdownDocument value={await getPost(slug)} className="prose" components={{ Alert }} />
+}
+```
+
+### Astro
+
+Parse the body of a content collection entry, and render it with the React renderer. Astro renders it to HTML on the server:
+
+```astro [src/pages/posts/[...id].astro]
+---
+import { parseMarkdown } from 'comark'
+import { MarkdownDocument } from '@comark/react'
+import Alert from '../../components/Alert'
+
+const document = await parseMarkdown(Astro.props.post.body!)
+---
+
+<MarkdownDocument value={document} className="prose" components={{ Alert }} />
+```
+
+### SvelteKit
+
+Parse in a server `load` function. The Svelte `<Markdown>` component doesn't render during SSR, so use `<MarkdownDocument>`:
+
+```svelte [src/routes/blog/[slug]/+page.svelte]
+<script lang="ts">
+  import { MarkdownDocument } from '@comark/svelte'
+  import Alert from '$lib/components/comark/Alert.svelte'
+
+  let { data } = $props() // from +page.server.ts: { tree: await getPost(params.slug) }
+</script>
+
+<MarkdownDocument value={data.tree} components={{ alert: Alert }} />
+```
+
+Each framework has a complete example: [Nuxt](/examples/frameworks/nuxt), [Next.js](/examples/frameworks/nextjs), [Astro](/examples/frameworks/astro), and [SvelteKit](/examples/frameworks/sveltekit). For VitePress, the [VitePress example](/examples/frameworks/vitepress) adds Comark component syntax to its markdown-it setup.
+
+## Override Markdown elements
+
+Map a native tag in `components` to change how it renders, for example `pre` for code blocks with a copy button. In Nuxt, place components such as `ProseH2.vue` in `~/components/prose`. See [overriding HTML elements](/rendering/nuxt#overriding-html-elements) and [custom code blocks](/kb/custom-code-block).
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Do I need a content collection or a build plugin?"}
+  No. `parseMarkdown()` takes a string, so the Markdown can come from files, a collection, or an API. The Astro example uses a content collection to load the files.
+  :::
+
+  :::accordion-item{label="Where is the table of contents?"}
+  With the `toc` plugin, it is in `tree.meta.toc`. Render `tree.meta.toc.links` as a list of anchor links to the heading IDs.
+  :::
+
+  :::accordion-item{label="How does this compare to MDX?"}
+  MDX compiles content to JSX at build time. Comark parses at runtime or build time into a JSON document. See [Comark vs MDX](/compare/comark-vs-mdx).
+  :::
+::
+
+## Next steps
+
+- [Frontmatter syntax](/syntax/frontmatter)
+- [Component syntax](/syntax/components)
+- [Shiki plugin options](/plugins/built-in/shiki)
+- [Render Markdown from a CMS or database](/use-cases/cms-runtime-content)
diff --git a/docs/content/10.use-cases/7.cms-runtime-content.md b/docs/content/10.use-cases/7.cms-runtime-content.md
new file mode 100644
index 00000000..41cb0248
--- /dev/null
+++ b/docs/content/10.use-cases/7.cms-runtime-content.md
@@ -0,0 +1,137 @@
+---
+title: 'Render Markdown with Components from a CMS or Database'
+description: 'Parse Markdown with components at runtime, store the JSON document, and render it in Vue, React, Svelte, Angular, or HTML with no build step.'
+navigation:
+  title: CMS and runtime content
+  icon: i-lucide-database
+links:
+  - label: Document model
+    icon: i-lucide-braces
+    to: /getting-started/document-model
+    color: neutral
+    variant: soft
+  - label: Render API
+    icon: i-lucide-file-code
+    to: /reference/render
+    color: neutral
+    variant: soft
+---
+
+Parse the Markdown on the server when an editor saves it, and store the resulting `MarkdownDocument` as JSON. Any client then renders that JSON with `<MarkdownDocument>`, with no parser in the browser and no build step.
+
+Content from a CMS or a database changes after you deploy. A compiler such as MDX turns content into code at build time, so new content needs a new build. Comark parses at runtime into plain data. See [Comark vs MDX](/compare/comark-vs-mdx) for a full comparison.
+
+## Parse and store the document
+
+A `MarkdownDocument` is a JSON-safe object: `{ nodes, frontmatter, meta }`. Parse once when the content changes, then store or cache the result. Sanitize before you store, so unsafe content never reaches the database:
+
+```typescript [server/save-page.ts]
+import { createMarkdownParser } from 'comark'
+import security from 'comark/plugins/security'
+
+const parse = createMarkdownParser({
+  plugins: [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })],
+})
+
+export async function savePage(slug: string, markdown: string) {
+  const document = await parse(markdown)
+  // `db` stands for your database client
+  await db.pages.upsert({ slug, markdown, document: JSON.stringify(document) })
+}
+```
+
+This example keeps the source Markdown for the editor and the document for rendering. Your API then returns the stored document.
+
+## Render the document on any client
+
+`<MarkdownDocument>` renders a parsed document without a parse step. Map component tags to your own components with the `components` prop:
+
+::code-group
+
+```vue [Vue]
+<script setup lang="ts">
+import { MarkdownDocument } from '@comark/vue'
+import Alert from './components/Alert.vue'
+
+const document = await fetch('/api/pages/pricing').then(res => res.json())
+</script>
+
+<template>
+  <MarkdownDocument :value="document" :components="{ alert: Alert }" />
+</template>
+```
+
+```tsx [React]
+import { MarkdownDocument } from '@comark/react'
+import Alert from './components/Alert'
+
+export default async function PricingPage() {
+  const document = await fetch('https://cms.example.com/api/pages/pricing').then(res => res.json())
+  return <MarkdownDocument value={document} components={{ alert: Alert }} />
+}
+```
+
+```typescript [HTML]
+import { renderHtmlFromDocument } from '@comark/html'
+
+const document = await fetch('https://cms.example.com/api/pages/pricing').then(res => res.json())
+const html = await renderHtmlFromDocument(document)
+```
+
+::
+
+Svelte and Angular use the same pattern with `<MarkdownDocument>` and `<comark-markdown-document>`. The document format doesn't depend on a framework, so one stored document serves every client.
+
+## Pass runtime data
+
+Content often refers to values that change per request, for example the user name or a price. Pass them in the `data` prop, and reference them with a `:` prop in the Markdown:
+
+```mdc
+Welcome back, :badge{:label="data.user.name"}!
+```
+
+```vue
+<MarkdownDocument :value="document" :data="{ user: { name: 'Ada' } }" />
+```
+
+To insert a value directly into text, add the [binding plugin](/plugins/built-in/binding). It adds the `{{ data.user.name || guest }}` syntax. See [data binding](/syntax/components#data-binding) for the full scope.
+
+## Turn the document back into Markdown
+
+`renderMarkdown()` converts a document into a Markdown string, with frontmatter and component syntax. Use it when you edit the document in code, or when you keep only the document:
+
+```typescript [server/edit-page.ts]
+import { renderMarkdown } from 'comark/render'
+
+// `row` is the stored page from your database
+const document = JSON.parse(row.document)
+document.frontmatter.title = 'New pricing'
+
+const markdown = await renderMarkdown(document)
+await savePage(row.slug, markdown)
+```
+
+The output can differ from the original formatting. For example, a component with more than three attributes moves them to a YAML block. See the [`maxInlineAttributes`](/reference/render#options-maxinlineattributes) option.
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Does the client need the parser?"}
+  No. `<MarkdownDocument>` only renders. The `<Markdown>` component imports the parser, so use it only when you parse in the browser.
+  :::
+
+  :::accordion-item{label="Can authors use components the app doesn't have?"}
+  An unregistered tag renders as a plain element with the same name. To limit the tags in stored content, use the `allowedTags` or `blockedTags` options of the [security plugin](/plugins/built-in/security).
+  :::
+
+  :::accordion-item{label="How do I update a live page without a reload?"}
+  The renderers can subscribe to a context and apply patches by key. See [live documents](/rendering/vue#live-documents).
+  :::
+::
+
+## Next steps
+
+- [Document model](/getting-started/document-model)
+- [Render API reference](/reference/render)
+- [Migrate from MDX](/kb/migration-from-mdx)
+- [Build a docs site or blog](/use-cases/docs-and-blogs)
diff --git a/docs/content/10.use-cases/8.cli-and-agents.md b/docs/content/10.use-cases/8.cli-and-agents.md
new file mode 100644
index 00000000..dfc21e3c
--- /dev/null
+++ b/docs/content/10.use-cases/8.cli-and-agents.md
@@ -0,0 +1,134 @@
+---
+title: 'Render Markdown in the Terminal for Node CLIs and Coding Agents'
+description: 'Print Markdown as styled ANSI output in Node.js CLIs and coding agents, with highlighted code, tables, and alerts, using @comark/ansi.'
+navigation:
+  title: CLIs and agents
+  icon: i-lucide-terminal
+links:
+  - label: ANSI rendering
+    icon: i-lucide-terminal
+    to: /rendering/ansi
+    color: neutral
+    variant: soft
+  - label: Shiki plugin
+    icon: i-lucide-code
+    to: /plugins/built-in/shiki
+    color: neutral
+    variant: soft
+---
+
+Use `@comark/ansi` to turn Markdown into a string with ANSI styles. `printAnsi()` writes it to the terminal, and `renderAnsi()` returns it so you can write it yourself.
+
+Coding agents and CLIs often print Markdown: release notes, help text, or model answers. Raw Markdown in a terminal shows `**` and `#` characters. `@comark/ansi` styles headings, emphasis, lists, links, tables, GitHub alerts, and code blocks.
+
+## Print a Markdown file
+
+Install the package, and `shiki` for code highlighting:
+
+```bash [Terminal]
+npm install @comark/ansi shiki
+```
+
+This script reads a file and prints it with highlighted code and math:
+
+```typescript [render.ts]
+import { readFile } from 'node:fs/promises'
+import { printAnsi } from '@comark/ansi'
+import shiki from '@comark/ansi/plugins/shiki'
+import math, { Math } from '@comark/ansi/plugins/math'
+
+const md = await readFile('source.md', 'utf-8')
+
+await printAnsi(md, {
+  plugins: [shiki(), math()],
+  components: { Math },
+})
+```
+
+Run it with `node render.ts`. The [ANSI example](https://github.com/comarkdown/comark/tree/main/examples/3.cli/ansi) contains a sample file with every supported element.
+
+## Control width, colors, and output
+
+`printAnsi()` writes to `stdout` by default. These options change the output:
+
+- **`width`** sets the width of horizontal rules and code block headers. The default is `80`.
+- **`colors`** turns ANSI escape codes on or off. It is `false` when the `NO_COLOR` environment variable is set.
+- **`writer`** receives the output string, for example to write to `stderr`.
+
+```typescript [cli.ts]
+import { createAnsiPrinter } from '@comark/ansi'
+import shiki from '@comark/ansi/plugins/shiki'
+
+const print = createAnsiPrinter({
+  plugins: [shiki()],
+  width: process.stdout.columns ?? 80,
+  writer: output => process.stderr.write(output),
+})
+
+await print('> [!WARNING]\n> This command deletes the build cache.')
+await print('| Step | Status |\n| --- | --- |\n| Build | Done |')
+```
+
+`createAnsiPrinter()` and `createAnsiRenderer()` set up the parser once. Use them when you print more than one document.
+
+## Render custom components
+
+Map a component tag to a function that returns a string. The function receives the node and a `render()` helper for its children:
+
+```typescript [cli.ts]
+import { renderAnsi } from '@comark/ansi'
+
+const output = await renderAnsi('::badge{type="success"}\nBuild passed\n::', {
+  components: {
+    badge: async ([, attrs, ...children], { render }) =>
+      `[${String(attrs.type).toUpperCase()}] ${await render(children)}`,
+  },
+})
+// [SUCCESS] Build passed
+```
+
+## Show streaming model output
+
+`@comark/ansi` renders complete strings, and it has no `streaming` option. Because `autoClose` closes unfinished syntax, you can render the partial text at any time.
+
+The following code is a pattern, not a Comark API. It re-renders the accumulated text after each chunk, and redraws the screen:
+
+```typescript [agent.ts]
+import { streamText } from 'ai'
+import { createAnsiRenderer } from '@comark/ansi'
+
+const render = createAnsiRenderer({ width: process.stdout.columns ?? 80 })
+const result = streamText({ model: 'anthropic/claude-sonnet-4.6', prompt: 'Explain git rebase.' })
+
+let text = ''
+for await (const chunk of result.textStream) {
+  text += chunk
+  // Clear the screen, move the cursor to the top, then print the new frame
+  process.stdout.write('\x1b[2J\x1b[H' + await render(text))
+}
+```
+
+A full redraw replaces the scrollback on each frame. For long answers, print the final output once when the stream ends.
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="How do I turn off colors in CI?"}
+  Set the `NO_COLOR` environment variable, or pass `colors: false`. The output then contains no escape codes.
+  :::
+
+  :::accordion-item{label="Can I render a document that I already parsed?"}
+  Yes. Pass it to `renderAnsiFromDocument()`. It renders without a parse step.
+  :::
+
+  :::accordion-item{label="Can I change how a heading or link looks?"}
+  Yes. Pass a function for the native tag, for example `h1` or `a`, in `components`. See [overriding terminal output](/rendering/ansi#overriding-terminal-output).
+  :::
+::
+
+## Next steps
+
+- [ANSI rendering reference](/rendering/ansi)
+- [Shiki plugin](/plugins/built-in/shiki)
+- [Render streaming Markdown from an LLM](/use-cases/ai-chat-streaming)
+- [Markdown to HTML for emails and RSS](/use-cases/email-and-rss)
diff --git a/docs/content/10.use-cases/9.email-and-rss.md b/docs/content/10.use-cases/9.email-and-rss.md
new file mode 100644
index 00000000..ee950303
--- /dev/null
+++ b/docs/content/10.use-cases/9.email-and-rss.md
@@ -0,0 +1,145 @@
+---
+title: 'Convert Markdown to HTML for Emails and RSS Feeds'
+description: 'Turn Markdown with components into HTML strings for emails and RSS feeds with @comark/html, including inline-styled components and CDATA output.'
+navigation:
+  title: Emails and RSS
+  icon: i-lucide-mail
+links:
+  - label: HTML rendering
+    icon: i-lucide-file-code
+    to: /rendering/html
+    color: neutral
+    variant: soft
+  - label: Component syntax
+    icon: i-lucide-component
+    to: /syntax/components
+    color: neutral
+    variant: soft
+---
+
+Use `renderHtml()` from `@comark/html` to turn Markdown into an HTML string, with no framework. Map component tags to functions that return HTML, for example a button with inline styles for an email.
+
+Emails and RSS readers receive static HTML. They don't run your Vue, React, or Svelte components. `@comark/html` renders each component with a function that you write, so you control the exact markup.
+
+## Render Markdown to HTML
+
+Install the package:
+
+```bash [Terminal]
+npm install @comark/html
+```
+
+`renderHtml()` parses and renders in one call:
+
+```typescript [newsletter.ts]
+import { renderHtml } from '@comark/html'
+
+const html = await renderHtml('# October update\n\nWe shipped **dark mode**.')
+// <h1 id="october-update">October update</h1>
+// <p>We shipped <strong>dark mode</strong>.</p>
+```
+
+When you render many messages or feed items, create the renderer once with `createHtmlRenderer()`. The returned function reuses the same parser and plugins.
+
+## Add an email button component
+
+Write the component as an async function. It receives the node as `[tag, attrs, ...children]` and a `render()` helper for the children. Escape attribute values with `escapeHtml()`:
+
+```typescript [email.ts]
+import { createHtmlRenderer } from '@comark/html'
+import { escapeHtml } from '@comark/html/utils'
+
+export const renderEmail = createHtmlRenderer({
+  components: {
+    button: async ([, attrs, ...children], { render }) =>
+      `<a href="${escapeHtml(String(attrs.href))}" style="display:inline-block;padding:12px 20px;background:#0f172a;color:#ffffff;border-radius:6px;text-decoration:none">${await render(children)}</a>`,
+  },
+})
+```
+
+The email content uses the component like any other Comark component:
+
+```mdc [welcome.md]
+Thanks for signing up.
+
+:button[Confirm your email]{href="https://example.com/confirm"}
+```
+
+`@comark/html` returns only the content HTML. Put it inside your email layout. Email clients differ in their CSS support. We recommend that you inline the styles of your layout and of the Markdown elements with a separate CSS inliner tool.
+
+## Pass per-recipient data
+
+Pass values in the `data` option. Every component function receives them in its context:
+
+```typescript [order-email.ts]
+import { renderHtml } from '@comark/html'
+import { escapeHtml } from '@comark/html/utils'
+
+const html = await renderHtml('::greeting\nYour order has shipped.\n::', {
+  data: { name: 'Ada' },
+  components: {
+    greeting: async ([, , ...children], { render, data }) =>
+      `<p>Hi ${escapeHtml(String(data?.name))},</p>${await render(children)}`,
+  },
+})
+```
+
+## Build an RSS feed
+
+Put the HTML of each item in a CDATA section, so the feed stays valid XML. A CDATA section can't contain `]]>`, so split that sequence:
+
+```typescript [rss.ts]
+import { createHtmlRenderer } from '@comark/html'
+
+const renderHtml = createHtmlRenderer()
+
+export async function renderItem(post: { title: string, url: string, markdown: string }) {
+  const html = (await renderHtml(post.markdown)).replaceAll(']]>', ']]]]><![CDATA[>')
+  return `<item>
+  <title><![CDATA[${post.title}]]>
+  ${post.url}
+  
+`
+}
+```
+
+## Make links absolute
+
+Emails and feed readers can't resolve relative URLs such as `/docs`. Override the native `a` tag and resolve each `href` against your site:
+
+```typescript [email.ts]
+import { createHtmlRenderer } from '@comark/html'
+import { escapeHtml } from '@comark/html/utils'
+
+export const renderEmail = createHtmlRenderer({
+  components: {
+    a: async ([, attrs, ...children], { render }) =>
+      `${await render(children)}`,
+  },
+})
+```
+
+Use the same approach for `img` and its `src` attribute. If the Markdown comes from users, also add the [security plugin](/plugins/built-in/security).
+
+## FAQ
+
+::accordion
+  :::accordion-item{label="Does @comark/html make the HTML work in every email client?"}
+  No. It renders the Markdown and your component functions to HTML. Email client support depends on the markup and the CSS that you write.
+  :::
+
+  :::accordion-item{label="Can I use the same Markdown on my website?"}
+  Yes. Render the same source with a framework renderer on the site, and with `@comark/html` for email. Register a component for each target.
+  :::
+
+  :::accordion-item{label="Can I render a document that I already parsed?"}
+  Yes. `renderHtmlFromDocument()` renders a stored `MarkdownDocument` without a parse step.
+  :::
+::
+
+## Next steps
+
+- [HTML rendering reference](/rendering/html)
+- [Render Markdown from a CMS or database](/use-cases/cms-runtime-content)
+- [Binding plugin](/plugins/built-in/binding)
+- [Render Markdown in the terminal](/use-cases/cli-and-agents)
diff --git a/docs/content/10.use-cases/index.md b/docs/content/10.use-cases/index.md
new file mode 100644
index 00000000..4201cdae
--- /dev/null
+++ b/docs/content/10.use-cases/index.md
@@ -0,0 +1,55 @@
+---
+title: Comark use cases
+description: 'Guides for common Comark tasks: AI chat streaming, generative UI, docs sites and blogs, CMS content, terminal output, emails, and RSS feeds.'
+navigation: false
+---
+
+Each guide answers one question with working code. Pick the task that matches your project.
+
+## AI and streaming
+
+::card-group{cols="2"}
+  :::card{icon="i-lucide-message-square-text" title="Render streaming Markdown from an LLM" to="/use-cases/ai-chat-streaming"}
+  Render model output while it streams, with no raw syntax or layout jumps.
+  :::
+
+  :::card{icon="i-simple-icons-nuxt" title="Vue and Nuxt chat with the AI SDK" to="/use-cases/vue-ai-chat"}
+  Stream AI SDK messages into Comark's Vue `` component.
+  :::
+
+  :::card{icon="i-simple-icons-svelte" title="Svelte chat with the AI SDK" to="/use-cases/svelte-ai-chat"}
+  Stream AI SDK messages in Svelte 5 and SvelteKit.
+  :::
+
+  :::card{icon="i-simple-icons-angular" title="Angular chat with the AI SDK" to="/use-cases/angular-ai-chat"}
+  Stream AI SDK messages in Angular 17 or later.
+  :::
+
+  :::card{icon="i-lucide-sparkles" title="Generative UI" to="/use-cases/generative-ui"}
+  Let a model render your UI components inside Markdown, with no code execution.
+  :::
+::
+
+## Content
+
+::card-group{cols="2"}
+  :::card{icon="i-lucide-book-open" title="Docs sites and blogs" to="/use-cases/docs-and-blogs"}
+  Parse Markdown files with frontmatter, a table of contents, and highlighted code.
+  :::
+
+  :::card{icon="i-lucide-database" title="CMS and runtime content" to="/use-cases/cms-runtime-content"}
+  Store parsed documents as JSON and render them on any client.
+  :::
+::
+
+## Other outputs
+
+::card-group{cols="2"}
+  :::card{icon="i-lucide-terminal" title="CLIs and coding agents" to="/use-cases/cli-and-agents"}
+  Print styled Markdown in the terminal with `@comark/ansi`.
+  :::
+
+  :::card{icon="i-lucide-mail" title="Emails and RSS feeds" to="/use-cases/email-and-rss"}
+  Render Markdown to HTML strings with `@comark/html`.
+  :::
+::

From dec38206a6529c393cd4192f3db050535875caff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= 
Date: Wed, 7 Oct 2026 15:06:38 +0200
Subject: [PATCH 07/19] docs(compare): add Markstream, marked, markdown-it,
 remark, markdown-to-jsx, svelte-markdown, vue-markdown-render pages and an AI
 chat roundup

Also correct two existing claims: raw HTML renders by default (react-markdown page), and
an unregistered component renders as a plain element; Streamdown highlighting is a plugin.
---
 .../0.markdown-libraries-for-ai-chat.md       | 140 ++++++++++++++++
 .../6.compare/10.comark-vs-svelte-markdown.md | 153 +++++++++++++++++
 .../11.comark-vs-vue-markdown-render.md       | 156 ++++++++++++++++++
 .../6.compare/2.comark-vs-react-markdown.md   |   2 +-
 .../6.compare/3.comark-vs-streamdown.md       |   4 +-
 .../6.compare/5.comark-vs-markstream.md       | 152 +++++++++++++++++
 docs/content/6.compare/6.comark-vs-marked.md  | 126 ++++++++++++++
 .../6.compare/7.comark-vs-markdown-it.md      | 133 +++++++++++++++
 docs/content/6.compare/8.comark-vs-remark.md  | 145 ++++++++++++++++
 .../6.compare/9.comark-vs-markdown-to-jsx.md  | 143 ++++++++++++++++
 10 files changed, 1151 insertions(+), 3 deletions(-)
 create mode 100644 docs/content/6.compare/0.markdown-libraries-for-ai-chat.md
 create mode 100644 docs/content/6.compare/10.comark-vs-svelte-markdown.md
 create mode 100644 docs/content/6.compare/11.comark-vs-vue-markdown-render.md
 create mode 100644 docs/content/6.compare/5.comark-vs-markstream.md
 create mode 100644 docs/content/6.compare/6.comark-vs-marked.md
 create mode 100644 docs/content/6.compare/7.comark-vs-markdown-it.md
 create mode 100644 docs/content/6.compare/8.comark-vs-remark.md
 create mode 100644 docs/content/6.compare/9.comark-vs-markdown-to-jsx.md

diff --git a/docs/content/6.compare/0.markdown-libraries-for-ai-chat.md b/docs/content/6.compare/0.markdown-libraries-for-ai-chat.md
new file mode 100644
index 00000000..7ef81992
--- /dev/null
+++ b/docs/content/6.compare/0.markdown-libraries-for-ai-chat.md
@@ -0,0 +1,140 @@
+---
+title: Choosing a Markdown Library for AI Chat in 2026
+description: Compare Streamdown, Markstream, Comark, @humanspeak/svelte-markdown, markdown-to-jsx, and plain parsers for rendering streamed LLM output. See how each handles incomplete Markdown, components, security, and frameworks.
+navigation:
+  title: 'Markdown for AI chat'
+seo:
+  title: 'Choosing a Markdown Library for AI Chat in 2026: Streaming Renderers Compared'
+links:
+  - label: AI chat streaming
+    icon: i-lucide-message-square
+    to: /use-cases/ai-chat-streaming
+    color: neutral
+    variant: soft
+  - label: Streaming playground
+    icon: i-lucide-radio
+    to: /stream
+    color: neutral
+    variant: soft
+---
+
+**TL;DR**: For a React-only chat where the model writes standard Markdown, start with Streamdown. If the model must place components, or you ship Vue, Svelte, Angular, or a terminal UI, use Comark. For Vue 2 or very long documents that need virtualization, look at Markstream. For a Svelte-only app built on marked extensions, use @humanspeak/svelte-markdown.
+
+This guide is part of Comark's documentation. Each claim about another library links to that library's repository or documentation, so you can check it.
+
+## What an AI chat renderer needs
+
+A model sends its answer a few characters at a time. A renderer built for complete documents re-parses the whole text on each update. Between updates, it shows broken output. Five questions separate the options:
+
+- **Incomplete syntax**: what does the reader see while `**bold`, a code fence, a link, or a table is still open? The renderer can complete the syntax, hide it, or show it raw.
+- **Re-render cost**: does each new token re-parse and re-render the whole message, or only the last block? This matters for long answers.
+- **Components in the output**: can the model place a chart, a product card, or a tool result? Can that component take props and nested content?
+- **Security of model output**: model output is untrusted input. Check what happens to raw `
+
+Click Me'} {renderers} />
+```
+
+Comark uses a [component syntax](/syntax/components) with typed props and named slots. Each `#slot` becomes a Svelte 5 snippet prop:
+
+```mdc [content.md]
+::card{title="Order summary" :items="3"}
+Your order ships tomorrow.
+
+#footer
+Questions? Reply to this message.
+::
+```
+
+```svelte [Card.svelte]
+
+
+
+

{title} ({items} items)

+ {@render children?.()} +
{@render footer?.()}
+
+``` + +Register it with ``. For lazy loading, pass a `componentsManifest` function instead. See [component bindings](/rendering/svelte#component-bindings). + +## Streaming + +`@humanspeak/svelte-markdown` has a detailed streaming engine. You bind the component and call `writeChunk()`, or append to `source`. It re-lexes only the open block at the end of the text and coalesces updates once per animation frame. Offset chunks can arrive out of order, and `streamId` resets the buffer between messages. Its documentation doesn't describe completing unterminated inline syntax; half-open tags wait in a buffer. Streaming turns off when you use an async extension such as Mermaid. + +Comark completes the syntax with [`autoClose`](/reference/auto-close). `**bold` renders as bold, and an open `::card` renders as a card while its content streams. You pass the growing text to `value`: + +```svelte [Message.svelte] + + + +``` + +While `streaming` is `true`, `` reuses completed blocks. Set it to `false` when the stream ends to run a final full parse. With the AI SDK, use `part.state === 'streaming'` for the prop. See [streaming in Svelte](/rendering/svelte#streaming). + +## SvelteKit SSR + +Comark's `` uses `$effect`, so it doesn't render during SSR. For SvelteKit pages, parse in `load()` and render the JSON-safe document with ``: + +```svelte [src/routes/docs/[slug]/+page.svelte] + + + +``` + +`` ships no parser to the browser. To render lazy-loaded components in server HTML, use `` from `@comark/svelte/async`. It requires Svelte's `experimental.async` option. + +## What @humanspeak/svelte-markdown does well + +- **Svelte 5-native API**: snippet overrides let you change a renderer inline, without a separate component file. +- **Streaming engineering**: open-block re-lexing, per-frame coalescing, and out-of-order offset chunks are well documented. +- **Secure defaults**: a URL allowlist, `on*` and `srcdoc` stripping, and escaped `script` and `style` work without setup. Its documentation lists gaps, for example `iframe`, `form`, and `embed` still render. Comark keeps raw ` + + +``` + +Because the output is an HTML string, Vue can't mount components inside it. It inherits markdown-it's safe defaults: raw HTML is off, and `validateLink` rejects `javascript:` and similar URLs. If you set `html: true`, markdown-it recommends an external sanitizer. + +**@crazydos/vue-markdown** renders VNodes from a unified pipeline. You replace an element with a scoped slot, and you render its `children` with ``: + +```vue [@crazydos/vue-markdown] + +``` + +**Comark** parses to a document and renders each node as a VNode. The `components` prop maps a tag to a Vue component: + +```vue [Comark] + + + +``` + +Comark looks up `ProseH2`, then `H2`, then `h2`, then globally registered components. In Nuxt, the `@comark/nuxt` module registers the components in `~/components/prose` for you. + +## Components in the content + +`@crazydos/vue-markdown` supports a `` tag in the Markdown, filled from the parent with `