Next.js

Create a Next.js documentation site with Heyo Docs, or add the smallest App Router integration to an existing app.

For a new documentation site, use create-heyo-docs. The Next.js template configures MDX, produces the static content registry that the App Router requires, writes endpoint assets for OpenAPI pages, and supplies the route and deployment glue. It is the recommended option for both a fast start and a fully static documentation build.

Pass next as the template to create a repeatable project. Without flags, the same creator opens an interactive setup flow.

bun create @heyo-sh/heyo-docs my-docs --template nextcd my-docsbun run dev

The creator makes a standalone app rather than changing an existing Next.js project. It watches content during development and runs the generator before a production build, so edits to MDX and configuration become type-safe generated modules under app/_heyo-docs.

Manual, minimal installation

Use this route when documentation must be added to an existing Next.js App Router project. The setup below intentionally omits icons, colour-mode storage, SEO/resource routes, OpenAPI endpoint details, and the optional request proxy. Choose the generator whenever those features are required.

1. Install dependencies

bun add @heyo-sh/heyo-docs @fontsource-variable/figtree shadcn tw-animate-cssbun add -d @next/mdx @mdx-js/loader @mdx-js/react @tailwindcss/postcss rehype-slug remark-frontmatter remark-gfm tsx tailwindcss

2. Create the config and first page

At the application root, add heyo-docs.config.ts and content/index.mdx.

heyo-docs.config.ts
import { heyoDocs } from "@heyo-sh/heyo-docs/config";

export default heyoDocs({
  title: "Acme Docs",
  description: "Documentation for Acme.",
  content: "content",
  theme: "grain",
  groups: [
    {
      group: "Documentation",
      sections: [{ section: "Get started", pages: ["index"] }],
    },
  ],
  branding: { name: "Acme" },
});
mdx
---title: Welcomedescription: Start here.---# WelcomeYour documentation is ready.

3. Compile MDX and generate the registry

Next.js cannot consume Vite virtual modules, so the adapter writes a derived registry under app/_heyo-docs. Keep that directory generated and out of your authored content.

next.config.ts
import createMDX from "@next/mdx";
import { heyoDocsMdxOptions } from "@heyo-sh/heyo-docs/next";

import config from "./heyo-docs.config";

export default createMDX({
  options: heyoDocsMdxOptions({ root: process.cwd(), content: config.content }),
})({
  pageExtensions: ["ts", "tsx", "md", "mdx"],
});
scripts/generate-heyo-docs.ts
import { generateNextContent } from "@heyo-sh/heyo-docs/next";
import config from "../heyo-docs.config";

await generateNextContent({ config, root: process.cwd() });

Run tsx scripts/generate-heyo-docs.ts before next dev, next build, and type checking. The generated template already wires this into its scripts and watches the content directory during development.

4. Render static documentation routes

Import the generated page registry in a small client component:

app/components/docs-app.tsx
"use client";

import { DocsApp } from "@heyo-sh/heyo-docs";
import { docsConfig, pages } from "../_heyo-docs/content";

export function NextDocsApp({ pathname }: { pathname: string }) {
  return <DocsApp config={docsConfig} pages={pages} pathname={pathname} />;
}

Then add the optional catch-all route. generateStaticParams() makes every MDX URL a static page at build time.

app/[[...slug]]/page.tsx
import { NextDocsApp } from "../components/docs-app";
import { docsModel } from "../lib/docs";

export const dynamic = "force-static";

export async function generateStaticParams() {
  return docsModel.pages.map(({ slug }) => ({
    slug: slug === "/" ? [] : slug.slice(1).split("/"),
  }));
}

export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug } = await params;
  const pathname = slug?.length ? `/${slug.join("/")}` : "/";
  return <NextDocsApp pathname={pathname} />;
}

The route above reads docsModel from a server-only helper. For the minimal case, create app/lib/docs.ts with the generated page metadata:

ts
import { createDocsModel, type DocsPage } from "@heyo-sh/heyo-docs/node";

import config from "../../heyo-docs.config";
import { docsPages } from "../_heyo-docs/server";

export const docsModel = createDocsModel(
  config,
  docsPages as unknown as DocsPage[],
);

Finally, import the theme in app/app.css:

css
@import "@fontsource-variable/figtree";@import "@heyo-sh/heyo-docs/theme/grain.css";:root {  --heyo-docs-font-family: "Figtree Variable", sans-serif;}

This is enough for static MDX documentation. The generated template is the right next step when you need navigation without full reloads, API reference data, metadata routes, or a runtime request action.

Rendering strategy for the Next.js template

This rendering strategy applies to the Next.js template generated by create-heyo-docs.

The Next.js template uses the App Router to statically generate every known documentation and OpenAPI page. It retains a runtime for the optional Try it proxy and the existing Markdown/discovery route handlers. This is not a pure output: "export" application: the reader-facing HTML pages and OpenAPI data are static and CDN-cacheable, while a small set of auxiliary actions can still run on demand.

flowchart LR
A[MDX and OpenAPI sources] --> B[generate-heyo-docs script]
B --> C[Generated page and server registries]
B --> D[public endpoint JSON shards]
C --> E[next build and generateStaticParams]
E --> F[Static HTML]
D --> G[CDN]
F --> G
H[Try it POST] --> I[Next Route Handler]
I --> J[Declared API server]

Build pipeline

Before development and production builds, scripts/generate-heyo-docs.ts runs generateNextContent(). It scans MDX, parses OpenAPI documents, validates the navigation model, and writes generated modules beneath app/_heyo-docs.

The catch-all documentation page exports generateStaticParams() from that model and is configured with dynamic = "force-static". Every known MDX and OpenAPI URL is rendered during next build; no request-time OpenAPI lookup is needed for a reader to reach a page or receive its metadata.

Static endpoint payloads

The generated client module holds a compact endpoint index. Detailed endpoint information is emitted under public/_heyo-docs/openapi and is served as static JSON:

text
/_heyo-docs/openapi/<group>/<tag>/<operation>.json

While rendering each static API page, the Server Component passes that route’s endpoint payload to the interactive documentation component. The generated HTML and React hydration start with the same complete data, so an API page does not render a compact shell and then visibly change when detail data arrives.

The JSON remains a public build artefact, served by the CDN rather than a Route Handler, for integrations and as a static fallback when an integration starts from the compact index alone. Endpoint payloads retain only the component schemas reachable from that operation, which prevents a large OpenAPI document from appearing in the shared JavaScript bundle.

Dynamic boundary: API requests

POST /heyo-docs-internal/openapi-request remains a Next Route Handler. It is used exclusively by the Send request button and forwards a request to a server declared by the OpenAPI document. The route is dynamic because it executes an action, not because documentation data is dynamic.

Do not switch this project to output: "export" while the proxy is enabled: static export has no runtime server for POST handling. On Vercel the handler is served by the platform runtime; on Cloudflare the OpenNext deployment provides the Worker runtime. Static pages and assets remain independently cacheable.

The Markdown mirror and discovery route handlers retain their current on-demand behaviour so an installation without siteUrl can derive absolute URLs from its deployment origin. They are not involved in rendering documentation pages or loading endpoint data. Set siteUrl before deployment to make generated canonical and discovery URLs deterministic.

Operational model

  • Content and OpenAPI changes are published by running the generator, building, and deploying.
  • Pin remote schema URLs when reproducibility matters.
  • Large APIs increase build output and build time instead of user-facing SSR latency.
  • Remove the proxy route if the site does not offer interactive API calls; the documentation HTML and endpoint details continue to work as static content.

Next steps