Reader System
BluePeriodのReader(読書)システムのアーキテクチャ、コンテンツ配信、多言語対応、読書UI機能を網羅的に解説
Reader System (24_reader_system.md)
1. 概要
Readerシステムは、BluePeriodプラットフォーム上で公開された作品を閲覧するための読書環境を提供します。5層構造のプロット(Part → Arc → Chapter → Story → Section)に基づく階層的レンダリング、多言語マニュスクリプトの切替、目次ドロワー、読書進捗の自動保存など、長時間の読書に最適化された没入感のあるUIを実現しています。
主な特徴:
- 階層的レンダリング(Part → Story → Section)
- マニュスクリプトの多言語切替
- StructuredPlot に基づく目次ナビゲーション
- 読書進捗の自動保存(localStorage)
- セリフ体による没入感のあるタイポグラフィ
- JSON-LD構造化データによるSEO最適化
2. 画面構成とレイアウト
2.1. ルーティング
| パス | 役割 |
|---|---|
/read/[id] | リダイレクトページ。最初のStoryを特定し /read/[id]/[storyId] にリダイレクト |
/read/[id]/[storyId] | メイン読書ページ |
/read/[id]/loading.tsx | ローディングスケルトン |
2.2. リダイレクトページ
/read/[id]/page.tsx はServer Componentとして実装:
published_projectsからcontent_pathを取得- Storage から
ProjectContentをダウンロード - StructuredPlot をトラバースして最初のStoryを特定
- 新構造:
parts → arcs → chapters → stories - 旧構造:
chapters → stories(後方互換)
- 新構造:
- 最初のStory IDにリダイレクト、または
notFound()
2.3. メイン読書ページ
/read/[id]/[storyId]/page.tsx:
generateMetadata()でSEO向けメタデータを動的生成getPublishedProjectContent()でコンテンツ取得extractAvailableLanguages()で利用可能言語を検出- JSON-LD構造化データを出力(
<script type="application/ld+json">)
ページ構成:
┌──────────────────────────────────────┐
│ ReaderHeader │
│ [←戻る] Title [🌐言語] [🔖] [≡TOC]│
├──────────────────────────────────────┤
│ │
│ ReaderContent │
│ ┌──────────────────────────────┐ │
│ │ Story Title │ │
│ │ │ │
│ │ Section 1 │ │
│ │ Manuscript content... │ │
│ │ │ │
│ │ Section 2 │ │
│ │ Manuscript content... │ │
│ └──────────────────────────────┘ │
│ │
├──────────────────────────────────────┤
│ Navigation (Prev/Next Story) │
└──────────────────────────────────────┘3. コンテンツ配信アーキテクチャ
3.1. コンテンツ取得フロー
1. Supabase Database: published_projects.content_path を取得
2. Supabase Storage: contents バケットから JSON ファイルをダウンロード
3. JSON.parse → ProjectContent として型付きオブジェクトに変換downloadProjectContent() の実装:
async function downloadProjectContent(supabase, contentPath: string) {
const { data: fileBlob, error } = await supabase.storage
.from("contents")
.download(contentPath);
const contentText = await fileBlob.text();
return JSON.parse(contentText) as ProjectContent;
}コンテンツパスの形式: {userId}/{projectId}.json
3.2. Storage アクセス制御
- contents バケット: 公開プロジェクトのコンテンツはパスベースでアクセス可能
- RLS ポリシーにより、公開済み作品のコンテンツのみ読み取り可能
4. StructuredPlot と Manuscript の対応関係
4.1. 型構造
interface StructuredPlot {
language: string; // BCP 47 言語タグ
parts?: Part[]; // 新構造(Part → Arc → Chapter → Story → Section)
chapters?: Chapter[]; // 旧構造(後方互換)
}
interface Part {
id: string;
part_number: number;
part_title: string;
arcs: Arc[];
}
interface Arc {
id: string;
arc_number: number;
arc_title: string;
chapters: Chapter[];
}
interface Chapter {
id: string;
chapter_number: number;
chapter_title: string;
theme: string;
expected_word_count?: string;
stories: Story[];
}
interface Story {
id: string;
story_number: number;
story_title: string;
story_purpose: string;
sections: Section[];
}
interface Section {
id: string;
section_number: number;
section_label?: string;
content: string;
target_word_count?: number;
}4.2. Manuscript の構造
interface ManuscriptVersion {
isOriginal: boolean;
content: string;
createdAt: string;
updatedAt: string;
sourceLang?: string;
debugInfo?: {
prompt?: string;
model?: string;
temperature?: number;
topP?: number;
maxTokens?: number;
inputTokens?: number;
outputTokens?: number;
timestamp?: string;
contextSummary?: {
writingPrompts?: number;
structurePlot?: number;
manuscripts?: number;
total?: number;
};
};
}
interface Manuscript {
[sectionId: string]: {
versions: {
[lang: string]: ManuscriptVersion;
};
};
}4.3. セクションとマニュスクリプトの紐付け
- StructuredPlot の各
Section.idがManuscriptのキーに対応 - 各セクションは複数言語バージョンを保持(
versions[lang]) - Readerではストーリー内のセクションを順次表示
4.4. reorderManuscriptsByPlot
lib/publishUtils.ts に定義。Publish時にマニュスクリプトをプロットの物語順に並び替える関数:
parts → arcs → chapters → stories → sectionsの順でトラバース- 各
section.idを出現順に収集 - 収集したID順に
Manuscriptオブジェクトを再構築
これにより、Publish後のコンテンツJSONは常に物語の進行順で保存されます。
5. 言語切替
5.1. 利用可能言語の検出
extractAvailableLanguages() が全マニュスクリプトから言語コードを収集:
export function extractAvailableLanguages(manuscripts: Manuscript): string[] {
const availableLanguages = new Set<string>();
Object.values(manuscripts).forEach((m) => {
if (m.versions) {
Object.keys(m.versions).forEach((lang) => availableLanguages.add(lang));
}
});
return Array.from(availableLanguages);
}5.2. 言語選択の永続化
export const selectedLanguageAtom = atomWithStorage<string>(
"reading-language", "ja"
);読書中の言語設定は localStorage に永続化され、次回以降も同じ言語で表示されます。
ReaderHeader はマウント時に selectedLanguage がマニュスクリプト存在言語(availableLanguages)に含まれるかを検証し、含まれない場合は originalLanguage または先頭のマニュスクリプト言語にフォールバックします。これによりメタデータ言語と原稿言語の乖離を防ぎます。
5.2.1. メタデータ言語の制約
displayTitle の解決はマニュスクリプト存在言語に制限されます:
1. availableLanguages に selectedLanguage が含まれる → localizations[selectedLanguage].title
2. 含まれない → localizations[originalLanguage].title
3. 最終フォールバック → project.titleこの制約により「日本語タイトル + 英語原稿」のミスマッチが防止されます。
5.3. マニュスクリプトの言語フォールバック
各セクションの表示テキスト決定:
1. manuscript[section.id].versions[selectedLanguage]
2. manuscript[section.id].versions[structuredPlot.language](原語)選択言語のバージョンが存在しない場合は、原語バージョンにフォールバックします。
5.4. プロット翻訳(plotTranslations)
ProjectContent.plotTranslations は目次や見出しの多言語表示に使用:
plotTranslations?: {
[lang: string]: { // 言語コード
[id: string]: PlotTranslation; // 要素ID(Part/Chapter/Story等)
};
}目次(TableOfContents)での翻訳取得は getTranslatedTitle() により、以下のフォールバックチェーンで処理:
- 完全一致(例:
zh-Hans) - ベース言語フォールバック(例:
zh→zh-Hans) - バリアント確認(例:
zh-Hans→zh) - 最終フォールバック: 原語の
story_title/chapter_title等
6. 読書UI機能
6.1. ReaderHeader
画面上部の固定ヘッダー:
| 要素 | 機能 |
|---|---|
| 戻るボタン | 内部/外部ナビゲーションを判定して適切な遷移先へ |
| タイトル | プロジェクト名 + 現在のストーリー名を動的表示 |
| 言語切替 | ドロップダウンでマニュスクリプト言語を変更 |
| ブックマーク | ライブラリへの追加/削除 |
| TOC切替 | 目次ドロワーの開閉 |
6.2. TableOfContents(目次)
components/reader/TableOfContents.tsx:
- Sheet コンポーネントによる右側ドロワー
- 階層表示:
- Parts(最高レベル)
- Arcs(Part内)
- Chapters(Arc内)
- Stories(Chapter内)
- 旧構造(parts/arcsなし)にも対応
- 現在のストーリーを
primaryカラーでハイライト + BookIcon - 翻訳フォールバックチェーンによる多言語タイトル表示
6.3. タイポグラフィ
読書画面専用のCSS(markdown-reader.css):
- line-height: 1.625(読みやすい行間)
- letter-spacing: 0.025em(日本語可読性向上)
- text-align: justify(両端揃え)
フォント設定:
| Atom | デフォルト | 説明 |
|---|---|---|
manuscriptFontAtom | 'Noto Serif', serif | 原稿用フォント |
manuscriptFontSizeAtom | 16px | フォントサイズ |
manuscriptFontWeightAtom | 400 | フォントウェイト |
デザインシステムでは、読書画面のタイポグラフィに var(--font-manuscript) を使用し、没入感を高める設計としています(09_design_system 参照)。
6.4. MarkdownRenderer
components/MarkdownRenderer.tsx:
- Unified パイプライン(remark / rehype)
- カスタムコンポーネント:
- コードブロック(コピー機能付き)
- 特殊ブロック(thought, progress, call, plan)
- セクション内ローカルナビゲーション
- KaTeX 数式レンダリング
- シンタックスハイライト
- ID接頭辞によるメッセージ間コンフリクト防止
6.5. 読書進捗
export const readingProgressAtom = atomWithStorage<Record<string, string>>(
"reading-progress", {}
);- プロジェクトIDをキーに、最後に読んだStory IDを保存
- ページ読み込み時に
useReadingProgress(projectId)で更新 - ユーザーが再度アクセスした際に続きから読める仕組み
6.6. 統計トラッキング
updateDailyStats()がページ読み込み時に呼び出しreadPublishIdを IndexedDB のdaily_statsテーブルに記録- 読書アクティビティの分析に利用
6.7. ブックマーク
- クライアントサイドコンポーネントによる楽観的更新
- Library API エンドポイントと連携:
POST /api/library- 追加DELETE /api/library?projectId={id}- 削除
- トースト通知によるフィードバック
- 削除時の確認ダイアログ
7. コンポーネント一覧
Reader画面構成
| コンポーネント | 場所 | 責務 |
|---|---|---|
| ReaderRedirectPage | app/read/[id]/page.tsx | Server Component。最初のStoryにリダイレクト |
| ReaderPage | app/read/[id]/[storyId]/page.tsx | Server Component。コンテンツ取得、メタデータ生成 |
| ReaderHeader | components/reader/ReaderHeader.tsx | 固定ヘッダー(ナビゲーション、言語切替、ブックマーク、TOC) |
| ReaderContent | components/reader/ReaderContent.tsx | ストーリーコンテンツのレンダリング |
| TableOfContents | components/reader/TableOfContents.tsx | 目次ドロワー |
| MarkdownRenderer | components/MarkdownRenderer.tsx | Markdown描画エンジン |
ユーティリティ
| モジュール | 場所 | 責務 |
|---|---|---|
| publishUtils | lib/publishUtils.ts | reorderManuscriptsByPlot, extractAvailableLanguages, fileToBase64 |
| types | lib/types.ts | ProjectContent, StructuredPlot, Manuscript, Section 等の型定義 |
| projects/service | lib/features/projects/service.ts | getPublishedProjectContent |
状態管理
| Atom | ストレージキー | 説明 |
|---|---|---|
selectedLanguageAtom | reading-language | 読書言語設定(デフォルト: "ja") |
readingProgressAtom | reading-progress | プロジェクト別の読書進捗 |
manuscriptFontAtom | - | 原稿フォント設定 |
manuscriptFontSizeAtom | - | フォントサイズ |
manuscriptFontWeightAtom | - | フォントウェイト |
8. データフロー図
┌──────────────────────────────────────────────────────────────┐
│ /read/[id] (Redirect) │
│ 1. SELECT content_path FROM published_projects WHERE id=? │
│ 2. Storage download: contents/{userId}/{projectId}.json │
│ 3. Traverse StructuredPlot → find first story │
│ 4. redirect(/read/{id}/{firstStoryId}) │
└──────────────────────┬───────────────────────────────────────┘
│ redirect
▼
┌──────────────────────────────────────────────────────────────┐
│ /read/[id]/[storyId] (Reader Page) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Server-side │ │
│ │ 1. SELECT * FROM published_projects WHERE id=? │ │
│ │ 2. Storage download → ProjectContent │ │
│ │ 3. extractAvailableLanguages(manuscripts) │ │
│ │ 4. Check library bookmark status │ │
│ │ 5. Generate JSON-LD structured data │ │
│ └────────────────────┬───────────────────────────────────┘ │
└───────────────────────┼──────────────────────────────────────┘
│ props
▼
┌──────────────────────────────────────────────────────────────┐
│ Reader UI (Client) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ReaderHeader │ │
│ │ [←] Title [🌐 Language] [🔖 Bookmark] [≡ TOC] │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ReaderContent │ │
│ │ For each Story: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Story Title │ │ │
│ │ │ For each Section: │ │ │
│ │ │ ┌────────────────────────────────────────────┐ │ │ │
│ │ │ │ manuscript[sectionId].versions[lang].content│ │ │ │
│ │ │ │ → MarkdownRenderer → HTML │ │ │ │
│ │ │ └────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ TableOfContents (Sheet/Drawer) │ │
│ │ Part I │ │
│ │ Arc 1 │ │
│ │ Chapter 1: Theme │ │
│ │ ● Story 1 (current) ← primary highlight │ │
│ │ ○ Story 2 │ │
│ │ Chapter 2: Theme │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘9. Supabase連携
9.1. データベースアクセス
Readerでは以下のデータを取得:
| 取得対象 | テーブル | カラム |
|---|---|---|
| コンテンツパス | published_projects | content_path |
| メタデータ | published_projects | title, headline, synopsis, cover_image_url, localizations, original_language 等 |
| ブックマーク状態 | library_items | user_id, project_id |
9.2. Storage アクセス
- バケット名:
contents - ファイルパス:
{userId}/{projectId}.json - アクセス制御: 公開プロジェクトのコンテンツは誰でも読み取り可能
9.3. RLS ポリシー
- published_projects: 全ユーザー SELECT 可能(
true) - library_items: ユーザー自身のアイテムのみアクセス可能
10. 関連ドキュメント
- 22_publish_system - Publishシステム(コンテンツの上流)
- 23_explore_system - Exploreシステム(作品発見)
- 04_database_schema_supabase - Supabaseデータベーススキーマ
- 07_locale_system - BCP 47言語タグの管理方式
- 09_design_system - デザインシステム(Reader向けタイポグラフィ規定)
- 07_reader_viewer_components - Reader/Viewer コンポーネント詳細