@agenetix/docs
v1.4.0
Published
App Router-native MDX documentation for Next.js with reusable layouts, search, TOC, and locale-aware routing.
Maintainers
Readme
Agenetix Docs
@agenetix/docs is an App Router-native MDX docs library for Next.js.
It is built for teams that want documentation content in the repo while still getting SEO-friendly prerendered routes, locale-aware file conventions, search, heading extraction, mobile-friendly docs UX, and a configurable docs shell that can either stay simple or plug into a richer app chrome.
This repository contains both the published package and the example site that dogfoods it:
srccontains the library sourcetestscontains Vitest coveragesiteis the example Next.js app and the package's own docs/showcase
Preview
The example site ships with a polished docs shell and an optional live theme studio for tuning presets, density, surface styles, and accent behavior.
Why use it
- Keep docs as repo-local MDX instead of a separate CMS
- Build navigation, route resolution, metadata, headings, and search from one
createDocsSource(...)call - Group pages into collapsible sidebar sections, with optional nested groups and
sidebar: falseto keep a page out of the nav - Render docs with App Router-friendly primitives like
DocsLayout,DocsPage, andDocsHomePage - Ship a polished baseline with
DocsSearch,DocsSidebar,DocsToc,HeadingLinks, andMobileDocsChrome - Support locale-specific files like
getting-started/en.mdx,getting-started/es.mdx, andgetting-started/zh.mdx - Restyle the shell with a nested theme object or a live theme provider
- Use bundled MDX building blocks such as banners, callouts, cards, tabs, steps, files, accordions, and code blocks
Requirements
- Node.js
>=20 - Next.js
>=15.5.0 <17 - React
^19 - React DOM
^19
Install
npm install @agenetix/docsThen import the shared stylesheet once in your docs layout:
import "@agenetix/docs/styles.css";Quick start
1. Create a docs source
Create lib/docs.ts:
import path from "node:path";
import { createDocsSource } from "@agenetix/docs";
export const docsSource = createDocsSource({
contentDir: path.join(process.cwd(), "content/docs"),
basePath: "/docs",
defaultLocale: "en",
locales: ["en", "es", "zh"],
hideDefaultLocaleInUrl: true,
sectionOrder: ["", "guides", "guides/search", "reference"],
groupLabels: {
"guides/search": "Search",
},
});docsSource becomes the single source of truth for route resolution, navigation, metadata, headings, and search.
2. Create the docs layout
Create app/docs/layout.tsx:
import { DocsLayout } from "@agenetix/docs";
import "@agenetix/docs/styles.css";
import { docsSource } from "@/lib/docs";
import { searchDocsAction } from "@/app/doc-actions";
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DocsLayout
navigation={docsSource.getNavigation()}
searchAction={searchDocsAction}
>
{children}
</DocsLayout>
);
}3. Create the docs route
Create app/docs/[[...slug]]/page.tsx:
import { notFound, redirect } from "next/navigation";
import { DocsHomePage, DocsPage } from "@agenetix/docs";
import { docsSource } from "@/lib/docs";
interface PageProps {
params: Promise<{ slug?: string[] }>;
}
export function generateStaticParams() {
return docsSource.getStaticParams();
}
export async function generateMetadata({ params }: PageProps) {
const { slug } = await params;
return docsSource.getMetadata(slug);
}
export default async function DocsRoutePage({ params }: PageProps) {
const { slug } = await params;
const resolved = docsSource.resolveRoute(slug);
if (resolved.type === "redirect" && resolved.href) {
redirect(resolved.href);
}
if (resolved.type !== "entry" || !resolved.entry) {
notFound();
}
if (resolved.entry.isHome) {
return (
<DocsHomePage
entry={resolved.entry}
navigation={docsSource.getNavigation()}
/>
);
}
return (
<DocsPage
entry={resolved.entry}
previousEntry={resolved.previousEntry}
nextEntry={resolved.nextEntry}
backHref={docsSource.getHref()}
/>
);
}4. Add your first doc
Create content/docs/getting-started/en.mdx:
---
title: "Getting Started"
description: "Your first Agenetix Docs page"
order: 0
---
Welcome to your docs.At that point, your app can render a static docs section from repo-local MDX under /docs.
Optional: faster route transitions
DocsPage streams the MDX body inside a Suspense boundary so the shell and title can paint first. For even snappier navigations in large doc sets, add a route-level loading.tsx next to your [[...slug]] page:
export default function DocsLoading() {
return <div className="emcydocs-prose emcydocs-mdx-loading" aria-busy="true" />;
}Navigation passed to DocsLayout should come from docsSource.getNavigation() — it returns metadata-only items (no MDX bodies).
Outside Next.js: @agenetix/docs/source
The content side of the library has no framework attached. @agenetix/docs/source exports createDocsSource, createBlogSource, extractHeadings, searchEntries, the href and slug helpers and the content types, and nothing in its import graph touches Next.js, React DOM or a component (a test pins that). Any server that runs Node can read the same content/docs/<slug>/<locale>.mdx folders and get routes, navigation, headings, metadata and search back.
import path from "node:path";
import { createDocsSource } from "@agenetix/docs/source";
export const docsSource = createDocsSource({
contentDir: path.join(process.cwd(), "content/docs"),
basePath: "/docs",
defaultLocale: "en",
locales: ["en"],
});
// In an Expo Router loader, an Express handler or a build script:
const route = docsSource.resolveRoute(slugs); // "entry" | "redirect" | "notFound"
const navigation = docsSource.getNavigation(); // sidebar sections, metadata only
const metadata = docsSource.getMetadata(slugs); // title, description, canonicalnext is an optional peer dependency, so a consumer that only uses this entry does not install Next.js. Rendering is yours on that side: route.entry.content is the MDX body, and route.entry.headings the table of contents. The root entry's DocsMdx, layouts and components stay Next.js-only.
Theme system
DocsLayout accepts a nested theme object for static theming, and it can also read live theme state from DocsThemeProvider when you want a runtime switcher, persisted preferences, or a theme studio.
import {
DocsLayout,
DocsThemeProvider,
type DocsThemeConfig,
} from "@agenetix/docs";
import { docsSource } from "@/lib/docs";
const docsTheme: DocsThemeConfig = {
color: {
preset: "ocean",
mode: "dark",
accentHue: 188,
accentStrength: "bold",
surfaceStyle: "elevated",
},
layout: {
density: "compact",
layoutWidth: "1480px",
contentWidth: "50rem",
sidebarWidth: "272px",
tocWidth: "232px",
},
shape: {
radius: "xl",
},
};
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<DocsThemeProvider initialTheme={docsTheme}>
<DocsLayout navigation={docsSource.getNavigation()}>
{children}
</DocsLayout>
</DocsThemeProvider>
);
}If you do not need live editing, pass the same object directly to DocsLayout via theme={docsTheme} and skip the provider entirely.
What ships today
- Source builders:
createDocsSource,extractHeadings - Docs shell primitives:
DocsLayout,DocsPage,DocsHomePage,DocsSearch,DocsSidebar,DocsToc,HeadingLinks,MobileDocsChrome - Theme primitives:
DocsThemeProvider,useDocsTheme(),resolveDocsTheme(theme) - MDX helpers and components:
DocsMdx,getDefaultMdxComponents(), plus banners, callouts, cards, tabs, steps, file trees, accordions, and code blocks - Optional blog primitives:
createBlogSource,BlogDirectoryPage,BlogPostPage,BlogSearch, and related components - Shared stylesheet:
@agenetix/docs/styles.css
Local development
npm install
npm run devThis starts:
- the library TypeScript build in watch mode
- the example site in
siteathttp://localhost:4003
The example site is both a development sandbox and the canonical reference for how the package is wired into a real Next.js app.
Useful scripts
npm run build:lib
npm run build:site
npm run lint
npm run test
npm run typecheckDogfooding in SqlOS
From sqlos/web, use a local file dependency:
{
"dependencies": {
"@agenetix/docs": "file:../../emcydocs"
}
}The SqlOS repo also includes helper scripts to switch between a local file dependency and a published npm version.
Publishing
publish-npm.yml is the only npm publish entry point. Merging to main does not publish latest.
- Pull request against
main— publishes a preview dist-tag, for example@agenetix/docs@pr-12. The workflow comments with the exact package ref. v*tag (for examplev1.1.3) — publisheslatest. Bumppackage.jsonfirst; npm will not overwrite an already-published version.
npm provenance is omitted because this source repository is private. The package itself still publishes publicly through trusted publishing.
To try a preview in a platform PR, add the ref to infra/preview-packages.json:
{
"@agenetix/docs": "@agenetix/docs@pr-12"
}