# 通知設計書 41-Pages Routerルートアナウンサー

## 概要

本ドキュメントは、Next.jsのPages Routerにおけるルートアナウンサー機能の通知設計について記述する。RouteAnnouncerコンポーネントがクライアントサイドナビゲーション時にスクリーンリーダー向けにページ遷移情報をaria-liveリージョンを通じてアナウンスする仕組みを定義する。

### 本通知の処理概要

Pages Routerルートアナウンサーは、クライアントサイドルーティングにおけるアクセシビリティを確保するための通知機能である。SPAアーキテクチャでは従来のフルページリロードが発生しないため、スクリーンリーダーが新しいページへの遷移を自動的に検知できない。本通知はこの課題を解決し、視覚障害を持つユーザーに対してページ遷移が行われたことを確実に伝達する。

**業務上の目的・背景**：SPAアーキテクチャにおいてクライアントサイドルーティングが行われると、ブラウザはフルページリロードを行わない。これにより、スクリーンリーダーなどの支援技術がページ遷移を検知できず、視覚障害を持つユーザーがページ遷移を認識できない問題が発生する。本通知はWCAG 2.1のアクセシビリティ要件を満たすために必要であり、Marcy Suttonのアクセシブルクライアントルーティングに関するユーザーテスト研究に基づいて実装されている。

**通知の送信タイミング**：Pages Routerの`asPath`（URLパス）が変更されるたびに通知が発生する。初回ロード時は通知を行わない（スクリーンリーダーが自動的に初回ロード時のページ内容を読み上げるため）。具体的には`useEffect`フック内で`previouslyLoadedPath`と現在の`asPath`を比較し、変更が検出された場合にのみアナウンスを実行する。

**通知の受信者**：スクリーンリーダー（NVDA、JAWS、VoiceOverなど）を使用しているユーザー。HTMLの`aria-live="assertive"`属性と`role="alert"`属性により、支援技術が即座にアナウンス内容を読み上げる。

**通知内容の概要**：以下の優先順位でアナウンス内容が決定される。(1) `document.title`（headで設定されたページタイトル）、(2) ページ内の最初の`h1`要素のテキスト内容（`innerText`または`textContent`）、(3) URLの`asPath`（上記いずれも取得できない場合のフォールバック）。

**期待されるアクション**：スクリーンリーダーユーザーは、ページ遷移が行われたことをアナウンスにより認識し、新しいページのコンテンツを確認する。開発者側では特にアクションは不要で、Next.jsフレームワークが自動的に処理を行う。

## 通知種別

ブラウザ内アクセシビリティ通知（aria-live assertiveリージョンによるスクリーンリーダーへの音声出力）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（React useEffectによるDOM更新） |
| 優先度 | 高（aria-live="assertive"による即時アナウンス） |
| リトライ | 無（DOM更新ベースのため不要） |

### 送信先決定ロジック

Pages Routerを使用するすべてのページで自動的にRouteAnnouncerが有効化される。`packages/next/src/client/index.tsx`の行778にて、`<Portal type="next-route-announcer">`コンポーネント内に`<RouteAnnouncer />`がレンダリングされ、アプリケーションのルートに自動的に挿入される。受信者はブラウザの支援技術（スクリーンリーダー）であり、ユーザー単位の制御は不要。

## 通知テンプレート

### メール通知の場合

本通知はメール通知ではない。ブラウザ内のaria-liveリージョンによるスクリーンリーダー通知である。

### 本文テンプレート

```
{routeAnnouncement}
```

`routeAnnouncement`は以下の優先順位で決定される:
1. `document.title` - ページタイトルが存在する場合
2. `document.querySelector('h1').innerText` または `.textContent` - h1要素のテキスト
3. `asPath` - URLパス（フォールバック）

### 添付ファイル

該当なし。

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| routeAnnouncement | アナウンスするテキスト | document.title / h1要素 / asPath | Yes |
| asPath | 現在のURLパス | useRouter().asPath | Yes |
| previouslyLoadedPath | 前回のURLパス | React.useRef | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | クライアントサイドナビゲーション | asPathが前回と異なる | Link/router.pushによるページ遷移 |
| 画面操作 | ブラウザ履歴操作 | asPathが前回と異なる | ブラウザの戻る/進むボタン操作 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 初回ロード時 | previouslyLoadedPath.currentがasPathと一致するため通知しない |
| 同一パスへの遷移 | asPathが変わらない場合は通知しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[クライアントサイドナビゲーション発生] --> B[useEffect発火 - asPath変更検知]
    B --> C{asPath === previouslyLoadedPath?}
    C -->|同一| D[処理終了 - アナウンスなし]
    C -->|異なる| E[previouslyLoadedPathを更新]
    E --> F{document.titleが存在?}
    F -->|Yes| G[routeAnnouncement = document.title]
    F -->|No| H{h1要素が存在?}
    H -->|Yes| I[routeAnnouncement = h1のテキスト]
    H -->|No| J[routeAnnouncement = asPath]
    G --> K[setRouteAnnouncement実行]
    I --> K
    J --> K
    K --> L[aria-live領域のDOM更新]
    L --> M[スクリーンリーダーがアナウンス]
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし。本通知はクライアントサイドのみで完結し、データベースアクセスは発生しない。

### 更新テーブル一覧

該当なし。

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| h1要素取得失敗 | ページにh1要素がない場合 | asPathをフォールバックとして使用 |
| document.title未設定 | headにtitleがない場合 | h1要素→asPathの順でフォールバック |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0（リトライなし） |
| リトライ間隔 | N/A |
| リトライ対象エラー | N/A |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（ナビゲーション頻度に依存） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし。ブラウザ内で動作するためサーバー側の時間帯制限は適用されない。

