一键导入
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.