Skip to Content
UtilitiesNextra Export

Nextra Export

Export a complete Nextra site as one Markdown file or a print-ready book. The PDF view renders the site’s real MDX and React components. Markdown components use a project-owned renderer registry.

Install

Terminal
pnpm dlx shadcn@latest add @utilities/nextra-export

The command writes the exporter below src/lib/nextra-export. It installs unified and the Remark packages used for MDX normalization. The target site must already use Nextra 4.

Load the documentation tree

Create one project-specific loader:

src/lib/docs-export.ts
import { collectNextraExportPages, loadNextraPageSources, } from "@/lib/nextra-export"; import { importPage } from "nextra/pages"; import { getPageMap } from "nextra/page-map"; export async function loadDocsExportPages() { const pages = collectNextraExportPages(await getPageMap()); return loadNextraPageSources(pages, importPage); }

Page order follows Nextra navigation. Set export: false in a page’s frontmatter to omit it.

Add the PDF route

Reserve /export/pdf inside the existing Nextra catch-all. Next 16 can let the root optional catch-all shadow a sibling page at the same URL.

app/[[...mdxPath]]/page.tsx
import { ExportBook } from "@/lib/nextra-export"; import { loadDocsExportPages } from "@/lib/docs-export"; async function PdfExportPage() { const pages = await loadDocsExportPages(); return ( <ExportBook title="My Docs" siteHref="https://docs.example.com" siteLabel="docs.example.com" pages={pages.map(({ Content, route, sectionPath, title }) => ({ content: <Content />, route, sectionPath, title, }))} /> ); } export default async function Page({ params }) { const { mdxPath } = await params; if (mdxPath?.join("/") === "export/pdf") return <PdfExportPage />; // Keep the site's existing importPage(...) implementation here. }

If the Nextra theme layout wraps the catch-all route, return children directly from that layout for export/pdf. This keeps the navbar and sidebar out of the book.

Add the button to the Nextra navbar:

import { ExportPdfButton } from "@/lib/nextra-export"; <Navbar logo={logo}> <ExportPdfButton /> </Navbar>;

The button opens the complete book and starts the browser print flow after its fonts and images settle. Choose Save as PDF to create one bundled file. The table of contents uses internal PDF jumps. Links inside exported pages are rewritten to canonical absolute URLs and printed in full after their labels, so they remain usable in strict PDF viewers and on paper. siteHref sets that canonical origin and adds the documentation link to the cover.

Add the Markdown route

Create a route handler outside the Nextra catch-all:

app/api/export/markdown/route.ts
import { exportMarkdownBundle } from "@/lib/nextra-export"; import { loadDocsExportPages } from "@/lib/docs-export"; export async function GET() { const markdown = await exportMarkdownBundle({ title: "My Docs", pages: await loadDocsExportPages(), siteUrl: "https://docs.example.com", }); return new Response(markdown, { headers: { "Content-Disposition": 'attachment; filename="my-docs.md"', "Content-Type": "text/markdown; charset=utf-8", }, }); }

Internal page links become anchors in the bundled document. Frontmatter and MDX module statements are removed.

Render custom MDX components

Pass a renderer for each project-specific component:

import type { ComponentRendererRegistry } from "@/lib/nextra-export"; export const components: ComponentRendererRegistry = { ProductCard: ({ attributes }) => `**${attributes.title}** — [Open product](${attributes.href})`, Diagram: async ({ attributes }) => `![${attributes.alt}](${await renderDiagramImage(attributes)})`, };

Renderers receive literal attributes, normalized Markdown children, and the current page. They may return text, tables, code, links, or generated image URLs. Unregistered components keep their children and produce a labeled fallback instead of disappearing.

Pass the registry as components to exportMarkdownBundle.

Last updated on