markdown-content
npx skills add https://github.com/joyco-studio/skills --skill markdown-content
Agent 安装分布
Skill 文档
Markdown Content Pages â Next.js
This skill defines how to build content-driven pages (blog posts, changelogs, legal pages, knowledge bases) in a Next.js App Router project using MDX.
The goal is visual and behavioral consistency between:
- Markdown codeblocks written in
.mdxfiles (processed at build time) - Custom React code components rendered on the page (highlighted at runtime)
Stack Overview
| Layer | Library | Purpose |
|---|---|---|
| Content framework | fumadocs-mdx |
MDX collection definitions, frontmatter schemas, build-time processing |
| Content runtime | fumadocs-core |
Source loader, page tree, slug resolution |
| Syntax highlighting | shiki + rehype-pretty-code |
Dual-theme code highlighting at build time (MDX) and runtime (components) |
| Schema validation | zod |
Typed frontmatter with defaults and optional fields |
| Styling | Tailwind CSS v4 | Utility-first styles, custom prose CSS, code block styles |
| Framework | Next.js (App Router) | File-based routing with catch-all [[...slug]] for content pages |
npm install fumadocs-mdx fumadocs-core shiki rehype-pretty-code zod
Project Structure
âââ app/
â âââ layout.tsx # Root layout
â âââ styles/
â â âââ globals.css # Tailwind + stylesheet imports (order matters)
â â âââ prose.css # Self-contained typography for .prose class
â â âââ shiki.css # Code block styling (themes, line numbers, highlights)
â â âââ theming.css # Color variables (light/dark) â imported first
â âââ (content)/ # Route group for content pages
â âââ layout.tsx # Content layout wrapper
â âââ [[...slug]]/
â âââ page.tsx # Catch-all page rendering MDX content
âââ content/
â âââ meta.json # Top-level page ordering
â âââ index.mdx # Landing page
â âââ <category>/ # e.g. blog/, legal/, changelog/
â âââ meta.json # Category page ordering
â âââ <page>.mdx # Individual content pages
âââ components/ # Code block components (see Code Block Components)
âââ lib/
â âââ source.ts # fumadocs-core source loader
â âââ shiki.ts # Shared shiki config, transformers, highlightCode()
â âââ cn.ts # clsx + tailwind-merge utility
âââ mdx-components.tsx # MDX element â React component mappings
âââ source.config.ts # fumadocs-mdx collection + rehype pipeline
Content Collection
source.config.ts
Defines two things:
- The content collection â
defineDocs()pointing at thecontent/directory with a Zod-extended frontmatter schema. - The MDX pipeline â
defineConfig()with rehype-pretty-code replacing the default fumadocs highlighter.
Key patterns:
- Call
plugins.shift()insiderehypePluginsto remove the built-in fumadocs syntax highlighter, thenplugins.push()rehype-pretty-code. - Configure dual shiki themes (one light, one dark) so both color sets are embedded in the markup. CSS toggles which is visible.
- Pass custom shiki transformers that inject metadata into the AST (raw source text) for components to read at render time.
- Use
onVisitTitleto addnot-proseto code block title elements so prose typography doesn’t affect them.
next.config.ts
Wrap the Next.js config with createMDX() from fumadocs-mdx/next.
lib/source.ts
Create a loader() from fumadocs-core/source with a baseUrl and the collection’s .toFumadocsSource(). Export the source and a Page type via InferPageType.
Content Files
MDX Frontmatter
Every .mdx file starts with YAML frontmatter validated by the Zod schema in source.config.ts. Imports go after the closing ---. React components can be used inline alongside standard markdown. Code blocks use fenced syntax with language tags, and a title="filename.tsx" meta string adds a title bar.
meta.json
Controls page ordering within a directory â an array of slug strings:
{
"pages": ["index", "terms-of-service", "privacy-policy"]
}
Catch-All Content Page
The [[...slug]]/page.tsx route resolves slugs via source.getPage(params.slug), returns notFound() if missing, and renders:
<article className="prose">
<h1>{page.data.title}</h1>
<p className="text-muted-foreground">{page.data.description}</p>
<MDX components={getMDXComponents()} />
</article>
Export generateStaticParams using source.generateParams() for static generation.
Shiki Configuration
lib/shiki.ts
Centralizes all syntax highlighting config. Used by both the MDX pipeline (build-time) and custom code components (runtime).
This module should export:
-
A language map â
Record<string, BuiltinLanguage>mapping file extensions (ts,tsx,sh, etc.) to shiki language identifiers. Plus agetLanguageFromExtension()helper. -
Shiki transformers â run during MDX compilation, attaching custom properties to AST nodes that components read at render time. Attach the raw source string (for copy button) so the component mapping can use it.
-
highlightCode(code, language)â an async function usingcodeToHtml()with the same dual themes as the MDX pipeline. This is the consistency mechanism â both build-time and runtime highlighting use identical shiki config and produce the same data attributes (data-line-numbers,data-line), ensuring matching visual output everywhere.
MDX Component Mappings
mdx-components.tsx
Maps HTML elements from the MDX compiler to React components. This is the bridge between raw markdown and the component system.
Key mappings:
-
codeâ Renders a styled inline<code>element. -
preâ Reads transformer metadata from props. Renders a<pre>with aCopyButtonoverlay using the raw source text. -
imgâ Optionally wrap with Next.jsImagefor optimization.
The flow: author writes fenced code block â rehype-pretty-code + shiki produce <pre><code> with syntax tokens â shiki transformers attach metadata â MDX calls these mappings â components read metadata and render accordingly.
Code Block Components
Build these client components for the code display system:
CodeBlock
Single code display. Accepts highlightedCode (HTML string), language, optional title, rawCode (for copy), maxHeight, wrap. Renders a <figure data-rehype-pretty-code-figure> with optional <figcaption> title bar, the highlighted HTML via dangerouslySetInnerHTML, and a CopyButton.
CodeBlockTabs
Multi-file tabbed code. Accepts an array of { filename, highlightedCode, rawCode } tabs. Uses controlled tab state. Renders tabs in a <figcaption> with a CopyButton for the active tab.
CopyButton
Clipboard button with visual feedback. Uses a useCopyToClipboard hook that calls navigator.clipboard.writeText() and shows a checkmark for ~2 seconds. Positioned absolutely (top-right) with opacity transition on group hover.
Styling Architecture
globals.css â Import Order
@import 'tailwindcss';
@import './theming.css';
@import './prose.css';
@import './shiki.css';
theming.css must come first â it defines the color variables that prose and shiki consume.
theming.css â Color Variables
Define light and dark mode variables consumed by shiki.css and component classes:
--color-codeâ code block background--color-code-foregroundâ code block text--color-code-highlightâ highlighted line background
prose.css â Self-Contained Typography
A hand-rolled .prose class that doesn’t depend on @tailwindcss/typography or any UI framework. Use :where() selectors to keep specificity at zero. Use .not-prose as an escape hatch for embedded components.
Cover at minimum: body color and line-height, headings (h1âh6), paragraphs, links, strong, lists (ul/ol/li), blockquotes, horizontal rules, tables, and code figures (figure[data-rehype-pretty-code-figure]).
Design decisions:
- Body text at reduced opacity (e.g. 70% of
--foregroundviacolor-mix) for softer reading - Headings at full
--foregroundwith tighter line-height - Code figures with vertical margin and first/last child resets
shiki.css â Code Block Styles
Style all code blocks uniformly â MDX-rendered and component-rendered both use the same data attributes.
Key rules:
- Dual-theme switching â In
.dark, swapcolortovar(--shiki-dark)on[data-rehype-pretty-code-figure] span. Light theme is the default; shiki embeds both color sets as CSS variables. - Container â
[data-rehype-pretty-code-figure]gets background from--color-code, rounded corners, overflow hidden. - Line numbers â CSS counters on
[data-line-numbers] [data-line]::before. Sticky left position so they stay visible on horizontal scroll. - Line highlighting â
[data-highlighted-line]gets a subtle background and left border. - Title bar â
[data-rehype-pretty-code-title]styled with mono font, muted color, bottom border. - Text wrapping â
[data-wrap='true'] codeenableswhite-space: pre-wrap.
Consistency Model
The system ensures a fenced markdown code block and a <CodeBlock> React component produce identical visual output. This is achieved through:
- Same shiki themes â Both MDX pipeline and
highlightCode()use the same dual-theme pair. - Same CSS targets â Both produce
[data-rehype-pretty-code-figure]elements styled byshiki.css. - Same data attributes â
data-line,data-line-numbers,data-highlighted-line,data-wrapare used by both paths. - Same color variables â
--color-codefamily fromtheming.cssapplies uniformly. - Shared copy button â Every code display uses the same
CopyButtoncomponent.
Implementation Checklist
- Install dependencies:
fumadocs-mdx,fumadocs-core,shiki,rehype-pretty-code,zod - Create
source.config.tswith frontmatter schema and rehype-pretty-code pipeline - Wrap
next.config.tswithcreateMDX() - Create
lib/source.tswith the source loader - Create
lib/shiki.tswith language map, transformers, andhighlightCode() - Create
mdx-components.tsxmappingpreandcodeto custom components - Create code block components (
CodeBlock,CodeBlockTabs,CopyButton) - Set up
prose.css(self-contained typography) andshiki.css(code block styling) - Define theme variables in
theming.cssincluding--color-codefamily - Create content directory with
meta.jsonfiles - Create catch-all
[[...slug]]/page.tsxroute - Import stylesheets in
globals.cssin correct order (theming â prose â shiki)