## セキュリティ考慮事項

- 本通知はクライアントサイドのみで動作し、外部へのネットワーク通信は発生しない
- `document.title`や`h1`要素のテキストをそのまま使用するため、XSSリスクはReactのDOM操作によって軽減される
- aria-liveリージョンに挿入されるテキストはReactの`{routeAnnouncement}`によるテキストノードとして挿入されるため、HTMLインジェクションのリスクはない
- 視覚的に非表示（clip: rect(0 0 0 0), height: 1px, overflow: hidden）で描画されるため、レイアウトへの影響はない

## 備考

- 本コンポーネントはPages Routerでのみ使用される。App RouterにはAppRouterAnnouncer（No.40）という別の実装が存在する
- CSSスタイルは"smushed off-screen accessible text"テクニックに基づく（参照: https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe）
- Marcy Suttonのアクセシブルクライアントルーティングユーザーテスト研究に基づく実装（参照: https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/）
- `id="__next-route-announcer__"`が付与されており、外部からの参照やテストが可能

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

RouteAnnouncerは単純なReactコンポーネントであり、複雑なデータ構造は使用しない。主要な状態は`routeAnnouncement`（string型）と`previouslyLoadedPath`（useRefによるstring型）の2つである。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | route-announcer.tsx | `packages/next/src/client/route-announcer.tsx` | コンポーネントの全体構造、state定義（行22-23）、CSSスタイル定義（行4-18） |

**読解のコツ**: `aria-live="assertive"`と`role="alert"`の組み合わせにより、スクリーンリーダーは現在読み上げ中の内容を中断して即座にアナウンスを行う。`whiteSpace: 'nowrap'`と`wordWrap: 'normal'`は視覚的に非表示のテキストがスクリーンリーダーで正しく読み上げられるために必要。

#### Step 2: エントリーポイントを理解する

Pages Routerのクライアントエントリーポイントで、RouteAnnouncerがどのようにアプリケーションに組み込まれるかを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.tsx | `packages/next/src/client/index.tsx` | RouteAnnouncerのimport（行29）、Portalによるレンダリング（行773-779） |

**主要処理フロー**:
1. **行29**: `import { RouteAnnouncer } from './route-announcer'` - コンポーネントのインポート
2. **行773-779**: `<Portal type="next-route-announcer"><RouteAnnouncer /></Portal>` - Portalを使用してアプリケーションのルートにレンダリング

#### Step 3: アナウンスロジックを理解する

RouteAnnouncerコンポーネント内のuseEffectフックで実装されるアナウンステキスト決定ロジックを読み解く。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | route-announcer.tsx | `packages/next/src/client/route-announcer.tsx` | useEffectフック（行34-51）、アナウンステキスト決定の優先順位ロジック |

**主要処理フロー**:
- **行37**: `previouslyLoadedPath.current === asPath` - パス変更チェック
- **行38**: `previouslyLoadedPath.current = asPath` - 前回パスの更新
- **行40-41**: `document.title`の確認と設定
- **行43-44**: `h1`要素の検索とテキスト取得
- **行46**: フォールバックとして`asPath`を使用

#### Step 4: サーバーサイドレンダリングとの関係を理解する

サーバーサイドレンダリング時のRouteAnnouncerの扱いを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | render.tsx | `packages/next/src/server/render.tsx` | SSR時のRouteAnnouncerの配置（行793付近、コメントアウトされたNoop） |

### プログラム呼び出し階層図

```
packages/next/src/client/index.tsx (Pages Routerクライアントエントリーポイント)
    │
    ├─ AppContainer (行773)
    │      └─ Portal type="next-route-announcer" (行777)
    │             └─ RouteAnnouncer (行778)
    │                    ├─ useRouter() → asPath取得 (行21)
    │                    ├─ useState('') → routeAnnouncement (行22)
    │                    ├─ useRef(asPath) → previouslyLoadedPath (行26)
    │                    └─ useEffect() → アナウンスロジック (行34-51)
    │                           ├─ document.title チェック (行40)
    │                           ├─ document.querySelector('h1') (行43)
    │                           └─ setRouteAnnouncement() (行41/46)
    │
    └─ <p aria-live="assertive" role="alert"> → スクリーンリーダー出力 (行53-61)
```

### データフロー図

```
[入力]                        [処理]                              [出力]

useRouter().asPath ──────▶ useEffect内パス変更検知 ──────▶ aria-live="assertive"
                                │                              リージョンへの
                                ├─ document.title               テキスト出力
                                ├─ document.querySelector('h1')     │
                                └─ asPath(フォールバック)           ▼
                                                            スクリーンリーダー
                                                            が音声アナウンス
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| route-announcer.tsx | `packages/next/src/client/route-announcer.tsx` | ソース | RouteAnnouncerコンポーネントの実装 |
| index.tsx | `packages/next/src/client/index.tsx` | ソース | Pages Routerクライアントエントリー、RouteAnnouncerの組み込み |
| render.tsx | `packages/next/src/server/render.tsx` | ソース | SSR時のAppContainer内レンダリング構造 |
| router.ts | `packages/next/src/shared/lib/router/router.ts` | ソース | useRouterフックの提供元、asPathの管理 |
| portal.tsx | `packages/next/src/client/portal.tsx` | ソース | Portalコンポーネント（DOMの特定位置にレンダリング） |
