NookDocs renders your MDX with a built-in shell — sidebar, header, search modal, theme system, footer. For 95% of tenants this is the right default. The other 5% need something the default shell doesn't provide: docs embedded in the product UI, docs rendered via a mobile app, docs as part of a larger marketing site with custom chrome, docs that feed into a third-party helpdesk tool.
This guide covers the headless pattern — fetching your docs via API, rendering with your own UI — when it makes sense, and when to push back against it.
When to go headless
In-product docs panel
You want docs to appear inside your product's dashboard as a slide-out panel, with your own chrome. Users never leave the product to get help.
Mobile app docs
Your product has a native iOS / Android app. Users need in-app documentation that matches the app's visual identity, not a web-shell-in-a-WebView.
Marketing-site integration
Docs are one section of a larger corporate site with its own nav + footer. You want the MDX content, not the docs shell.
Customised search / chat UX
You want to build a domain-specific chat interface (e.g. an AI assistant that blends docs + your internal KB + real-time product state).
When to NOT go headless
You want a different theme
NookDocs ships 10 themes + full token-level customisation. Try theme before rebuilding from scratch.
You dislike the default header
Replace via config — logo, links, CTA, banner all tenant-configurable. See site settings.
You want your brand colour
colors.primary / colors.light / colors.dark cover it.
You want to add a GA pixel
integrations.ga4 exists. 16 analytics providers ship. See integrations overview.
Most "I need custom docs" asks turn out to be "I haven't explored the config". Exhaust config options before committing to headless work — the maintenance cost is real.
The trade-offs
Going headless shifts responsibility:
| Concern | Default shell | Headless |
| Rendering the MDX | NookDocs | You — use @mdx-js/react or similar |
| Component library | NookDocs ships 47 components | You implement every component you use |
| Search | Built-in ⌘K | You build or integrate |
| Theme tokens | Automatic light/dark | You manage |
| SEO meta + sitemap | Auto-emitted | You emit |
/llms.txt + RSS | Auto-generated | Optional — you integrate if needed |
| Versioning + multi-language | Built-in | You implement |
| Analytics beacon | Auto | Your tool chain |
You're not losing the content-management story (still author MDX, still edit in the dashboard, still auto-sync from Git) — you're losing the rendering-to-production pipeline. That's more work than it sounds.
The API surface for headless
NookDocs exposes JSON + Markdown endpoints for programmatic consumption:
Page content
# Raw MDX for one page (agent-filtered)
GET https://{tenant}.nookdocs.site/<page-path>.md
# Same page, JSON envelope with frontmatter + source
GET https://api.nookdocs.com/v1/projects/{projectId}/pages/<page-path>Search
POST https://api.nookdocs.com/v1/projects/{projectId}/pages/search
Content-Type: application/json
{
"query": "custom domain",
"limit": 10
}Returns ranked results with snippet highlights. See API reference for the full shape.
Navigation
# The parsed navigation tree (tabs + groups + pages)
GET https://api.nookdocs.com/v1/projects/{projectId}/configReturns the nookdocs.config.json navigation as structured JSON. Your frontend renders a sidebar from this.
AI assistant
POST https://{tenant}.nookdocs.site/api/v1/projects/{projectId}/assistant/ask
Content-Type: application/json
{
"query": "How do I set up webhooks?",
"history": [...]
}Returns a streamed SSE response. Your frontend subscribes + renders incrementally. See AI assistant.
llms.txt + sitemap
Still auto-generated at the tenant URL. Useful even in headless mode for LLM crawlers that expect the standard endpoints:
GET https://{tenant}.nookdocs.site/llms.txt
GET https://{tenant}.nookdocs.site/llms-full.txt
GET https://{tenant}.nookdocs.site/sitemap.xmlA minimal headless React example
// DocsPanel.tsx — in-product slide-out docs
import { useState, useEffect } from "react";
import { MDXRemote } from "next-mdx-remote/rsc";
import { serialize } from "next-mdx-remote/serialize";
async function fetchDoc(path: string) {
const res = await fetch(
`https://api.nookdocs.com/v1/projects/${PROJECT_ID}/pages/${path}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } },
);
const json = await res.json();
return await serialize(json.source_mdx, { parseFrontmatter: true });
}
export function DocsPanel({ path }: { path: string }) {
const [content, setContent] = useState<any>(null);
useEffect(() => {
fetchDoc(path).then(setContent);
}, [path]);
if (!content) return <div>Loading...</div>;
return (
<aside className="docs-panel">
<MDXRemote
source={content}
components={{
// Your component implementations
Callout: YourCallout,
Card: YourCard,
ParamField: YourParamField,
// ...47 more components to implement
}}
/>
</aside>
);
}The components prop is where the real work lives — you're implementing every NookDocs component in your own design system. Ship 1 at a time as pages need them.
Component strategy
Three approaches, increasing effort:
1. Minimal set (ship fast)
Implement only the ~15 most-used components: <Callout>, <Note>, <Warning>, <Info>, <Card>, <CardGroup>, <Steps>, <Step>, <Tabs>, <CodeGroup>, <ParamField>, <ResponseField>, <Frame>, <Accordion>.
Unknown components render as raw children (ugly but functional). Good for shipping an MVP.
2. Full set (parity)
Implement all 47 components to match NookDocs's built-in library. Expensive — 2-4 weeks of component work. Pays off if docs are central to your product.
3. Subset + redirect
Implement 15-20 core components. For pages that use fancy components (like <ColorPalette> or <Mermaid>), render a "View this page in full" link that opens the NookDocs-hosted version.
Most teams land at approach 2 or 3.
SSR vs client-only
Headless docs work either as part of an SSR app (Next.js, Remix, Astro) OR a fully-client-rendered SPA:
SSR — fetch at build time or per-request, serialize MDX, hydrate. Better SEO, faster first paint. Preferred for public-facing headless docs.
Client-only — fetch on mount, serialize + render in the browser. Worse SEO (search engines see empty shell), faster iteration. OK for in-product panels where SEO doesn't matter.
For in-product docs (private, authenticated), client-only is usually fine — your users authenticate before seeing docs, so Google can't crawl anyway.
Performance considerations
Don't fetch MDX on every render — cache aggressively. Most docs pages don't change minute-to-minute.
Lazy-load heavy components —
<Mermaid>needs Mermaid.js;<CodeGroup>needs Prism. Lazy-load to keep initial bundle lean.Stream where possible — the assistant endpoint streams SSE; render tokens as they arrive, don't wait for completion.
Prefetch likely next pages — docs readers tend to follow the "Next" footer link; prefetch on hover.
When to reconsider
At some point, implementing + maintaining a headless docs pipeline becomes more work than learning to customise the default shell. Signs you should reconsider:
You've built 20+ components and are still hitting edge cases.
Every shipping change needs a coordinated update across MDX + your frontend + your search index.
Users ask for features the default shell has (search modal, copy-for-LLMs, RSS, etc.).
Migration back to the default shell is usually straightforward — your MDX is portable, your config translates, your search index is already in NookDocs. The hard part is sunsetting your custom work without disruption.
Hybrid pattern — partial headless
Compromise pattern: use the NookDocs default shell for docs.acme.com + embed specific pages in-product via the API:
docs.acme.com — full default shell (public docs)
app.acme.com/help-panel — headless in-product docs (private)Authoring + editing happens in one place. Rendering branches by audience. Less work than full headless; still covers the in-product use case.
Common headless pitfalls
Related
Theme — full theme + token customisation before headless
Site settings — banner / SEO / navbar / footer config
AI assistant config — the streaming chat API
Markdown export — the
.mdURL patternllms.txt — LLM crawler index
Schema reference — full config surface