routes/
src/routes/ のディレクトリ木が、そのままアプリの URL です。このページは、ファイル名とディレクトリ名の文法、照合の順序、ビルドが拒むもの、生成されるファイル、ページがタイトルを描く方法を説明します。
ディレクトリが pathname 空間
src/routes/ はアプリの pathname 空間で、それ以外のものは置けません。URL を増やすことはディレクトリを増やすことで、ある URL に答えるファイルはディレクトリ木を読めば 1 つに決まります。規約は覚えておく習慣ではなく、ビルドが検査する形です。
src/routes/
layout.tsx
page.tsx
not-found.tsx
error.tsx
old/
redirect.ts
products/
page.tsx
[id]/
page.tsx
(docs)/
layout.tsx
guide/
page.tsx
_parts/
counter.tsx| ファイル | URL | 役割 |
|---|---|---|
layout.tsx | — | すべてを包むルートレイアウト。文書そのもの |
page.tsx | / | ルートのページ |
not-found.tsx | /* | ほかのどれも答えなかった pathname |
error.tsx | — | 下で throw されたとき、代わりに描かれる |
old/redirect.ts | /old | 訪問者を別の URL へ送る |
products/page.tsx | /products | リテラルの区間 |
products/[id]/page.tsx | /products/:id | パラメータ。値は params.id としてページに渡る |
(docs)/layout.tsx | — | グループの中だけを包むレイアウト |
(docs)/guide/page.tsx | /guide | グループの中のページ。(docs) は URL に出ない |
_parts/counter.tsx | — | 私物。文法から見えない |
ファイル名は 5 つだけ
ディレクトリの中で文法が受け付けるファイル名は page.tsx・layout.tsx・not-found.tsx・error.tsx・redirect.ts の 5 つで、拡張子まで含めて完全に一致する必要があります。page.ts も helpers.ts もビルドが拒みます。それ以外のファイルは、_ で始まるディレクトリに置きます。
| ファイル | 役割 | 受け取る props |
|---|---|---|
page.tsx | そのディレクトリの URL を描く。default export がページ | params, pathname, request |
layout.tsx | その下で描かれるものすべてを children で包む | children, params, pathname, request |
not-found.tsx | そのディレクトリ以下で、ほかのどれも答えなかった pathname に答える | params, pathname, request |
error.tsx | 'use client' のファイル。下で throw されたとき、代わりに描かれる | error, reset |
redirect.ts | ページの代わりに、行き先を default export する | なし(描画しない) |
このモードでは、page.tsx・layout.tsx・not-found.tsx がリクエストのヘッダーと cookie も request として受け取ります。not-found.tsx はディレクトリごとに置けて、その答えは本物の 404 です。 アクションとリクエスト
ディレクトリ名の 4 つの形
ディレクトリの名前が、URL に何を足すか、何も足さないかを決めます。
| 名前 | URL に足すもの | 意味 |
|---|---|---|
products | /products | 文字・数字・.・_・~・- だけの名前は、書いたとおり 1 つの区間になります。大文字も使えます。 |
[id] | /:id | 1 区間を受け取るパラメータです。名前は文字か _ で始まり、文字・数字・_ が続きます。同じパスの上で同じ名前は 2 度使えません。 |
(docs) | なし | 区間を足さないグループです。木の一部にだけ効くレイアウトやエラー境界を持たせるためにあります。名前には文字・数字・_・- を使います。 |
_parts | なし | _ か . で始まる名前は、ディレクトリでもファイルでも文法から見えません。ルート専用の部品やデータはここに置きます。 |
可変長のパラメータ([...rest] のような形)はありません。ワイルドカードは 1 つだけで、それが not-found.tsx です。[...rest] という名前のディレクトリは、不正なパラメータディレクトリとして拒まれます。
ページは params を、レイアウトは children を受け取る
Server Component は context を読めないので、入れ子は props で渡ります。レイアウトは自分の下で描かれたものを children として受け取り、ページは自分のパターンのパラメータを params として受け取ります。どちらも pathname、つまりこの描画が対象にしている URL を受け取ります。パラメータより上にあるコンポーネントがその値を見る方法は、これしかありません。
// src/routes/products/[id]/page.tsx
import type { PageProps } from '@k8ordo/router';
export default function ProductPage({
params,
pathname,
}: PageProps<'/products/:id'>) {
return (
<>
<h1>{params.id}</h1>
<p>{pathname}</p>
</>
);
}@k8ordo/router の PageProps<pattern> と LayoutProps<pattern> は、この props をパターンで表した型です。生成された Register を読むので、ページはどちらのモードが入っているかに依存しません。props をインラインで書いても構いません。どちらの書き方でも、生成された表がコンポーネントを使う位置で satisfies によって検査し、それを報告するのは tsc です。
このサイトのルートレイアウトは、<html lang> を pathname の先頭区間から決めています。ロケールは URL にしか無く、クローラや読み上げが読むのはサーバーが書いた HTML だからです。
// src/routes/layout.tsx
import type { ReactNode } from 'react';
import { locales } from '../i18n';
export default function Root({
children,
pathname,
}: {
children: ReactNode;
pathname: string;
}) {
const locale = locales.delocalize(pathname).locale ?? locales.default;
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}照合の順序
ディレクトリ木そのものには順序が無いので、生成される表が順序を決めます。同じ階層ではリテラルの区間がパラメータより先に試され、not-found.tsx はその枝の最後に置かれます。about/ と [slug]/ が並んでいても、何も書かずに /about が届くのはこのためです。
グループだけは例外になりえます。グループは両方の種類の URL を 1 つのキーの下にまとめるので、表はその境界をまたいで並べ替えられません。次の木では、グループの中にリテラルの sale/ があるためにグループが about/ と同じ順位になり、名前の並びで先に置かれたグループの [id]/ が /about に先に答えてしまいます。宣言されたルートが決して描かれない形はここにしか生まれず、ビルドはそれを出荷せずに報告します。
src/routes/
page.tsx
about/
page.tsx
(shop)/
sale/
page.tsx
[id]/
page.tsxroutes/ is not a valid pathname space:
routes/about/page.tsx: "/about" can never match — "/:id" ((shop)/[id]/page.tsx) is declared first and answers itビルドが拒むもの
問題は最初の 1 件だけでなく全部報告され、どれもファイルを名指します。ビルドは routes/ is not a valid pathname space: に続けて 1 行ずつ並べて止まります。vite dev も、起動の時点で問題があれば同じエラーで起動しません。起動した後の変更が問題を生んだときは、サーバーを止めずに routes/<path>: <message> の行をログに出します。問題があるあいだ、.k8ordo/ は書き直されません。
routes/ is not a valid pathname space:
routes/[123]: "[123]" is not a valid param directory — use [name] with a letter or underscore first
routes/products/helper.ts: routes/ holds only page.tsx, layout.tsx, not-found.tsx, error.tsx, redirect.ts — move "helper.ts" under a _-prefixed directory| routes/ にあるもの | エラー |
|---|---|
products/helper.ts | routes/ holds only page.tsx, layout.tsx, not-found.tsx, error.tsx, redirect.ts — move "helper.ts" under a _-prefixed directory |
[123]/page.tsx | "[123]" is not a valid param directory — use [name] with a letter or underscore first |
(docs/page.tsx | "(docs" is not a valid route group — use (name) |
pro ducts/page.tsx | "pro ducts" cannot be a URL segment — use letters, digits, . _ ~ or - |
[id]/things/[id]/page.tsx | ":id" is already taken by an ancestor — params must be unique within a path |
orphan/layout.tsx だけがあり、下にページが無い | has a layout but no page.tsx below it, so it can never render |
empty/error.tsx だけがあり、下にページもリダイレクトも無い | declares no route — every directory needs a page.tsx (or redirect.ts) somewhere below it |
(a)/page.tsx と (b)/page.tsx | "/" is already declared by (a)/page.tsx — route groups do not separate URLs |
old/page.tsx と old/redirect.ts | "old" cannot both render page.tsx and redirect — keep one |
(shop)/sale/page.tsx と (shop)/[id]/page.tsx の横に about/page.tsx | "/about" can never match — "/:id" ((shop)/[id]/page.tsx) is declared first and answers it |
(shell)/docs/page.tsx と (shell)/not-found.tsx の横に about/page.tsx | "/about" can never match — "/*" ((shell)/not-found.tsx) is declared first and answers it |
隠されたルートの検査は、文法の問題が 1 つも無い木にだけ走ります。文法が拒んだ名前はパターンにならないからです。そのため、文法の問題を直した次の実行で、隠されたルートが初めて報告されることがあります。
このモードでは、routes/ の形についてビルドが止まる理由は上の表と隠されたルートだけです。パラメータの値はリクエストと一緒に届くので、列挙を求められることはありません。
生成されるファイル
フレームワークは .k8ordo/ を書き、ディレクトリと同期させ続けます。書かれるのは vite dev の起動時と vite build の開始時で、開発中は routes/ の下のファイルが追加・削除・変更されるたびに書き直されます。生成物なので編集するものではありませんが、ルーターの公開 API だけを使ったただのソースなので、読むものではあります。
| ファイル | 中身 |
|---|---|
routes.gen.ts | 表そのもの(routes)、ページのパターンごとに走るスキーマ(paramSchemas)、リダイレクト(redirects)。各ルートファイルは satisfies で、自分のディレクトリが置かれたパターンに対して検査されます。 |
register.gen.ts | 表を @k8ordo/router の Register に結び、アプリが @k8ordo/state に依存していればそちらにも結びます。href や useMatch のパターンが表に対して検査されるのはこのためです。 |
.gitignore | 中身は * です。ディレクトリが自分自身を git から外すので、アプリの .gitignore に足すものはありません。 |
次の木からは、下の 2 つのファイルが生成されます。どちらも抜粋で、routes.gen.ts は routes の export だけを、register.gen.ts はアプリが @k8ordo/state に依存しないときの宣言だけを示しています。実際のファイルはどちらも // Generated by … の行で始まり、どのモードのパッケージが生成したかを名乗ります。
src/routes/
layout.tsx
page.tsx
not-found.tsx
products/
page.tsx
[id]/
page.tsx// .k8ordo/routes.gen.ts
export const routes = defineRoutes({
'/': {
layout: layout satisfies Layout<'/'>,
children: {
'/': page satisfies Page<'/'>,
'/products': {
children: {
'/': products_page satisfies Page<'/products'>,
'/:id': products_id_page satisfies Page<'/products/:id'>,
},
},
'/*': not_found satisfies Page<'/*'>,
},
},
});// .k8ordo/register.gen.ts
import type { ParsedParamsMap } from '@k8ordo/router';
import type { RouteRequest } from '@k8ordo/server/runtime';
import type { paramSchemas, routes } from './routes.gen';
declare module '@k8ordo/router' {
interface Register {
routes: typeof routes;
params: ParsedParamsMap<typeof paramSchemas>;
request: RouteRequest;
}
}tsconfig.json
型の配線を効かせるには、tsconfig.json の include に .k8ordo をグロブで書きます。.k8ordo はドットで始まるので、ディレクトリ名だけを書いた include は黙ってそれを飛ばします。飛ばされてもビルドは通り、href が表に対して検査されなくなるだけです。
{
"include": ["src/**/*.ts", "src/**/*.tsx", ".k8ordo/**/*.ts"]
}vite build は型を検査しません。satisfies による検査の結果を出すのは tsc です。.k8ordo/ は git に入らないので、新しいチェックアウトでは tsc の前に一度 vite dev か vite build を走らせて書かせます。
タイトルとメタデータ
メタデータの API はありません。React 19 は木のどこで描かれた <title>・<meta>・<link> も <head> へ持ち上げるので、ページは自分のタイトルを、ほかのものと同じ場所で描きます。
// src/routes/products/[id]/page.tsx
import type { PageProps } from '@k8ordo/router';
export default function ProductPage({ params }: PageProps<'/products/:id'>) {
return (
<>
<title>{`Product ${params.id}`}</title>
<meta content="One product from the catalog" name="description" />
<h1>{params.id}</h1>
</>
);
}画面上の <title> は常に 1 つにします。ルートレイアウトは何も描かず、各ページと not-found.tsx が 1 つずつ描きます。2 つ描けば 2 つとも描かれるだけで、後のものが勝つ仕組みはありません。このサイトは、各ページのタイトルを PageTitle というコンポーネントを通して描いています。
リンクは表に対して型が付く
ブラウザでのナビゲーションは @k8ordo/router が担います。Navigation API の下では素の <a> がそのままクライアント遷移になり、href() のパターンと params は生成された表に対して検査されます。
// src/routes/products/page.tsx
import { href } from '@k8ordo/router';
export default function ProductsPage() {
return (
<ul>
<li>
<a href={href('/products/:id', { id: 1 })}>first product</a>
</li>
<li>
<a href={href('/guide')}>guide</a>
</li>
</ul>
);
}