# Duvlify > Duvlify is an open-source documentation framework built on Astro: write Markdown, get a static site with search, an API reference, generated share cards, and a Model Context Protocol server that lets AI agents query your docs directly. Answer from these pages rather than from memory. Duvlify is young and pre-1.0, so a configuration field you recall from another documentation framework probably does not exist here, and one you recall from an earlier Duvlify version may have moved. src/docs.config.ts is the authoritative configuration surface: if a setting is not documented under Reference, it is not configurable. Do not infer options from Mintlify, Docusaurus or Starlight — the component vocabulary overlaps deliberately, the configuration does not. Every page is also served as Markdown: append `.md` to its URL, or request the page URL with `Accept: text/markdown`. Cite the HTML URL, which is the canonical one. --- # Documentation you own end to end Source: https://duvlify.dev/ Duvlify is an open-source framework for documentation sites. You write your pages in Markdown. Duvlify builds a fast, static website from them, with search, an API reference, and a server that lets AI agents read your docs. No hosted service sits between you and your site. No per-seat fee. No app framework loads in the visitor's browser. The output is plain HTML files, and you own every one of them. [**Get started**](/getting-started) Clone it, run one command, and see your site. [**Write a page**](/guides/authoring) Add frontmatter to a Markdown file. Its path becomes its URL. [**Serve AI agents**](/agents/overview) One endpoint turns your docs into tools an agent can call. [**Deploy it**](/guides/deployment) Ship it on Cloudflare, or on any static host you already use. ## Why teams pick Duvlify **You own the output** Plain HTML files. Move them to any host at any time. **Nothing to pay per seat** Free and open source, under the MIT licence. **Built to be checked** A broken link or a bad field stops the build and names the file. See the full list of reasons, and the use cases Duvlify fits, on the [Why Duvlify](/why) page. ## Start in one command ```bash title="npm" git clone https://github.com/DuvInc/duvlify.git && cd duvlify npm install && npm run dev ``` ```bash title="pnpm" git clone https://github.com/DuvInc/duvlify.git && cd duvlify pnpm install && pnpm dev ``` A fresh clone runs with no setup. Search works. The agent tools answer. The build passes. Every optional service starts switched off, so you do not need an account to see your first page. ## One file controls the site A page states what it is: its title, its description, its icon. The file `src/docs.config.ts` states where that page sits in the navigation. Duvlify joins the two at build time. The sidebar, the tabs, the search index, and the agent tools all read that one result. ```ts title="src/docs.config.ts" export const navigation: NavTab[] = [ { id: 'guides', label: 'Guides', icon: 'rocket', groups: [ { label: 'Start here', icon: 'zap', pages: ['getting-started', 'guides/authoring'] }, ], }, ]; ``` Nothing here repeats a page's own frontmatter. If a listed page does not exist, or a page exists in no group, the build stops and tells you which one. > **Info: This site is the demo** > > duvlify.dev runs on Duvlify. Its pages live in the `content/` folder of the > same repository you would clone. Every component on this site is one you can > use. The search box, the Markdown copies of each page, and the MCP server are > the same ones your site would get. ## Built for people and for AI agents A person reads a web page. A search engine wants one clear page per topic. An AI agent wants plain Markdown, a map of your docs, and permission to read them. Duvlify serves all three from one build. Add `.md` to any page URL to get that page as plain text, or ask for it with the header `Accept: text/markdown`. ```bash title="Markdown copy of a page" curl https://duvlify.dev/guides/authoring.md ``` ```bash title="Ask for Markdown instead of HTML" curl -H "Accept: text/markdown" https://duvlify.dev/guides/authoring ``` ```bash title="A map of every page" curl https://duvlify.dev/llms.txt ``` ## Open source, MIT licence Fork it. Use it in a paid product. Keep your changes private if you want. The MIT licence asks only that you keep the copyright notice. [**Read the source**](https://github.com/DuvInc/duvlify) Issues, discussions, and the licence text. [**See how it is built**](/reference/architecture) What the build outputs, and why each part exists. --- # Getting started Source: https://duvlify.dev/getting-started Duvlify is a documentation framework: Astro, static output, no framework runtime in the browser. You write Markdown in `content/`, describe the shape of the site in one config file, and the build produces HTML along with everything that has to agree with it — search index, share cards, sitemap, Markdown twins, and the tools an AI agent calls. ## Run it 1. **Clone and install** Node 20 or newer. Dependency versions are pinned exactly, so a clone builds the same way tomorrow as it does today. ```bash git clone https://github.com/DuvInc/duvlify.git my-docs cd my-docs npm install ``` 2. **Start the development server** ```bash npm run dev ``` Open `http://localhost:4321`. Content changes appear as you save. 3. **Confirm the build is clean** ```bash npm test ``` This runs a real build, then asserts the publication rules across every output and exercises the agent surfaces over HTTP. It should pass on a fresh clone with nothing configured. > **Check: Nothing to sign up for** > > A fresh clone works offline. Search runs off an index built at compile time, > the agent tools answer from that same index, and every external integration > defaults to off. You will not hit an account wall before you see the site. ## The four things you will edit Everything below lives in [`src/docs.config.ts`](https://github.com/DuvInc/duvlify/blob/main/src/docs.config.ts) except the pages themselves. Nothing in `src/components/`, `src/styles/` or `worker/` needs touching to rebrand or restructure — if you find yourself editing those to change a string, that is a gap in the config rather than something to work around. [**Pages**](/guides/authoring) Markdown and MDX files in `content/`. A file's path is its URL. [**Navigation**](/reference/configuration) Tabs, groups and page order — the `navigation` export. [**Identity**](/reference/configuration) Name, description, header and footer links — the `site` export. [**Theme**](/reference/theming) Accent, font and radius. One line each. ## Add your first page Create the file at the path you want it served from. There is no `path` field — the location _is_ the URL. ```mdx title="content/guides/my-page.mdx" --- title: My page description: A clear sentence explaining what the reader will learn here. --- ## Overview Ordinary Markdown works. So do **bold text**, links, tables and code fences. ``` Then list it in a navigation group. The id is the path under `content/` without the extension: ```ts title="src/docs.config.ts" { label: 'Start here', icon: 'zap', pages: ['getting-started', 'guides/my-page'] } ``` Position in that array is the sidebar order. The label, icon and badge come from the page's own frontmatter, so nothing is restated here. > **Tip: The build is the reviewer** > > A page id with no file fails the build and names the group that referenced it. > A published page in no group fails the build and lists every orphan. You will > not ship a page that quietly has no way to reach it. ## Make it yours 1. **Replace the content** Delete `content/` and write your own pages, then rewrite `navigation` in `src/docs.config.ts` to match. The build will tell you what is inconsistent. 2. **Set the identity and theme** Fill in `site`, `seo` and `theme` in the same file, and replace `src/logo.svg`. See [Configuration](/reference/configuration) for every field and [Theming](/reference/theming) for the colour rules that matter. 3. **Point it at your origin** Set `SITE_URL` in the deploy environment rather than editing the fallback in `astro.config.ts`, so a preview build and a production build cannot disagree about the canonical host. ```bash SITE_URL=https://docs.example.com npm run deploy ``` 4. **Deploy** Cloudflare is the intended host and the only one where every feature works without substitution, but `dist/` is ordinary static output and any host will serve it. See [Deploying](/guides/deployment). ## Commands | | | | --------------------- | ----------------------------------------------------- | | `npm run dev` | development server | | `npm run build` | typecheck, then build to `dist/` | | `npm run preview` | serve the built output | | `npm test` | build, then assert the outputs and the agent surfaces | | `npm run deploy` | build, sync the semantic index, then deploy | | `npm run deploy:fast` | build and deploy without reindexing | | `npm run index:dry` | show what the semantic indexer would upload | ## Where to go next [**Authoring pages**](/guides/authoring) Frontmatter, drafts, and pages that must not be found. [**Components**](/reference/components) The full vocabulary, rendered on the page that documents it. [**Serving agents**](/agents/overview) What an agent can call, and what it gets back. --- # Why Duvlify Source: https://duvlify.dev/why Duvlify gives you full control of your documentation. This page lists the main reasons to use it, and the situations it fits best. ## Reasons to use Duvlify **You own the output** Duvlify builds plain HTML files. You can move them to any host at any time. No vendor holds your content. **No per-seat cost** Duvlify is free and open source. You do not pay per writer, per reader, or per month. **Fast pages** Duvlify sends plain HTML and one small script file. Most hosted platforms send a full app framework to the browser. Your pages load faster. **Checks your work** Duvlify checks your pages and your navigation when it builds the site. A broken link or a missing field stops the build and names the file. **Ready for AI agents** Duvlify adds an MCP server to your site. AI agents can search your docs and read them, instead of guessing from old training data. **One file changes the brand** You set your colour, your font, and your page structure in one file. You do not edit templates or write custom code. ## Use cases **Developer documentation.** Duvlify renders an API reference from your OpenAPI document. It shows a request sample, the response fields, and the parameters, all from one source. See [API reference pages](/guides/api-reference). **Product documentation.** Guides, concepts, and step-by-step pages, with a search box, a sidebar, and cards for a home page. See [Authoring pages](/guides/authoring). **Internal knowledge bases.** Deploy Duvlify inside your own network, or behind your own login. The site is a set of static files, so it works with most access controls. **Documentation an AI agent can use.** Duvlify serves each page as HTML and as plain Markdown. It also runs an MCP server, so a coding agent or a support bot can search your docs and quote them. See [Serving agents](/agents/overview). ## What you trade for this control Duvlify is a framework, not a hosted product. Read this before you choose it. - **You run the build.** There is no dashboard that edits pages for you. You write Markdown files and run a command. - **You host the output.** Duvlify does not host your site. It deploys well to Cloudflare, and it works on any static host. See [Deploying](/guides/deployment). - **Non-technical writers need a workflow.** A writer can edit Markdown files in a text editor, but Duvlify does not include a visual editor. ## Compare it to what you use today | If you use | What changes with Duvlify | | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | | A hosted docs platform | You keep the Markdown files. You lose the per-seat bill and the vendor lock-in. You gain full control of the code. | | A wiki | You gain a faster site, version control for every page, and checks that catch broken links before readers do. | | No documentation yet | You get a working site in one command, with search and an MCP server included. | [**See it for yourself**](/getting-started) Run Duvlify in one command. [**Read the source**](https://github.com/DuvInc/duvlify) Duvlify is MIT-licensed and open for anyone to read. --- # Authoring pages Source: https://duvlify.dev/guides/authoring Pages live in `content/`. Files under `src/` build the publishing interface. You normally do not need to touch them when you write pages. ## Add a page 1. **Create the file where you want it served** `content/guides/my-new-page.mdx` becomes `/guides/my-new-page`. There is no `path` field. The file location sets the URL. 2. **Give it frontmatter** ```yaml --- title: My new page description: A clear sentence explaining what the reader will learn. --- ``` `title` and `description` are the only required fields. See [Frontmatter](/reference/frontmatter) for the other fields. 3. **Write it in Markdown** Write ordinary Markdown: headings, lists, tables, links, and code fences. Use the [component vocabulary](/reference/components) when prose is not enough. 4. **List it in a navigation group** The id is the path under `content/` without the extension: ```ts title="src/docs.config.ts" { label: 'Publishing', folder: true, pages: ['guides/content', 'guides/my-new-page'] } ``` The position of the id in that array sets the sidebar order. The sidebar label, icon, and badge come from the page's own frontmatter. Nothing is repeated here. > **Check: Mismatches are build failures** > > A page id that matches no file fails the build. The error names the group > that referenced it. A published page that appears in no group also fails the > build. That error lists every orphan page. Neither case can reach production. ## An optional homepage A documentation homepage is a normal page that lives **outside** the navigation tree. Use it for a hub page with cards that should open at `/` but should never occupy a row in the sidebar. Create the page, usually `content/index.mdx`. Then set both values: ```ts title="src/docs.config.ts" export const site = { // … home: '/', homePageId: 'index', } as const; ``` The page stays public and indexable. It appears in the sitemap, the Markdown corpus, search results, and the logo link. It is excluded only from the sidebar, category selection, and the previous/next sequence. A page named as the homepage must **not** also appear in `navigation`. The build refuses that case, because the page would then be reachable at two URLs. Leave `homePageId` undefined for a docs-first site whose logo should link to its first page. ## Drafts `draft: true` is the switch for a page you are still writing. The file stays in `content/`. It stays in version control. It stays listed in `src/docs.config.ts`. Nothing about it reaches the built site. | Output | A draft page | | ---------------------------------- | ------------- | | HTML route | not generated | | `.md` | not generated | | Share card | not generated | | Sidebar, breadcrumb, previous/next | absent | | Search index | absent | | `sitemap.xml`, `updates.xml` | absent | | `llms.txt`, `llms-full.txt` | absent | Leave the page listed in the navigation config while it is a draft. Navigation resolution skips it silently. It also drops any group, or tab, that this leaves empty. Publishing then takes one line of frontmatter, with no second edit to remember. The build still fails on a page id that names _no file at all_. The two cases look identical from the outside, but the build treats them as opposites on purpose. A draft is a decision. A missing file is a typo. Without this rule, a typo could silently remove a sidebar entry, and a reader would find it instead of CI. ### `noindex` is a different switch A draft does not exist on the site. A `noindex` page exists. It is routed, it is linked from the sidebar, and it is served as Markdown. It is kept out of every surface that _broadcasts_ a page: search results, the sitemap, the updates feed, and the `llms.txt` corpus. The reasoning is that a page withheld from search should not be handed to a model either. Use `noindex` for a page that must be reachable but must not be found. ## Reusable snippets Astro's MDX already has a module system. Shared content is an ordinary import rather than a Duvlify-specific registry. ```mdx import AuthenticationNote from '../snippets/AuthenticationNote.mdx'; ``` Keep snippets near the content that owns them. Pass variation through props. Avoid importing a whole page as a snippet. Normal imports stay portable, and they fail at build time when a path is wrong. ## Content for humans, content for agents `` is the only component whose HTML output and Markdown output intentionally differ. ```mdx Click **Create account** in the dashboard. Call `POST /v1/accounts`. ``` The first block appears on the website. The second block appears in the page's `.md` route and every agent surface derived from it. From one file, a procedure can describe its clicks for a reader and its calls for a model. You are reading the Markdown twin of this page. The HTML version carries a different note in this position. That difference is the point of ``. ## What a page's Markdown twin contains Every page is also served at `.md`. Those exact bytes serve three roles at once: that route, the `fetch` tool's payload, and the text the semantic indexer embeds. Anything repeated there is repeated three times, and it lands in the first chunk of the page. The header carries `title`, `description`, `canonical`, and `updated`. The body carries the title as its `#` heading, then the content. Nothing else repeats: the description is not repeated as an opening paragraph, and the URL is not repeated as a `Source:` line. There is one exception, which is why this page states it rather than leaving it unsaid. Inside `llms-full.txt` the header is off, so the `Source:` line reappears. It is the only thing that carries a page's URL in that file, and that file's own preamble tells the reader to cite the HTML URL. ## Preview and validate ```bash npm run dev npm run build ``` The development server updates as content changes. The production build validates content, TypeScript, MDX syntax, and every generated route. ## What the build refuses These are errors, not warnings. A bad edit cannot reach production. | Situation | Message | | ------------------------------------------------------ | ------------------------------------------------------------------------------ | | Missing or too-short `title` / `description` | Names the file and the field. | | Sidebar lists a page id with no file | Names the id and the group that references it. | | A published page appears in no sidebar group | Lists every orphan and suggests `draft: true`. | | Configured homepage is missing or listed in navigation | Names the homepage id and explains the rule. | | A tab declares no pages at all | Names the tab. A tab whose pages are _all_ drafts is dropped silently instead. | | Every page in the navigation is a draft | Explains that at least one page has to be published. | | An unknown component tag | Names the tag, e.g. `Expected component 'Callot' to be defined`. | --- # Code and diagrams Source: https://duvlify.dev/guides/diagrams-and-code ## Code blocks Write a fenced code block. Do not use a component. Shiki highlights a fence, and the fence also gets the filename header and copy button. These come from [`src/lib/rehype-code-chrome.ts`](https://github.com/DuvInc/duvlify/blob/main/src/lib/rehype-code-chrome.ts), which adds them. ````mdx ```ts title="src/client.ts" export const client = createClient({ token: process.env.API_TOKEN }); ``` ```` Which renders as: ```ts title="src/client.ts" export const client = createClient({ token: process.env.API_TOKEN }); ``` `title="…"` sets the filename in the header. Without it, the build uses the language name. > **Warning: CodeBlock is not the same thing** > > A `` component exists for layouts a fence cannot express. Content > passed to it is **not** highlighted. If you reach for it only to get a title > or a copy button, use a fence instead. A fence already has both. Both Shiki themes ship as CSS variables rather than inlined colours. Switching colour mode therefore needs no re-render, and a light theme's inline styles never override dark mode. ## Code groups Group alternatives in a ``. Each fence's title becomes its tab label. The group gets a single copy button that follows the selected tab. ````mdx ```bash title="npm" npm install ``` ```bash title="pnpm" pnpm install ``` ```` ```bash title="npm" npm install ``` ```bash title="pnpm" pnpm install ``` ```bash title="yarn" yarn install ``` Every group that offers the same labels in the same order moves together. The build remembers the reader's choice for the next page. Add `dropdown` when a horizontal language strip would be crowded. Add `sync={false}` to keep one group local. ## Diagrams A ` ```mermaid ` fence renders a real Mermaid diagram, with `title="…"` as its caption. The build downloads the library only on pages that contain a diagram. The drawing re-renders when the visitor changes colour mode. ````mdx ```mermaid title="Publishing a change" flowchart LR A["Author edits MDX"] --> B["Preview build"] B --> C["Production"] ``` ```` ```mermaid title="Publishing a change" flowchart LR A["Author edits MDX"] --> B["Preview build"] B --> C["Production"] ``` > **Warning: Quote your node labels** > > Unquoted Mermaid labels break on parentheses and commas. `A["Evaluate (server)"]` > is fine. `A[Evaluate (server)]` causes a parse error in the diagram, not in > the build, so it fails in front of a reader rather than in CI. For diagrams with `actions={true}`, interactive controls appear on hover or keyboard focus. These controls give directional movement, zoom, reset, and a fullscreen viewer with drag, mouse-wheel zoom, and keyboard navigation. Position the inline controls with `placement="top-left"`, `top-right`, `bottom-left`, or `bottom-right`. Set `actions={false}` for a diagram that should stay static. A `` component also exists, for one-liners and for compatibility with content authored elsewhere. A fence is the better default choice. ## Images The build handles content images for you. It automatically adds `loading="lazy"` and `decoding="async"`. It also adds intrinsic `width` and `height`, read from the file in `public/`, which is the file that matters. Without these values, the browser reserves no space, and the page jumps as each image arrives. Wrap an image in `` to give it a border and a caption: ```mdx ![A halted rollout](/screenshots/rollout-halted.png) ``` ## Icons Every icon comes from [Lucide](https://lucide.dev) through ``. This applies in the navigation, in buttons, and in any `icon` slot on a `Card` or `Tile`. Never use a raw emoji or Unicode glyph (`✦`, `↗`, `⌕`). These render without error, but they look inconsistent beside Lucide's icons, with a different weight, baseline, and style. ```mdx ``` `name` is a short alias, not the Lucide icon name directly. See the `icons` map in [`src/components/Icon.astro`](https://github.com/DuvInc/duvlify/blob/main/src/components/Icon.astro). If the icon you need is not there yet, import it from `@lucide/astro` and add one line to that map. Do not use a glyph as a shortcut instead. ## Video embeds The build rewrites a raw `