Docs

Remix Guides

A runnable Remix app for the in-progress Remix 3 guide docs. Use it to browse, write, and test narrative docs with live Remix examples.

The guides are the hand-authored docs: Start Here, Core App Structure, Server Runtime, and the rest of the chapter sequence. The generated API reference lives in ../api.

#Where things live

  • app/actions/controller.tsx — top-level asset route handling.
  • app/actions/docs/chapters/*.md — guide chapters.
  • app/actions/docs/markdown/render.tsx — chapter metadata and ::frame rendering built on the shared unified/remark/rehype pipeline.
  • app/actions/docs/markdown-chapters.tsx — chapter loading, ordering, slugs, navigation, summaries, and mtime-based render caches.
  • app/actions/docs/layout.tsx and chapter-navigation.tsx — guides-specific content and navigation rendered inside the shell from ../shared/ui/.
  • app/actions/docs/public/ — browser behavior owned by the Guides route, including its active-chapter animation.
  • app/actions/docs/examples/ — frame handlers used by chapters. Each chapter group's public/ directory contains its browser-hydrated demo modules and their local dependencies.
  • app/actions/public/ — root-owned browser entrypoint, development refresh module, and stylesheets.
  • app/assets.ts — the source asset server and root entry hrefs/preloads, configured around colocated public/ directories.
  • app/middleware/render.ts — the request-scoped render() helper and frame resolver.
  • app/routes.ts and app/router.ts — the typed route contract and controller wiring.
  • app/ui/ — shared UI used across routes.
  • public/ — guides-only static files served as-is by the static middleware. Shared docs assets live in ../shared/assets/.

#How chapters work

Chapter files live in app/actions/docs/chapters/. The file name controls order, chapter label, URL slug, and previous/next links:

01-start-here.md -> Chapter 1 -> /start-here/

Each chapter needs frontmatter. Level-2 headings power the docs index and "On this page" navigation. IDs are generated from heading text unless you add an explicit one:

---
title: Start Here
description: A high-level introduction to Remix.
---

An optional chapter introduction can go here.

## Build your first page

## Stable custom anchor {#custom-anchor}

Code fences support filename headers and line highlighting:

```tsx filename=app/actions/projects/create.tsx lines=[4-5,8]
export async function createProject(request: Request) {
  let formData = await request.formData()
  let name = String(formData.get('name') ?? '').trim()

  if (name === '') {
    return new Response('Project name is required', { status: 400 })
  }

  return new Response(`Created ${name}`)
}
```

#Adding frame examples

Use a frame directive in Markdown:

::frame{src="/examples/17-markdown-style-demo/counter/"}

The examples controller maps /examples/:chapter/:example/ to app/actions/docs/examples/<chapter>/<example>.tsx and dynamically imports the module, so no route changes are needed. The validator requires the :chapter segment to match the chapter directory name (e.g. 17-markdown-style-demo) so examples stay scoped to the chapter that references them. Example directories are prefixed with the chapter order number to match the chapter file name.

At render time, markdown.tsx turns the directive into <Frame src="..." />. The render middleware resolves that frame by doing an internal router.fetch() for the frame URL, so examples are normal Remix routes that return normal Response objects.

#Demos with code

A "demo with code" shows a live, hydrated component next to its own highlighted source. It takes three co-located files:

  1. The demo code — a .demo.tsx module inside the frame handler's public/ directory that exports the component as a named export whose name matches the function name:

    app/actions/docs/examples/17-markdown-style-demo/public/counter.demo.tsx
    
    import { css, on } from 'remix/ui'
    import type { Handle } from 'remix/ui'
    
    export function Counter(handle: Handle) {
      let count = 3
    
      return () => (
        <button
          mix={[
            on('click', () => {
              count++
              handle.update()
            }),
            css({ borderRadius: '999px', padding: '0.7rem 1rem' }),
          ]}
          type="button"
        >
          Count: {count}
        </button>
      )
    }
    
  2. The frame handler — a <example>.tsx module that exports a handler built with demoWithCode, pointing at the demo module and its component:

    app/actions/docs/examples/17-markdown-style-demo/counter.tsx
    
    import { demoWithCode } from '../demo-with-code.tsx'
    import { Counter } from './public/counter.demo.tsx'
    
    let demoUrl = new URL('./public/counter.demo.tsx', import.meta.url)
    
    export const handler = demoWithCode(demoUrl, Counter)
    
  3. The shared shellapp/actions/docs/examples/demo-with-code.tsx exports demoWithCode (which loads and highlights the .demo.tsx source, hydrates the component via clientEntry, and renders the preview + source) and the Demo component that lays them out. You don't touch this per example.

The named export matters: demoWithCode resolves the client entry from the function's name, so the export name and the function name must be the same token (e.g. export function Counter), not a default export.

For route-style frames that need full control, export a named handler that returns a Response directly instead of using demoWithCode.

#Commands

Run from the repo root or from docs/guides/.

pnpm install                            # once, from the repo root
pnpm --filter remix-guides run dev      # watch + serve
pnpm --filter remix-guides run start           # serve once
pnpm --filter remix-guides run validate        # check frame URLs and example files
pnpm --filter remix-guides run prerender       # build the static site in docs/guides/build/site
pnpm --filter remix-guides run prerender:serve # serve the static site
pnpm --filter remix-guides run test
pnpm --filter remix-guides run typecheck

The dev server listens on http://localhost:44100 by default. Set PORT to override. The guides index is served at /.

#Static site

pnpm --filter remix-guides run prerender renders the docs index, every chapter, referenced frame examples, browser modules, styles, and public files into docs/guides/build/site/, then builds the Pagefind search index. The output uses directory index.html files for clean URLs and can be served directly by GitHub Pages or another static host.

Production deployment is owned by the remix-run/remix-guides-docs GitHub Pages workflow. It checks out this repository, runs the prerender command, and uploads the static output; server.ts and its middleware run locally only. The Pagefind version is pinned in this repository's pnpm catalog.

The development server serves an existing search index from docs/guides/build/site/assets/pagefind/. Run prerender once to enable search during development, and rerun it when the guide content changes to refresh the index.

Pass --dir to write into another directory:

pnpm --filter remix-guides run prerender --dir ../../remix-guides-site

Sites hosted beneath a URL prefix can pass --base-path or set REMIX_GUIDES_BASE_PATH:

pnpm --filter remix-guides run prerender --base-path /remix-guides-docs

The base path is applied to generated site URLs without changing the output directory layout. The output directory is cleared before each build so removed chapters and outdated fingerprinted assets cannot survive into the next Pagefind index or deployment.