Repository: scalar/scalar
Pull request #5591: feat(openapi-to-markdown): Markdown API Reference
Author: login: hanspagel
Created: 2025-05-06T15:23:12Z | Merged: 2025-05-13T11:23:06Z

**Problem**

People use AI today, but LLMs prefer Markdown over beautiful HTML.

**Solution**

This PR adds the new `@scalar/openapi-to-markdown` which generates plain Markdown from OpenAPI files.

The goal is to have an easy tool to add `/llms.txt` to your backend.

**Tasks**

Pretty early, but it should be pretty digestible for LLMs already. 

**Preview**

Comes with a Hono playground that serves a plain HTML page on `/` and a `/llm.txt` with Markdown.

<img width="1317" alt="Screenshot 2025-05-06 at 17 23 05" src="https://github.com/user-attachments/assets/922ae60f-c6ff-4119-840a-48a484c16d17" />

**Checklist**

I’ve gone through the following:

- [x] I’ve added an explanation _why_ this change is needed.
- [x] I’ve added a changeset (`pnpm changeset`).
- [x] I’ve added tests for the regression or new feature.
- [x] I’ve updated the documentation.

<!-- See the [contributing guide](https://github.com/scalar/scalar/blob/main/CONTRIBUTING.md) for more information. -->



Diff:
diff --git a/.changeset/fresh-penguins-try.md b/.changeset/fresh-penguins-try.md
new file mode 100644
index 00000000000..1d8d40f29d5
--- /dev/null
+++ b/.changeset/fresh-penguins-try.md
@@ -0,0 +1,5 @@
+---
+'@scalar/hono-api-reference': patch
+---
+
+docs: add an example for /llms.txt
diff --git a/.changeset/neat-years-give.md b/.changeset/neat-years-give.md
new file mode 100644
index 00000000000..5b466f16226
--- /dev/null
+++ b/.changeset/neat-years-give.md
@@ -0,0 +1,5 @@
+---
+'@scalar/openapi-to-markdown': minor
+---
+
+init :)
diff --git a/integrations/hono/README.md b/integrations/hono/README.md
index 6a99b7cd373..37707a0b05e 100644
--- a/integrations/hono/README.md
+++ b/integrations/hono/README.md
@@ -63,7 +63,7 @@ app.get('/scalar', Scalar({
 
 ### Custom CDN
 
-You can use a custom CDN ，default is `https://cdn.jsdelivr.net/npm/@scalar/api-reference`.
+You can use a custom CDN, default is `https://cdn.jsdelivr.net/npm/@scalar/api-reference`.
 
 You can also pin the CDN to a specific version by specifying it in the CDN string like `https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.28`
 
@@ -80,6 +80,54 @@ app.get('/scalar', Scalar({
 }))
 ```
 
+### Markdown for LLMs
+
+If you want to create a Markdown version of the API reference (for LLMs), install `@scalar/openapi-to-markdown`:
+
+```bash
+npm install @scalar/openapi-to-markdown
+```
+
+And add an additional route for it:
+
+```ts
+import { Hono } from 'hono'
+import { createMarkdownFromOpenApi } from '@scalar/openapi-to-markdown'
+
+const app = new Hono()
+
+// Generate Markdown from your OpenAPI document
+const markdown = await createMarkdownFromOpenApi(content)
+
+/**
+ * Register a route to serve the Markdown for LLMs
+ *
+ * Q: Why /llms.txt?
+ * A: It's a proposal to standardise on using an /llms.txt file.
+ *
+ * @see https://llmstxt.org/
+ */
+app.get('/llms.txt', (c) => c.text(markdown))
+
+export default app
+```
+
+Or, if you are using Zod OpenAPI Hono:
+
+```ts
+// Get the OpenAPI document
+const content = app.getOpenAPI31Document({
+  openapi: '3.1.0',
+  info: { title: 'Example', version: 'v1' },
+})
+
+const markdown = await createMarkdownFromOpenApi(JSON.stringify(content))
+
+app.get('/llms.txt', async (c) => {
+  return c.text(markdown)
+})
+```
+
 ## Community
 
 We are API nerds. You too? Let’s chat on Discord: <https://discord.gg/scalar>
diff --git a/integrations/hono/package.json b/integrations/hono/package.json
index 3b516bb4b93..3c54b5653a3 100644
--- a/integrations/hono/package.json
+++ b/integrations/hono/package.json
@@ -48,6 +48,7 @@
     "@hono/node-server": "^1.11.0",
     "@hono/zod-openapi": "^0.8.6",
     "@scalar/build-tooling": "workspace:*",
+    "@scalar/openapi-to-markdown": "workspace:*",
     "hono": "^4.6.5",
     "vite": "catalog:*",
     "vitest": "catalog:*"
diff --git a/integrations/hono/playground/index.ts b/integrations/hono/playground/index.ts
index 82b70e62da8..b8c37b22fd2 100644
--- a/integrations/hono/playground/index.ts
+++ b/integrations/hono/playground/index.ts
@@ -1,5 +1,6 @@
 import { serve } from '@hono/node-server'
 import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
+import { createMarkdownFromOpenApi } from '@scalar/openapi-to-markdown'
 import { cors } from 'hono/cors'
 import { Scalar } from '../src/index'
 
@@ -195,6 +196,18 @@ app.get(
   }),
 )
 
+// Markdown for LLMs
+const content = app.getOpenAPI31Document({
+  openapi: '3.1.0',
+  info: { title: 'Example', version: 'v1' },
+})
+
+const markdown = await createMarkdownFromOpenApi(JSON.stringify(content))
+
+app.get('/llms.txt', async (c) => {
+  return c.text(markdown)
+})
+
 // Listen
 serve(
   {
diff --git a/packages/openapi-to-markdown/README.md b/packages/openapi-to-markdown/README.md
new file mode 100644
index 00000000000..591c880b90f
--- /dev/null
+++ b/packages/openapi-to-markdown/README.md
@@ -0,0 +1,106 @@
+# Scalar OpenAPI to Markdown
+
+[![Version](https://img.shields.io/npm/v/%40scalar/openapi-to-markdown)](https://www.npmjs.com/package/@scalar/openapi-to-markdown)
+[![Downloads](https://img.shields.io/npm/dm/%40scalar/openapi-to-markdown)](https://www.npmjs.com/package/@scalar/openapi-to-markdown)
+[![License](https://img.shields.io/npm/l/%40scalar%2Fopenapi-to-markdown)](https://www.npmjs.com/package/@scalar/openapi-to-markdown)
+[![Discord](https://img.shields.io/discord/1135330207960678410?style=flat&color=5865F2)](https://discord.gg/scalar)
+
+A Node.js package to generate LLM-friendly Markdown from OpenAPI documents.
+
+## Installation
+
+```bash
+npm install @scalar/openapi-to-markdown
+```
+
+## Usage
+
+```ts
+import { createMarkdownFromOpenApi } from '@scalar/openapi-to-markdown'
+
+const content = {
+  openapi: '3.1.1',
+  info: {
+    title: 'My API',
+    version: '1.0',
+  },
+  paths: {
+    // …
+  },
+}
+
+// Generate Markdown from an OpenAPI document
+const markdown = await createMarkdownFromOpenApi(content)
+```
+
+### With Hono
+
+You use the package with any Node.js framework. Here is an example for [Hono](https://hono.dev/):
+
+```ts
+import { Hono } from 'hono'
+import { createMarkdownFromOpenApi } from '@scalar/openapi-to-markdown'
+
+// Generate Markdown from an OpenAPI document
+const markdown = await createMarkdownFromOpenApi(content)
+
+const app = new Hono()
+
+/**
+ * Register a route to serve the Markdown for LLMs
+ *
+ * Q: Why /llms.txt?
+ * A: It's a proposal to standardise on using an /llms.txt file.
+ *
+ * @see https://llmstxt.org/
+ */
+app.get('/llms.txt', (c) => c.text(markdown))
+
+serve(app)
+```
+
+### Generate HTML
+
+This is not really the purpose of the package, but maybe good to know: This package actually renders HTML at first, and
+transforms the HTML to Markdown then.
+
+So if you’d like to have a really light-weight HTML API Reference, here you are:
+
+```ts
+import { Hono } from 'hono'
+import { createHtmlFromOpenApi } from '@scalar/openapi-to-markdown'
+
+// Generate HTML from an OpenAPI document
+const html = await createHtmlFromOpenApi(content)
+
+const app = new Hono()
+
+app.get('/', (c) =>
+  c.html(
+    `<!doctype html>
+<html lang="en" data-theme="light">
+<head>
+  <meta charset="UTF-8" />
+  <title>Scalar Galaxy</title>
+  <!-- Basic styling for semantic HTML tags (optional) -->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
+</head>
+<body>
+  <main class="container">
+    ${html}
+  </main>
+</body>
+</html>`,
+  ),
+)
+
+serve(app)
+```
+
+## Community
+
+We are API nerds. You too? Let’s chat on Discord: <https://discord.gg/scalar>
+
+## License
+
+The source code in this repository is licensed under [MIT](https://github.com/scalar/scalar/blob/main/LICENSE).
diff --git a/packages/openapi-to-markdown/package.json b/packages/openapi-to-markdown/package.json
new file mode 100644
index 00000000000..f6b1ed29e48
--- /dev/null
+++ b/packages/openapi-to-markdown/package.json
@@ -0,0 +1,85 @@
+{
+  "name": "@scalar/openapi-to-markdown",
+  "description": "Create plain Markdown from OpenAPI documents (for LLMs)",
+  "license": "MIT",
+  "author": "Scalar (https://github.com/scalar)",
+  "homepage": "https://github.com/scalar/scalar",
+  "bugs": "https://github.com/scalar/scalar/issues/new/choose",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/scalar/scalar.git",
+    "directory": "packages/openapi-to-markdown"
+  },
+  "keywords": [
+    "openapi",
+    "markdown",
+    "llm",
+    "swagger"
+  ],
+  "version": "0.0.0",
+  "engines": {
+    "node": ">=18"
+  },
+  "scripts": {
+    "build": "scalar-build-vite",
+    "dev": "nodemon --exec \"vite-node playground/index.ts\" --ext ts,vue --quiet --watch ./",
+    "format": "scalar-format",
+    "format:check": "scalar-format-check",
+    "test": "vitest",
+    "types:build": "scalar-types-build-vue",
+    "types:check": "scalar-types-check-vue"
+  },
+  "type": "module",
+  "main": "dist/index.js",
+  "exports": {
+    ".": {
+      "import": "./dist/index.js",
+      "types": "./dist/index.d.ts",
+      "default": "./dist/index.js"
+    },
+    "./*.css": {
+      "import": "./dist/*.css",
+      "require": "./dist/*.css",
+      "default": "./dist/*.css"
+    },
+    "./css/*.css": {
+      "import": "./dist/css/*.css",
+      "require": "./dist/css/*.css",
+      "default": "./dist/css/*.css"
+    }
+  },
+  "files": [
+    "dist",
+    "CHANGELOG.md"
+  ],
+  "module": "dist/index.js",
+  "dependencies": {
+    "@scalar/components": "workspace:*",
+    "@scalar/oas-utils": "workspace:*",
+    "@scalar/openapi-parser": "workspace:*",
+    "@scalar/openapi-types": "workspace:*",
+    "@scalar/snippetz": "workspace:*",
+    "@scalar/types": "workspace:*",
+    "html-minifier-terser": "^7.2.0",
+    "object-to-xml": "^2.0.0",
+    "rehype-parse": "^9.0.0",
+    "rehype-remark": "^10.0.1",
+    "rehype-sanitize": "^6.0.0",
+    "rehype-stringify": "^10.0.0",
+    "remark-gfm": "^4.0.0",
+    "remark-stringify": "^11.0.0",
+    "unified": "^11.0.4",
+    "vue": "catalog:*"
+  },
+  "devDependencies": {
+    "@hono/node-server": "^1.11.0",
+    "@scalar/build-tooling": "workspace:*",
+    "@scalar/galaxy": "^0.3.2",
+    "@types/html-minifier-terser": "^7.0.2",
+    "@vitejs/plugin-vue": "catalog:*",
+    "@vue/test-utils": "^2.4.1",
+    "hono": "^4.6.5",
+    "vite": "catalog:*",
+    "vue": "catalog:*"
+  }
+}
diff --git a/packages/openapi-to-markdown/playground/index.ts b/packages/openapi-to-markdown/playground/index.ts
new file mode 100644
index 00000000000..a1298e9f1f4
--- /dev/null
+++ b/packages/openapi-to-markdown/playground/index.ts
@@ -0,0 +1,44 @@
+import { createHtmlFromOpenApi, createMarkdownFromOpenApi } from '@/create-markdown-from-openapi'
+import { serve } from '@hono/node-server'
+import content from '@scalar/galaxy/latest.yaml?raw'
+import { Hono } from 'hono'
+import { logger } from 'hono/logger'
+
+const html = await createHtmlFromOpenApi(content)
+
+const markdown = await createMarkdownFromOpenApi(content)
+
+const app = new Hono()
+app.use(logger())
+
+app.get('/', (c) =>
+  c.html(
+    `<!doctype html>
+<html lang="en" data-theme="light">
+<head>
+  <meta charset="UTF-8" />
+  <title>Scalar Galaxy</title>
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
+</head>
+<body>
+  <main class="container">
+    ${html}
+  </main>
+</body>
+</html>`,
+  ),
+)
+
+app.get('/llms.txt', (c) => c.text(markdown))
+
+const server = serve(app, (info) => {
+  console.log()
+  console.log(`🚧 Server listening on http://localhost:${info.port}`)
+  console.log()
+})
+
+if (import.meta.hot) {
+  import.meta.hot.on('vite:beforeFullReload', () => {
+    server.close()
+  })
+}
diff --git a/packages/openapi-to-markdown/src/components/MarkdownReference.test.ts b/packages/openapi-to-markdown/src/components/MarkdownReference.test.ts
new file mode 100644
index 00000000000..c732d50b447
--- /dev/null
+++ b/packages/openapi-to-markdown/src/components/MarkdownReference.test.ts
@@ -0,0 +1,196 @@
+// @vitest-environment jsdom
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+import { mount } from '@vue/test-utils'
+
+import { describe, expect, it, vi } from 'vitest'
+import MarkdownReference from './MarkdownReference.vue'
+
+// Mock the ScalarMarkdown component
+vi.mock('@scalar/components', () => ({
+  ScalarMarkdown: {
+    name: 'ScalarMarkdown',
+    props: ['value'],
+    template: '<div class="scalar-markdown">{{ value }}</div>',
+  },
+}))
+
+describe('MarkdownReference', () => {
+  it('renders basic API information', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: {
+        title: 'Test API',
+        version: '1.0.0',
+        description: 'Test description',
+      },
+      paths: {},
+    }
+
+    const wrapper = mount(MarkdownReference, {
+      props: { content },
+    })
+
+    expect(wrapper.text()).toContain('Test API')
+    expect(wrapper.text()).toContain('OpenAPI Version:')
+    expect(wrapper.text()).toContain('3.1.1')
+    expect(wrapper.text()).toContain('API Version:')
+    expect(wrapper.text()).toContain('1.0.0')
+    expect(wrapper.text()).toContain('Test description')
+  })
+
+  it('renders servers section with variables', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: { title: 'Test API', version: '1.0.0' },
+      servers: [
+        {
+          url: 'https://test.com',
+          description: 'Test server',
+        },
+        {
+          url: 'https://test.com/{version}',
+          description: 'Test server v2',
+          variables: {
+            version: {
+              default: 'v2',
+              description: 'Test version',
+            },
+          },
+        },
+      ],
+      paths: {},
+    }
+
+    const wrapper = mount(MarkdownReference, { props: { content } })
+    expect(wrapper.text()).toContain('Servers')
+    expect(wrapper.text()).toContain('https://test.com')
+    expect(wrapper.text()).toContain('Test server')
+    expect(wrapper.text()).toContain('https://test.com/{version}')
+    expect(wrapper.text()).toContain('Test server v2')
+    expect(wrapper.text()).toContain('version')
+    expect(wrapper.text()).toContain('v2')
+    expect(wrapper.text()).toContain('Test version')
+  })
+
+  it('renders operations section with summary, tags, stability, and request/response', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/users': {
+          get: {
+            summary: 'Get users',
+            description: 'Get all users',
+            tags: ['users'],
+            'x-scalar-stability': 'stable',
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: { filter: { type: 'string' } },
+                  },
+                },
+              },
+            },
+            responses: {
+              '200': {
+                description: 'Success',
+                content: {
+                  'application/json': {
+                    schema: {
+                      type: 'array',
+                      items: { type: 'string' },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    }
+    const wrapper = mount(MarkdownReference, { props: { content } })
+    expect(wrapper.text()).toContain('Operations')
+    expect(wrapper.text()).toContain('Get users')
+    expect(wrapper.text()).toContain('GET')
+    expect(wrapper.text()).toContain('/users')
+    expect(wrapper.text()).toContain('users')
+    expect(wrapper.text()).toContain('stable')
+    expect(wrapper.text()).toContain('Get all users')
+    expect(wrapper.text()).toContain('Request Body')
+    expect(wrapper.text()).toContain('filter')
+    expect(wrapper.text()).toContain('Responses')
+    expect(wrapper.text()).toContain('200')
+    expect(wrapper.text()).toContain('Array of:')
+    expect(wrapper.text()).toContain('string')
+  })
+
+  it('renders deprecated operation', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/deprecated': {
+          post: {
+            deprecated: true,
+            description: 'Deprecated operation',
+          },
+        },
+      },
+    }
+    const wrapper = mount(MarkdownReference, { props: { content } })
+    expect(wrapper.text()).toContain('Deprecated')
+    expect(wrapper.text()).toContain('Deprecated operation')
+  })
+
+  it('renders webhooks section', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: { title: 'Test API', version: '1.0.0' },
+      webhooks: {
+        newUser: {
+          post: {
+            summary: 'New user webhook',
+            description: 'Triggered when a new user is created',
+            tags: ['webhook'],
+          },
+        },
+      },
+      paths: {},
+    }
+    const wrapper = mount(MarkdownReference, { props: { content } })
+    expect(wrapper.text()).toContain('Webhooks')
+    expect(wrapper.text()).toContain('New user webhook')
+    expect(wrapper.text()).toContain('newUser')
+    expect(wrapper.text()).toContain('webhook')
+    expect(wrapper.text()).toContain('Triggered when a new user is created')
+  })
+
+  it('renders schemas section', () => {
+    const content: OpenAPIV3_1.Document = {
+      openapi: '3.1.1',
+      info: { title: 'Test API', version: '1.0.0' },
+      components: {
+        schemas: {
+          User: {
+            type: 'object',
+            title: 'User',
+            description: 'A user object',
+            properties: {
+              id: { type: 'string' },
+              name: { type: 'string' },
+            },
+          },
+        },
+      },
+      paths: {},
+    }
+    const wrapper = mount(MarkdownReference, { props: { content } })
+    expect(wrapper.text()).toContain('Schemas')
+    expect(wrapper.text()).toContain('User')
+    expect(wrapper.text()).toContain('A user object')
+    expect(wrapper.text()).toContain('id')
+    expect(wrapper.text()).toContain('name')
+  })
+})
diff --git a/packages/openapi-to-markdown/src/components/MarkdownReference.vue b/packages/openapi-to-markdown/src/components/MarkdownReference.vue
new file mode 100644
index 00000000000..35b045b3555
--- /dev/null
+++ b/packages/openapi-to-markdown/src/components/MarkdownReference.vue
@@ -0,0 +1,313 @@
+<script setup lang="ts">
+import { ScalarMarkdown } from '@scalar/components'
+import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters'
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+
+// import { snippetz, type HarRequest } from '@scalar/snippetz'
+
+import Schema from './Schema.vue'
+import XmlOrJson from './XmlOrJson.vue'
+
+const { content } = defineProps<{
+  content: OpenAPIV3_1.Document
+}>()
+
+// const getRequestExample = (harRequest: Partial<HarRequest>) => {
+//   const snippet = snippetz().print('shell', 'curl', {
+//     httpVersion: 'HTTP/1.1',
+//     headers: [],
+//     queryString: [],
+//     cookies: [],
+//     headersSize: -1,
+//     bodySize: -1,
+//     method: 'get',
+//     ...harRequest,
+//   })
+
+//   return snippet
+// }
+</script>
+
+<template>
+  <section>
+    <header>
+      <h1>{{ content?.info?.title }}</h1>
+      <ul>
+        <li>
+          <strong>OpenAPI Version:</strong>&nbsp;<code>{{
+            content?.openapi
+          }}</code>
+        </li>
+        <li>
+          <strong>API Version:</strong>&nbsp;<code>{{
+            content?.info?.version
+          }}</code>
+        </li>
+      </ul>
+    </header>
+
+    <ScalarMarkdown
+      :value="content?.info?.description"
+      v-if="content?.info?.description" />
+
+    <section v-if="content?.servers?.length">
+      <h2>Servers</h2>
+      <ul>
+        <template
+          v-for="server in content.servers"
+          :key="server.url">
+          <li>
+            <strong>URL:</strong>&nbsp;<code>{{ server.url }}</code>
+            <ul>
+              <template v-if="server.description">
+                <li>
+                  <strong>Description:</strong>&nbsp;{{ server.description }}
+                </li>
+              </template>
+              <template
+                v-if="server.variables && Object.keys(server.variables).length">
+                <li>
+                  <strong>Variables:</strong>
+                  <ul>
+                    <template
+                      v-for="(variable, name) in server.variables"
+                      :key="name">
+                      <li>
+                        <code>{{ name }}</code> (default:
+                        <code>{{ variable.default }}</code
+                        >)<template v-if="variable.description"
+                          >: {{ variable.description }}
+                        </template>
+                      </li>
+                    </template>
+                  </ul>
+                </li>
+              </template>
+            </ul>
+          </li>
+        </template>
+      </ul>
+    </section>
+
+    <section v-if="Object.keys(content?.paths ?? {}).length">
+      <h2>Operations</h2>
+
+      <template
+        v-for="path in Object.keys(content?.paths ?? {})"
+        :key="path">
+        <template
+          v-for="(operation, method) in content?.paths?.[path]"
+          :key="operation">
+          <section>
+            <header>
+              <h3>
+                <template v-if="operation.summary">
+                  {{ operation.summary }}
+                </template>
+                <template v-else>
+                  {{ method.toString().toUpperCase() }} {{ path }}
+                </template>
+                <template v-if="operation['x-scalar-stability']">
+                  ({{ operation['x-scalar-stability'] }})
+                </template>
+                <template v-else-if="operation.deprecated">
+                  ⚠️ Deprecated
+                </template>
+              </h3>
+            </header>
+
+            <ul>
+              <li>
+                <strong>Method:</strong>&nbsp;<code>{{
+                  method.toString().toUpperCase()
+                }}</code>
+              </li>
+              <li>
+                <strong>Path:</strong>&nbsp;<code>{{ path }}</code>
+              </li>
+              <template v-if="operation.tags">
+                <li>
+                  <strong>Tags:</strong>&nbsp;{{ operation.tags.join(', ') }}
+                </li>
+              </template>
+              <template v-if="operation['x-scalar-stability']">
+                <li>
+                  <strong>Stability:</strong>&nbsp;{{
+                    operation['x-scalar-stability']
+                  }}
+                </li>
+              </template>
+            </ul>
+
+            <ScalarMarkdown :value="operation.description" />
+
+            <!-- TODO: We need way more context to generate proper request examples -->
+            <!-- <section>
+              <h4>Request Example</h4>
+              <pre><code>{{ getRequestExample({
+                method: method.toString(),
+                url: content.servers?.[0]?.url + path,
+              }) }}</code></pre>
+            </section> -->
+
+            <template v-if="operation.requestBody?.content">
+              <section>
+                <h4>Request Body</h4>
+                <template
+                  v-for="(content, mediaType) in operation.requestBody.content"
+                  :key="mediaType">
+                  <h5>Content-Type: {{ mediaType }}</h5>
+                  <template v-if="content.schema">
+                    <Schema :schema="content.schema" />
+                    <p><strong>Example:</strong></p>
+                    <XmlOrJson
+                      :xml="mediaType?.toString().includes('xml')"
+                      :model-value="
+                        getExampleFromSchema(content.schema, {
+                          xml: mediaType?.toString().includes('xml'),
+                        })
+                      " />
+                  </template>
+                </template>
+              </section>
+            </template>
+
+            <template v-if="operation.responses">
+              <section>
+                <h4>Responses</h4>
+
+                <template
+                  v-for="(response, statusCode) in operation.responses"
+                  :key="statusCode">
+                  <section>
+                    <header>
+                      <h5>
+                        Status: {{ statusCode }}
+                        <template v-if="response.description">
+                          {{ response.description }}
+                        </template>
+                      </h5>
+                    </header>
+                    <template
+                      v-for="(content, mediaType) in response.content"
+                      :key="mediaType">
+                      <section>
+                        <h6>Content-Type: {{ mediaType }}</h6>
+                        <template v-if="content.schema">
+                          <Schema :schema="content.schema" />
+                          <p><strong>Example:</strong></p>
+                          <XmlOrJson
+                            :xml="mediaType?.toString().includes('xml')"
+                            :model-value="
+                              getExampleFromSchema(content.schema, {
+                                xml: mediaType?.toString().includes('xml'),
+                              })
+                            " />
+                        </template>
+                      </section>
+                    </template>
+                  </section>
+                </template>
+              </section>
+            </template>
+          </section>
+        </template>
+      </template>
+    </section>
+
+    <section v-if="Object.keys(content?.webhooks ?? {}).length">
+      <h2>Webhooks</h2>
+
+      <template
+        v-for="(webhook, name) in content?.webhooks"
+        :key="name">
+        <template
+          v-for="(operation, method) in webhook"
+          :key="operation">
+          <section>
+            <header>
+              <h3>
+                <template v-if="operation.summary">
+                  {{ operation.summary }}
+                </template>
+                <template v-else>
+                  {{ name }}
+                </template>
+                <template v-if="operation['x-scalar-stability']">
+                  <span>({{ operation['x-scalar-stability'] }})</span>
+                </template>
+                <template v-else-if="operation.deprecated">
+                  <span>⚠️ Deprecated</span>
+                </template>
+              </h3>
+            </header>
+
+            <ul>
+              <li>
+                <strong>Method:</strong>
+                <code>{{ method.toString().toUpperCase() }}</code>
+              </li>
+              <li>
+                <strong>Path:</strong>
+                <code>/webhooks/{{ name }}</code>
+              </li>
+              <template v-if="operation.tags">
+                <li>
+                  <strong>Tags:</strong>
+                  {{ operation.tags.join(', ') }}
+                </li>
+              </template>
+              <template v-if="operation.deprecated">
+                <li><strong>Deprecated</strong></li>
+              </template>
+            </ul>
+
+            <ScalarMarkdown :value="operation.description" />
+
+            <!-- TODO: We need way more context to generate proper request examples -->
+            <!-- <section>
+              <h4>Request Example</h4>
+              <pre><code>{{ getRequestExample({
+                method: method.toString(),
+                url: content.servers?.[0]?.url + '/webhooks/' + name,
+              }) }}</code></pre>
+            </section> -->
+          </section>
+        </template>
+      </template>
+    </section>
+
+    <section
+      v-if="
+        content?.components?.schemas &&
+        Object.keys(content.components.schemas).length
+      ">
+      <h2>Schemas</h2>
+      <template
+        v-for="(schema, name) in content.components.schemas"
+        :key="name">
+        <section>
+          <header>
+            <h3>{{ schema.title ?? name }}</h3>
+          </header>
+          <ul>
+            <li>
+              <strong>Type:</strong>
+              <code>{{ schema.type }}</code>
+            </li>
+          </ul>
+          <template v-if="schema.description">
+            <ScalarMarkdown :value="schema.description" />
+          </template>
+          <Schema
+            v-if="schema.type === 'object'"
+            :schema="schema" />
+          <p><strong>Example:</strong></p>
+          <template v-if="schema.type === 'object'">
+            <XmlOrJson :model-value="getExampleFromSchema(schema)" />
+          </template>
+        </section>
+      </template>
+    </section>
+  </section>
+</template>
diff --git a/packages/openapi-to-markdown/src/components/Schema.test.ts b/packages/openapi-to-markdown/src/components/Schema.test.ts
new file mode 100644
index 00000000000..adfdbd64942
--- /dev/null
+++ b/packages/openapi-to-markdown/src/components/Schema.test.ts
@@ -0,0 +1,178 @@
+// @vitest-environment jsdom
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import Schema from './Schema.vue'
+
+describe('Schema', () => {
+  it('renders composition keywords (allOf)', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      allOf: [
+        { type: 'object', properties: { name: { type: 'string' } } },
+        { type: 'object', properties: { age: { type: 'number' } } },
+      ],
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('All of:')
+    expect(wrapper.text()).toContain('name')
+    expect(wrapper.text()).toContain('age')
+  })
+
+  it('renders composition keywords (anyOf)', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      anyOf: [{ type: 'string' }, { type: 'number' }],
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('Any of:')
+    expect(wrapper.text()).toContain('string')
+    expect(wrapper.text()).toContain('number')
+  })
+
+  it('renders composition keywords (oneOf)', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      oneOf: [{ type: 'boolean' }, { type: 'integer' }],
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('One of:')
+    expect(wrapper.text()).toContain('boolean')
+    expect(wrapper.text()).toContain('integer')
+  })
+
+  it('renders composition keywords (not)', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      not: { type: 'string' },
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('Not:')
+    expect(wrapper.text()).toContain('string')
+  })
+
+  it('renders object type schema with properties', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      type: 'object',
+      properties: {
+        name: { type: 'string', description: 'User name' },
+        age: { type: 'number', description: 'User age' },
+      },
+      required: ['name'],
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('name')
+    expect(wrapper.text()).toContain('(required)')
+    expect(wrapper.text()).toContain('User name')
+    expect(wrapper.text()).toContain('age')
+    expect(wrapper.text()).toContain('User age')
+  })
+
+  it('renders array type schema with items', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      type: 'array',
+      items: { type: 'string' },
+      minItems: 1,
+      maxItems: 10,
+      uniqueItems: true,
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('Array of:')
+    expect(wrapper.text()).toContain('string')
+    expect(wrapper.text()).toContain('Min items: 1')
+    expect(wrapper.text()).toContain('Max items: 10')
+    expect(wrapper.text()).toContain('Unique items: true')
+  })
+
+  it('renders primitive type schema with format and enum', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      type: 'string',
+      format: 'email',
+      enum: ['user@example.com', 'admin@example.com'],
+      default: 'user@example.com',
+      description: 'User email address',
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('string')
+    expect(wrapper.text()).toContain('format: email')
+    expect(wrapper.text()).toContain('possible values: "user@example.com", "admin@example.com"')
+    expect(wrapper.text()).toContain('default: "user@example.com"')
+    expect(wrapper.text()).toContain('User email address')
+  })
+
+  it('renders nested object schema', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      type: 'object',
+      properties: {
+        user: {
+          type: 'object',
+          properties: {
+            name: { type: 'string' },
+            address: {
+              type: 'object',
+              properties: {
+                street: { type: 'string' },
+                city: { type: 'string' },
+              },
+            },
+          },
+        },
+      },
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('user')
+    expect(wrapper.text()).toContain('name')
+    expect(wrapper.text()).toContain('address')
+    expect(wrapper.text()).toContain('street')
+    expect(wrapper.text()).toContain('city')
+  })
+
+  it('renders array of objects schema', () => {
+    const schema: OpenAPIV3_1.SchemaObject = {
+      type: 'array',
+      items: {
+        type: 'object',
+        properties: {
+          id: { type: 'string' },
+          name: { type: 'string' },
+        },
+      },
+    }
+
+    const wrapper = mount(Schema, {
+      props: { schema },
+    })
+
+    expect(wrapper.text()).toContain('Array of:')
+    expect(wrapper.text()).toContain('id')
+    expect(wrapper.text()).toContain('name')
+  })
+})
diff --git a/packages/openapi-to-markdown/src/components/Schema.vue b/packages/openapi-to-markdown/src/components/Schema.vue
new file mode 100644
index 00000000000..0559eb84000
--- /dev/null
+++ b/packages/openapi-to-markdown/src/components/Schema.vue
@@ -0,0 +1,203 @@
+<script setup lang="ts">
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+
+defineProps<{
+  schema: OpenAPIV3_1.SchemaObject
+}>()
+
+// Sort properties to show required fields first, then optional, then metadata
+const sortProperties = (
+  properties: Record<string, OpenAPIV3_1.SchemaObject>,
+  required?: string[],
+) => {
+  const sorted = Object.entries(properties).sort(([a], [b]) => {
+    const aRequired = required?.includes(a)
+    const bRequired = required?.includes(b)
+    if (aRequired && !bRequired) return -1
+    if (!aRequired && bRequired) return 1
+    return a.localeCompare(b)
+  })
+  return Object.fromEntries(sorted)
+}
+</script>
+
+<template>
+  <section v-if="schema">
+    <!-- Composition keywords -->
+    <template v-if="schema.allOf">
+      <section>
+        <header>
+          <strong>All of:</strong>
+        </header>
+        <section
+          v-for="(subSchema, index) in schema.allOf"
+          :key="index">
+          <Schema :schema="subSchema" />
+        </section>
+      </section>
+    </template>
+
+    <template v-else-if="schema.anyOf">
+      <section>
+        <header>
+          <strong>Any of:</strong>
+        </header>
+        <section
+          v-for="(subSchema, index) in schema.anyOf"
+          :key="index">
+          <Schema :schema="subSchema" />
+        </section>
+      </section>
+    </template>
+
+    <template v-else-if="schema.oneOf">
+      <section>
+        <header>
+          <strong>One of:</strong>
+        </header>
+        <section
+          v-for="(subSchema, index) in schema.oneOf"
+          :key="index">
+          <Schema :schema="subSchema" />
+        </section>
+      </section>
+    </template>
+
+    <template v-else-if="schema.not">
+      <section>
+        <header>
+          <strong>Not:</strong>
+        </header>
+        <section>
+          <Schema :schema="schema.not" />
+        </section>
+      </section>
+    </template>
+
+    <!-- Object type -->
+    <template v-else-if="schema.type === 'object' || schema.properties">
+      <section>
+        <ul>
+          <template
+            v-for="(propSchema, propName) in sortProperties(
+              schema.properties || {},
+              schema.required,
+            )"
+            :key="propName">
+            <li>
+              <strong>
+                <code>{{ propName }}</code>
+                <span v-if="schema.required?.includes(propName)">
+                  (required)
+                </span>
+              </strong>
+              <p>
+                <code>
+                  {{
+                    Array.isArray(propSchema.type)
+                      ? propSchema.type.join(' | ')
+                      : propSchema.type || 'object'
+                  }}
+                </code>
+                <template v-if="propSchema.format">
+                  <span
+                    >, format: <code>{{ propSchema.format }}</code></span
+                  >
+                </template>
+                <template v-if="propSchema.enum">
+                  <span
+                    >, possible values:
+                    <code>{{
+                      (propSchema.enum as unknown[])
+                        .map((e: unknown) => JSON.stringify(e))
+                        .join(', ')
+                    }}</code>
+                  </span>
+                </template>
+                <template v-if="propSchema.default !== undefined">
+                  <span
+                    >, default:
+                    <code>{{ JSON.stringify(propSchema.default) }}</code></span
+                  >
+                </template>
+                <template v-if="propSchema.description">
+                  <span> — {{ propSchema.description }}</span>
+                </template>
+              </p>
+              <Schema
+                v-if="propSchema.type === 'object' || propSchema.properties"
+                :schema="propSchema" />
+              <template v-if="propSchema.type === 'array' && propSchema.items">
+                <section>
+                  <header>
+                    <strong>Items:</strong>
+                  </header>
+                  <Schema :schema="propSchema.items" />
+                </section>
+              </template>
+            </li>
+          </template>
+        </ul>
+      </section>
+    </template>
+
+    <!-- Array type -->
+    <template v-else-if="schema.type === 'array' && schema.items">
+      <section>
+        <header>
+          <strong>Array of:</strong>
+        </header>
+        <section>
+          <Schema :schema="schema.items" />
+        </section>
+        <ul
+          v-if="
+            schema.minItems !== undefined ||
+            schema.maxItems !== undefined ||
+            schema.uniqueItems
+          ">
+          <li v-if="schema.minItems !== undefined">
+            Min items: <code>{{ schema.minItems }}</code>
+          </li>
+          <li v-if="schema.maxItems !== undefined">
+            Max items: <code>{{ schema.maxItems }}</code>
+          </li>
+          <li v-if="schema.uniqueItems">Unique items: <code>true</code></li>
+        </ul>
+      </section>
+    </template>
+
+    <!-- Primitive types -->
+    <template v-else>
+      <section>
+        <p>
+          <code>{{ schema.type }}</code>
+          <template v-if="schema.format">
+            <span
+              >, format: <code>{{ schema.format }}</code></span
+            >
+          </template>
+          <template v-if="schema.enum">
+            <span
+              >, possible values:
+              <code>{{
+                (schema.enum as unknown[])
+                  .map((e: unknown) => JSON.stringify(e))
+                  .join(', ')
+              }}</code>
+            </span>
+          </template>
+          <template v-if="schema.default !== undefined">
+            <span
+              >, default:
+              <code>{{ JSON.stringify(schema.default) }}</code></span
+            >
+          </template>
+          <template v-if="schema.description">
+            <span> — {{ schema.description }}</span>
+          </template>
+        </p>
+      </section>
+    </template>
+  </section>
+</template>
diff --git a/packages/openapi-to-markdown/src/components/XmlOrJson.vue b/packages/openapi-to-markdown/src/components/XmlOrJson.vue
new file mode 100644
index 00000000000..065de9794b1
--- /dev/null
+++ b/packages/openapi-to-markdown/src/components/XmlOrJson.vue
@@ -0,0 +1,23 @@
+<script setup lang="ts">
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+// @ts-expect-error Doesn’t come with types
+import objectToXML from 'object-to-xml'
+
+withDefaults(
+  defineProps<{
+    xml?: boolean
+    modelValue: OpenAPIV3_1.SchemaObject
+  }>(),
+  {
+    xml: false,
+  },
+)
+</script>
+<template>
+  <template v-if="xml">
+    <pre><code>{{ `<?xml version="1.0" encoding="UTF-8"?>${objectToXML(modelValue)}` }}</code></pre>
+  </template>
+  <template v-else>
+    <pre><code>{{ JSON.stringify(modelValue, null, 2) }}</code></pre>
+  </template>
+</template>
diff --git a/packages/openapi-to-markdown/src/create-markdown-from-openapi.test.ts b/packages/openapi-to-markdown/src/create-markdown-from-openapi.test.ts
new file mode 100644
index 00000000000..feffdc39900
--- /dev/null
+++ b/packages/openapi-to-markdown/src/create-markdown-from-openapi.test.ts
@@ -0,0 +1,169 @@
+import { describe, expect, it } from 'vitest'
+import { createMarkdownFromOpenApi } from './create-markdown-from-openapi'
+
+describe('createMarkdownFromOpenApi', () => {
+  it('renders title, version and OpenAPI version', async () => {
+    const content = {
+      openapi: '3.1.1',
+      info: {
+        title: 'Test API',
+        version: '1.0.0',
+        description: 'Test description',
+      },
+      paths: {},
+    }
+
+    const markdown = `# Test API
+
+- **OpenAPI Version:** \`3.1.1\`
+- **API Version:** \`1.0.0\`
+
+Test description`
+
+    const result = await createMarkdownFromOpenApi(content)
+
+    expect(result).toContain(markdown)
+  })
+
+  it('renders servers', async () => {
+    const content = {
+      openapi: '3.1.1',
+      info: {
+        title: 'Test API',
+        version: '1.0.0',
+      },
+      servers: [
+        {
+          url: 'https://test.com',
+          description: 'Test server',
+        },
+        {
+          url: 'https://test.com/{version}',
+          description: 'Test server v2',
+          variables: {
+            version: {
+              default: 'v2',
+              description: 'Test version',
+            },
+          },
+        },
+      ],
+      paths: {},
+    }
+
+    const markdown = `# Test API
+
+- **OpenAPI Version:** \`3.1.1\`
+- **API Version:** \`1.0.0\`
+
+## Servers
+
+- **URL:** \`https://test.com\`
+  - **Description:** Test server
+
+- **URL:** \`https://test.com/{version}\`
+
+  - **Description:** Test server v2
+  - **Variables:**
+    - \`version\` (default: \`v2\`): Test version
+`
+
+    const result = await createMarkdownFromOpenApi(content)
+
+    expect(result).toContain(markdown)
+  })
+
+  it('renders operations', async () => {
+    const content = {
+      openapi: '3.1.1',
+      info: {
+        title: 'Test API',
+        version: '1.0.0',
+      },
+      paths: {
+        '/test': {
+          get: {
+            summary: 'Test operation',
+            description: 'Test description',
+          },
+        },
+      },
+    }
+
+    const markdown = `# Test API
+
+- **OpenAPI Version:** \`3.1.1\`
+- **API Version:** \`1.0.0\`
+
+## Operations
+
+### Test operation
+
+- **Method:** \`GET\`
+- **Path:** \`/test\`
+
+Test description`
+
+    const result = await createMarkdownFromOpenApi(content)
+
+    expect(result).toContain(markdown)
+  })
+
+  it('renders request body and response schemas', async () => {
+    const content = {
+      openapi: '3.1.1',
+      info: {
+        title: 'Test API',
+        version: '1.0.0',
+      },
+      paths: {
+        '/users': {
+          post: {
+            summary: 'Create user',
+            description: 'Creates a new user',
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      name: { type: 'string', description: 'User name' },
+                      email: { type: 'string', format: 'email' },
+                    },
+                    required: ['name', 'email'],
+                  },
+                },
+              },
+            },
+            responses: {
+              '201': {
+                description: 'User created',
+                content: {
+                  'application/json': {
+                    schema: {
+                      type: 'object',
+                      properties: {
+                        id: { type: 'string' },
+                        name: { type: 'string' },
+                        email: { type: 'string', format: 'email' },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    }
+
+    const result = await createMarkdownFromOpenApi(content)
+
+    expect(result).toContain('Request Body')
+    expect(result).toContain('name')
+    expect(result).toContain('email')
+    expect(result).toContain('Responses')
+    expect(result).toContain('201')
+    expect(result).toContain('id')
+  })
+})
diff --git a/packages/openapi-to-markdown/src/create-markdown-from-openapi.ts b/packages/openapi-to-markdown/src/create-markdown-from-openapi.ts
new file mode 100644
index 00000000000..c2b0f3c43f8
--- /dev/null
+++ b/packages/openapi-to-markdown/src/create-markdown-from-openapi.ts
@@ -0,0 +1,59 @@
+import { dereference, upgrade } from '@scalar/openapi-parser'
+import type { OpenAPI } from '@scalar/openapi-types'
+import { minify } from 'html-minifier-terser'
+import rehypeParse from 'rehype-parse'
+import rehypeRemark from 'rehype-remark'
+import rehypeSanitize from 'rehype-sanitize'
+import remarkGfm from 'remark-gfm'
+import remarkStringify from 'remark-stringify'
+import { unified } from 'unified'
+import { createSSRApp } from 'vue'
+import { renderToString } from 'vue/server-renderer'
+import MarkdownReference from './components/MarkdownReference.vue'
+
+type AnyDocument = OpenAPI.Document | Record<string, unknown> | string
+
+export async function createHtmlFromOpenApi(input: AnyDocument) {
+  // TODO: Use the new store here.
+  const { specification: upgraded } = upgrade(input)
+  const { schema: content } = await dereference(upgraded)
+
+  // Create and configure a server-side rendered Vue app
+  const app = createSSRApp(MarkdownReference, {
+    content,
+  })
+
+  // Get static HTML
+  const html = await renderToString(app)
+
+  // Clean the output
+  return minify(html, {
+    removeComments: true,
+    removeEmptyElements: true,
+    collapseWhitespace: true,
+    continueOnParseError: true,
+    noNewlinesBeforeTagClose: true,
+    preserveLineBreaks: true,
+    removeEmptyAttributes: true,
+    decodeEntities: true,
+    useShortDoctype: true,
+  })
+}
+
+export async function createMarkdownFromOpenApi(content: AnyDocument) {
+  return markdownFromHtml(await createHtmlFromOpenApi(content))
+}
+
+export async function markdownFromHtml(html: string): Promise<string> {
+  const file = await unified()
+    .use(rehypeParse, { fragment: true })
+    .use(remarkGfm)
+    .use(rehypeSanitize)
+    .use(rehypeRemark)
+    .use(remarkStringify, {
+      bullet: '-',
+    })
+    .process(html)
+
+  return String(file)
+}
diff --git a/packages/openapi-to-markdown/src/index.ts b/packages/openapi-to-markdown/src/index.ts
new file mode 100644
index 00000000000..d5349235b40
--- /dev/null
+++ b/packages/openapi-to-markdown/src/index.ts
@@ -0,0 +1,2 @@
+export { createMarkdownFromOpenApi } from './create-markdown-from-openapi'
+export { createHtmlFromOpenApi } from './create-markdown-from-openapi'
diff --git a/packages/openapi-to-markdown/tsconfig.build.json b/packages/openapi-to-markdown/tsconfig.build.json
new file mode 100644
index 00000000000..560fa18e797
--- /dev/null
+++ b/packages/openapi-to-markdown/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+  "extends": "./tsconfig.json",
+  "exclude": ["playground"],
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "dist",
+    "declaration": true,
+    "declarationMap": true,
+    "noEmit": false,
+    "emitDeclarationOnly": true
+  }
+}
diff --git a/packages/openapi-to-markdown/tsconfig.json b/packages/openapi-to-markdown/tsconfig.json
new file mode 100644
index 00000000000..7b2850d978b
--- /dev/null
+++ b/packages/openapi-to-markdown/tsconfig.json
@@ -0,0 +1,11 @@
+{
+  "extends": "../../tsconfig.strict.json",
+  "include": ["src", "playground"],
+  "compilerOptions": {
+    "types": ["vite/client"],
+    "paths": {
+      "@/*": ["./src/*"],
+      "@/test/*": ["./test/*"]
+    }
+  }
+}
diff --git a/packages/openapi-to-markdown/vite.config.ts b/packages/openapi-to-markdown/vite.config.ts
new file mode 100644
index 00000000000..ac186dfdeff
--- /dev/null
+++ b/packages/openapi-to-markdown/vite.config.ts
@@ -0,0 +1,17 @@
+import { alias, createViteBuildOptions, findEntryPoints } from '@scalar/build-tooling'
+import vue from '@vitejs/plugin-vue'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: alias(import.meta.url),
+    dedupe: ['vue'],
+  },
+  build: createViteBuildOptions({
+    entry: await findEntryPoints({ allowCss: true }),
+    options: {
+      ssr: false,
+    },
+  }),
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2bed9137cce..e72d237e29c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -485,7 +485,7 @@ importers:
         version: link:../../integrations/nuxt
       nuxt:
         specifier: ^3.12.4
-        version: 3.14.159(@biomejs/biome@1.9.4)(@parcel/watcher@2.4.1)(@types/node@22.15.3)(encoding@0.1.13)(eslint@9.20.1(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.31.2)(typescript@5.6.2)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
+        version: 3.14.159(@biomejs/biome@1.9.4)(@parcel/watcher@2.4.1)(@types/node@22.15.3)(encoding@0.1.13)(eslint@9.20.1(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.31.2)(typescript@5.6.2)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
     devDependencies:
       '@scalar/api-client':
         specifier: workspace:*
@@ -793,6 +793,9 @@ importers:
       '@scalar/build-tooling':
         specifier: workspace:*
         version: link:../../packages/build-tooling
+      '@scalar/openapi-to-markdown':
+        specifier: workspace:*
+        version: link:../../packages/openapi-to-markdown
       hono:
         specifier: ^4.6.5
         version: 4.6.5
@@ -1928,6 +1931,82 @@ importers:
         specifier: ^2.8.0
         version: 2.8.0
 
+  packages/openapi-to-markdown:
+    dependencies:
+      '@scalar/components':
+        specifier: workspace:*
+        version: link:../components
+      '@scalar/oas-utils':
+        specifier: workspace:*
+        version: link:../oas-utils
+      '@scalar/openapi-parser':
+        specifier: workspace:*
+        version: link:../openapi-parser
+      '@scalar/openapi-types':
+        specifier: workspace:*
+        version: link:../openapi-types
+      '@scalar/snippetz':
+        specifier: workspace:*
+        version: link:../snippetz
+      '@scalar/types':
+        specifier: workspace:*
+        version: link:../types
+      html-minifier-terser:
+        specifier: ^7.2.0
+        version: 7.2.0
+      object-to-xml:
+        specifier: ^2.0.0
+        version: 2.0.0
+      rehype-parse:
+        specifier: ^9.0.0
+        version: 9.0.0
+      rehype-remark:
+        specifier: ^10.0.1
+        version: 10.0.1
+      rehype-sanitize:
+        specifier: ^6.0.0
+        version: 6.0.0
+      rehype-stringify:
+        specifier: ^10.0.0
+        version: 10.0.0
+      remark-gfm:
+        specifier: ^4.0.0
+        version: 4.0.0
+      remark-stringify:
+        specifier: ^11.0.0
+        version: 11.0.0
+      unified:
+        specifier: ^11.0.4
+        version: 11.0.4
+      vue:
+        specifier: catalog:*
+        version: 3.5.12(typescript@5.6.2)
+    devDependencies:
+      '@hono/node-server':
+        specifier: ^1.11.0
+        version: 1.11.3
+      '@scalar/build-tooling':
+        specifier: workspace:*
+        version: link:../build-tooling
+      '@scalar/galaxy':
+        specifier: ^0.3.2
+        version: 0.3.2
+      '@types/html-minifier-terser':
+        specifier: ^7.0.2
+        version: 7.0.2
+      '@vitejs/plugin-vue':
+        specifier: catalog:*
+        version: 5.2.0(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vue/test-utils':
+        specifier: ^2.4.1
+        version: 2.4.6
+      hono:
+        specifier: ^4.6.5
+        version: 4.6.5
+      vite:
+        specifier: catalog:*
+        version: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
+
   packages/openapi-types:
     dependencies:
       zod:
@@ -1974,16 +2053,16 @@ importers:
         version: 19.0.2(@types/react@19.0.1)
       '@vitejs/plugin-react':
         specifier: ^4.3.4
-        version: 4.3.4(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+        version: 4.3.4(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       '@vitejs/plugin-vue':
         specifier: catalog:*
-        version: 5.2.0(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+        version: 5.2.0(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       '@vue/test-utils':
         specifier: ^2.4.1
         version: 2.4.6
       vite:
         specifier: ^5.1.1
-        version: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+        version: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
 
   packages/snippetz:
     dependencies:
@@ -7339,6 +7418,10 @@ packages:
   '@rushstack/ts-command-line@4.23.2':
     resolution: {integrity: sha512-JJ7XZX5K3ThBBva38aomgsPv1L7FV6XmSOcR6HtM7HDFZJkepqT65imw26h9ggGqMjsY0R9jcl30tzKcVj9aOQ==}
 
+  '@scalar/galaxy@0.3.2':
+    resolution: {integrity: sha512-Qc56uxOKzgEIryDw0q1ujqR/B5LwsGrQnErC8QnDbAUUnifa/ewTnVesNKTOweZD6v58sMot+1F0/k58hs2itQ==}
+    engines: {node: '>=18'}
+
   '@segment/loosely-validate-event@2.0.0':
     resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==}
 
@@ -8050,6 +8133,9 @@ packages:
   '@types/html-minifier-terser@6.1.0':
     resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
 
+  '@types/html-minifier-terser@7.0.2':
+    resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==}
+
   '@types/http-cache-semantics@4.0.4':
     resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
 
@@ -11924,6 +12010,9 @@ packages:
   hast-util-to-jsx-runtime@2.3.0:
     resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==}
 
+  hast-util-to-mdast@10.1.2:
+    resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==}
+
   hast-util-to-parse5@8.0.0:
     resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
 
@@ -15984,6 +16073,9 @@ packages:
   rehype-raw@7.0.0:
     resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
 
+  rehype-remark@10.0.1:
+    resolution: {integrity: sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==}
+
   rehype-sanitize@6.0.0:
     resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
 
@@ -17256,6 +17348,9 @@ packages:
     resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==}
     engines: {node: '>=12'}
 
+  trim-trailing-lines@2.1.0:
+    resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==}
+
   trough@2.2.0:
     resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
 
@@ -18045,37 +18140,6 @@ packages:
     peerDependencies:
       vue: '>=3.2.13'
 
-  vite@5.4.10:
-    resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
-    engines: {node: ^18.0.0 || >=20.0.0}
-    hasBin: true
-    peerDependencies:
-      '@types/node': ^18.0.0 || >=20.0.0
-      less: '*'
-      lightningcss: ^1.21.0
-      sass: '*'
-      sass-embedded: '*'
-      stylus: '*'
-      sugarss: '*'
-      terser: ^5.4.0
-    peerDependenciesMeta:
-      '@types/node':
-        optional: true
-      less:
-        optional: true
-      lightningcss:
-        optional: true
-      sass:
-        optional: true
-      sass-embedded:
-        optional: true
-      stylus:
-        optional: true
-      sugarss:
-        optional: true
-      terser:
-        optional: true
-
   vite@5.4.19:
     resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -22978,7 +23042,7 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -23058,7 +23122,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -23076,7 +23140,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -23098,7 +23162,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.25
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       chalk: 4.1.2
       collect-v8-coverage: 1.0.2
       exit: 0.1.2
@@ -23655,12 +23719,12 @@ snapshots:
       - rollup
       - supports-color
 
-  '@nuxt/devtools-kit@1.6.0(magicast@0.3.5)(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))':
+  '@nuxt/devtools-kit@1.6.0(magicast@0.3.5)(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))':
     dependencies:
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
       '@nuxt/schema': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
       execa: 7.2.0
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
     transitivePeerDependencies:
       - magicast
       - rollup
@@ -23728,7 +23792,7 @@ snapshots:
       sirv: 2.0.4
       unimport: 3.13.2(rollup@3.29.4)
       vite-plugin-inspect: 0.8.4(@nuxt/kit@3.12.3(magicast@0.3.5)(rollup@3.29.4))(rollup@3.29.4)
-      vite-plugin-vue-inspector: 5.1.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      vite-plugin-vue-inspector: 5.1.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       which: 3.0.1
       ws: 8.18.0
     transitivePeerDependencies:
@@ -23743,7 +23807,7 @@ snapshots:
       '@nuxt/devtools-kit': 1.6.0(magicast@0.3.5)(rollup@3.29.4)
       '@nuxt/devtools-wizard': 1.6.0
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@3.29.4)
-      '@vue/devtools-core': 7.4.4(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vue/devtools-core': 7.4.4(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       '@vue/devtools-kit': 7.4.4
       birpc: 0.2.17
       consola: 3.2.3
@@ -23773,7 +23837,7 @@ snapshots:
       tinyglobby: 0.2.10
       unimport: 3.13.2(rollup@3.29.4)
       vite-plugin-inspect: 0.8.7(@nuxt/kit@3.14.159(magicast@0.3.5)(rollup@3.29.4))(rollup@3.29.4)
-      vite-plugin-vue-inspector: 5.1.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      vite-plugin-vue-inspector: 5.1.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       which: 3.0.1
       ws: 8.18.0
     transitivePeerDependencies:
@@ -23783,13 +23847,13 @@ snapshots:
       - utf-8-validate
       - vue
 
-  '@nuxt/devtools@1.6.0(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
+  '@nuxt/devtools@1.6.0(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
     dependencies:
       '@antfu/utils': 0.7.10
-      '@nuxt/devtools-kit': 1.6.0(magicast@0.3.5)(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      '@nuxt/devtools-kit': 1.6.0(magicast@0.3.5)(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       '@nuxt/devtools-wizard': 1.6.0
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
-      '@vue/devtools-core': 7.4.4(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vue/devtools-core': 7.4.4(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       '@vue/devtools-kit': 7.4.4
       birpc: 0.2.17
       consola: 3.2.3
@@ -23818,9 +23882,9 @@ snapshots:
       sirv: 2.0.4
       tinyglobby: 0.2.10
       unimport: 3.13.2(rollup@4.40.1)
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
-      vite-plugin-inspect: 0.8.7(@nuxt/kit@3.14.159(magicast@0.3.5)(rollup@4.40.1))(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
-      vite-plugin-vue-inspector: 5.1.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
+      vite-plugin-inspect: 0.8.7(@nuxt/kit@3.14.159(magicast@0.3.5)(rollup@4.40.1))(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
+      vite-plugin-vue-inspector: 5.1.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       which: 3.0.1
       ws: 8.18.0
     transitivePeerDependencies:
@@ -24147,8 +24211,8 @@ snapshots:
     dependencies:
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@3.29.4)
       '@rollup/plugin-replace': 6.0.1(rollup@3.29.4)
-      '@vitejs/plugin-vue': 5.2.0(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
-      '@vitejs/plugin-vue-jsx': 4.1.0(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vitejs/plugin-vue': 5.2.0(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vitejs/plugin-vue-jsx': 4.1.0(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       autoprefixer: 10.4.20(postcss@8.5.3)
       clear: 0.1.0
       consola: 3.2.3
@@ -24175,9 +24239,9 @@ snapshots:
       ufo: 1.5.4
       unenv: 1.10.0
       unplugin: 1.16.0
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
+      vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
       vite-node: 2.1.5(@types/node@20.17.10)(terser@5.31.2)
-      vite-plugin-checker: 0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
+      vite-plugin-checker: 0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
       vue: 3.5.12(typescript@5.6.2)
       vue-bundle-renderer: 2.1.1
     transitivePeerDependencies:
@@ -24207,8 +24271,8 @@ snapshots:
     dependencies:
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
       '@rollup/plugin-replace': 6.0.1(rollup@4.40.1)
-      '@vitejs/plugin-vue': 5.2.0(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
-      '@vitejs/plugin-vue-jsx': 4.1.0(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vitejs/plugin-vue': 5.2.0(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@vitejs/plugin-vue-jsx': 4.1.0(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       autoprefixer: 10.4.20(postcss@8.5.3)
       clear: 0.1.0
       consola: 3.2.3
@@ -24235,9 +24299,9 @@ snapshots:
       ufo: 1.5.4
       unenv: 1.10.0
       unplugin: 1.16.0
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
       vite-node: 2.1.5(@types/node@22.15.3)(terser@5.31.2)
-      vite-plugin-checker: 0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
+      vite-plugin-checker: 0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2))
       vue: 3.5.12(typescript@5.6.2)
       vue-bundle-renderer: 2.1.1
     transitivePeerDependencies:
@@ -25297,6 +25361,8 @@ snapshots:
       - '@types/node'
     optional: true
 
+  '@scalar/galaxy@0.3.2': {}
+
   '@segment/loosely-validate-event@2.0.0':
     dependencies:
       component-type: 1.2.2
@@ -26478,7 +26544,7 @@ snapshots:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       '@types/responselike': 1.0.3
 
   '@types/compression@1.7.5':
@@ -26496,7 +26562,7 @@ snapshots:
 
   '@types/connect@3.4.36':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/connect@3.4.38':
     dependencies:
@@ -26508,7 +26574,7 @@ snapshots:
 
   '@types/cross-spawn@6.0.6':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/debug@4.1.12':
     dependencies:
@@ -26548,7 +26614,7 @@ snapshots:
 
   '@types/express-serve-static-core@4.19.3':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       '@types/qs': 6.9.15
       '@types/range-parser': 1.2.7
       '@types/send': 0.17.4
@@ -26586,7 +26652,7 @@ snapshots:
 
   '@types/graceful-fs@4.1.9':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/gtag.js@0.0.12': {}
 
@@ -26602,13 +26668,15 @@ snapshots:
 
   '@types/html-minifier-terser@6.1.0': {}
 
+  '@types/html-minifier-terser@7.0.2': {}
+
   '@types/http-cache-semantics@4.0.4': {}
 
   '@types/http-errors@2.0.4': {}
 
   '@types/http-proxy@1.17.15':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/is-empty@1.2.3': {}
 
@@ -26637,7 +26705,7 @@ snapshots:
 
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/lodash@4.17.5': {}
 
@@ -26661,11 +26729,11 @@ snapshots:
 
   '@types/mysql@2.15.26':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/node-forge@1.3.11':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/node@12.20.55': {}
 
@@ -26693,7 +26761,7 @@ snapshots:
 
   '@types/pg@8.6.1':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       pg-protocol: 1.7.0
       pg-types: 2.2.0
 
@@ -26748,13 +26816,13 @@ snapshots:
 
   '@types/responselike@1.0.3':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/retry@0.12.0': {}
 
   '@types/sax@1.2.7':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/semver@7.5.8': {}
 
@@ -26799,7 +26867,7 @@ snapshots:
 
   '@types/tedious@4.0.14':
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
 
   '@types/text-table@0.2.5': {}
 
@@ -27027,17 +27095,6 @@ snapshots:
       - encoding
       - supports-color
 
-  '@vitejs/plugin-react@4.3.4(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))':
-    dependencies:
-      '@babel/core': 7.26.0
-      '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0)
-      '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0)
-      '@types/babel__core': 7.20.5
-      react-refresh: 0.14.2
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
-    transitivePeerDependencies:
-      - supports-color
-
   '@vitejs/plugin-react@4.3.4(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))':
     dependencies:
       '@babel/core': 7.26.0
@@ -27070,36 +27127,26 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
+  '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
     dependencies:
       '@babel/core': 7.26.0
       '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0)
       '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0)
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
+      vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
       vue: 3.5.12(typescript@5.6.2)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
+  '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
     dependencies:
       '@babel/core': 7.26.0
       '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0)
       '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0)
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
       vue: 3.5.12(typescript@5.6.2)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@5.2.0(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
-    dependencies:
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
-      vue: 3.5.12(typescript@5.6.2)
-
-  '@vitejs/plugin-vue@5.2.0(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
-    dependencies:
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
-      vue: 3.5.12(typescript@5.6.2)
-
   '@vitejs/plugin-vue@5.2.0(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
     dependencies:
       vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
@@ -27377,18 +27424,18 @@ snapshots:
       mitt: 3.0.1
       nanoid: 3.3.11
       pathe: 1.1.2
-      vite-hot-client: 0.2.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      vite-hot-client: 0.2.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
     transitivePeerDependencies:
       - vite
 
-  '@vue/devtools-core@7.4.4(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
+  '@vue/devtools-core@7.4.4(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))':
     dependencies:
       '@vue/devtools-kit': 7.4.4
       '@vue/devtools-shared': 7.6.4
       mitt: 3.0.1
       nanoid: 3.3.11
       pathe: 1.1.2
-      vite-hot-client: 0.2.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))
+      vite-hot-client: 0.2.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))
       vue: 3.5.12(typescript@5.6.2)
     transitivePeerDependencies:
       - vite
@@ -31750,6 +31797,23 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  hast-util-to-mdast@10.1.2:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/mdast': 4.0.4
+      '@ungap/structured-clone': 1.2.0
+      hast-util-phrasing: 3.0.1
+      hast-util-to-html: 9.0.1
+      hast-util-to-text: 4.0.2
+      hast-util-whitespace: 3.0.0
+      mdast-util-phrasing: 4.1.0
+      mdast-util-to-hast: 13.2.0
+      mdast-util-to-string: 4.0.0
+      rehype-minify-whitespace: 6.0.0
+      trim-trailing-lines: 2.1.0
+      unist-util-position: 5.0.0
+      unist-util-visit: 5.0.0
+
   hast-util-to-parse5@8.0.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -32610,7 +32674,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.3
@@ -32754,7 +32818,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -32764,7 +32828,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.9
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -32803,7 +32867,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -32838,7 +32902,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -32866,7 +32930,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       chalk: 4.1.2
       cjs-module-lexer: 1.3.1
       collect-v8-coverage: 1.0.2
@@ -32931,7 +32995,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -32940,13 +33004,13 @@ snapshots:
 
   jest-worker@27.5.1:
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -34778,10 +34842,10 @@ snapshots:
       - vue-tsc
       - xml2js
 
-  nuxt@3.14.159(@biomejs/biome@1.9.4)(@parcel/watcher@2.4.1)(@types/node@22.15.3)(encoding@0.1.13)(eslint@9.20.1(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.31.2)(typescript@5.6.2)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
+  nuxt@3.14.159(@biomejs/biome@1.9.4)(@parcel/watcher@2.4.1)(@types/node@22.15.3)(encoding@0.1.13)(eslint@9.20.1(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.31.2)(typescript@5.6.2)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
     dependencies:
       '@nuxt/devalue': 2.0.2
-      '@nuxt/devtools': 1.6.0(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
+      '@nuxt/devtools': 1.6.0(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue@3.5.12(typescript@5.6.2))
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
       '@nuxt/schema': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
       '@nuxt/telemetry': 2.6.0(magicast@0.3.5)(rollup@4.40.1)
@@ -36130,7 +36194,7 @@ snapshots:
       '@protobufjs/pool': 1.1.0
       '@protobufjs/utf8': 1.1.0
       '@types/long': 4.0.2
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       long: 4.0.0
 
   protobufjs@7.3.2:
@@ -36145,7 +36209,7 @@ snapshots:
       '@protobufjs/path': 1.1.2
       '@protobufjs/pool': 1.1.0
       '@protobufjs/utf8': 1.1.0
-      '@types/node': 20.17.10
+      '@types/node': 22.15.3
       long: 5.2.3
 
   protocols@2.0.1: {}
@@ -36799,6 +36863,14 @@ snapshots:
       hast-util-raw: 9.0.3
       vfile: 6.0.1
 
+  rehype-remark@10.0.1:
+    dependencies:
+      '@types/hast': 3.0.4
+      '@types/mdast': 4.0.4
+      hast-util-to-mdast: 10.1.2
+      unified: 11.0.4
+      vfile: 6.0.1
+
   rehype-sanitize@6.0.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -38450,6 +38522,8 @@ snapshots:
     dependencies:
       escape-string-regexp: 5.0.0
 
+  trim-trailing-lines@2.1.0: {}
+
   trough@2.2.0: {}
 
   ts-api-utils@1.3.0(typescript@5.3.3):
@@ -39328,9 +39402,9 @@ snapshots:
       unist-util-stringify-position: 4.0.0
       vfile-message: 4.0.2
 
-  vite-hot-client@0.2.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2)):
+  vite-hot-client@0.2.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2)):
     dependencies:
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
 
   vite-node@1.6.0(@types/node@20.17.10)(terser@5.31.2):
     dependencies:
@@ -39373,7 +39447,7 @@ snapshots:
       cac: 6.7.14
       debug: 4.3.7(supports-color@5.5.0)
       pathe: 1.1.2
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
+      vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -39391,7 +39465,7 @@ snapshots:
       debug: 4.4.0(supports-color@9.4.0)
       es-module-lexer: 1.5.4
       pathe: 1.1.2
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
+      vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -39409,7 +39483,7 @@ snapshots:
       debug: 4.4.0(supports-color@9.4.0)
       es-module-lexer: 1.5.4
       pathe: 1.1.2
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -39441,7 +39515,7 @@ snapshots:
 
   vite-plugin-banner@0.7.1: {}
 
-  vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.10(@types/node@20.17.10)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
+  vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.19(@types/node@20.17.10)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
     dependencies:
       '@babel/code-frame': 7.26.2
       ansi-escapes: 4.3.2
@@ -39453,7 +39527,7 @@ snapshots:
       npm-run-path: 4.0.1
       strip-ansi: 6.0.1
       tiny-invariant: 1.3.3
-      vite: 5.4.10(@types/node@20.17.10)(terser@5.31.2)
+      vite: 5.4.19(@types/node@20.17.10)(terser@5.31.2)
       vscode-languageclient: 7.0.0
       vscode-languageserver: 7.0.0
       vscode-languageserver-textdocument: 1.0.11
@@ -39465,7 +39539,7 @@ snapshots:
       typescript: 5.6.2
       vue-tsc: 2.1.10(typescript@5.6.2)
 
-  vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
+  vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@9.20.1(jiti@2.4.0))(optionator@0.9.4)(typescript@5.6.2)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2))(vue-tsc@2.1.10(typescript@5.6.2)):
     dependencies:
       '@babel/code-frame': 7.26.2
       ansi-escapes: 4.3.2
@@ -39477,7 +39551,7 @@ snapshots:
       npm-run-path: 4.0.1
       strip-ansi: 6.0.1
       tiny-invariant: 1.3.3
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
       vscode-languageclient: 7.0.0
       vscode-languageserver: 7.0.0
       vscode-languageserver-textdocument: 1.0.11
@@ -39546,7 +39620,7 @@ snapshots:
       - rollup
       - supports-color
 
-  vite-plugin-inspect@0.8.7(@nuxt/kit@3.14.159(magicast@0.3.5)(rollup@4.40.1))(rollup@4.40.1)(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2)):
+  vite-plugin-inspect@0.8.7(@nuxt/kit@3.14.159(magicast@0.3.5)(rollup@4.40.1))(rollup@4.40.1)(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2)):
     dependencies:
       '@antfu/utils': 0.7.10
       '@rollup/pluginutils': 5.1.3(rollup@4.40.1)
@@ -39557,7 +39631,7 @@ snapshots:
       perfect-debounce: 1.0.0
       picocolors: 1.1.1
       sirv: 2.0.4
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
     optionalDependencies:
       '@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.40.1)
     transitivePeerDependencies:
@@ -39572,7 +39646,7 @@ snapshots:
       picocolors: 1.1.0
       vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
 
-  vite-plugin-vue-inspector@5.1.3(vite@5.4.10(@types/node@22.15.3)(terser@5.31.2)):
+  vite-plugin-vue-inspector@5.1.3(vite@5.4.19(@types/node@22.15.3)(terser@5.31.2)):
     dependencies:
       '@babel/core': 7.26.0
       '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.26.0)
@@ -39583,7 +39657,7 @@ snapshots:
       '@vue/compiler-dom': 3.5.13
       kolorist: 1.8.0
       magic-string: 0.30.12
-      vite: 5.4.10(@types/node@22.15.3)(terser@5.31.2)
+      vite: 5.4.19(@types/node@22.15.3)(terser@5.31.2)
     transitivePeerDependencies:
       - supports-color
 
@@ -39613,26 +39687,6 @@ snapshots:
       svgo: 3.3.2
       vue: 3.5.12(typescript@5.6.2)
 
-  vite@5.4.10(@types/node@20.17.10)(terser@5.31.2):
-    dependencies:
-      esbuild: 0.21.5
-      postcss: 8.5.3
-      rollup: 4.40.1
-    optionalDependencies:
-      '@types/node': 20.17.10
-      fsevents: 2.3.3
-      terser: 5.31.2
-
-  vite@5.4.10(@types/node@22.15.3)(terser@5.31.2):
-    dependencies:
-      esbuild: 0.21.5
-      postcss: 8.5.3
-      rollup: 4.40.1
-    optionalDependencies:
-      '@types/node': 22.15.3
-      fsevents: 2.3.3
-      terser: 5.31.2
-
   vite@5.4.19(@types/node@20.17.10)(terser@5.31.2):
     dependencies:
       esbuild: 0.21.5


Reviews:
user: login: amritk
body: I dunno when you build these things!? nice work as always (didn't test but its new)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T16:03:35Z
user: login: hanspagel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T11:22:56Z

Review comments:
diff_hunk: @@ -0,0 +1,23 @@
+<script setup lang="ts">
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+// @ts-expect-error Doesn’t come with types
path: packages/openapi-to-markdown/src/components/XmlOrJson.vue
user: login: amritk
body: 🫨 in this day and age?
created_at: 2025-05-12T16:01:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+<script setup lang="ts">
+import type { OpenAPIV3_1 } from '@scalar/openapi-types'
+// @ts-expect-error Doesn’t come with types
path: packages/openapi-to-markdown/src/components/XmlOrJson.vue
user: login: hanspagel
body: yeah, I should look for an alternative. but it’s used in another package already, so that was easy.
created_at: 2025-05-13T11:22:56Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-06T15:23:17Z
body: ###  🦋  Changeset detected

Latest commit: ce833cb708f243a9131a1a1f5484075c64d60c10

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                        | Type  |
| --------------------------- | ----- |
| @scalar/hono-api-reference  | Patch |
| @scalar/openapi-to-markdown | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/scalar/scalar/new/feat/markdown-api-reference?filename=.changeset/sweet-lies-appear.md&value=---%0A%22%40scalar%2Fhono-api-reference%22%3A%20patch%0A%22%40scalar%2Fopenapi-to-markdown%22%3A%20patch%0A---%0A%0Afeat(openapi-to-markdown)%3A%20Markdown%20API%20Reference%0A)


author_association: NONE
user: login: relativeci[bot]
created_at: 2025-05-07T08:45:45Z
body: ## [#9753](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS?utm_source=github&utm_campaign=pr-report&utm_content=title "Bundle Size — 2.45MiB (0%).") Bundle Size — 2.45MiB (0%).
ce833cb(current) vs 0c6d7bc **[main#9751](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9751-jj4dzcBTU3u8K7dsmWpX?utm_source=github&utm_content=report-header&utm_campaign=pr-report "View baseline job report")**(baseline)

> [!WARNING]
> Bundle contains 6 duplicate packages – [View duplicate packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_campaign=pr-report&utm_content=insights)
<details>
<summary><strong>Bundle metrics</strong>&nbsp;&nbsp;<code>no changes</code></summary>
<br>


|            |       Current<br />[#9753](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS?utm_source=github&utm_content=summary&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#9751](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9751-jj4dzcBTU3u8K7dsmWpX?utm_source=github&utm_content=summary&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Initial JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial JS assets")</span> | <span>`2.45MiB`</span> | <span>`2.45MiB`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Initial CSS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-1_ft.JS-0_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial CSS assets")</span> | <span>`0B`</span> | <span>`0B`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Cache Invalidation](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22changed-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View changed assets")</span> | <span>`0%`</span> | <span>`0%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Chunks](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-0_et.chunk-1_et.asset-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View chunks")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Assets](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View assets")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/modules?bm=%7B%22metric%22%3A%22totalSize%22%2C%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View modules")</span> | <span>`1493`</span> | <span>`1493`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0`</span> | <span>`0`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Code](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0%`</span> | <span>`0%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/packages?bp=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View packages")</span> | <span>`172`</span> | <span>`172`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate packages")</span> | <span>`6`</span> | <span>`6`</span> |
</details>


<details>
<summary><strong>Bundle size by type</strong>&nbsp;&nbsp;<code>no changes</code></summary>
<br>


|            |       Current<br />[#9753](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS?utm_source=github&utm_content=totals&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#9751](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9751-jj4dzcBTU3u8K7dsmWpX?utm_source=github&utm_content=totals&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS/assets?ba=%7B%22filters%22%3A%22ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=totals&utm_campaign=pr-report "View JS assets")</span> | <span>`2.45MiB`</span> | <span>`2.45MiB`</span> |
</details>


[Bundle analysis report](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9753-14pU0cbKyL1ORAhKh9CS?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Branch **feat/markdown-api-reference**](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/tree/feat%2Fmarkdown-api-reference?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Project dashboard](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX?utm_source=github&utm_content=report-footer&utm_campaign=pr-report)---<sup>Generated by [RelativeCI](https://relative-ci.com/?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Documentation](https://relative-ci.com/documentation?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Report issue](https://github.com/relative-ci/roadmap/issues)</sup>
<!--RELATIVE_CI_PROJECT_NAME="scalar"-->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-07T08:46:07Z
body: **Preview Examples**

https://f9b9b8f4-25a7-477d-88f1-5416c18fc082--scalar-deploy-preview.netlify.app
<!-- thollander/actions-comment-pull-request "netlify-preview" -->
author_association: CONTRIBUTOR
user: login: hanspagel
created_at: 2025-05-13T11:22:37Z
body: Got lots of ideas, but it’s new, it’s okay if it’s not perfect right-away. Let’s merge it and take it from there.
author_association: MEMBER