Nuxt
Use Docd as a normal Nuxt application with layouts, components, modules, and server logic.
Docd is a Nuxt layer, not a closed theme runtime. Your documentation site is still a normal Nuxt app, so you can use standard Nuxt features whenever you need them.
That means you can add pages, components, composables, plugins, modules, server routes, and layouts exactly as you would in any other Nuxt project.
What the layer gives you
When your app extends docd, the layer wires in the docs stack for you:
@nuxt/contentfor content collections and routing@nuxtjs/mdcfor Markdown components- UI Thing-based docs UI components
- Tailwind CSS styling and theme tokens
nuxt-llmsand raw markdown output- the built-in MCP server at
/mcp
Your app remains the top-level Nuxt project. The layer only provides defaults and reusable UI.
Typical Docd app structure
A minimal Docd app often starts with:
content/
index.md
1.getting-started/
2.concepts/
app/app.config.ts
nuxt.config.ts
package.json
From there, you can add any standard Nuxt directories you need:
app/
assets/
components/
composables/
layouts/
pages/
server/
api/
mcp/
public/
Use normal Nuxt modules
Because this is a Nuxt app, installing modules works the same way it does anywhere else.
For example, adding Vercel Analytics is just a normal module install:
Register it in nuxt.config.ts
nuxt.config.ts
export default defineNuxtConfig({
extends: ["@baybreezy/docd"],
modules: ["@vercel/analytics/nuxt"],
})
The same pattern applies to auth, analytics, images, monitoring, feature flags, or any other module your docs site needs.
Custom components in Markdown
Docd uses Nuxt Content and MDC, so you can place global components in your app and call them directly from Markdown.
For example, create a custom browser frame:
app/components/content/BrowserFrame.global.vue
<script setup lang="ts">
defineProps<{
/**
* URL displayed in the address bar.
* @example "https://example.com"
*/
url?: string;
}>();
const emit = defineEmits<{
/**
* Emitted when the close (red) traffic light button is clicked.
*/
close: [];
/**
* Emitted when the minimize (yellow) traffic light button is clicked.
*/
minimize: [];
/**
* Emitted when the maximize (green) traffic light button is clicked.
*/
maximize: [];
/**
* Emitted when the sidebar/panel toggle button is clicked.
*/
panel: [];
/**
* Emitted when the back navigation button is clicked.
*/
back: [];
/**
* Emitted when the forward navigation button is clicked.
*/
forward: [];
/**
* Emitted when the page refresh button is clicked.
*/
refresh: [];
/**
* Emitted when the download button is clicked.
*/
download: [];
/**
* Emitted when the share button is clicked.
*/
share: [];
/**
* Emitted when the new tab button is clicked.
*/
newTab: [];
/**
* Emitted when the copy button is clicked.
*/
copy: [];
}>();
defineSlots<{
/**
* Replaces the three traffic light buttons (close, minimize, maximize).
*/
"traffic-lights": () => void;
/**
* Replaces the sidebar panel toggle and back/forward navigation buttons.
*/
"nav-controls": () => void;
/**
* Replaces the address bar area.
*/
"address-bar": () => void;
/**
* Replaces the right-side action buttons (download, share, new tab, copy).
*/
actions: () => void;
/**
* Content rendered inside the browser frame body.
*/
default: () => void;
}>();
</script>
<template>
<div class="w-full overflow-hidden rounded-xl border shadow-md">
<div class="flex items-center gap-3 border-b px-3 py-2">
<!-- Traffic lights -->
<div class="flex shrink-0 items-center gap-1.5">
<slot name="traffic-lights">
<button
type="button"
aria-label="Close"
class="size-3 rounded-full bg-red-500 transition-opacity hover:opacity-80"
@click="emit('close')"
/>
<button
type="button"
aria-label="Minimize"
class="size-3 rounded-full bg-yellow-400 transition-opacity hover:opacity-80"
@click="emit('minimize')"
/>
<button
type="button"
aria-label="Maximize"
class="size-3 rounded-full bg-green-500 transition-opacity hover:opacity-80"
@click="emit('maximize')"
/>
</slot>
</div>
<!-- Panel + back/forward -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="nav-controls">
<button
type="button"
aria-label="Toggle sidebar"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('panel')"
>
<Icon name="lucide:panel-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go back"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('back')"
>
<Icon name="lucide:chevron-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go forward"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('forward')"
>
<Icon name="lucide:chevron-right" class="size-4" />
</button>
</slot>
</div>
<!-- Address bar -->
<div class="flex min-w-0 flex-1 justify-center">
<slot name="address-bar">
<div
class="flex w-full max-w-sm items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs text-muted-foreground dark:border-muted/50 dark:bg-muted/50"
>
<Icon name="lucide:shield" class="size-3 shrink-0" />
<span class="min-w-0 flex-1 truncate text-center">{{ url ?? "about:blank" }}</span>
<button
type="button"
aria-label="Refresh"
class="shrink-0 transition-opacity hover:opacity-70"
@click="emit('refresh')"
>
<Icon name="lucide:rotate-cw" class="size-3" />
</button>
</div>
</slot>
</div>
<!-- Right-side actions -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="actions">
<button
type="button"
aria-label="Download"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('download')"
>
<Icon name="lucide:arrow-down-to-line" class="size-4" />
</button>
<button
type="button"
aria-label="Share"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('share')"
>
<Icon name="lucide:share" class="size-4" />
</button>
<button
type="button"
aria-label="New tab"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('newTab')"
>
<Icon name="lucide:plus" class="size-4" />
</button>
<button
type="button"
aria-label="Copy"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('copy')"
>
<Icon name="lucide:copy" class="size-4" />
</button>
</slot>
</div>
</div>
<div>
<slot mdc-unwrap="p" />
</div>
</div>
</template>
If you want Docd to append a component API reference for one of your app components, add componentApi to the page frontmatter and point it at the component path in your project.
componentApi:
heading: Browser Frame API
path: /app/components/content/BrowserFrame.global.vue
layout: table
At the bottom of the page, Docd will render a component API reference for the BrowserFrame.global.vue component, showing all its props, events, and slots.
Vue pages still work
Markdown is the default authoring path, but you can create Vue pages in app/pages/ whenever you need fully custom experiences.
app/pages/status.vue
<template>
<section class="container py-10">
<h1 class="text-3xl font-bold">Status</h1>
<p class="mt-3 text-muted-foreground">
This page is rendered with Vue instead of Markdown.
</p>
</section>
</template>
This is useful for:
- dashboards
- changelog feeds
- custom landing pages
- embedded demos
- authenticated internal tooling
Layouts
Docd currently uses two layouts:
defaultfor the landing page and custom Vue pagesdocsfor documentation pages
You can override either layout in your app:
app/layouts/custom.vue
<template>
<main class="min-h-dvh bg-background text-foreground">
<slot />
</main>
</template>
Then use it in a Vue page:
app/pages/hello.vue
<script setup lang="ts">
definePageMeta({
layout: "custom",
})
</script>
For Markdown content pages, set the layout in frontmatter:
content/docs/hello.md
---
title: Hello
description: A custom layout page
layout: custom
---
# Hello
Server routes and API handlers
Because the site is a full Nuxt app, you can add server code like any other Nitro project.
server/api/health.get.ts
export default defineEventHandler(() => {
return {
ok: true,
service: "docd-site",
}
})
This is useful for small integrations, status endpoints, or documentation-specific APIs.
AI features are just Nuxt modules
Docd's MCP server and LLM integration are normal module-backed capabilities:
That means you can extend them inside your app with:
server/mcp/tools/server/mcp/resources/server/mcp/prompts/- additional
llmsconfiguration innuxt.config.ts
When to stay in Markdown
Prefer Markdown when the page is mostly content.
Prefer Vue when the page is mostly application logic.
Docd works best when docs content stays in content/, and only genuinely interactive or app-like screens move into Vue pages.
Browser Frame API
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
url | string | — | No | URL displayed in the address bar. |