Routing

EmberKit uses file-based routing. Files under src/routes/ map to URLs. The Vite plugin generates virtual:emberkit-routes for the client; the build writes the same routes into ssr-manifest.json for SSR and pre-rendering.

Basic Routes

FileURL
src/routes/index.tsx/
src/routes/about.tsx/about
src/routes/docs/index.tsx/docs
src/routes/docs/installation.tsx/docs/installation
src/routes/blog/[slug].tsx/blog/:slug
src/routes/docs/[...rest].tsx/docs/:rest* (catch-all)

Supported extensions: .tsx, .ts, .jsx, .js, .md, .mdx.

Route component props

Dynamic routes receive typed params on the server and client:

// src/routes/blog/[slug].tsx
import type { RouteParams } from '@emberkit/core';

export default function BlogPost({ params }: RouteParams<{ slug: string }>) {
  return <h1>Post: {params.slug}</h1>;
}

RouteParams also includes:

  • query — parsed search params (Record<string, string | string[]>)
  • request — Request built from the current URL (client navigation)

Catch-all params use the bracket name: [...rest].tsx → params.rest.

Locale-prefixed routes

For internationalized URLs, add a [locale] segment and wire i18n in the layout loader:

src/routes/[locale]/index.tsx   →  /en, /es
src/routes/[locale]/about.tsx   →  /en/about

See Internationalization for JSON catalogs, createI18nFromGlob, and resolveLocaleFromRequest.

Layouts

_layout.tsx wraps routes in the same directory tree:

// src/routes/_layout.tsx — wraps all routes
import type { RouteComponent } from '@emberkit/core';

const RootLayout: RouteComponent = ({ children }) => (
  <div className="app">
    <nav>...</nav>
    <main>{children}</main>
  </div>
);

export default RootLayout;
// src/routes/docs/_layout.tsx — wraps /docs/*
export default function DocsLayout({ children }: { children: unknown }) {
  return (
    <div className="docs">
      <aside>Sidebar</aside>
      <article>{children}</article>
    </div>
  );
}

Special files (not URL segments): _layout.tsx, _error.tsx, _loading.tsx, and anything under _api/.

Custom error pages

FilePurpose
src/routes/404.tsxShown when no route matches (404)
src/routes/500.tsxShown when rendering throws (500)
src/routes/_error.tsxRoute-level error boundary (planned / layout errors)

If you omit 404.tsx or 500.tsx, EmberKit uses built-in default pages for SSR and client-side navigation. The Vite plugin wires them through notFoundRoute and errorRoute on virtual:emberkit-routes; pass both to render() alongside routes. Custom 404.tsx receives pathname; custom 500.tsx receives error: { status, message, error }. Override the defaults by adding the files above, or import DefaultNotFoundPage / DefaultServerErrorPage from @emberkit/core.

// src/routes/404.tsx
export default function NotFound() {
  return <h1>404 — Not found</h1>;
}

Route metadata

Export metadata for the built-in SSR head injector:

export const metadata = {
  title: 'Blog — My App',
  description: 'Latest posts',
};

export default function BlogIndex() {
  return <h1>Blog</h1>;
}
import { navigate, preload } from '@emberkit/core';

// Client navigation (optionally with View Transitions)
navigate('/about');
navigate('/settings', { replace: true });

// Prefetch hint
preload('/about');

Prefer <a href="/path"> for links that should work without JavaScript and participate in SSR.

Route loaders

Loaders return a discriminated LoaderResult<T>:

import type { LoaderFunction } from '@emberkit/core';
import { createLoaderData } from '@emberkit/core';

interface PostData {
  title: string;
  content: string;
}

export const loader: LoaderFunction<PostData> = async ({ params }) => {
  const res = await fetch(`/api/posts/${params.slug}`);
  if (!res.ok) {
    return {
      error: { code: 'NOT_FOUND', message: 'Post not found', status: 404 },
    };
  }
  const post = await res.json();
  return createLoaderData(post);
};

export default function BlogPost({ data }: { data: PostData }) {
  return (
    <article>
      <h1>{data.title}</h1>
    </article>
  );
}

Use createLoaderData or { data } / { error } objects matching LoaderResult<T>.

Loading states

// src/routes/_loading.tsx
export default function Loading() {
  return <div className="spinner">Loading...</div>;
}

API routes

Server-only handlers under src/routes/_api/ are excluded from the page manifest:

// src/routes/_api/hello.ts
export async function GET(request: Request) {
  return new Response(JSON.stringify({ message: 'Hello' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

SSR and static behavior

Route typehybrid buildhybrid request
Static (/about)Pre-rendered to dist/about/index.htmlServed as static HTML
Dynamic (/blog/:slug)Not pre-renderedRendered via entry-server.js

See SSR & SSG for mode details.

Next Steps

  • SSR & SSG — Build pipeline and rendering modes
  • Signals — Reactive state
  • Head — Declarative head tags
  • Hydration — Client bindings after SSR