Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Framework Routing Adapters

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Framework Routing Adapters provide a unified abstraction layer that decouples Fumadocs UI components from underlying framework-specific routing and navigation primitives. By defining a common contract for path resolution, parameter extraction, and navigation hooks, Fumadocs supports multiple meta-frameworks without binding its core logic to any single ecosystem.

Sources: packages/core/src/framework/index.tsx:29-41

Core Framework Abstraction Interface

Public API Contract and Framework Context

The core abstraction layer defines the structural contract for framework integration in fumadocs-core. The Framework interface specifies required navigation and routing functions alongside optional component overrides for links and images. A React context initialized via createContext stores this interface, defaulting to notImplemented stubs that throw an error prompting developers to wrap their application inside FrameworkProvider.

export interface Router {
  push: (url: string) => void | Promise<void>;
  refresh: () => void | Promise<void>;
}
 
export interface Framework {
  usePathname: () => string;
  useParams: () => Record<string, string | string[]>;
  useRouter: () => Router;
 
  Link?: FC<
    ComponentProps<'a'> & {
      prefetch?: boolean;
    }
  >;
 
  Image?: FC<ImageProps>;
}
 
const notImplemented = () => {
  throw new Error('You need to wrap your application inside `FrameworkProvider`.');
};
 
const FrameworkContext = createContext<Framework>({
  useParams: notImplemented,
  useRouter: notImplemented,
  usePathname: notImplemented,
});

Sources: packages/core/src/framework/index.tsx:24-51

MemberTypePurpose
usePathname() => stringReturns the current URL pathname string
useParams() => Record<string, string | string[]>Extracts dynamic route parameters as a key-value record
useRouter() => RouterProvides navigation primitives (push and refresh)
LinkFC<ComponentProps<'a'> & { prefetch?: boolean }>Optional framework-specific anchor component override
ImageFC<ImageProps>Optional framework-specific image component override

Sources: packages/core/src/framework/index.tsx:29-41

Warning

Failing to wrap component trees in FrameworkProvider triggers a runtime error when any hook or core component attempts to access framework context, because the default stub functions throw immediately upon invocation.

Sources: packages/core/src/framework/index.tsx:43-51

Provider Memoization and Routing Hooks

The FrameworkProvider component accepts Link, useRouter, useParams, usePathname, Image, and children. It wraps its children with FrameworkContext.Provider (rendered as <FrameworkContext value={framework}>) utilizing useMemo to memoize the context object based on its provider dependencies. Individual custom hooks (usePathname, useRouter, useParams) consume this context using React's use hook to extract the active framework implementation.

export function FrameworkProvider({
  Link,
  useRouter,
  useParams,
  usePathname,
  Image,
  children,
}: Framework & { children: ReactNode }) {
  const framework = useMemo(
    () => ({
      usePathname,
      useRouter,
      Link,
      Image,
      useParams,
    }),
    [Link, usePathname, useRouter, useParams, Image],
  );
 
  return <FrameworkContext value={framework}>{children}</FrameworkContext>;
}
 
export function usePathname() {
  return use(FrameworkContext).usePathname();
}
 
export function useRouter() {
  return use(FrameworkContext).useRouter();
}
 
export function useParams() {
  return use(FrameworkContext).useParams();
}

Sources: packages/core/src/framework/index.tsx:53-85

Tip

The framework context consumption relies on React's use API, allowing hooks to be safely called within consumer components as long as a valid FrameworkProvider exists higher in the tree.

Sources: packages/core/src/framework/index.tsx:75-85

The core package provides robust fallback implementations for Image and Link components when no framework-specific override is supplied through the context. The Image component checks for a registered context Image component; if absent, it falls back to a standard <img> element mapping the priority boolean prop to a fetchPriority attribute (high or auto). Similarly, the Link component falls back to a standard <a> element when no framework link implementation is provided, destructuring out the prefetch prop to prevent invalid DOM attribute warnings.

