ワンクリックで
nuxt-content
Author or edit Nuxt Content Markdown using MDC syntax and Vue components.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Author or edit Nuxt Content Markdown using MDC syntax and Vue components.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Shell out to claude CLI.
Run a multi-agent debate to compare options and converge on a decision.
Run the same task with multiple agents for reviews, critiques, or model comparison.
Autonomous large-task delivery agent. Use for long-running coding work that should go from objective or plan to implemented code, review fixes, PR/MR, and green CI with minimal human-in-the-loop gates.
Manage ClickUp tasks.
Generate a project template for Coolify
| name | nuxt-content |
| description | Author or edit Nuxt Content Markdown using MDC syntax and Vue components. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
MDC (Markdown Components) extends standard Markdown with Vue component support. Use this guide when authoring .md files for Nuxt Content.
| Syntax | Description |
|---|---|
::component | Block component |
:component | Inline component |
{prop="value"} | Props |
#slotname | Named slot |
[text]{.class} | Span with attributes |
{{ $doc.var }} | Variable binding |
These are the MDC rules most commonly violated. Get them right and everything else follows.
::, any child inside its slot opens with :::, a grandchild with ::::, and so on. The closing fence must match the opening fence exactly (:::: closes ::::, not ::). Using :: for both outer and inner — a common mistake — silently closes the outer component at the inner component's opening line. Convention: indent each child component (and its content) by 2 spaces per level to make the structure obvious at a glance.:icon{name="x"} inside a paragraph is inline; the same text on a blank line on both sides is parsed as a block. If you need it to stay inline, keep surrounding text on the same line.{}. :hello-world is read as a component named hello-world. Write :hello{}-world to mean the component hello followed by -world. Same trick separates a component from punctuation when needed.: and wrapped in single quotes. {:items='["a","b"]'} — the leading : marks it as an expression, and single quotes let the inside use standard JSON double quotes.--- on the line after the component opener, close with ---, then put slot content below. Don't mix YAML block props with inline props on the same component.#slotname) must align with the parent :: fence column. The directive line itself is treated as part of the fence, not body content — any indentation mismatch reverts the parser and the directive leaks as literal text. Body content under each directive may be flush or indented; only the #slot line is constrained. This rule is invisible for default-slot-only components (no # lines exist), but bites hard on multi-slot components like Nuxt UI's UPageHero/UPageSection/UPageFeature. Enforced by remark-mdc (dist/index.mjs:1702 checks sectionIndentSize === initialPrefix).:: does not raise a parse error or console warning — the parser silently consumes all subsequent content (including later headings, components, even the rest of the file) as the unclosed component's body. Symptoms: a page suddenly renders blank below some point, or a later component appears nested inside an earlier one. When debugging "where did the rest of my page go?", grep for unbalanced fences first. Self-closing components are not a thing in MDC; ::divider still needs a :: on the next line.YAML metadata block at the top of the file:
---
title: My Article
description: A brief description
author: John Doe
date: 2024-01-15
tags:
- tutorial
- nuxt
draft: false
---
Access frontmatter values in content using {{ $doc.propertyName }}.
Block components use :: syntax and can contain Markdown content:
::alert{type="warning"}
This is a warning message with **Markdown** support.
::
Every level of nesting adds one colon. Outer ::, child :::, grandchild ::::. Open and close must match.
::card
:::card-header
Card Title
:::
Card body content here.
:::card-footer
Footer text
:::
::
A three-level example:
::hero
:::card
A nested card.
::::callout
A deeply nested callout inside the card.
::::
:::
::
Anti-pattern (this does NOT do what it looks like — the inner ::card-header opener is parsed as the closer for ::card, so card-header ends up outside):
::card
::card-header
Card Title
::
::
Use #slotname to define named slots. Each #slotname line must sit in the same column as its parent :: fence (see Core Rule 6).
::card
Default slot content goes here.
#header
This goes in the header slot.
#footer
This goes in the footer slot.
::
For components without content:
::divider
::
Inline components use single : and flow with text:
Here is an :icon{name="heroicons:star"} icon inline.
Status: :badge[Active]{color="success"}
Click :button[Submit]{@click="handleSubmit"} to continue.
Use square brackets for default slot content:
:badge[Premium]
:button[Click Me]{variant="outline"}
:badge[Premium] surrounded by blank lines is parsed as a block component, not inline. To keep it inline, embed it in a paragraph: This plan is :badge[Premium] tier.{} escape. Component names greedily consume [A-Za-z0-9-], so :hello-world is the single component hello-world, and :icon{name="x"}s tries to make s part of the props syntax. Insert empty props to terminate the name: :hello{}-world, :icon{name="x"}{}s.[...], not {...}. Props go in {...}, slot text goes in [...]. Order: :component[slot text]{prop="value"}.::alert{type="info" icon="heroicons:information-circle"}
Content here
::
:icon{name="heroicons:check" class="text-green-500" size="24"}
For complex props, use a YAML block after the component declaration:
::card
---
title: My Card
image: /images/hero.jpg
tags:
- featured
- new
---
Card content with complex props defined above.
::
Prefix with : for JavaScript expressions:
::dropdown{:items='["Option A", "Option B", "Option C"]'}
::
::chart{:data='{"labels": ["Jan", "Feb"], "values": [10, 20]}'}
::
::modal{closable} <!-- true -->
::modal{:closable="false"} <!-- explicit false -->
Bind to frontmatter variables:
---
cardTitle: Welcome
---
::card{:title="$doc.cardTitle"}
::
Content directly inside the component:
::callout
This is the default slot content.
It supports **Markdown** formatting.
::
::hero
#title
Welcome to Our Site
#subtitle
Build amazing things with Nuxt
#actions
:button[Get Started]{to="/docs"}
:button[Learn More]{to="/about" variant="outline"}
::
Children of a :: block must open with ::: (and close with :::):
::tabs
:::tab{label="Preview"}
Preview content here.
:::
:::tab{label="Code"}
```ts
const example = 'code'
::: ::
## Spans & Attributes
Apply attributes to inline text using `[text]{attributes}`:
```markdown
This is [highlighted text]{.text-primary}.
[Custom styled]{.font-bold .text-lg #my-id style="color: red"}
[Link with class](/about){.nav-link}
**bold text**{.text-red-500}
*italic*{.text-sm}
`inline code`{lang="ts"}
{width="300" loading="lazy"}
[styled text]{.class-one .class-two .class-three}
Interpolate frontmatter values in content:
---
author: Jane Smith
publishDate: 2024-01-15
version: 2.0.0
---
# {{ $doc.title }}
Written by {{ $doc.author }} on {{ $doc.publishDate }}.
Current version: **{{ $doc.version }}**
```typescript
const greeting: string = 'Hello, World!'
console.log(greeting)
### Filename Display
```markdown
```ts [utils/helpers.ts]
export function formatDate(date: Date): string {
return date.toLocaleDateString()
}
### Line Highlighting
```markdown
```ts {2-4,6}
function example() {
// Lines 2-4 highlighted
const a = 1
const b = 2
// Line 5 not highlighted
return a + b // Line 6 highlighted
}
### Meta String
Combine filename and highlighting:
```markdown
```vue [components/Button.vue] {3-5}
<template>
<button
class="btn"
:class="variant"
@click="$emit('click')"
>
<slot />
</button>
</template>
### Code Groups
```markdown
::code-group
```bash [npm]
npm install @nuxt/content
pnpm add @nuxt/content
yarn add @nuxt/content
::
## Prose Components
Customize how standard Markdown elements render by creating components in `components/content/`:
| Element | Component | Description |
|---------|-----------|-------------|
| `<p>` | `ProseP` | Paragraphs |
| `<h1>` | `ProseH1` | Heading 1 |
| `<h2>` | `ProseH2` | Heading 2 |
| `<h3>` | `ProseH3` | Heading 3 |
| `<a>` | `ProseA` | Links |
| `<code>` | `ProseCode` | Inline code |
| `<pre>` | `ProsePre` | Code blocks |
| `<ul>` | `ProseUl` | Unordered lists |
| `<ol>` | `ProseOl` | Ordered lists |
| `<li>` | `ProseLi` | List items |
| `<blockquote>` | `ProseBlockquote` | Blockquotes |
| `<img>` | `ProseImg` | Images |
| `<table>` | `ProseTable` | Tables |
**Plain-markdown lists nested inside MDC blocks introduce a new visual level.** When a project drives list styling with CSS counters scoped per nesting level (e.g., a `Steps` component that renders `1, 2, 3 → a, b, c → i, ii, iii`), a plain `1. 2. 3.` ordered list written *inside* a step body counts as one level deeper than its parent — it does NOT share the parent's depth. So a plain-markdown ordered list inside a level-2 step renders as visual level 3 (lower-roman, in the example cycle). The CSS selector that drives this (in projects following the ShipStream pattern) targets `.step-item > .steps-list > .step-item > .prose-ol-nested > li::before` — note the `.prose-ol-nested` segment, which is the marker that the plain-markdown list adds its own depth. Only relevant in projects with depth-sensitive list styling.
## Excerpts
Use `<!--more-->` to define excerpt boundaries:
```markdown
---
title: My Article
---
This is the excerpt that appears in listings.
<!--more-->
This is the full article content that only shows on the detail page.
::alert{type="info"}
This is an informational message.
::
::alert{type="warning" icon="heroicons:exclamation-triangle"}
**Warning:** Please read carefully before proceeding.
::
::alert{type="error"}
An error occurred. Please try again.
::
::alert{type="success"}
Operation completed successfully!
::
::card{image="/images/feature.jpg"}
#header
Feature Title
#default
This card showcases a new feature with an image header and action buttons.
#footer
:button[Learn More]{to="/features" variant="link"}
::
::accordion
:::accordion-item{title="What is Nuxt Content?"}
Nuxt Content is a file-based CMS for Nuxt applications.
:::
:::accordion-item{title="How do I install it?"}
Run `npx nuxi module add content` to add it to your project.
:::
::
::tabs
:::tab{label="Vue"}
```vue
<template>
<div>Hello Vue!</div>
</template>
:::
:::tab{label="React"}
function Hello() {
return <div>Hello React!</div>
}
::: ::
### Badge/Tag
```markdown
Status: :badge[Published]{color="success"} :badge[Featured]{color="primary"}
Tags: :tag[Vue] :tag[Nuxt] :tag[TypeScript]
:icon{name="heroicons:home" class="w-5 h-5"}
:icon{name="lucide:github" size="24"}
:icon{name="mdi:vuejs" class="text-green-500"}
::callout{icon="heroicons:light-bulb"}
#title
Pro Tip
#default
Use MDC syntax to create rich, interactive documentation with Vue components.
::
When the project uses Nuxt UI v4 (including Docus, Nuxt UI Pro docs starter, etc.), @nuxt/content auto-registers a set of prose components. Read nuxt-ui-components.md for the full reference with examples and props.
Available components: accordion / accordion-item, badge, callout (+ note / tip / warning / caution shortcuts), card / card-group, code-collapse, code-group, code-preview, code-tree, collapsible, field / field-group, icon, kbd, steps, tabs / tabs-item. (prompt is upcoming.)
Remember the universal nesting rule (see Core Rules above): a child inside a :: block opens with :::, not ::. This matters for pairs like tabs / tabs-item, accordion / accordion-item, card-group / card, field-group / field, and steps with its step children. Source: https://ui.nuxt.com/docs/typography.
Create Vue components in components/content/ to use in MDC:
<!-- components/content/Alert.vue -->
<script setup lang="ts">
defineProps<{
type?: 'info' | 'warning' | 'error' | 'success'
icon?: string
}>()
</script>
<template>
<div :class="['alert', `alert-${type}`]">
<Icon v-if="icon" :name="icon" />
<slot />
</div>
</template>
Then use in Markdown:
::alert{type="info" icon="heroicons:information-circle"}
Custom alert component content.
::
If the project uses Nuxt UI v4, several Prose* components are auto-registered globally (ProseTip, ProseNote, ProseWarning, ProseCaution, ProseSteps, etc. — see nuxt-ui-components.md for the full list). Defining a custom component in components/content/ with the same name as a Nuxt UI built-in works in pnpm dev but silently breaks in pnpm build. Vite's dev resolver tolerates the duplicate registration and picks one at runtime; the production prerender bakes a frozen component map and emits the literal MDC tag (e.g., <ProseTip>...</ProseTip>) into the prerendered HTML, skipping rendering entirely.
Two safe patterns:
OlSteps instead of Steps to avoid ProseSteps). If the markdown source already uses the Nuxt UI tag name, rename in markdown too — or add an mdc.components.map entry in nuxt.config.ts to remap the tag (less preferred — silent indirection).Always verify in production builds, not just dev. A component that renders correctly in pnpm dev may emit literal tags in pnpm build. Run a quick curl localhost:3000/<page> | grep -c '<Prose' to count any leaked tags after building.
Beyond the prose set and Nuxt UI's typography components, projects often add their own MDC components for domain-specific patterns (e.g., ::version-badge, ::ol-steps, ::pill-green). When picking up an unfamiliar Nuxt Content project, list app/components/content/ (or components/content/) before authoring — that directory is the ground truth for what custom MDC tags are available locally.
When creating a documentation section or any area using dynamic routes for content, avoid using optional catch-all routes like [[...slug]].vue if you also need to support the root path (e.g., /docs) and sub-paths (e.g., /docs/getting-started) reliably.
Nuxt/Vue Router can have trouble disambiguating the root path when using [[...slug]].vue.
Recommended Pattern: Split the implementation into two explicit files:
pages/docs/index.vue - Handles the root /docs path
<script setup>
const { data: page } = await useAsyncData('docs-index', () =>
queryCollection('docs').path('/docs').first()
)
// ... handle page not found ...
</script>
pages/docs/[...slug].vue - Handles all nested paths (slug is required)
<script setup>
const route = useRoute()
const slug = route.params.slug.join('/')
const { data: page } = await useAsyncData(`docs-${slug}`, () =>
queryCollection('docs').path(`/docs/${slug}`).first()
)
// ... handle page not found ...
</script>
This ensures predictable routing behavior and prevents 404 errors on the root path.