ルーティング
src/app 上のファイルベースルーティング — ページ、レイアウト、動的セグメント、ナビゲーション。
src/app/**/page.tsx はそれぞれ自動的にルートになります — ルーター設定を書く必要はありません。Vite プラグインが src/app をスキャンして仮想のルートテーブルを生成し、(スキャフォールドのエントリによってマウントされる)<AppRouter> がナビゲーションのたびにそれとマッチングします。
src/app/
├─ layout.tsx # すべてのルートをラップする
├─ page.tsx # "/"
├─ about/
│ └─ page.tsx # "/about"
└─ blog/
└─ [slug]/
└─ page.tsx # "/blog/:slug"レイアウト
layout.tsx はその配下のすべてのルートをラップします(ネストしたレイアウトはルートからリーフへと合成されます)。ルートレイアウトは、murasaki が提供するフルウィンドウのフレームコンポーネントである <App> で子要素をラップする必要があります:
import type { ReactNode } from 'react'
import { App, useContextMenu } from 'murasaki'
export default function Layout({ children }: { children: ReactNode }) {
useContextMenu([{ label: 'Reload', shortcut: 'command,R', action: () => location.reload() }])
return <App className="flex items-center justify-center">{children}</App>
}動的セグメント
[param] フォルダは URL のセグメントをキャプチャします。useParams() で読み取れます:
import { useParams } from 'murasaki'
export default function BlogPost() {
const { slug } = useParams()
return <p>Post: {slug}</p>
}静的セグメントは常に動的セグメントより優先され、より具体的なマッチ(リテラルセグメントが多い方)がより非具体的なものより優先されます — Next.js と同じ優先順位です。(group) フォルダ(括弧を使い、ブラケットは使わない)は、URL セグメントを追加せずにルートを整理します。
キャッチオールセグメント
[...name] フォルダは残りのセグメントすべてを string[] としてキャプチャします。少なくとも 1 つのセグメントがマッチする必要があります。[[...name]](二重ブラケット)はオプショナルな形式で、親パス自体にもマッチし、その場合パラメータは undefined のままになります:
import { useParams } from 'murasaki'
export default function DocsPage() {
const { slug } = useParams()
// "/docs" -> slug は undefined
// "/docs/a/b" -> slug は ["a", "b"]
return <p>{slug ? slug.join('/') : 'index'}</p>
}優先順位は Next.js と同じです: 静的 > 動的([name]) > キャッチオール([...name]) > オプショナルキャッチオール([[...name]])。
loading / error / not-found バウンダリ
これらのファイルを page.tsx の隣(またはその上位)に置きます:
loading.tsx— ページがサスペンドしている間に表示されます(<Suspense>でラップされます)。error.tsx— エラーバウンダリです。{ error, reset }を受け取るコンポーネントをエクスポートします。そのルート配下で発生したレンダーエラーをキャッチします。not-found.tsx— マッチするルートがない場合にレンダーされます。URL の祖先チェーンの中で最も近いものが優先されます。
それぞれが Next.js の App Router と同様に、自身のサブツリーにスコープされます。
ナビゲーション
クライアントサイドナビゲーション(フルリロードなし)には <Link> を使います:
import { Link } from 'murasaki'
<Link href="/about">About</Link>useRouter() は push / replace / back / pathname を命令的に提供し、usePathname() は現在のパスを読み取ります。push / replace / <Link href> に付けたクエリ文字列(例: /search?q=cats)は自動的にパスから分離されます — ルートマッチングはパスだけを見るため、クエリだけのナビゲーションはmiddlewareを再実行せず、レンダーされるページも変わりません。
useSearchParams() は現在のクエリ文字列を URLSearchParams として読み取り、すべてのナビゲーションに反応します:
import { useSearchParams } from 'murasaki'
function SearchResults() {
const params = useSearchParams()
return <p>Query: {params.get('q')}</p>
}オリジンが異なる URL への通常の <a href="https://..."> は、アプリのウィンドウ内ではなく、ユーザーのデフォルトのシステムブラウザで開きます — 外部リンクを特別扱いする必要はありません。