export function Image(props: ImageProps) {
  const { Image } = use(FrameworkContext);
  if (!Image) {
    const { src, alt, priority, ...rest } = props;
 
    return (
      <img alt={alt} src={src as string} fetchPriority={priority ? 'high' : 'auto'} {...rest} />
    );
  }
 
  return <Image {...props} />;
}
 
export function Link(props: LinkProps) {
  const { Link } = use(FrameworkContext);
  if (!Link) {
    const { href, prefetch: _, ...rest } = props;
    return <a href={href} {...rest} />;
  }
 
  return <Link {...props} />;
}

Sources: packages/core/src/framework/index.tsx:87-108

Next.js Routing Adapter Implementation

NextProvider and App Router Primitives

The Next.js framework adapter bridges Next.js App Router navigation hooks and UI components into the core framework context. Defined in packages/core/src/framework/next.tsx, the NextProvider component marks itself with 'use client' and passes Next.js routing hooks—usePathname, useRouter, and useParams from next/navigation—directly into the underlying FrameworkProvider. It also accepts optional custom overrides for Link and Image, defaulting to next/link and next/image when custom components are omitted.

'use client';
import { type Framework, FrameworkProvider } from '@/framework/index';
import type { ReactNode } from 'react';
import { useParams, usePathname, useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
 
export function NextProvider({
  children,
  Link: CustomLink,
  Image: CustomImage,
}: {
  children: ReactNode;
  Link?: Framework['Link'];
  Image?: Framework['Image'];
}) {
  return (
    <FrameworkProvider
      usePathname={usePathname}
      useRouter={useRouter}
      useParams={useParams}
      Link={CustomLink ?? (Link as Framework['Link'])}
      Image={CustomImage ?? (Image as Framework['Image'])}
    >
      {children}
    </FrameworkProvider>
  );

Sources: packages/core/src/framework/next.tsx:1-27

PropTypeDefault ValuePurpose
childrenReactNode(Required)React subtree wrapped by the provider
LinkFramework['Link']next/linkCustom anchor component override
ImageFramework['Image']next/imageCustom image component override

Sources: packages/core/src/framework/next.tsx:8-27

Note

NextProvider relies on Next.js client-side navigation modules (next/navigation, next/link, next/image), requiring all consuming tree branches to execute within a Client Component boundary ('use client').

Sources: packages/core/src/framework/next.tsx:1-6

UI Package Integration Wrappers

The Radix UI and Base UI packages provide their own RootProvider components that encapsulate NextProvider alongside their respective design system providers (BaseProvider). Both packages/base-ui/src/provider/next.tsx and packages/radix-ui/src/provider/next.tsx accept RootProviderProps containing a components object with optional Link and Image overrides, forwarding them to NextProvider while spreading remaining props into BaseProvider.

'use client';
import type { ComponentProps } from 'react';
import { RootProvider as BaseProvider } from '@/provider/base';
import { NextProvider } from 'fumadocs-core/framework/next';
import type { Framework } from 'fumadocs-core/framework';
 
export interface RootProviderProps extends ComponentProps<typeof BaseProvider> {
  /**
   * Custom framework components to override Next.js defaults
   */
  components?: {
    Link?: Framework['Link'];
    Image?: Framework['Image'];
  };
}
 
export function RootProvider({ components, ...props }: RootProviderProps) {
  return (
    <NextProvider Link={components?.Link} Image={components?.Image}>
      <BaseProvider {...props}>{props.children}</BaseProvider>
    </NextProvider>
  );
}

Sources: packages/radix-ui/src/provider/next.tsx:1-23, packages/base-ui/src/provider/next.tsx:1-22

Tip

By nesting BaseProvider inside NextProvider, UI packages ensure that design system primitives and documentation layouts access active Next.js routing context and custom component overrides simultaneously.

Sources: packages/radix-ui/src/provider/next.tsx:17-22, packages/base-ui/src/provider/next.tsx:17-21

React Router Framework Integration

Core Bridge and Navigation Hooks

The core React Router adapter translates React Router hooks into the Fumadocs framework interface contract. Located in packages/core/src/framework/react-router.tsx, the adapter extracts the current path via useLocation().pathname and retrieves route parameters through useParams(), casting them to Record<string, string | string[]>.

const framework: Framework = {
  usePathname() {
    return useLocation().pathname;
  },
  useParams() {
    return useParams() as Record<string, string | string[]>;
  },

Sources: packages/core/src/framework/react-router.tsx:5-11

Navigation actions are wrapped inside a memoized useRouter hook that combines useNavigate and useRevalidator. The push method delegates directly to navigate(url), while refresh triggers void revalidator.revalidate(), with both actions dependent on [navigate, revalidator].

useRouter() {
  const navigate = useNavigate();
  const revalidator = useRevalidator();
 
  return useMemo(
    () => ({
      push(url) {
        navigate(url);
      },
      refresh() {
        void revalidator.revalidate();
      },
    }),
    [navigate, revalidator],
  );
},

Sources: packages/core/src/framework/react-router.tsx:12-27

The framework's Link implementation maps the generic href property to React Router's to prop and converts boolean prefetch flags into 'intent' or 'none' string values.

Link({ href, prefetch, ...props }) {
  return (
    <Link to={href!} prefetch={prefetch ? 'intent' : 'none'} {...props}>
      {props.children}
    </Link>
  );
},

Sources: packages/core/src/framework/react-router.tsx:28-34

Note

React Router's useRevalidator returns a revalidation state object where revalidate() is an asynchronous function; the adapter explicitly voids the returned promise to satisfy lint rules within synchronous callback contexts.

Sources: packages/core/src/framework/react-router.tsx:21-23

Provider Wrappers and UI Package Integration

The ReactRouterProvider component accepts children alongside optional custom Link and Image overrides, supplying them to the underlying FrameworkProvider by falling back to the default framework implementation when custom components are omitted.

export function ReactRouterProvider({
  children,
  Link: CustomLink,
  Image: CustomImage,
}: {
  children: ReactNode;
  Link?: Framework['Link'];
  Image?: Framework['Image'];
}) {
  return (
    <FrameworkProvider
      {...framework}
      Link={CustomLink ?? framework.Link}
      Image={CustomImage ?? framework.Image}
    >
      {children}
    </FrameworkProvider>
  );
}

Sources: packages/core/src/framework/react-router.tsx:37-55

Both Radix UI and Base UI provide specialized RootProvider components that bridge React Router into their respective design system providers. Found in packages/radix-ui/src/provider/react-router.tsx and packages/base-ui/src/provider/react-router.tsx, these providers accept RootProviderProps containing an optional components object with Link and Image properties, forwarding them into ReactRouterProvider while spreading remaining props into BaseProvider.

export interface RootProviderProps extends ComponentProps<typeof BaseProvider> {
  /**
   * Custom framework components to override React Router defaults
   */
  components?: {
    Link?: Framework['Link'];
    Image?: Framework['Image'];
  };
}
 
export function RootProvider({ components, ...props }: RootProviderProps) {
  return (
    <ReactRouterProvider Link={components?.Link} Image={components?.Image}>
      <BaseProvider {...props}>{props.children}</BaseProvider>
    </ReactRouterProvider>
  );

Sources: packages/radix-ui/src/provider/react-router.tsx:7-22, packages/base-ui/src/provider/react-router.tsx:7-22

PropTypePurpose
components{ Link?: Framework['Link']; Image?: Framework['Image']; }Custom framework components to override React Router defaults
childrenReactNodeReact subtree rendered inside the base design system provider

Sources: packages/radix-ui/src/provider/react-router.tsx:7-15, packages/base-ui/src/provider/react-router.tsx:7-15

Warning

UI package root providers for React Router require the 'use client' directive at the module boundary to ensure React context hooks operate correctly in client-side routing environments.

Sources: packages/radix-ui/src/provider/react-router.tsx:1-1, packages/base-ui/src/provider/react-router.tsx:1-1

TanStack Start Provider Layer

TanStack Router Hooks and State Ref Caching

The TanStack Start framework integration bridges TanStack Router primitives into Fumadocs via core definitions in fumadocs-core. The framework object configures a custom Link component mapping href to to and setting prefetch behavior to 'intent' when enabled.

const framework: Framework = {
  Link({ href, prefetch = true, ...props }) {
    return (
      <Link to={href} preload={prefetch ? 'intent' : false} {...props}>
        {props.children}
      </Link>
    );
  },

Sources: packages/core/src/framework/tanstack.tsx:5-12

The usePathname hook leverages useRouterState to select both loading status and location pathname. To prevent layout flickering during route transitions, pathname updates use a persistent useRef combined with useMemo to return the cached active pathname while loading state is true.

usePathname() {
  const { isLoading, pathname } = useRouterState({
    select: (state) => ({
      isLoading: state.isLoading,
      pathname: state.location.pathname,
    }),
  });
 
  const activePathname = useRef(pathname);
  return useMemo(() => {
    if (isLoading) {
      return activePathname.current;
    }
 
    activePathname.current = pathname;
    return pathname;
  }, [isLoading, pathname]);
},

Sources: packages/core/src/framework/tanstack.tsx:13-30

Note

During route transitions when isLoading is active, usePathname returns the cached reference value rather than the incoming intermediate pathname to maintain stable navigation highlighting.

Sources: packages/core/src/framework/tanstack.tsx:21-29

Router actions are wrapped inside a memoized useRouter hook that invokes TanStack Router's useRouter. The push method delegates to router.navigate({ href: url }) while refresh calls router.invalidate().

useRouter() {
  const router = useRouter();
 
  return useMemo(
    () => ({
      push(url) {
        void router.navigate({
          href: url,
        });
      },
      refresh() {
        void router.invalidate();
      },
    }),
    [router],
  );
},

Sources: packages/core/src/framework/tanstack.tsx:31-47

Parameter extraction is handled by useParams({ strict: false }), and the entire definition is wrapped by TanstackProvider to supply context descendants with framework bindings.

  useParams() {
    return useParams({ strict: false });
  },
};
 
/**
 * Fumadocs adapter for Tanstack Router/Start
 */
export function TanstackProvider({
  children,
  Link: CustomLink,
  Image: CustomImage,
}: {
  children: ReactNode;
  Link?: Framework['Link'];
  Image?: Framework['Image'];
}) {
  return (
    <FrameworkProvider
      {...framework}
      Link={CustomLink ?? framework.Link}
      Image={CustomImage ?? framework.Image}
    >
      {children}
    </FrameworkProvider>
  );
}

Sources: packages/core/src/framework/tanstack.tsx:48-74

UI Package Root Providers

Both Base UI and Radix UI expose client-side RootProvider components that incorporate TanstackProvider from fumadocs-core. These providers accept optional component overrides for Link and Image within a components prop.

export interface RootProviderProps extends ComponentProps<typeof BaseProvider> {
  /**
   * Custom framework components to override Tanstack Router defaults
   */
  components?: {
    Link?: Framework['Link'];
    Image?: Framework['Image'];
  };
}
 
export function RootProvider({ components, ...props }: RootProviderProps) {
  return (
    <TanstackProvider Link={components?.Link} Image={components?.Image}>
      <BaseProvider {...props}>{props.children}</BaseProvider>
    </TanstackProvider>
  );

Sources: packages/base-ui/src/provider/tanstack.tsx:7-22, packages/radix-ui/src/provider/tanstack.tsx:7-22

PropTypePurpose
components{ Link?: Framework['Link']; Image?: Framework['Image']; }Custom framework components to override TanStack Router defaults
childrenReactNodeReact subtree rendered inside the design system provider

Sources: packages/base-ui/src/provider/tanstack.tsx:7-15, packages/radix-ui/src/provider/tanstack.tsx:7-15

Warning

Both package root provider files require the 'use client' directive at the top of the module to operate correctly within React client component trees.

Sources: packages/base-ui/src/provider/tanstack.tsx:1-1, packages/radix-ui/src/provider/tanstack.tsx:1-1

Waku Minimal Router Adapter

Waku Router Context Bindings and Framework Definitions

The Waku minimal router adapter connects Waku routing primitives into Fumadocs through core definitions in fumadocs-core. The framework object implements usePathname by calling Waku's useRouter().path, while useParams returns an empty object memoized with useMemo accompanied by a console warning.

const framework: Framework = {
  usePathname() {
    return useRouter().path;
  },
  useParams() {
    console.warn('[Fumadocs] useParams() is not supported on Waku');
    return useMemo(() => ({}), []);
  },

Sources: packages/core/src/framework/waku.tsx:7-14

Warning

The useParams hook on Waku emits a console warning ([Fumadocs] useParams() is not supported on Waku) and returns an empty object because route parameters are accessed directly via page props rather than context hooks.

Sources: packages/core/src/framework/waku.tsx:11-14

Navigation actions are wrapped inside a memoized useRouter hook that caches the router instance using a useRef. The returned object provides push(url) delegating to routerRef.current.push(url) and refresh() calling routerRef.current.reload().

useRouter() {
  const router = useRouter();
  const routerRef = useRef(router);
  routerRef.current = router;
 
  return useMemo(
    () => ({
      push(url: string) {
        return routerRef.current.push(url);
      },
      refresh() {
        return routerRef.current.reload();
      },
    }),
    [],
  );
},

Sources: packages/core/src/framework/waku.tsx:15-31

The framework Link component maps Fumadocs link properties to Waku's Link element, translating href to to and setting unstable_prefetchOnEnter to the prefetch boolean value defaulting to true.

  Link({ href, prefetch = true, ...props }) {
    return (
      <Link to={href!} unstable_prefetchOnEnter={prefetch} {...props}>
        {props.children}
      </Link>
    );
  },
};

Sources: packages/core/src/framework/waku.tsx:32-39

The WakuProvider component wraps FrameworkProvider, accepting optional custom Link and Image overrides and falling back to the framework defaults.

export function WakuProvider({
  children,
  Link: CustomLink,
  Image: CustomImage,
}: {
  children: ReactNode;
  Link?: Framework['Link'];
  Image?: Framework['Image'];
}) {
  return (
    <FrameworkProvider
      {...framework}
      Link={CustomLink ?? framework.Link}
      Image={CustomImage ?? framework.Image}
    >
      {children}
    </FrameworkProvider>
  );
}

Sources: packages/core/src/framework/waku.tsx:41-59

UI Provider Wrappers and Preview Integration

Both Base UI and Radix UI expose client-side RootProvider components specifically adapted for Waku. These components wrap children with WakuProvider and BaseProvider, accepting an optional components object to override Link and Image.

export interface RootProviderProps extends ComponentProps<typeof BaseProvider> {
  /**
   * Custom framework components to override Waku defaults
   */
  components?: {
    Link?: Framework['Link'];
    Image?: Framework['Image'];
  };
}
 
export function RootProvider({ components, ...props }: RootProviderProps) {
  return (
    <WakuProvider Link={components?.Link} Image={components?.Image}>
      <BaseProvider {...props}>{props.children}</BaseProvider>
    </WakuProvider>
  );

Sources: packages/base-ui/src/provider/waku.tsx:7-22, packages/radix-ui/src/provider/waku.tsx:7-22

PropTypePurpose
components{ Link?: Framework['Link']; Image?: Framework['Image']; }Custom framework components to override Waku defaults
childrenReactNodeReact subtree rendered inside the design system provider

Sources: packages/base-ui/src/provider/waku.tsx:7-15, packages/radix-ui/src/provider/waku.tsx:7-15

The preview application leverages fumadocs-ui/provider/waku within its client provider component and configures dynamic routing inside [...slugs].tsx. The markdown compiler processes documents with remark plugins including remarkGfm, remarkMath, remarkMdxMermaid, remarkHeading, remarkNpm, and remarkCodeTab, alongside rehype plugins for rehypeKatex, rehypeCode, and rehypeToc.

const compiler = createMarkdownCompiler({
  remarkPlugins: [
    remarkGfm,
    remarkMath,
    remarkMdxMermaid,
    plugin(remarkHeading, { generateToc: false }),
    plugin(remarkNpm, { persist: { id: 'package-manager' } }),
    remarkCodeTab,
  ],
  remarkRehypeOptions: {
    passThrough: ['mdxJsxFlowElement', 'mdxJsxTextElement'],
  },
  rehypePlugins: [
    rehypeKatex,
    plugin(rehypeCode, { lazy: true, fallbackLanguage: 'text' }),
    plugin(rehypeToc, { exportToc: { as: 'data' } }),
  ],
});

Sources: packages/preview/src/pages/...slugs.tsx:29-46

Note

The preview application uses dynamic configuration (getConfig returning render: 'dynamic') and dynamically selects between docs and flux layout presets based on runtime configuration settings.

Sources: packages/preview/src/pages/[...slugs].tsx:76-96, packages/preview/src/pages/[...slugs].tsx:225-229

The DynamicLink component extends standard navigation links by substituting route parameters into templated path strings at runtime. By retrieving active route parameters through the useParams hook, DynamicLink computes updated URLs via useMemo before rendering the underlying Link element.

export const DynamicLink = forwardRef<HTMLAnchorElement, DynamicLinkProps>(
  ({ href, ...props }, ref) => {
    const params = useParams();
 
    const url = useMemo(() => (href ? updateHref(href, params) : href), [params, href]);
 
    return <Link ref={ref} href={url} {...props} />;
  },
);

Sources: packages/core/src/dynamic-link.tsx:15-23

The underlying updateHref function parses parameter placeholders using regular expressions. If a matching key exists within the parameter dictionary, it substitutes the value—joining array segments with slashes if necessary—and preserves trailing slashes from the template pattern.

export function updateHref(href: string, params: Record<string, string | string[]>) {
  return href.replace(/\[(.*)]\/?/, (match, key) => {
    const hasEndingSlash = match[match.length - 1] === '/';
    const value = key in params ? params[key] : undefined;
    if (!value) return '';
 
    const replacement = typeof value === 'string' ? value : value.join('/');
    return hasEndingSlash ? `${replacement}/` : replacement;
  });
}

Sources: packages/core/src/dynamic-link.tsx:27-36

Warning

If a route parameter placeholder lacks a matching key in the params record, updateHref evaluates value as undefined and returns an empty string for that segment.

Sources: packages/core/src/dynamic-link.tsx:30-31

Active Navigation and Layout Tabs

Layout structures determine active states for navigation links and folder tabs by evaluating the current pathname against registered URLs and page tree nodes. The isLinkItemActive function inspects link configuration objects, immediately returning false for custom item types or entries configured with active: 'none'.

export function updateHref(href: string, params: Record<string, string | string[]>) {
  return href.replace(/\[(.*)]\/?/, (match, key) => {
    const hasEndingSlash = match[match.length - 1] === '/';
    const value = key in params ? params[key] : undefined;
    if (!value) return '';
 
    const replacement = typeof value === 'string' ? value : value.join('/');
    return hasEndingSlash ? `${replacement}/` : replacement;
  });
}

Sources: packages/core/src/dynamic-link.tsx:27-36

Layout tabs generated from page tree structures evaluate active states by searching folder hierarchies for matching child page URLs or checking explicit URL sets and normalized path comparisons.

export function isLayoutTabActive(tab: LayoutTab, pathname: string) {
  if (tab.$folder) {
    return (
      PageTree.findPath(
        tab.$folder.children,
        (node) => node.type === 'page' && isActive(node.url, pathname),
      ) !== null
    );
  }
 
  if (tab.urls) {
    return tab.urls.has(normalize(pathname));
  }
 
  return isActive(tab.url, pathname, true);
}

Sources: packages/radix-ui/src/layouts/shared/index.tsx:146-161

PropertyTypeDefaultPurpose
active'url' | 'nested-url' | 'none''url'Defines active state matching criteria for navigation items
urlstring—Target destination URL for navigation and link resolution
externalbooleanfalseIndicates whether the link points to an external resource
on'menu' | 'nav' | 'all''all'Restricts where link items are rendered within the layout

Sources: packages/radix-ui/src/layouts/shared/index.tsx:163-181

Was this page helpful?