The headless Docusaurus content and MDX bridge for Octane.
Docusaurus has a useful boundary before React: its Node-side config, presets, and plugins load content, create data modules, and emit a serializable route tree. This package adopts that graph, normalizes it into a renderer-neutral manifest, resolves Docusaurus module aliases for Vite, and compiles Docusaurus-shaped MDX exports into real Octane components.
It does not run React themes or React-authored swizzles unchanged. Octane is compiler-first; the renderer and theme layers must be authored for Octane.
Version contract
The headless loader is intentionally pinned to @docusaurus/core@3.10.1.
Docusaurus does not publish the server/site loader as a stable public entry,
so accepting an untested minor would turn an internal upstream refactor into a
silent route-data corruption. The loader checks both the package version and
Docusaurus's Node >=20.0 runtime requirement before importing that seam. This
package retains Octane's repository-wide Node >=22.22.2 baseline.
allowUnsupportedVersion: true is available only for explicit compatibility
experiments.
Inspect the headless site graph
import { createDocusaurusManifest, loadDocusaurusSite, } from '@octanejs/docusaurus'; const loaded = await loadDocusaurusSite({ siteDir: process.cwd() }); const manifest = await createDocusaurusManifest(loaded);
The manifest contains:
- nested routes with
component,modules,props, and plugin context; - generated global data and per-document metadata;
- serializable site config, locale/document attributes, and plugin HTML tags;
- client modules discovered through the Docusaurus plugin lifecycle;
@site,@generated,~docs,@theme,@theme-original, and@theme-initresolution;- the exact Docusaurus version and route-path inventory.
The CLI exposes the same boundary:
octane-docusaurus inspect --site-dir . --out .octane-docusaurus/manifest.json octane-docusaurus clear --site-dir .
Vite
import { defineConfig } from 'vite'; import { octane } from 'octane/compiler/vite'; import { docusaurus } from '@octanejs/docusaurus/vite'; export default defineConfig({ plugins: [...docusaurus(), octane()], });
The bridge publishes virtual:octane-docusaurus-manifest and resolves
Docusaurus aliases. Its MDX plugin chooses Octane client/server compilation per
Vite environment and injects metadata discovered by the content plugins. The
route virtual module also imports plugin client modules, so theme CSS and other
side-effect assets participate in the client and SSR build graphs.
Octane classic theme
Add the first-party Octane theme beside the content plugins in
docusaurus.config.mjs:
import octaneClassicTheme from '@octanejs/docusaurus/theme'; export default { title: 'My documentation', url: 'https://docs.example.com', baseUrl: '/', themes: [octaneClassicTheme], plugins: ['@docusaurus/plugin-content-docs'], };
The theme supplies Octane-native DocsRoot, DocVersionRoot, DocRoot,
DocItem, category-index, and tag-page route modules. Its initial classic shell
renders configured navbar/footer links, recursive documentation sidebars,
document metadata, canonical links, previous/next navigation, and responsive
CSS. React-authored theme modules and swizzles still need Octane equivalents.
Client routing
The route virtual module turns every component, content module, generated data module, and route-context module into a static dynamic import. Only the matched branch loads in the browser:
import { createDocusaurusBrowserRouter } from '@octanejs/docusaurus/client'; import { manifest, routeModules, } from 'virtual:octane-docusaurus-routes'; export const router = createDocusaurusBrowserRouter(manifest, routeModules);
Render it from an Octane component:
import { DocusaurusRouterProvider } from '@octanejs/docusaurus/client'; import { manifest } from 'virtual:octane-docusaurus-routes'; import { router } from './router'; export function App() @{ <DocusaurusRouterProvider manifest={manifest} router={router} /> }
Nested Docusaurus routes render through children; route components also
receive loaded modules, static props, route, location, params, and
navigate. useDocusaurusRouteContext() exposes the inherited plugin identity
and merged route data. Use Link from @octanejs/remix-router for client-side
navigation.
Static rendering and hydration
The server entry resolves the requested lazy route branch through Remix's static handler, then prerenders fully resolved Octane markup. Use the route API when a host owns the outer HTML, or the document API to compose Docusaurus plugin tags, metadata, scoped CSS, build assets, and the hydration entry into a complete page:
import { prerenderDocusaurusDocument } from '@octanejs/docusaurus/server'; import { manifest, routeModules, } from 'virtual:octane-docusaurus-routes'; const rendered = await prerenderDocusaurusDocument( new Request('https://docs.example.com/guide/intro'), manifest, routeModules, { document: { assets: { stylesheets: ['assets/site.css'], modulePreloads: ['assets/intro.js'], }, hydrate: 'assets/hydrate.js', }, }, ); if (rendered instanceof Response) { return rendered; } const { html, bodyHtml, head, css, context } = rendered;
html is the complete <!DOCTYPE html> document. bodyHtml remains the
prerendered router root for integrations that need both forms. Relative asset
paths resolve against manifest.baseUrl; URL attributes are escaped, duplicate
asset entries are removed without reordering, and nonce from the render
options is carried onto module scripts. Docusaurus injectHtmlTags output is
trusted site configuration and retains upstream ordering around the
#__docusaurus root.
prerenderDocusaurusRoute() remains available and returns hoisted metadata in
head so an existing static-site host can place it in its own document head.
context.statusCode, loaderHeaders, and actionHeaders preserve the static
router result for the surrounding build or request handler. Generate a site by
calling this function for the paths in manifest.routesPaths; each render
imports only its matched route branch.
Hydrate the same root after the browser receives that markup:
import { hydrateDocusaurusRoot } from '@octanejs/docusaurus/hydrate'; import { manifest, routeModules, } from 'virtual:octane-docusaurus-routes'; const container = document.getElementById('__docusaurus'); if (container === null) throw new Error('Missing Docusaurus root.'); const { root, router } = await hydrateDocusaurusRoot( container, manifest, routeModules, );
Hydration capture starts before lazy imports, waits for the initial matched
branch, and then adopts the prerendered nodes. Dispose both returned owners when
the application is torn down with root.unmount() and router.dispose().
Docusaurus-aware MDX
import { compileDocusaurusMdx } from '@octanejs/docusaurus/mdx'; const result = await compileDocusaurusMdx(source, '/docs/intro.mdx', { metadata: docMetadata, resolveMarkdownLink: ({ linkPathname }) => linkPathname.startsWith('./') ? `/guide/${linkPathname.slice(2)}` : null, });
Alongside the default document component, compiled modules export Docusaurus's document shape:
frontMatter(plus@octanejs/mdx's existingfrontmatter);contentTitle;toc;metadata;assets.
GitHub-compatible heading IDs (including explicit Docusaurus IDs), duplicate slugs, first-title wrapping/removal, TOC bounds, and Markdown link/image rewriting happen before Octane compilation. User remark/rehype/recma plugins remain composable.
Current scope
Phases 1–6 are implemented here: headless loading, manifest/Vite integration,
MDX compilation, lazy client routing, static route rendering, hydration,
document/asset orchestration, and the initial Octane classic documentation
theme. A complete start/build CLI workflow and broader classic-theme feature
parity remain later phases, so the CLI does not present those commands as
working yet.