[Feature] 問題集のURLでカリキュラム別と解法別を追加できるようにしましょう
Description / 説明
- A clear and concise description of what you want to happen.
Motivation / 動機
- 問題集の種類別にデータおよびUI を明示的に分けるため
Other notes / その他
- Add any other context or screenshots about the feature request here.
- Will you try to create a pull request?
- yes / no
See
https://svelte.dev/tutorial/kit/breaking-out-of-layouts
SvelteKitのルーティングシステムでは、同じページで複数のルートの内容を表示することは可能です。これを実現するには、いくつかのアプローチがあります。
1. ネストされたレイアウトを使用する方法
SvelteKitでは、ネストされたレイアウトを使用して、親ルートの内容を保持しながら子ルートの内容を追加表示することができます。
src/
routes/
hoge/
+page.svelte // /hogeのコンテンツ
+layout.svelte // レイアウトファイル
foo/
+page.svelte // /hoge/fooのコンテンツ
/hoge/+layout.svelte:
<div>
<h1>Hogeページの共通コンテンツ</h1>
<!-- 現在のページのコンテンツをレンダリング -->
<slot />
<!-- 追加のコンテンツエリア -->
<div id="additional-content">
{#if $page.url.pathname === '/hoge/foo'}
<!-- fooページの内容を表示する場合の処理 -->
<svelte:component this={import('./foo/+page.svelte')} />
{/if}
</div>
</div>
2. ページパラメータを使用する方法
クエリパラメータやURLパラメータを使用して、同じページ内でコンテンツを動的に追加表示することもできます。
<script>
import { page } from '$app/stores';
import { onMount } from 'svelte';
import FooContent from './foo/+page.svelte';
let showFooContent = false;
$: {
// URLが/hoge/fooの場合、fooのコンテンツも表示する
if ($page.url.pathname === '/hoge/foo') {
showFooContent = true;
}
}
</script>
<div>
<h1>Hogeページのメインコンテンツ</h1>
<!-- fooへのリンク -->
<a href="/hoge/foo">Fooコンテンツを表示</a>
<!-- fooのコンテンツ(条件付き表示) -->
{#if showFooContent}
<div class="foo-content">
<FooContent />
</div>
{/if}
</div>
3. SvelteKitのロードファンクションを使用する方法
+page.jsまたは+page.server.jsでデータをロードし、それをページコンポーネントで表示することもできます。
/hoge/+page.js:
export async function load({ url }) {
const showFoo = url.pathname === '/hoge/foo';
let fooData = null;
if (showFoo) {
// fooに関連するデータを取得する処理
fooData = await fetchFooData();
}
return {
showFoo,
fooData
};
}
/hoge/+page.svelte:
<script>
export let data;
</script>
<div>
<h1>Hogeページのメインコンテンツ</h1>
<a href="/hoge/foo">Fooコンテンツを表示</a>
{#if data.showFoo}
<div class="foo-content">
<!-- fooのコンテンツ -->
<h2>Fooのコンテンツ</h2>
<p>{data.fooData}</p>
</div>
{/if}
</div>
いずれの方法でも、URLが/hoge/fooのときに/hogeのコンテンツに加えてfooのコンテンツを表示させることができます。SvelteKitのルーティングシステムは柔軟なので、アプリケーションの要件に合わせて最適な方法を選択できます。
SvelteKitでの複数ルート表示方法の比較
各アプローチの長所、短所、適したユースケースを以下に詳しく解説します。
1. ネストされたレイアウトを使用する方法
長所
- SvelteKitの規約に沿っている: レイアウトシステムはSvelteKitの標準機能である
- コードの再利用性: 共通レイアウトを一箇所で管理でき、複数ページで再利用できる
- SEO対応: 各ルートは独自のページとして扱われるため、SEO最適化がしやすい
- ブラウザ履歴の管理: URLが変わるため、ブラウザの戻るボタンが期待通り動作する
短所
- ページ遷移が発生する: 完全なページ更新が発生するため、ユーザー体験が損なわれる可能性がある
- 状態の維持が難しい: ページ間の遷移で状態を保持するには追加の実装が必要
ユースケース
- ドキュメントサイト(メニューは固定で本文だけ変わる)
- 管理画面(サイドバーは固定でメインコンテンツが変わる)
- マルチステップフォーム(進捗状況を表示しながら内容が変わる)
2. ページパラメータを使用する方法
長所
- SPAのような体験: ページ全体を再読み込みせずにコンテンツを動的に変更できる
- 状態の保持: ページ遷移なしでコンポーネント状態を保持できる
- 柔軟性: URLパラメータに基づいて様々なコンテンツの組み合わせが可能
短所
- 実装の複雑さ: 条件分岐が多くなると管理が難しくなる
- コンポーネント間の結合度: ページ間の依存関係が強くなりがち
- 初期ロード時間: すべての可能なコンテンツをロードすると初期表示が遅くなる可能性がある
ユースケース
- ダッシュボード(タブ切り替えでURLも変わる)
- フィルタリング機能のあるリスト表示
- 詳細情報の展開(リストから項目を選んで詳細を同ページに表示)
3. ロードファンクションを使用する方法
長所
- データ取得の分離: UIとデータ取得ロジックを明確に分離できる
- SSR対応: サーバーサイドレンダリングと相性が良い
- 効率的なデータ取得: 必要なデータのみを条件付きで取得できる
- 型の安全性: TypeScriptと組み合わせると型安全なデータの受け渡しが可能
短所
- 実装の手間: 各ページでロードファンクションを実装する必要がある
- 複雑なデータ依存関係: 複数のデータ取得パターンがあると管理が難しくなる
- パフォーマンスへの影響: 多くのデータを取得するとレンダリングが遅くなる可能性がある
ユースケース
- APIからデータを取得して表示するページ
- 認証が必要なコンテンツ
- 条件に基づいて異なるデータセットを表示するページ
4. 追加オプション: スプリットパネルUIの実装
上記3つの方法を組み合わせた応用例として、スプリットパネルUIが考えられます。
<script>
import { getContext } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
// 状態管理用のrunes
const mainPath = $state('/hoge');
let detailPath = $derived($page.url.pathname.startsWith('/hoge/') ? $page.url.pathname : null);
let showDetail = $derived(!!detailPath);
// パネルを開く関数
function openDetailPanel(id) {
goto(`/hoge/${id}`, { keepfocus: true });
}
// パネルを閉じる関数
function closeDetailPanel() {
goto('/hoge', { keepfocus: true });
}
</script>
<div class="split-panel-layout">
<div class="main-panel">
<!-- メインコンテンツ -->
<h1>メインコンテンツ</h1>
<ul>
<li><button on:click={() => openDetailPanel('foo')}>Fooの詳細を見る</button></li>
<li><button on:click={() => openDetailPanel('bar')}>Barの詳細を見る</button></li>
</ul>
</div>
{#if showDetail}
<div class="detail-panel">
<button on:click={closeDetailPanel}>閉じる</button>
<!-- URLに基づいて動的にコンポーネントを読み込む -->
{#if detailPath === '/hoge/foo'}
<svelte:component this={import('./foo/+page.svelte')} />
{:else if detailPath === '/hoge/bar'}
<svelte:component this={import('./bar/+page.svelte')} />
{/if}
</div>
{/if}
</div>
<style>
.split-panel-layout {
display: flex;
height: 100%;
}
.main-panel {
flex: 1;
}
.detail-panel {
flex: 1;
border-left: 1px solid #ccc;
padding: 1rem;
}
</style>
主な変更点:
-
$:リアクティブステートメントを$stateと$derivedに置き換え-
mainPathは$stateで定義(実際には変更されていないのでconstでも問題ありません) -
detailPathとshowDetailは$derivedで定義(計算値のため)
-
-
onMountのインポートを削除(この例では使用されていないため) -
$pageストアの使用方法は基本的に同じですが、runesモードではストア値へのアクセスの仕方が若干異なります
注意点として、Svelte v5はまだ開発中であり、runesの正確な構文やAPIは変更される可能性があります。最新のドキュメントを参照することをお勧めします。 主な変更点:
-
$:リアクティブステートメントを$stateと$derivedに置き換え-
mainPathは$stateで定義(実際には変更されていないのでconstでも問題ありません) -
detailPathとshowDetailは$derivedで定義(計算値のため)
-
-
onMountのインポートを削除(この例では使用されていないため) -
$pageストアの使用方法は基本的に同じですが、runesモードではストア値へのアクセスの仕方が若干異なります
選定の指針
- SEOが重要: レイアウトベースのアプローチ
- ユーザー体験を重視: パラメータベースのアプローチ
- データ駆動型アプリ: ロードファンクションアプローチ
- 複雑なUI: 複数アプローチの組み合わせ
適切な方法は、アプリケーションの要件、複雑さ、およびターゲットユーザーの期待に基づいて選定するのが望ましいです。