Snippets
A snippet is a file you write once and import into as many pages as you want — either a chunk of MDX prose, or a real React component. Both live in a snippets/ folder next to docs/ and writedocs.json:
my-docs/
├── writedocs.json
├── index.mdx
├── docs/
│ └── getting-started.mdx
└── snippets/
├── upgrade-note.mdx
└── Counter.jsxsnippets/ isn’t scanned for pages the way docs/ is — nothing in it appears in the sidebar or gets its own URL. It’s purely a place to put things other pages import.
MDX snippets
An MDX snippet is just an .mdx file with no frontmatter — import its default export and use it like any other component. Props you pass are available inside the snippet via props:
<Callout type="note">
Available since v{props.since}.
</Callout>import UpgradeNote from '../snippets/upgrade-note.mdx';
<UpgradeNote since="2.0" />Notice upgrade-note.mdx uses <Callout> with no import of its own — Writedocs’ built-in components (Callout, Card, Tabs, …) are available inside a snippet exactly the same way they’re available on an ordinary page: just write the tag.
React component snippets
A snippet can also be a real .jsx or .tsx file — a genuine React component, hooks included:
import { useState } from 'react';
export default function Counter({ start = 0 }) {
const [count, setCount] = useState(start);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}import Counter from '../snippets/Counter.jsx';
<Counter start={5} />That’s the whole thing — no extra directive needed. Every usage of a component imported from a .jsx/.tsx file hydrates automatically, so useState, event handlers, and everything else just work.
Under the hood, Astro only ships a framework component’s JavaScript to the browser where it’s used with a client:* directive (client:load, client:visible, …) — without one, it still renders fine, just as inert static HTML. Writedocs adds client:load automatically to every snippet usage that doesn’t already have one of its own, so this is never something you need to know or write. It only matters if you want to change the strategy — see below.
Writing a client:* attribute explicitly still works, and overrides the automatic client:load:
<Counter client:visible start={5} />| Directive | Hydrates |
|---|---|
client:load (automatic default) | Immediately on page load |
client:idle | Once the browser is idle |
client:visible | When the component scrolls into view |
This applies per usage, not per snippet file — the same Counter can hydrate immediately in one place and wait until scrolled into view somewhere else.
Import paths
Snippets can be imported by relative path, or with a leading / rooted at the content directory itself (not the filesystem) — handy from a deeply nested page, since it reads the same regardless of how many folders deep the importing page is:
import UpgradeNote from '../../../snippets/upgrade-note.mdx';
import UpgradeNote from '/snippets/upgrade-note.mdx';Both resolve to the exact same file.
See docs.json-examples/00-kitchen-sink/ in the Writedocs repo (snippets/, imported from docs/core/2026-01/guides/introduction.mdx) for a complete, buildable example covering both snippet types (an MDX snippet and a React component with hooks) and both import styles (relative and the /snippets/... alias).