Skip to main content

Overview

COSMOS RSC uses file-system based routing where each file in the app/pages/ directory automatically becomes a route in your application.

Creating routes

Create a new file in app/pages/ to define a route:
export default function HomePage() {
  return (
    <div>
      <h1>Welcome to COSMOS RSC</h1>
    </div>
  );
}
These files create the following routes:
  • /app/pages/index.js
  • /aboutapp/pages/about.js

Nested routes

Create nested routes using directories:
app/pages/features/streaming.js
export default function StreamingDemo() {
  return <div>Streaming Demo</div>;
}
This creates the route /features/streaming.

Example from the demo

The COSMOS RSC demo includes several nested feature routes:
app/pages/
├── index.js              → /
└── features/
    ├── server-components.js → /features/server-components
    ├── streaming.js        → /features/streaming
    └── forms.js            → /features/forms

Route implementation

The server maps URL paths to page components:
core/server/index.js
async function requestHandler(req, res) {
  // Map URL path directly to file path
  const pagePath = `../../app/pages${req.path}`;
  
  try {
    // Dynamically import the page component
    Page = require(pagePath).default;
  } catch (error) {
    logger.error(`Failed to import page: ${pagePath}`, error);
    res.status(500).send('Internal Server Error');
    return;
  }
  
  // Render the page
  const tree = createElement(Page, { searchParams: { ...req.query } });
}
Every page file must export a default function that returns a React component.

Query parameters

Access query parameters through the searchParams prop:
app/pages/search.js
export default function SearchPage({ searchParams }) {
  const query = searchParams.q;
  const filter = searchParams.filter;
  
  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {query}</p>
      <p>Filter: {filter}</p>
    </div>
  );
}
Visiting /search?q=react&filter=new will pass:
{ q: 'react', filter: 'new' }

Client-side navigation

Use the router from context to navigate without full page reloads:
'use client';

import { useRouter } from '#cosmos-rsc/client';

function NavigationButton() {
  const router = useRouter();
  
  return (
    <button onClick={() => router.push('/about')}>
      Go to About
    </button>
  );
}
COSMOS RSC integrates with the browser’s Navigation API:
core/client/components/app/browser-app.js
window.navigation.addEventListener('navigate', (navigateEvent) => {
  if (shouldNotInterceptNavigation(navigateEvent)) {
    return;
  }
  
  const { url } = navigateEvent.destination;
  const path = getFullPath(url);
  
  navigateEvent.intercept({
    precommitHandler() {
      startTransition(() => {
        dispatch({
          type: APP_ACTION.NAVIGATE,
          payload: { path, navigationType },
        });
      });
    },
  });
});
This enables:
  • Smooth transitions between routes
  • Back/forward button support
  • Browser history management

RSC payload fetching

Client-side navigation fetches just the RSC payload, not full HTML:
core/client/lib/get-rsc-payload.js
export async function getRSCPayload(url) {
  const headers = new Headers();
  headers.append('accept', 'text/x-component');
  
  const response = await fetch(url, { headers });
  
  const { tree } = await createFromReadableStream(response.body, {
    callServer,
  });
  
  return tree;
}
The server detects the text/x-component accept header and returns only the RSC stream:
core/server/index.js
if (req.headers.accept === 'text/x-component') {
  res.setHeader('Content-Type', 'text/x-component');
  rscStream.pipe(res);
  return;
}

Router cache

Navigated pages are cached to speed up back/forward navigation:
core/client/lib/app-reducer.js
case APP_ACTION.NAVIGATE: {
  const { path, navigationType } = action.payload;
  
  // Check cache for traverse (back/forward) navigation
  if (navigationType === 'traverse' && routerCache.has(path)) {
    return {
      ...prevState,
      tree: routerCache.get(path),
    };
  }
  
  // Fetch and cache new pages
  const tree = await getRSCPayload(path);
  routerCache.set(path, tree);
  
  return { ...prevState, tree };
}
COSMOS RSC supports view transitions for smooth page changes:
app/components/navigation-transition.js
'use client';

import { useTransition } from 'react';

export function NavigationTransition({ children }) {
  const [isPending, startTransition] = useTransition();
  
  return (
    <div style={{
      viewTransitionName: 'page-title',
      opacity: isPending ? 0.7 : 1,
    }}>
      {children}
    </div>
  );
}
Use this component to wrap page headings:
app/pages/features/server-components.js
export default function ServerComponentsDemo() {
  return (
    <div>
      <NavigationTransition>
        <h1>Server Components Demo</h1>
      </NavigationTransition>
      {/* Rest of page */}
    </div>
  );
}

Linking between pages

You can use standard HTML anchor tags for navigation:
app/pages/index.js
export default function Page() {
  return (
    <div>
      <h1>Features</h1>
      <ul>
        <li>
          <a href='/features/server-components'>
            Server Components Demo
          </a>
        </li>
        <li>
          <a href='/features/streaming'>
            Streaming SSR Demo
          </a>
        </li>
        <li>
          <a href='/features/forms'>
            Server Actions Form Demo
          </a>
        </li>
      </ul>
    </div>
  );
}
The Navigation API intercepts these clicks and handles them as client-side navigations automatically.
Standard <a> tags work for navigation thanks to Navigation API integration. No special Link component is required.

Root layout

All pages are wrapped in a root layout that provides the HTML document structure:
app/root-layout.js
export default function RootLayout({ children }) {
  return (
    <html lang='en'>
      <head>
        <meta charSet='utf-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1' />
        <title>COSMOS RSC</title>
        <link rel='stylesheet' href='/style.css' />
      </head>
      <body>{children}</body>
    </html>
  );
}
The {children} slot is filled with your page content via the SlotContext.

Next steps

Server Components

Learn about React Server Components

Server Actions

Handle forms with server actions

Build docs developers (and LLMs) love