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/content for content collections and routing
  • @nuxtjs/mdc for Markdown components
  • UI Thing-based docs UI components
  • Tailwind CSS styling and theme tokens
  • nuxt-llms and 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:

Install the module

Register it in nuxt.config.ts

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:

vue

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.

vue

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:

  • default for the landing page and custom Vue pages
  • docs for documentation pages

You can override either layout in your app:

vue

app/layouts/custom.vue

<template>
  <main class="min-h-dvh bg-background text-foreground">
    <slot />
  </main>
</template>

Then use it in a Vue page:

vue

app/pages/hello.vue

<script setup lang="ts">
definePageMeta({
  layout: "custom",
})
</script>

For Markdown content pages, set the layout in frontmatter:

md

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.

ts

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 llms configuration in nuxt.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

NameType Default Required Description
urlstring—NoURL displayed in the address bar.