Skip to content
CtrlK

Upgrade to Ardo 6

Move from React Router to Ardo's static site build, remove the router files, and switch to Ardo's link and page APIs.

On this page

Ardo 6 builds static sites with Vite alone. It no longer uses React Router: neither react-router nor @react-router/dev is part of the toolchain, and there is no client-side router. Ardo renders every page to HTML at build time, and each link loads the next prerendered document, which then hydrates.

Most of the migration removes files rather than rewriting them. Your routes in app/routes/, your root component, and your Ardo configuration stay. The background is recorded in ADR 0020. If the site used the typedoc option, also follow Migrate from TypeDoc.

How the build works now

  • ardo build runs Vite's builder API in-process. It builds the client and the server bundle, prerenders every route to build/client/<route>/index.html, inside a version folder such as build/client/v6/ when versioning is set, writes 404.html, and checks every internal link and anchor in the finished HTML.
  • ardo dev runs a Vite dev server with Ardo's server-rendering middleware. It serves the same document the build would write.
  • ardo preview serves the built output like a static host: /guide serves guide/index.html, and unknown paths get the nearest 404.html.

Keep using the three commands. Do not call react-router build or react-router dev; those CLIs are gone from the project.

Delete the removed files

Delete these files. Ardo now provides what they did:

FileReplacement
react-router.config.tsPrerendering is always on. Base path settings live in Ardo config.
app/entry.server.tsxArdo's built-in server renderer. ardo/entry.server no longer exists.
app/entry.client.tsxArdo's built-in client entry, which hydrates every page.
app/routes.tsA virtual route table that Ardo generates from app/routes/.
.react-router/Nothing. Remove it from .gitignore and from tsconfig.json.

Also remove react-router, @react-router/dev, @react-router/node, and isbot from package.json if the project declared them. Ardo manages React, React DOM, and Vite as its own dependencies. If tsconfig.json lists .react-router/types in include or rootDirs, remove it.

Customized entry files have no direct replacement. Move provider setup into the root component, and move document-level markup into a Layout export (see below).

Update content icons

ardo/icons now exports a selected set of Streamline Core Line components instead of forwarding an entire icon library. The icon catalog lists all 50 Core exports. Brand exports such as Github stay available.

All 50 Core icons are registered automatically. Remove their imports and registerIcons() calls from app/root.tsx; keep registration calls for additional custom icons. Use Code where an older example used Code2, including <Icon name="Code" /> and <Card icon="Code"> in MDX.

The components support size, className, and SVG attributes. Remove the previous icon-library dependency if nothing else uses it. For other motifs, install your preferred icon library explicitly and register its React components. See the icon catalog for all included motifs and their usage terms.

Update app/root.tsx

The default export stays the root component. <ArdoRoot> now reads the site config from Ardo's entries and generates a sidebar from the route folders, so the config prop, the virtual:ardo/config import, and the ardo/ui/styles.css import can go. Three optional exports remain:

  • meta: typed as ArdoMetaFunction from ardo/runtime. It takes no arguments and returns the same descriptor objects as before, such as { title: "My Docs" }. Drop it when it only set the site title: pages without a title get the title from ardo.config.ts.
  • Layout: the HTML shell. It now renders on the server only. Ardo uses ArdoRootLayout from ardo/ui when you omit it, so most projects delete their Layout export.
  • ErrorBoundary: renders inside the root for a missing page and replaces the root for a render error. Ardo uses ArdoErrorBoundary when you omit it.

Before:

import type { ReactNode } from "ardo/react"
import type { MetaFunction } from "ardo/router"
import { ArdoFooter, ArdoHeader, ArdoRoot, ArdoRootLayout, ArdoSidebar } from "ardo/ui"
import config from "virtual:ardo/config"
import "ardo/ui/styles.css"

export const meta: MetaFunction = () => [{ title: "My Docs" }]

export function Layout({ children }: { children: ReactNode }) {
  return <ArdoRootLayout>{children}</ArdoRootLayout>
}

export default function Root() {
  return (
    <ArdoRoot config={config}>
      <ArdoHeader />
      <ArdoSidebar />
      <ArdoFooter />
    </ArdoRoot>
  )
}

After:

import { ArdoRoot } from "ardo/ui"

export default function Root() {
  return <ArdoRoot />
}

Keep ArdoHeader, ArdoSidebar, or ArdoFooter as children when they carry props or navigation; without children, ArdoRoot renders the same three parts with their defaults.

Keep a Layout export when it passes options such as theme, favicon, or className to ArdoRootLayout. A fully custom shell replaces React Router's <Meta />, <Links />, and <Scripts /> with <ArdoHead /> in <head> and <ArdoScripts /> at the end of <body>, both from ardo/runtime. See Layout & Navigation.

Route modules keep their meta and handle exports, for example export const handle = { layout: "bare" }.

Trim tsconfig.json

Replace the compiler options with Ardo's preset. It sets jsx, jsxImportSource: "ardo", moduleResolution: "bundler", strict mode, and Ardo's type declarations:

{
  "extends": "ardo/tsconfig",
  "include": ["app/**/*", "*.ts"]
}

Replace router imports

