Skip to Content
Fumadocs

Local HTML

Content source for local HTML files.

Introduction

@fumadocs/local-html is a content source for local HTML files. It is bundleless by design, so it works fully at runtime.

It is made for pages you receive as HTML instead of author yourself, like exported decks, reports, and the output of other tools. Each file is adapted to your docs theme rather than rendered as it came.

Compared to Local Markdown, HTML is treated as data to adapt, not as a document format to write in.

Limitations

  • Scripts are stripped, the interactive behaviour of the original page is not preserved.
  • No MDX components in content, but you can map HTML tags to components at render phase.
  • Processing is a transform, not sanitization. Nothing that executes survives it, but the remaining markup is rendered as-is, and the resources it references (images above all) are fetched by everyone who opens the page. Keep the content directory to files you trust, like your Markdown content.

Setup

Install the package:

npm install @fumadocs/local-html

Create a localHtml instance and connect it to Fumadocs:

lib/source.ts
import { dynamicLoader } from 'fumadocs-core/source';
import { localHtml } from '@fumadocs/local-html';

const pages = localHtml({
  dir: 'content/pages',
  // options
});

const pagesLoader = dynamicLoader(pages.dynamicSource(), {
  baseUrl: '/docs',
});

export async function getSource() {
  return pagesLoader.get();
}

The returned source from pagesLoader.get() is a normal content loader instance.

Usage

page.data.load() returns a renderer for the processed content.

Render it in your page:

import type { ReactNode } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { getSource } from '@/lib/source';

export default async function Layout({ children }: { children: ReactNode }) {
  const source = await getSource();

  return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;
}

Pass components to render() to map HTML tags onto your own, like headings with anchors:

import { getMDXComponents } from '@/components/mdx';

const { body, toc } = await renderer.render(getMDXComponents());

Page Metadata

The title and description of each page are read from its <head>:

  • <title> becomes the page title.
  • <meta name="description"> becomes the description.

You can override them (and set an icon) without touching the visible content, using fumadocs: metas:

<meta name="fumadocs:title" content="My Page" />
<meta name="fumadocs:description" content="A short description." />
<meta name="fumadocs:icon" content="SquareBookmark" />

Every <meta name>/content pair of the document is also available on page.data.metadata.

Like other local sources, meta.json files customize the page tree, validated with metaSchema.

Content Options

A page keeps only its content element: the only <main>, then the only <article>, falling back to <body> with page chrome like <header> and <nav> dropped. Its class and style attributes are removed so your theme's prose styling takes over, and its headings get ids to power the table of contents and search.

Each part of that is configurable:

import { localHtml } from '@fumadocs/local-html';

const pages = localHtml({
  dir: 'content/pages',
  // keep the original class/style attributes instead of adapting to your theme
  adaptStyles: false,
  // drop additional tags from the content
  exclude: ['aside'],
  // custom logic to pick the element holding the page content
  selectContent: (root) => {
    // return a hast element, or undefined for the default behaviour
  },
});

Syntax Highlighting

<pre><code> blocks are highlighted with Shiki, using the same options as Local Markdown:

const pages = localHtml({
  dir: 'content/pages',
  rehypeCodeOptions: {
    themes: {
      light: 'github-light',
      dark: 'github-dark',
    },
  },
});

The language is read from the language-* class of the block, on the <code> or its enclosing <pre>:

<pre><code class="language-ts">const answer: number = 42;</code></pre>

A block without one falls back to plain text. Pass rehypeCodeOptions: false to disable syntax highlighting.

Hot Reload

@fumadocs/local-html reads files at runtime, so nothing watches them by default.

During development, the Vite plugin invalidates the source through Vite's file watcher, with no extra process or port. Add the plugin to your config and connect the source:

import { defineConfig } from 'waku/config';
import { localContentPlugin } from '@fumadocs/local-html/dev/vite';

export default defineConfig({
  vite: {
    plugins: [localContentPlugin()],
  },
});

To wire up your own watcher instead, call pages.invalidateFile(path) when a file changes.

Disable Revalidation

You can use staticSource() when you only need a one-time snapshot without revalidation.

It works with a normal loader() instead of only dynamicLoader():

lib/source.ts
import { loader } from 'fumadocs-core/source';
import { localHtml } from '@fumadocs/local-html';

const pages = localHtml({
  dir: 'content/pages',
});

export const source = loader(await pages.staticSource(), {
  baseUrl: '/docs',
});

How is this guide?

Last updated on