Import runtime values from ardo/runtime or ardo/ui. Do not import them from the package root ardo in browser code: that entry also contains Node-only build code. React APIs still come from ardo/react, and the ardo/react-dom/* subpaths still exist.

Ardo 5 (React Router)Ardo 6
Link from ardo/routerArdoLink with to and anchor attributes
NavLink from ardo/routerArdoActiveLink, which adds active and aria-current
useLocation().pathnameuseArdoPath(), the page path without the base path
useNavigate()navigateTo(href), a full page navigation
useHref(to)useArdoHref(to) or resolveArdoHref(to, page)
useMatches() for route handle datauseArdoPage().handle
useRouteError() in an error boundaryuseArdoPage().error
MetaFunction from ardo/routerArdoMetaFunction and ArdoMetaDescriptor
MemoryRouter in tests and component previewsArdoPageProvider with path and optional base
<Meta />, <Links />, <Scripts /><ArdoHead /> and <ArdoScripts />
ardo/router/dom, ardo/router/routesRemoved. Ardo provides the client entry and route table.
ardo/entry.serverRemoved. Delete app/entry.server.tsx.
ReactRouterConfig from ardo/configRemoved. Delete react-router.config.ts.
withArdoGitHubPages(), withArdoVersioning()base or versioning in Ardo config
githubPages option, detectGitHubBasename()Removed. Set base: "/repo/" for a GitHub Pages project site.
ardoRoutesPluginRemoved. The ardo() plugin generates the route table.
defineConfig, loadConfig from ardoRemoved. Use defineArdoConfig from ardo/config.
ardo/press.cssRemoved. Ardo loads its stylesheet itself.

All of these, except ArdoMetaFunction and ArdoMetaDescriptor, are values exported from ardo/runtime. ArdoLink and ArdoActiveLink are also exported from ardo/ui. UI props that took React Router's To type now take a string.

ArdoLink adds the base path to internal targets and renders a plain <a>:

import { ArdoLink, useArdoPath } from "ardo/runtime"

export function BackToGuide() {
  const path = useArdoPath()
  if (path.startsWith("/guide")) return null
  return <ArdoLink to="/guide/getting-started">Read the guide</ArdoLink>
}

In tests and stories, wrap components that read the current page in ArdoPageProvider:

import { ArdoPageProvider } from "ardo/runtime"

render(
  <ArdoPageProvider path="/guide/intro">
    <ComponentUnderTest />
  </ArdoPageProvider>
)

Configure the base path once

There is no separate router basename and no flatten step anymore. Set the base path in Ardo config, and Ardo applies it to assets, links, and the output folders:

  • GitHub Pages project sites: set base: "/repo/".
  • Root deployments and custom domains: nothing to set.
  • Versioned docs: use the versioning option. Ardo writes the current version's pages under its folder, such as build/client/v6/, and site-wide files such as versions.json at the top of build/client/.

See Deployment for complete examples.

Replace route loaders

Route loader and action exports are not supported. Pages are prerendered, so data they need must be available at build time. Provide it through a small Vite plugin with a virtual module, and import that module from the page:

// vite.config.ts
import { defineConfig, type Plugin } from "ardo/vite"

function releaseFacts(): Plugin {
  const id = "virtual:docs/release-facts"
  return {
    name: "docs:release-facts",
    resolveId(source) {
      return source === id ? `\0${id}` : undefined
    },
    async load(source) {
      if (source !== `\0${id}`) return undefined
      const response = await fetch("https://registry.npmjs.org/ardo/latest")
      const { version } = await response.json()
      return `export default ${JSON.stringify({ version })}`
    },
  }
}

export default defineConfig({
  plugins: [releaseFacts()],
})
// app/routes/releases.tsx
import facts from "virtual:docs/release-facts"

export default function Releases() {
  return <p>Latest release: {facts.version}</p>
}

Declare the module's type in a .d.ts file so TypeScript accepts the import. The Ardo docs site uses this pattern for the npm statistics on its home page. Because the prerendered HTML and the hydrating client import the same module, both see the same data.

Behavior changes

  • Frontmatter layout is bare or default. doc, home, and page never changed the rendering and now report an invalid-value warning.
  • Every page has a title. Pages whose module and root set no title meta get the site title from ardo.config.ts.
  • No GitHub remote detection. Ardo 5 used the repository name from a GitHub remote as the base path unless you set githubPages: false. Ardo 6 removes githubPages and detectGitHubBasename. Set base for a subpath; it applies in development and in the build alike.
  • Full page navigations. Every link is a plain <a>, and each click loads a new prerendered document. Client state, such as an open search dialog, does not survive navigation.
  • Trailing slashes. Ardo emits internal page links with a trailing slash, such as /guide/intro/, so static hosts serve each page's index.html without a redirect. Links to files such as llms.txt keep their form.
  • Link check on the built HTML. ardo build always checks every internal link, image, script, stylesheet, and #fragment in the generated HTML, including links from components and generated pages, and the destinations in its redirect and version files. Configure it with linkCheck.
  • Lazy search index. The search index, about 75 KB gzip on the Ardo docs, loads when a reader first focuses search instead of with every page.

Performance

Measured on the Ardo documentation site, M1 Ultra, warm build:

Versionardo build
Ardo 5 with React Router 8, before PR #38218–20 s
Ardo 5 with the build fixes from PR #382about 10.5 s
Ardo 6about 5.6 s

Eager client JavaScript across all pages dropped from about 425 KB to about 379 KB gzip. The lazy search index is not part of that number.

Verify the upgrade

Run the project's typecheck and tests, then ardo build. The build should report no broken internal links. Open a few pages with ardo preview, follow links between them, and check that the browser console shows no hydration errors.

Edit this page on GitHubLast updated: October 23, 2026