diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9638395..a56c472 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,6 +27,12 @@ jobs: run: npm ci --ignore-scripts - name: Build Project run: npm run build + - name: Generate llms.txt files + run: npm run generate-llms + - name: Copy llms.txt files to dist + run: | + cp llms.txt dist/ + cp llms-full.txt dist/ - name: Upload Pages Artifact uses: actions/upload-pages-artifact@v3 with: @@ -66,4 +72,4 @@ jobs: aws-region: ap-northeast-1 - name: Upload to S3 run: | - aws s3 sync artifact s3://${{ env.AWS_S3_BUCKET_NAME }}/accessibility-guidelines --exclude "*" --include "*.html" --delete + aws s3 sync artifact s3://${{ env.AWS_S3_BUCKET_NAME }}/accessibility-guidelines --exclude "*" --include "*.html" --include "*.txt" --delete diff --git a/llms-full.txt b/llms-full.txt new file mode 100644 index 0000000..500808f --- /dev/null +++ b/llms-full.txt @@ -0,0 +1,1844 @@ +# LIFULL Accessibility Guidelines + +> アクセシビリティに配慮したデザインと実装のためのガイドライン + +This file contains accessibility guidelines for designers and developers. + +## metadata +- url: https://lifull.github.io/accessibility-guidelines/ +- version: v3.0 +## 目次 + +- [ガイドラインについて](#ガイドラインについて) + - [アクセシブルなデザインパターン](#アクセシブルなデザインパターン) + - [代替テキストの考え方](#代替テキストの考え方) + - [はじめに](#はじめに) + - [利用方法](#利用方法) +- [デザインのガイドライン](#デザインのガイドライン) + - [自動再生するコンテンツ (レベル1)](#自動再生するコンテンツ) + - [閃光 (レベル1)](#閃光) + - [見出し (レベル1)](#見出し) + - [画像の代替テキスト (レベル1)](#画像の代替テキスト) + - [ページタイトル (レベル1)](#ページタイトル) + - [動画の字幕 (レベル1)](#動画の字幕) + - [音声のみのコンテンツ (レベル2)](#音声のみのコンテンツ) + - [グラフや図 (レベル2)](#グラフや図) + - [データ可視化 (レベル2)](#データ可視化) + - [テキスト画像 (レベル2)](#テキスト画像) + - [表 (レベル2)](#表) + - [動画コンテンツ(音声を含む) (レベル2)](#動画コンテンツ音声を含む) + - [リンクテキスト (レベル3)](#リンクテキスト) + - [コンテンツの順序 (レベル3)](#コンテンツの順序) + - [目次とサイトマップ (レベル3)](#目次とサイトマップ) + - [カスタムUIのキーボード操作 (レベル1)](#カスタムuiのキーボード操作) + - [定番のパターン (レベル1)](#定番のパターン) + - [キーボード操作 (レベル1)](#キーボード操作) + - [ホバーで表示されるコンテンツ (レベル2)](#ホバーで表示されるコンテンツ) + - [エラーメッセージ (レベル2)](#エラーメッセージ) + - [エラーメッセージの提示 (レベル2)](#エラーメッセージの提示) + - [フォームコントロールのラベル (レベル2)](#フォームコントロールのラベル) + - [シンプルなポインター操作 (レベル2)](#シンプルなポインター操作) + - [デバイスの向き (レベル3)](#デバイスの向き) + - [新しいタブで開くリンク (レベル3)](#新しいタブで開くリンク) + - [予測可能なパターン (レベル3)](#予測可能なパターン) + - [時間制限 (レベル3)](#時間制限) + - [ユーザー認証 (レベル3)](#ユーザー認証) + - [フォーカスインジケーター (レベル1)](#フォーカスインジケーター) + - [リンクの判別 (レベル1)](#リンクの判別) + - [状態の判別 (レベル1)](#状態の判別) + - [レスポンシブデザイン (レベル2)](#レスポンシブデザイン) + - [ターゲットサイズ (レベル2)](#ターゲットサイズ) + - [テキストの色コントラスト (レベル2)](#テキストの色コントラスト) + - [テキストの均等割付 (レベル2)](#テキストの均等割付) + - [アイコンやUIコンポーネントの色コントラスト (レベル2)](#アイコンやuiコンポーネントの色コントラスト) +- [実装のガイドライン](#実装のガイドライン) + - [背景画像 (レベル1)](#背景画像) + - [見出し (レベル1)](#見出し) + - [画像の代替テキスト (レベル1)](#画像の代替テキスト) + - [ページの言語 (レベル1)](#ページの言語) + - [ページタイトル (レベル1)](#ページタイトル) + - [調整可能な文字サイズ (レベル2)](#調整可能な文字サイズ) + - [グループ化された画像 (レベル2)](#グループ化された画像) + - [ランドマーク領域 (レベル2)](#ランドマーク領域) + - [意味のある順序 (レベル2)](#意味のある順序) + - [改行と空白文字 (レベル2)](#改行と空白文字) + - [正しい構文と文法 (レベル3)](#正しい構文と文法) + - [フォームコントロールのラベル (レベル1)](#フォームコントロールのラベル) + - [ラベルのないコントロール (レベル1)](#ラベルのないコントロール) + - [コピー&ペーストの許容 (レベル2)](#コピーペーストの許容) + - [フォームコントロールのグループ化 (レベル2)](#フォームコントロールのグループ化) + - [入力目的の特定 (レベル2)](#入力目的の特定) + - [フォームコントロールの説明文 (レベル3)](#フォームコントロールの説明文) + - [ズームの許容 (レベル1)](#ズームの許容) + - [フォーカスインジケーター (レベル1)](#フォーカスインジケーター) + - [挿入されるコンテンツ (レベル1)](#挿入されるコンテンツ) + - [ボタンの使用 (レベル1)](#ボタンの使用) + - [ホバーで表示されるコンテンツ (レベル2)](#ホバーで表示されるコンテンツ) + - [文脈に応じたフォーカス (レベル2)](#文脈に応じたフォーカス) + - [Escapeキー操作 (レベル2)](#escapeキー操作) + - [外部コンテンツおよびUIライブラリ (レベル2)](#外部コンテンツおよびuiライブラリ) + - [隠されているコンテンツ (レベル2)](#隠されているコンテンツ) + - [WAI-ARIA (レベル2)](#waiaria) + - [背後のコンテンツ (レベル3)](#背後のコンテンツ) + - [ダウンイベントの使用 (レベル3)](#ダウンイベントの使用) + - [ドラッグ操作の中断 (レベル3)](#ドラッグ操作の中断) + - [ステータスの通知 (レベル3)](#ステータスの通知) + +## ガイドラインについて + +### アクセシブルなデザインパターン + +アクセシブルなデザインパターン + + アクセシブルなデザイン・実装方法が確立された定番のデザインパターンを紹介します。ここでのアクセシビリティへの配慮とは、キーボードで操作でき、スクリーンリーダーに UI の役割や状態が十分に伝わることを指しています。 + + ガイドライン「[定番のパターン](/accessibility-guidelines/design-forms-and-interactions.html#established-pattern)」にあるように、独自の UI を考案しデザインする前に、ここにラインナップされたパターンを利用して目的が達成できるかどうかを検討してください。 + + **利用上の注意** + + ここにラインナップされたUIを採用する前に、**HTMLのみで実現できるかどうかを検討してください**。たとえばDisclosureパターンには`details`要素が使えます。Spinbuttonパターンには`type="number"`を持つ`input`要素が使えます。 + + また、**HTMLの要素を本来と違う使い方をすることは避けてください**。たとえば、チェックボックスを領域の開閉に使わないでください。`select`要素をメニュー代わり(ソート順の切り替えなど)に使わないでください。 + + ## パターン集 + + | パターン名 | 説明 | + | :------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | [Accordion (Sections With Show/Hide Functionality)](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) | アコーディオンは、縦に積み重なったインタラクティブな見出しのセットで、それぞれがコンテンツのセクションを表すタイトル、コンテンツスニペット、サムネイルを含んでいます。 | + | [Alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) | アラートは、ユーザーのタスクを中断させることなく、ユーザーの注意を引くような方法で、簡潔で重要なメッセージを表示する要素です。 | + | [Alert and Message Dialogs](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/) | アラートダイアログは、重要なメッセージを伝え、応答を得るためにユーザーのワークフローを中断させるモーダルダイアログである。 | + | [Breadcrumb](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/) | パンくずリストとは、現在のページの親ページへのリンクを階層的に並べたものである。 | + | [Button](https://www.w3.org/WAI/ARIA/apg/patterns/button/) | **HTML の button 要素を使用することを強く推奨します**。ボタンは、フォームの送信、ダイアログの表示、アクションのキャンセル、削除など、ユーザーがアクションやイベントを実行できるようにするウィジェットです。 | + | [Carousel (Slide Show or Image Rotator)](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/) | カルーセルは、1 つまたは複数のスライドのサブセットを順次表示することによって、スライドと呼ばれるアイテムのセットを提示します。 | + | [Checkbox](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/) | WAI-ARIA は、2 種類のチェックボックスウィジェットをサポートしています。デュアルステートでは、ユーザはチェック済みと未チェックという 2 つの選択肢を切り替えられ、トライステートでは、部分チェック済みという第 3 の状態を追加でサポートします。 | + | [Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) | コンボボックスは、入力ウィジェットにポップアップがついたもので、ユーザーが可能な値のコレクションからコンボボックスの値を選択することができます。 | + | [Dialog (Modal)](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) | ダイアログは、プライマリウィンドウまたは他のダイアログウィンドウにオーバーレイ表示されるウィンドウです。 | + | [Disclosure (Show/Hide)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) | ディスクロージャーは、コンテンツを折りたたみ(非表示)または展開(表示)することができるウィジェットです。 | + | [Feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/) | フィードは、ユーザーがスクロールすると自動的に新しいコンテンツがロードされるページのセクションです。 | + | [Grids : Interactive Tabular Data and Layout Containers](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) | グリッドウィジェットは、矢印キー、ホームキー、エンドキーなどのナビゲーションキーを使って、その中に含まれる情報やインタラクティブな要素を移動できるようにするコンテナです。 | + | [Link](https://www.w3.org/WAI/ARIA/apg/patterns/link/) | **HTML の a 要素を使用することを強く推奨します**。リンクウィジェットは、リソースへのインタラクティブなリファレンスを提供します。 | + | [Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) | リストボックスウィジェットは、オプションのリストを表示し、ユーザがそのうちの 1 つまたは複数を選択できるようにします。 | + | [Menu or Menu bar](https://www.w3.org/WAI/ARIA/apg/patterns/menu/) | メニューは、一連のアクションや機能など、ユーザに選択肢のリストを提供するウィジェットです。 | + | [Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/) | メニューボタンは、メニューを開くためのボタンです。 | + | [Meter](https://www.w3.org/WAI/ARIA/apg/patterns/meter/) | メーターは、定義された範囲内で変化する数値のグラフィック表示です。 | + | [Radio Group](https://www.w3.org/WAI/ARIA/apg/patterns/radiobutton/) | ラジオグループ ラジオボタンと呼ばれるチェック可能なボタンのセットで、一度に 1 つ以上のボタンをチェックすることができない。 | + | [Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) | スライダーは、ユーザーがある範囲内の値を選択するための入力です。 | + | [Slider (Multi-Thumb)](https://www.w3.org/WAI/ARIA/apg/patterns/slidertwothumb/) | マルチサムスライダーは、関連する値のグループ内の値をそれぞれ設定する 2 つ以上のサム(親指)を持つスライダーです。 | + | [Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) | スピンボタンは、その値を離散的な値のセットまたは範囲に制限する入力ウィジェットです。 | + | [Switch](https://www.w3.org/WAI/ARIA/apg/patterns/switch/) | スイッチは、ユーザーがオンとオフの 2 つの値から 1 つを選択できる入力ウィジェットです。 | + | [Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/) | **HTML の table 要素を使用することを強く推奨します**。WAI-ARIA のテーブルは、HTML のテーブル要素と同様に、1 つ以上のセルを含む 1 つ以上の行からなる静的な表形式構造で、対話型ウィジェットではありません。 | + | [Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) | タブは、タブパネルと呼ばれるコンテンツのレイヤーセクションのセットで、一度に 1 つのパネルを表示します。 | + | [Toolbar](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) | ツールバーは、ボタン、メニューボタン、チェックボックスなどのコントロール群をグループ化するためのコンテナである。 | + | [Tooltip Widget](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) | **ツールチップはタッチ端末でのアクセスしにくさがあるため非推奨です**。ツールチップは、キーボードフォーカスが当たったときや、マウスがその要素に重なったときに、その要素に関連する情報を表示するポップアップである。 | + | [Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) | ツリービューウィジェットは、階層化されたリストを表示します。 | + | [Treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/) | ツリーグリッドウィジェットは、編集可能な表形式の情報からなる階層的なデータグリッドを表示する。 | + | [Window Splitter](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) | ウィンドウスプリッターは、ウィンドウの 2 つのセクション(ペイン)間の移動可能なセパレータで、ユーザーがペインの相対的なサイズを変更できるようにするものです。 | + +### 代替テキストの考え方 + +代替テキストの考え方 + + この文書は許諾に基づいて[WebAIM: Alternative Text](https://webaim.org/techniques/alttext/)を日本語訳したものです。再利用にあたっては原著を参照してください。 + + --- + + ## はじめに + + 代替テキストとは、ウェブページの非テキストコンテンツをテキストで置き換えたものです。この記事は[画像](https://webaim.org/techniques/images)に焦点を当てていますが、その原則はマルチメディアや他の非テキストコンテンツにも適用されます。 + + 代替テキストにはいくつかの機能があります。 + + - スクリーンリーダーは、画像の代わりに代替テキストを表示し、視覚障害や特定の認知障害を持つユーザーが画像の内容や機能を認識できるようにします。 + - 画像の読み込みに失敗したり、ユーザーが画像をブロックしている場合、ブラウザは画像の代わりに代替テキストを視覚的に表示します。 + - 検索エンジンは代替テキストを利用し、ページの目的と内容の評価に反映させます。 + + 画像に**何が写っているか**を認識する技術は進歩していますが、アルゴリズムだけでは、ページ全体の文脈の中で画像が**何を意味しているか**を理解することはできません。カエデの葉はカナダを表しているのかもしれませんし、単に木の葉を表しているのかもしれません。ウェブページの作者は、画像の**内容**や**機能**を表す代替テキストを提供しなければなりません。 + + 代替テキストの提供には、2つの方法があります。 + + - ``要素の`alt`属性で指定する。 + - 画像の近くにある目に見える本文の中に表示する。または、同等のテキストを簡潔に表示できない場合、代替テキストを別のページに表示し、画像または画像に隣接するテキストリンクからリンクさせることができる。 + + このように、代替テキストは、`alt`属性だけではありません。 + + **すべての**画像には、`alt=""`(「空白」の代替テキストと呼ばれることもある)であっても、`alt`属性を付ける必要があります。 + + ## コンテキストがすべて + + エレン・オチョアの画像を見てみましょう。 + + この画像は、その使われ方によって、全く異なる代替テキストを必要とする場合があります。 + + ### ノート + + 代替テキストの原則を説明するために、この記事内のほとんどの画像には`alt="example image"`が指定されています。しかし、画像のコンテンツは通常、ページのコンテキスト内で表示されます。 + + ### 例1 + + ヒスパニック系女性として初めて宇宙へ行き、その後、ジョンソン宇宙センターでヒスパニック系初の所長を務めたエレン・オチョアは、ロールモデルとして広く知られています。 + + 例1の画像の`alt`テキストには何を選ぶか? + + - `"宇宙飛行士エレン・オチョア"` + - `"宇宙飛行士エレン・オチョアの画像"` + - `"エレン・オチョア、宇宙に行った最初のヒスパニック系女性"` + - 空の`alt`属性 (`alt=""`) + + まず、その**内容**と**機能**を考えてみましょう。画像は、リンクされている場合(あるいは``の中に``がある場合)、あるいは``の中にある場合のみ機能を持ちます。この場合、画像は機能を持ちません。 + + 画像の内容を評価し、要約することは、より困難な場合があります。画像の内容が周囲のテキストで表現されている場合は、`alt=""`で十分かもしれません。 + + 上の例では、画像のコンテンツが、これがエレン・オチョアであることをユーザーに伝えています。また、彼女の服装から、彼女が宇宙飛行士であることがわかりますが、これは、彼女の功績を考えると、とても意味のあることです。 + + **このことから、`alt="宇宙飛行士エレン・オチョア"` をお勧めします。** + + "宇宙飛行士エレン・オチョアの画像" は、画像を画像として説明する冗長なものです。 + + "エレン・オチョア、宇宙に行った最初のヒスパニック系女性" は、画像の一部ではない情報を含み、また本文と冗長になっています。 + + 空の`alt`属性も適切ではありません。本文にエレン・オチョアという名前があっても、視覚ユーザは画像の内容から直接それを知ることができます。したがって、画像は内容を伝えるものであり、空の`alt`属性以上のものが必要です。 + + #### 重要 + + `alt`属性は、通常、次のようにします。 + + - コンテンツと機能を**正確に**表現し、**同等**であること。 + - **簡潔であること**。内容(もしあれば)と機能(もしあれば)は、正確さを犠牲にすることなく、可能な限り簡潔に表現する必要があります。通常は数語でよいが、まれに短文や2文が適切な場合もある。 + - 画像の近くにあるテキストと同じ情報を提供したり、**冗長にならないように**する。 + - **「…の画像」、「…のグラフィック」などのフレーズを含めない**。スクリーンリーダーはすでに`alt`テキストと一緒に「グラフィック」とアナウンスしているので、これは冗長になります。画像が写真やイラストであることなどが重要な内容である場合は、代替テキストに含めると便利な場合があります。 + + ### 例2 + + **宇宙飛行士エレン・オチョア** + + ヒスパニック系女性として初めて宇宙へ行き、その後、ジョンソン宇宙センターでヒスパニック系初の所長を務めたエレン・オチョアは、ロールモデルとして広く知られています。 + + 例2の画像には、どのような`alt`テキストを選ぶのでしょうか? + + - `"画像"` + - `"エレン・オチョア"` + - 空の`alt`属性(alt="") + + **この場合、画像の内容は隣接するテキストで表示されるため、`alt=""`が最適です**。 + + "エレン・オチョア" は冗長になります。`"画像"`は有用な情報を提供しません。 + + ## 機能的な画像 + + 画像は、コンテンツを提供するだけでなく、ナビゲーションなどの重要な機能を提供するために使われることが多いのです。 + + ### 例3 + + **宇宙飛行士エレン・オチョア** + + この画像はリンクされている(そしてそのリンク内の唯一のコンテンツである)ことに注意してください。あなたなら、どんな`alt`テキストを選びますか? + + - `"もっと読む"` + - `"宇宙飛行士エレン・オチョア"` + - `"宇宙飛行士エレン・オチョアのウィキペディアの項目"` + - 空の`alt`属性(`alt=""`) + + 画像もリンクである以上、機能があります。**リンク内の**隣接するテキストにはリンクの機能が記述されていないため、画像の`alt`属性で伝える必要があります。 + + **ですから、`alt="宇宙飛行士エレン・オチョア"`が最適な選択です**。スクリーンリーダーは通常、「リンク、画像、宇宙飛行士エレン・オチョア、宇宙飛行士エレン・オチョア」と読みます。冗長ですが、リンクされた画像の機能を適切に説明するために必要です。特に、スクリーンリーダーのユーザーがリンクでナビゲートする場合など、隣接するテキストから切り離してアクセスする場合は、このような冗長性が必要になります。 + + "宇宙飛行士エレン・オチョアのウィキペディアの項目"は、画像によって伝達される内容**以外の**内容、つまりリンクがウィキペディアに行くという事実を提供します。 + + "もっと読む"は、特に文脈から外れていると、十分な情報を提供しません。 + + 空白の`alt`テキストは、ここでは決して適切ではありません。リンクやボタンの中にあるコンテンツが画像**だけ**である場合、スクリーンリーダーが判断する材料は`alt`テキストだけです。テキストが空であったり、欠けていたりすると、スクリーンリーダーは、画像のファイル名やリンク先のページのURLを読み取り、それがユーザーの役に立つことを期待するかもしれませんが、必ずしもそうとは限りません。 + + 次の例のように、画像とテキストのキャプションの両方が1つのリンクに含まれていれば、すべてのユーザーにとってよりアクセスしやすくなります。 + + **宇宙飛行士エレン・オチョア** + + リンクされた画像のコンテンツと機能の両方がリンク内のテキストとして表示されるため、重複を避けるために画像には`alt=""`が付けられます。スクリーンリーダーは、通常、「リンク、宇宙飛行士エレン・オチョア」と読み、先の例よりもはるかに効率的です。 + + 可能な限り、`alt`属性に「…へのリンク」「…へはここをクリック」などを使用するのは避けてください。スクリーンリーダーはすでにリンクをリンクとして読み上げています。 + + ### 例4 + + 採用応募書類のダウンロード + + 例4のアイコン画像には、どのような`alt`テキストを選択しますか?アイコンはリンクの中にあることに注意してください。 + + - `"採用応募"` + - `"PDF"` + - `"PDFアイコン"` + - 画像の内容は文脈で示されるため、`alt=""`が適切 + + **"PDF" が最適です。これはアイコンの内容を伝えるもので、それ以上でも以下でもありません**。 + + "採用応募 "は冗長になります。機能("採用応募書類のダウンロード")はリンクのテキストで示されるので、`alt`属性に再び含める必要はありません。 + + "PDFアイコン"は、画像がどのようなものかを説明していますが、この文脈では最も適切なものではありません。「アイコン」はここでは冗長です。(このアイコンを別の文脈で使用する場合、それがアイコンであることをユーザーに知らせることが重要な場合があります)。 + + 空の`alt`テキストは、画像が示す重要な情報、つまりリンク先がPDF文書であることを省略してしまいます。 + + アイコンのみで隣接するテキストなしでリンクする場合、「採用応募書類をPDF形式でダウンロードする」のように、`alt`テキストは、リンクと画像の組み合わせのコンテンツと機能を完全に伝える必要があります。「PDF形式」だけではリンクされたアイコンとして十分ではありません。ページに多くのPDFリンクとアイコンが含まれている場合は特にです。スクリーンリーダーのユーザーがリンクされたアイコンの中を移動すると、「PDF形式、PDF形式、PDF形式...」と聞こえてしまうからです。 + + ## 装飾的な画像 + + 装飾的な画像とは... + + - 重要な内容を表示しないもの。 + - レイアウトや情報提供以外の目的で使用され + - 機能を持たないもの(例:リンクではない)。 + + 装飾的な画像には、`alt=""`を付けるべきです。 + + ### 例5 + + 例5の水平セパレータ画像には、どのような`alt`テキストを選びますか? + + - `"装飾的なライン"` + - `"フッターの始まり"` + - `"セパレーター"` + - `alt=""` + + この画像は、ドキュメントのセクション間の区切りを伝えるものですが、すでにテキストで提示されている構造を視覚的に補強しているに過ぎません。 + + **この画像は追加の内容を伝えるものではないので、`alt=""`が最も適切な選択です。** + + #### ノート + + 画像が装飾のためだけに使われる場合、その画像をページのコンテンツから取り除き、代わりにCSSの背景画像として定義するのが最善です。こうすることで、代替テキストの必要性が完全になくなり、ページの意味的・構造的な流れから画像を取り除くことができます。 + + ### 例6 + + 私たちのビジネスは、あなたが地球上で見つけることができる最高のサービスを約束します。私たちのチームは、契約交渉のプロセスを通じて優れた顧客サービスを提供するために専門的に訓練されています。 + + お客様の満足が私たちの最優先事項であり、保証されるか、あるいはお金をお返しします。 + + 例6にある画像の適切な`alt`属性は何でしょうか? + + - `"握手"` + - `"契約を完了させるために握手するビジネスマン"` + - `alt=""` + - `"私たちはプロフェッショナルなサービスを保証します"` + + **画像は関連する内容や重要な内容を伝えていないため、ここでは空の`alt`テキストが最適です**。考えてみてください。もし画像が削除されたら、重要なコンテンツは失われるでしょうか?この場合、おそらくそうではないでしょう。このような画像の多くは、視覚的に有益なコンテンツを提供していないにもかかわらず、スクリーンリーダーのユーザーに対して無意味で冗長な`alt`テキストを強要しています。 + + "握手" と "契約を完了させるために握手するビジネスマン" は画像を説明していますが、これは余分な情報です。 + + "私たちはプロフェッショナルなサービスを保証します" は正しくない。これもウェブでよく見られる間違いです。`alt`を使って、他のどこにも「当てはまらない」余計な情報(または検索エンジン向けの情報)を挿入しているのです。コーダーの論理的根拠は、冗長性を避けることかもしれませんが、なぜそうするのでしょうか。 + + #### ノート + + 多くの場合、「この画像を使えないとしたら、その代わりに何を置くか?」と尋ねることができます。もしその答えが「何もない」なら、`alt=""`で十分でしょう。 + + ## 高度な画像 + + 場合によっては、`alt`テキストの決定がより主観的になることがあります。スクリーンリーダーを使用した場合と使用しない場合のユーザーテストは、いくつかのアイデアを生み出すのに役立ちます。 + + ### フォーム画像ボタン + + ラスタライズされたテキストを表示するだけのフォーム画像ボタンは、テキストに置き換えられ、CSSでスタイルが設定されるべきです。画像が避けられない場合は、ボタンの機能を説明する`alt`属性が必要です。`alt`テキストは、"検索", "送信", "登録", "注文を確定" などのように、ボタンが作動したときに何をするのかを説明する必要があります。例えば、``は、サイト内検索フォームの画像ボタンとして適切かもしれません。 + + ### イメージ・マップ + + クライアント・サイドのイメージ``を使う場合、メイン・イメージの`alt`属性は、イメージ・マップのホット・スポットでは表示されないが、イメージ内に表示される内容を伝えなければなりません。例えば、ニューヨーク州の地図で、各郡の``がある場合、`alt="ニューヨークの郡"`となります。メイン画像が内容を伝えず、主にホットスポットのコンテナである場合、`alt=""`が適切です。 + + 各``は機能を提供するため、同等の`alt`属性を持つ必要があります。ニューヨークの郡のイメージマップの場合、それぞれの``は郡の名前を含むでしょう。 + + 対照的に、**サーバーサイドの**イメージ・マップ(マウス・クリックの座標をサーバーに戻して解釈する)は、スクリーンリーダーが認識できず、キーボードやスクリーンリーダーのユーザーには操作不能です。サーバーサイド・イメージマップは、クライアントサイド・イメージマップに置き換えるべきでしょう。 + + ### CSS画像 + + CSS画像は、代替テキストを必要としない装飾的な画像のために使用する必要があります。内容を伝える画像は、一般にCSSで定義すべきではありません。ページコンテンツ内に配置する。CSSや他の背景画像に直接代替テキストを追加することはできないため、背景画像が内容を示す場合は、その内容をページマークアップの中でアクセスできるようにする必要があります。上記の例4のPDFアイコン画像をCSSの背景画像で表示した場合、テキスト置換のテクニックを使って、リンク内のコンテンツを表示させることができます。 + + ```html + 採用応募書類のダウンロード (PDF) + ``` + + 次に、CSSを使用して、「(PDF)」のテキスト(`span class="pdficon"`要素)を[画面外に](https://webaim.org/techniques/css/invisiblecontent/)配置します。視覚ユーザーにはPDFの背景画像が見え、スクリーンリーダー・ユーザーには画面外の「(PDF)」テキストが聞こえます。 + + ### ロゴ + + 多くのWebサイトでは、メインブランドのロゴをホームページにリンクしています。画像には、会社名(`alt="Acme Company"`)などの代替テキストを提供すれば、通常は十分でしょう。ロゴ」という言葉(`alt="Acme Company Logo"`)は、通常、画像の内容や機能にとって重要な部分ではありません。同様に、画像がホームページにリンクしていることを示す(`alt="Acme Company home page"`)ことも、通常は必要ありません。なぜなら、これは一般的な慣例だからです。ウェブページの一番上にある「リンク、グラフィック、アクメカンパニー」を聞けば、スクリーンリーダーのユーザーは、それがホームページにリンクしたロゴであると認識すれば十分なのです。 + + ### 複雑な画像 + + 複雑な画像(チャート、グラフ、地図など)の代替案が、簡潔な`alt`属性(おそらく数文の長さ)に収まらない場合、代替テキストを別の場所に提供する必要があります。この場合、同じページ内の隣接するデータテーブルか、画像を掲載しているページからリンクされた別のウェブページになります。リンクは画像に隣接していてもよいし、画像そのものを説明ページにリンクさせてもよい。画像の代替テキストは、その画像の一般的な内容を記述する必要があります。 + + ```html + 販売データを見る + ``` + + #### longdescに関するノート + + `longdesc`属性は非推奨で、使用しないでください。これは、長い説明ページへの参照を作成する HTML4 属性でしたが、スクリーンリーダーでは決してうまくサポートされていませんでした。 + + #### 例7 + + この絵では、画家エマニュエル・ロイツェは、光、色、形、遠近法、比率、動きを使って構図を作り上げました。 + + 例7の画像には、どのような`alt`テキストを選ぶのでしょうか? + + - `"ジョージ・ワシントン"` + - `"ジョージ・ワシントンの絵画"` + - `"デラウェア川を渡るジョージ・ワシントンの絵画"` + - `"光と色を使って構図を作ることを示した古典的な絵画"` + - `"デラウェア川を渡るジョージ・ワシントンの絵画。渦巻く波が船を取り囲み、そこで威厳あるジョージ・ワシントンは、警戒する軍隊を戦いに導くために、嵐の中から川の向こうの光線を前方に見ている。"` + + 画像はリンクされていないので、機能はありません。次に、画像の内容が周囲のテキストで表現されているかどうかを判断する必要があります。この場合、そうではありません(少なくとも完全ではありません)。しかし、この画像はもっと難しい。 + + "ジョージ・ワシントン" はおそらく不適切です。 + + "ジョージ・ワシントンの絵画" の方が良いのですが、この場合、写真や他の画像タイプとは異なる絵画であることを表現するのは適切ですが、同等と見なすには十分な内容を提供できているとは言えません。 + + "デラウェア川を渡るジョージ・ワシントンの絵画" は、ユーザーがコンテンツそのものを識別するのに役立つような、より多くの情報を提供しています。代替テキストは、目の見えないユーザーのためだけのものではないことを忘れないでください。多くの目の見えるユーザーは、「ジョージ・ワシントン」だけでは十分な説明になっていないのに対し、この説明を見れば、問題の特定の絵画を識別することができます。 + + 最後の(冗長な)オプションは、主題よりも技法が重要な場合に適切かもしれません。また、絵画の詳細な調査が必要な場合にも適切ですが、このレベルの詳細については、おそらく本文で説明した方がよいでしょう。 + + **たったひとつの**正しい答えはありません。最適な代替テキストは、画像の文脈と意図された内容によって異なります。 + + ### Figureとfigcaption + + ``要素は、``と``を含むように設計され、自己充足的 (self-contained) で、典型的には文書の主要な流れから単一のユニットとして参照されます。``は、文書の意味に影響を与えることなく、文書の主要な流れから切り離すことができます。 + + ``は、その含まれる``との意味的な関連付けを作成し、図についての要約や追加情報を提供したり、図を含む文書に関連付けたりすることができます。しかし、``はまだaltテキストを必要とし、冗長性を避けるために、この情報は``を介して伝達されるべきではありません。 + + ## 結論 + + ウェブアクセシビリティに影響を与える最大の問題の一つであるにもかかわらず、ウェブに代替テキストを実装するための多様で不正確な方法を依然として見続けています。開発コミュニティが同等の代替テキストを完全に受け入れることができれば、ウェブはよりアクセシブルな場所になるはずです。 + + --- + + *Ellen Ochoa Image Credit: NASA, Public domain, via Wikimedia Commons.* + +### はじめに + +ガイドラインについて + はじめに + + LIFULLアクセシビリティガイドラインの概要、特色、項目の見方などを説明しています。 + + ## アクセシビリティとは + + アクセシビリティは、プロダクトやサービス、情報、環境が、できるだけ多くの人々にとって利用可能であることを意味します。これには、障害者だけでなく、高齢者や一時的な障害を持つ人、そしてさまざまな状況や環境で利用する人々も含まれます。アクセシビリティを考慮することで、プロダクトやサービスはより幅広いユーザーや状況、環境で使いやすくなります。 + + ## LIFULLアクセシビリティガイドラインとは + + LIFULLアクセシビリティガイドラインは、LIFULLの全てのプロダクトやサービスを利用しやすくするために策定されました。LIFULLのプロダクトに関わる全ての人が対象です。ガイドラインを読み、背景を知り、自らの作業に取り入れていただくことを期待しています。 + + 近年、アクセシビリティへの取り組みはより重要になっています。現在、私たちの取り組みは途中段階にありますが、弊社のコーポレートメッセージ「あらゆるLIFEを、FULLに。」を実現するために、アクセシビリティの向上を積極的に目指しています。 + + ### 特徴 + + LIFULLアクセシビリティガイドラインはいくつかのコンセプトに基づいて編成されています。 + + #### 工程に応じた項目 + + 自分がやるべきことがわかるよう、ガイドラインの項目は工程(≒職種)ごとに分けられています。 + + #### 優先度が明確 + + ガイドラインの項目には優先度を記載し、対応すべき順序を明確にしています。優先度について詳しくは[レベルとは何か?](/accessibility-guidelines/usage.html#レベルとは何か)を参照してください。 + + #### わかりやすい記述 + + 初学者でも理解できるように、図表や例を使って具体的に記述しています。ガイドラインが必要とされる背景や、恩恵を受けるユーザーについて記述しています。 + + #### 関連リソースへのアクセス + + 理解の促進に役立つリソースや、関連するリソースへのリンクを設けています。 + + ### 各項目の構成 + + ガイドラインの各項目は次のような構成になっています。 + + チェック項目 + ガイドライン項目が満たされているかどうかを素早く確認するためのチェック項目です。 + 説明 + 「〜とは」という見出しから始まるセクションは、ガイドライン項目の説明です。ガイドラインが必要な理由や対象ユーザーについての知識、用語解説などを含みます。 + レベル + ガイドライン項目の優先度を示しています。詳しくは[レベルとは何か?](/accessibility-guidelines/usage.html#レベルとは何か)を参照してください。 + 具体例 + ガイドラインを満たしていない例や満たした場合の具体的な例を紹介しています。 + 参考情報 + ガイドラインの理解を深めるための有用なツールや文書など、外部リソースへのリンクを紹介しています。 + + ## 「障害者」表記について + + 一般に、「害」という漢字にマイナスイメージがあるとして、「障がい者」や「障碍者」と表記するほうが好ましいとされることがあります。しかし、「障害者」のほうが好ましいとする意見も存在し、人や組織によって意見が分かれています。LIFULLアクセシビリティガイドラインでは、中央省庁の表記に従って「障害者」に統一しています。ただし、表記については会社としての統一見解ではないことをご了承ください。 + + 参考:[「障害」の表記に関する国語分科会の考え方(令和3年3月12日文化審議会国語分科会)](https://www.bunka.go.jp/seisaku/bunkashingikai/kokugo/hokoku/pdf/92880801_03.pdf) + + ## アクセス解析に伴う情報取得について + + 当ウェブサイトでは、Google Analyticsを使用してアクセス解析を行っております。Google Analyticsは、ウェブサイト利用状況を収集・分析するためのツールであり、この情報はウェブサイトの改善や利便性の向上に役立てられます。なお、収集されるデータは匿名化されており、特定の個人を識別するものではありません。 + + 収集された情報は、当社のプライバシーポリシーに則り、適切に取り扱われます。詳細については、[Google のサービスを使用するサイトやアプリから収集した情報の Google による使用](https://policies.google.com/technologies/partner-sites?hl=ja)および[当社のプライバシーポリシー](https://lifull.com/privacy/)をご参照ください。 + +### 利用方法 + +ガイドラインについて + 利用方法 + + LIFULLアクセシビリティガイドラインを効果的に活用する方法を解説します。 + + ## ガイドラインの使い方 + + LIFULLアクセシビリティガイドラインは、プロダクトやサービスをアクセシブルにするための手引書です。以下の手順でガイドラインを効果的に活用しましょう。 + + ### ガイドラインの活用手順 + + #### 1. ガイドライン全体を把握する + + まずは、ガイドライン全体をざっと読んで、アクセシビリティに関する基本的な理解を深めましょう。各項目には説明や具体例、参考情報が記載されているので、自分にとって特に関連性の高い部分を重点的に読み進めてください。 + + #### 2. 自分の業務に関連する項目を確認する + + ガイドラインは工程(≒職種)ごとに大きく分かれています。自分の業務に関連する項目を特定し、それらに重点を置いて理解を深めましょう。また、チーム内で協力してアクセシビリティを向上させるために、他の職種に関する項目も把握しておくと良いでしょう。 + + #### 3. 目標とするレベルを決める + + レベルはガイドライン項目の優先順位です。「レベルの選び方」を参考にしながら、プロダクトやプロジェクトで目標とするレベルを決定してください。 + + #### 4. 目標レベルに従って対応を計画する + + 各ガイドライン項目には対応するレベルが明記されています。これを参考にして、対応が必要な項目をリストアップし、実施の順序を決めましょう。リソースや状況に応じて、適切な対応策を選んでください。 + + #### 5. ガイドラインに沿った改善を実施する + + 対応が決まったら、具体的な改善を実施しましょう。ガイドラインに記載されている具体例や参考情報を活用しながら、アクセシビリティを向上させていくことが重要です。 + + #### 6. 継続的な改善を行う + + プロダクトやサービスのアクセシビリティ向上は、一度で完了するものではありません。ユーザーからのフィードバックや自身で気づいた課題をバックログに取り込み、継続的に改善していくことが大切です。 + + 各手順において、社内に在籍する専門家の知識やリソースを活用できます。アクセシビリティ推進グループに積極的に声をかけてみてください。 + + ## レベルについて + + ### レベルとは何か? + + レベルは、ガイドラインの重要度、コスト、LIFULLの制作事情を総合的に判断した、**ガイドライン項目の優先順位**です。 + + #### レベル1…必ず達成 + + ユーザーに大きな影響があり、どのサービスでも必ず達成したい重要なタスクです。レベル1に対応できていない場合、特定の状況にあるユーザーが完全にアクセスできない状況が生じることがあります。これはほとんどの場合、「バグ」とみなされるべきものです。 + + #### レベル2…可能な限り達成 + + レベル1に次いで重要で、できるだけ達成してほしいタスクです。これにより、より多くのユーザーがコンテンツにアクセスしやすくなります。項目によってはレベル1と同様の重要性を持つこともありますが、対応の難易度やLIFULLでの実情を考慮してレベルを下げている場合があります。 + + #### レベル3…できれば考慮 + + できれば考慮してもらいたいタスクです。これにより、WCAGのA, AAの達成基準がおおむねカバーされます。レベル3を達成すると、さらに多くのユーザーにとって使いやすいプロダクトやサービスを実現できます。 + + ### レベルの選び方 + + プロダクトやプロジェクトごとに目標レベルを設定し、そのレベルに合わせてガイドラインを満たすデザインと実装を行ってください。 + + 選択したレベルに基づいて、原則としてそのレベルで必要とされるガイドライン項目をすべて満たすようにしてください。 + + #### 既存のプロダクトやサービスへの適用 + + 既存のプロダクトやサービスにLIFULLアクセシビリティガイドラインを適用する場合、以下の「[レベル選びのモデルケース](#レベル選びのモデルケース)」から類似ケースを探して参考にしてください。適用時点で既にリリースされている部分に関する問題が多くても心配はいりません。今後の継続的な改善のなかで対応していきましょう。 + + #### 新規プロダクトやサービスへの適用 + + 新規プロダクトやサービスにLIFULLアクセシビリティガイドラインを適用する場合、下記の「[レベル選びのモデルケース](#レベル選びのモデルケース)」から類似ケースを探して参考にしてください。 + + #### 改修プロジェクトへの適用 + + プロダクトやサービスレベルで既に目標レベルが設定されている場合は、そのレベルに従ってください。目標がまだ設定されていなくても、UIの追加や改善を含む改修案件がある場合は、以下の「[レベル選びのモデルケース](#レベル選びのモデルケース)」から類似ケースを探して参考にしてください。 + + ### レベル選びのモデルケース + + 注:以下のモデルケースは、考え方を示すための例であり、実際に採用されたものではなく、必ずしも同様の判断を求めるものではありません。 + + **LIFULL HOME** + + 多くのアクセスが見込まれるため、様々なユーザーが利用することが想定されます。テキスト中心の情報検索・閲覧がサービスの核であるため、アクセシビリティへの配慮を妨げる要素はほとんどありません。一方で、企業の基幹サービスであるため、開発の迅速さも重要です。 + + **総合して、レベル2を目標とします**。レベル3の項目については、容易に実施できるものから取り入れます。 + + ただし、物件情報はユーザー生成コンテンツであり、画像に適切な代替テキストを用意することは難しいです。クライアントに代替テキストを提供してもらうための仕組みや業界慣習が不十分で、現実的ではありません。そのため、物件情報に関連する画像の代替テキストは対象外とします。 + + **サービスの紹介や問合せ導線を含むプロモーションサイト** + + 情報提供が主な目的のウェブサイトであり、サービス利用に関わる重要な導線です。インタラクティブな要素として問合せフォームがありますが、一般的なメールフォームで、複雑なインタラクションやサーバーサイド機能は必要ありません。サービス説明に動画コンテンツを含む場合、動画字幕作成のノウハウが不足していることが懸念されますが、取り組めば実現可能と考えられます。 + + **総合して、レベル2を目標とします**。 + + **企業の顔となる静的ウェブサイト(コーポレートサイト、IRサイトなど)** + + コーポレートサイトやIRサイト、採用サイトは、企業の取り組みが具現化されていると外部から認識される企業の顔です。情報提供が主な目的のウェブサイトであり、複雑なインタラクションやサーバーサイド機能は必要ありません。ブランディングやメッセージを伝えるために、動画やアニメーションによる演出を行いますが、アクセシビリティとの両立は十分可能です。 + + **総合して、レべル3を目標とします**。現時点で達成できていない項目については、計画的に改善を重ねます。IRサイトや採用サイトで利用している外部サービスのアクセシビリティについては、サービス提供元に改善要望を提案したり、別のサービスへの移行を検討します。 + + **外部委託による読み物系サイト・特集サイト** + + ほとんど動的な要素がない静的ウェブサイトです。事業部内にデザイナーやエンジニアがいないため、デザイン・実装および記事の執筆は外部委託しています。委託先は特別にアクセシビリティの知識が豊富ではありません。 + + **総合して、レベル1を目標とします**。デザインや実装のために、アクセシビリティガイドラインのコピーを委託先に送り、遵守を求めます。記事についても、記事コンテンツ向けアクセシビリティガイドラインに沿って執筆するよう求めます。デザインや実装のレビューや受入テストについては、アクセシビリティ推進グループに相談し進め方を確認します。 + + **VR/ARを活用した先進的なプロジェクト** + + VR/ARを用いて、業界内であまり例のない実験的なインタラクションを実現するプロジェクトです。実験的な要素が強く、普遍的な価値向上よりも、話題性や革新性をアピールすることが重要です。VR/ARによる空間的な体験の代替コンテンツ作成は、試行錯誤の末にできるかどうか微妙であり、プロジェクトの中で研究開発して提供することは現実的ではありません。 + + **総合して、目標とするレベルは定めません**。 + + **MVPによる検証フェーズのプロダクト** + + プロダクトが問題を解決するかどうか、市場がプロダクトを受け入れるかどうかを迅速に検証することが最も重要なプロジェクトです。プロダクトが提供できる根本的な価値を評価することが目的であり、ユーザビリティやアクセシビリティは二の次になることが共通認識となっています。 + + **総合して、目標とするレベルは定めません**。ただし、検証フェーズが終了しプロダクトが成功した場合、アクセシビリティを向上させるための改善計画を立てるものとします。 + + ## フィードバックの方法 + + 誤字脱字や内容の妥当性、わかりやすさや事例の推薦など、さまざまな観点からのフィードバックを歓迎しています。フィードバックを送るには、[GitHubリポジトリ](https://github.com/lifull/accessibility-guidelines)に起票してください。 + +## デザインのガイドライン + +### 自動再生するコンテンツ (レベル1) + +**チェック項目** + +**チェック項目** + +画面内に自動再生するコンテンツがあることで、特定のユーザーがウェブページの利用が困難になることがあります。以下はその一例です。 + +- 注意欠陥障害をもつユーザーは動き続けるコンテンツに注意を取られ、ページのほかの部分を利用できなくなることがあります。 +- 情報の取得に時間がかかる人は、コンテンツを読み終わる前に表示が切り替わってしまうかもしれません。 +- コンテンツの変化に伴い、フォーカスがリセットされたり音声が途切れたりすることによって、スクリーンリーダー利用上の妨げになることがあります。 + +具体例①:自動再生する映像 + +

+ +

+ +✗ 悪い例: 演出ビデオが背景で動き続ける + +ページにアクセスすると背景で映像が流れ始める。映像の前面にはロゴ、ナビゲーション、テキストが表示されている。目立つ場所に一時停止ボタンが配置されておらず、ユーザーが動きを止めることができない。 + +✓ 良い例: 再生を一時停止できるようにする + +目立つ場所に一時停止ボタンを配置する。ユーザーは一時停止ボタンを押すことで再生を止めることができる。 + +✓ 良い例: 再生を5秒以内に停止する + +ページにアクセスすると背景で映像が流れるが、映像は5秒以内に終了し再生が止まる。 + +具体例②:自動でスライドが切り替わるカルーセル + +✗ 悪い例: 一時停止できない + +ページにアクセスするとカルーセルが自動的に再生を始める。目立つ場所に一時停止ボタンが配置されておらず、ユーザーが動きを止めることができない。 + +✓ 良い例: 再生を一時停止できるようにする + +目立つ場所に一時停止ボタンを配置する。ユーザーは一時停止ボタンを押すことで再生を止めることができる。 + +✓ 良い例: 自動再生しないようにする + +カルーセルを自動再生しないようにする。 + +具体例③:注意喚起のアイコン + +✗ 悪い例: 一定周期で動き続ける + +ユーザーの注意を促すためにヘッダーの通知アイコンが定期的に揺れ動く。ヘッダーはページスクロールに追随し常に表示されている。 + +✓ 良い例: 動きを5秒以内に停止する + +ページを読み込むと、ヘッダーの通知アイコンが3回揺れ動き停止する。揺れる動きは5秒以内に停止する。 + +詳細: [自動再生するコンテンツ](https://lifull.github.io/accessibility-guidelines/design/autostart-content) + +### 閃光 (レベル1) + +**チェック項目:** 閃光を放つコンテンツは避ける + +**チェック項目:** 閃光を放つコンテンツは、1秒間に3回より遅く点滅するようにする + +**チェック項目:** 1秒間に3回以上点滅する場合は、点滅するコンテンツのサイズを小さくする + +閃光を放つ(激しく点滅する)コンテンツは可能な限り避けてください。必要な場合は点滅の速度や色差・面積を小さくする対策を講じてください。 + +光感受性による発作性障害のある人は、閃光を放つコンテンツによって発作を引き起こす恐れがあります。 + +詳細: [閃光](https://lifull.github.io/accessibility-guidelines/design/flash) + +### 見出し (レベル1) + +**チェック項目:** 情報構造を階層化して整理し、セクションの内容を表す簡潔な見出しを付ける + +**チェック項目:** 連続した見出しレベル(H1→H2→H3→…)を用いる + +**チェック項目:** メインエリアにひとつのH1見出しを設定する + +ページの情報構造を見出しを使って整理すると、ユーザーが情報を素早く把握できるようになります。特に、スクリーンリーダーの利用者は見出しを拾い読みすることでページの全体構造を把握しています。 + +詳細: [見出し](https://lifull.github.io/accessibility-guidelines/design/heading) + +### 画像の代替テキスト (レベル1) + +**チェック項目:** 「[代替テキストの考え方](/accessibility-guidelines/alternative-text.html)」のガイドを参考に、代替テキストを指定する + +視覚障害者はスクリーンリーダーを通じて代替テキストを合成音声または点字で読むことになります。ネットワーク状況によって画像がうまく読み込まれなかったときにも代替テキストが使われます。代替テキストが不十分だったり指定されていなかったりすると、必要な情報が伝わらなくなってしまいます。 + +適切な代替テキストは前後の文脈やユーザーに伝えたい情報によって変化します。「[代替テキストの考え方](/accessibility-guidelines/alternative-text.html)」は適切な代替テキストを考えるときに有用なガイドとなっています。 + +詳細: [画像の代替テキスト](https://lifull.github.io/accessibility-guidelines/design/image-alternative) + +### ページタイトル (レベル1) + +**チェック項目:** ページのトピックを表す簡潔なテキストをページタイトルにする + +**チェック項目:** ページごとに異なるユニークなページタイトルにする + +ページタイトルは、ページの主題を表す簡潔なテキストを指定してください。ページタイトルはブラウザのタブに表示され、スクリーンリーダーによって読み上げられます。ページの内容を把握したり、たくさん並んだタブの中から目的のタブを探すための重要な手がかりとしてページタイトルは重要です。 + +詳細: [ページタイトル](https://lifull.github.io/accessibility-guidelines/design/page-title) + +### 動画の字幕 (レベル1) + +**チェック項目:** 動画に字幕を提供する(動画に埋め込むまたは、設定で有効化できるようにする) + +動画に字幕を付けると、聴覚障害者のみならず、音を出せない環境や状況にあるユーザーにとっても動画を利用しやすくなります。 + +具体例:字幕付きの動画 + +

+ 字幕付き動画のイラスト +

+ +✓ 良い例: YouTubeに動画をアップロードする + +YouTubeに動画をアップロードすると自動的に音声が文字起こしされます。生成された字幕をもとに手直しをすると、字幕をつける手間が大幅に省けます。 + +詳細: [動画の字幕](https://lifull.github.io/accessibility-guidelines/design/video-caption) + +### 音声のみのコンテンツ (レベル2) + +**チェック項目:** 音声のみコンテンツに、代替コンテンツを提供する + +映像を伴わない音声のみのコンテンツ(例えばポッドキャスト)は、聴覚障害者が利用することができません。書き起こしテキストなどを用いて、音声と同等の情報を含む代替コンテンツを提供してください。 + +詳細: [音声のみのコンテンツ](https://lifull.github.io/accessibility-guidelines/design/audio-only) + +### グラフや図 (レベル2) + +**チェック項目:** グラフの傾向や要点を簡潔にまとめ、テキストや代替テキストで提供する + +**チェック項目:** グラフの元データをHTMLの表で提供するか、ダウンロードできるようにする(有用な場合) + +**チェック項目:** 図が伝えている情報をテキストや代替テキストで提供する + +グラフや図は多くの情報を効率的に伝えられる表現方法ですが、視覚に偏った表現になりがちなため、視覚による情報取得に制約があるユーザーには情報が伝わりづらいことがあります。 + +グラフや図は複雑になりすぎないようにしてください。グラフや図に含まれる情報量が、本文または代替テキストが伝える情報量を大きく上回ることのないようにしてください。 + +グラフの元データを利用できるようにしておくと、データの利用性やアクセシビリティが向上します。 + +具体例:複雑な概念図 + +

+ +

+ +✗ 悪い例: 画像のタイトルのみ指定されている + +複雑な概念を説明するための図があり、画像のタイトルが代替テキストに設定されている。スクリーンリーダー利用者には画像の内容が伝わらない。 + +✓ 良い例: 複雑な図に具体的な代替テキストを設定する + +複雑な概念を説明するための図があり、概念の説明をテキスト化したものが代替テキストに設定されている。 + +✓ 良い例: 本文を用いて概念について説明する + +図が説明している内容を、本文で取り上げる。見出しや箇条書き等を利用しわかりやすく表現する。図はあくまでも補佐的な役割にとどめるようにする。 + +詳細: [グラフや図](https://lifull.github.io/accessibility-guidelines/design/chart-and-diagram) + +### データ可視化 (レベル2) + +**チェック項目:** グラフの視覚的要素と凡例を色以外の手段で紐づけられるようにする + +グラフを表現している視覚的要素(点や線・面など)と凡例を、色だけを手掛かりに紐づけていると、ロービジョンや色覚特性のあるユーザーにとって利用しづらくなることがあります。 + +具体例:凡例付きの円グラフ + +✗ 悪い例: グラフ面と凡例を色で紐づけている + +グラフ面と凡例を色だけを手掛かりに紐づけている。P型色覚での見え方をシミュレーションすると色の判別が困難になる。 + +✓ 良い例: グラフ面に直接ラベルを配置する + +グラフ面に直接ラベルを配置することで、色に依存しない必要がなくなる。 + +詳細: [データ可視化](https://lifull.github.io/accessibility-guidelines/design/data-visualization) + +### テキスト画像 (レベル2) + +**チェック項目:** テキスト画像を使用しない + +ロービジョンのユーザーはテキストを自分の読みやすい配色に変換する支援技術を使っていることがあります。テキストが画像化されていると、この変換が行われなくなります。ロゴやアプリケーションのスクリーンショットなど、テキストが画像と一体化している場合は例外です。 + +詳細: [テキスト画像](https://lifull.github.io/accessibility-guidelines/design/image-of-text) + +### 表 (レベル2) + +**チェック項目:** 表は画像ではなくマークアップされるものとしてデザインする + +**チェック項目:** 入れ子構造になっている表や、見出しが入り組んでいる表は、より簡潔に表現できないか検討する + +表は画像化せず、HTMLをつかってマークアップされるものとしてデザインしてください。 + +セルの結合を多用した表や1件のデータが複数行にわたる表、入れ子構造になっている表は、理解しやすさやスクリーンリーダーによる利用性が下がります。多くの情報量を表に含めようとせず、いくつかのシンプルな表に分割できないかどうか検討してください。 + +具体例①:セルの結合のある表 + +✗ 悪い例: セルを不必要に結合している + +✓ 良い例: 列を有効活用する + +具体例②:入れ子になった表 + +✗ 悪い例: 表が入れ子になっている + +✓ 良い例: 表を分割する + +詳細: [表](https://lifull.github.io/accessibility-guidelines/design/table) + +### 動画コンテンツ(音声を含む) (レベル2) + +**チェック項目:** 動画にキャプションを提供する + +**チェック項目:** 動画に音声解説を提供するか、代替コンテンツを提供する + +音声付きの動画コンテンツは、視覚障害者や聴覚障害者にとって情報取得の障壁になることがあります。 + +音を出せないユーザーや聴覚障害者は音声から情報を取得できません。会話や音声・音楽に含まれる情報をキャプションとして提供する必要があります。 + +視覚障害者は映像から情報を取得できません。動画と同等の情報を含む代替コンテンツを提供するか、音声解説を提供する必要があります。 + + 会話を文字にした字幕(subtitle)に対して、キャプション(caption)は動画の内容を理解するために必要な詳細な情報を含みます。たとえば効果音・音楽・笑い声・話者の特定・位置など。 + + 主音声のトラックだけでは理解できない重要で視覚的な詳細を説明するために、音声トラックに追加されたナレーション。動作、登場人物、場面の変化、画面上のテキスト、及びその他の視覚的なコンテンツに関する情報など。 + +詳細: [動画コンテンツ(音声を含む)](https://lifull.github.io/accessibility-guidelines/design/video-and-audio) + +### リンクテキスト (レベル3) + +**チェック項目:** 「詳細はこちら」「ここをクリック」などのリンクテキストを避ける + +**チェック項目:** 新しいタブを開くリンクにはテキストまたはアイコンを添える + +**チェック項目:** ファイルの種別を明示する + +リンクテキストは遷移先のページタイトルにするなど、遷移先が理解しやすいテキストにしてください。「ここをクリック」といったテキストは単体では遷移先が理解できないため、リンクの前後のテキストを読むまで遷移先が不明瞭な状態になります。 + +また、リンクをクリックしたときに新しいタブが開かれたり、ファイルがダウンロードされたりすることをあらかじめテキストやアイコンを通じてユーザーに知らせるようにしてください。 + +詳細: [リンクテキスト](https://lifull.github.io/accessibility-guidelines/design/link-text) + +### コンテンツの順序 (レベル3) + +**チェック項目:** コンテンツを意味のある順序で並べる + +スクリーンリーダーはコーディングされた順番どおりに読み上げます。視覚上の順序とコード上の順序が食い違っていたり、情報のヒエラルキーをうまくコード化できていなかったりすると、スクリーンリーダー利用者が混乱してしまうかもしれません。レイアウトを素直にコード化したときに情報のヒエラルキーが崩れてしまいそうなデザインは避けるか、エンジニアとコミュニケーションをとり意図したとおりに読み上げられるようにしてください。 + +詳細: [コンテンツの順序](https://lifull.github.io/accessibility-guidelines/design/order-of-content) + +### 目次とサイトマップ (レベル3) + +**チェック項目:** 必要に応じて目次とサイトマップを提供する + +目次やサイトマップは、ページやウェブサイト全体がどのような構造になっているかを把握するのに役立ちます。 + +詳細: [目次とサイトマップ](https://lifull.github.io/accessibility-guidelines/design/sitemap) + +### カスタムUIのキーボード操作 (レベル1) + +**チェック項目:** 妥当なキーボード操作方法についてエキスパートに相談する + +**チェック項目:** [ARIAのキーボードガイダンス](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)に従う + +HTMLや既存のデザインパターンでは賄いきれない独自のUIをデザインする場合であっても、基本的なキーボード操作の慣習に従うようにしてください。 + +詳細: [カスタムUIのキーボード操作](https://lifull.github.io/accessibility-guidelines/design/keyboard) + +### 定番のパターン (レベル1) + +**チェック項目:** プレーンなHTMLの組み合わせで目的が達成できるか検討する + +**チェック項目:** アクセシビリティが確保された定番のパターンを[アクセシブルなデザインパターン](/accessibility-guidelines/accessible-patterns.html)から探し、目的が達成できるか検討する + +HTMLにもともと備わっている要素はすべてアクセシブルになるように作られています。HTMLで賄いきれない場合でも、アクセシビリティ確保の方法論が確立されている定番のデザインパターン(タブ・ダイアログなど)を採用するようにしてください。 + +詳細: [定番のパターン](https://lifull.github.io/accessibility-guidelines/design/established-pattern) + +### キーボード操作 (レベル1) + +**チェック項目:** キーボードですべてのコンテンツや機能を利用できるようにする + +ポインター操作やタッチ操作が使えない人のために、キーボード操作ですべてのコンテンツや機能を利用できるようにしてください。項目の選択・右クリック・ダブルクリック・ホバー・ホイール・ドラッグ&ドロップ・タッチジェスチャーなどに依存している操作を見つけ、キーボード操作のみで完結できるようにデザインしてください。 + + 通常、ウェブページはキーボードのTabキーやEnterキー、矢印キーを使って操作することができます。細かい操作を苦手とする上肢障害のユーザーはマウスやタッチのようなポインティング操作が使えません。ユーザーが操作できる機能はすべて、マウスやタッチだけでなく、キーボードのみで操作できる必要があります。 + +詳細: [キーボード操作](https://lifull.github.io/accessibility-guidelines/design/keyboard) + +### ホバーで表示されるコンテンツ (レベル2) + +**チェック項目:** 可能なかぎり、ホバーではなく選択でコンテンツを表示する + +**チェック項目:** ユーザーによる明示的な閉じるアクションによって閉じるようにする + +拡大鏡を利用している弱視のユーザーにとって、ポインターホバーで表示されるコンテンツは気づきにくかったり、表示されたコンテンツを読もうとポインターを動かしたとき意図せず閉じてしまうことがあります。ホバーによるコンテンツの表示はできれば避け、明示的な開く・閉じるアクションをトリガーにして表示・非表示をしてください。 + +具体例:ツールチップ + +✗ 悪い例: マウスオーバーで表示されるツールチップ + +マウスオーバーで表示されるツールチップはタッチ端末からのアクセスが良くない。 + +✗ 悪い例: 拡大鏡に対応していないツールチップ + +拡大鏡をつかって画面を大きく拡大しているユーザーは、読みたいテキストが画面外に表示され気づけないことがある。気づけたとしても、ツールチップを読むためにアイコンからマウスカーソルを外すと、ツールチップが閉じてしまうかもしれない。 + +詳細: [ホバーで表示されるコンテンツ](https://lifull.github.io/accessibility-guidelines/design/content-on-hover) + +### エラーメッセージ (レベル2) + +**チェック項目:** エラーの発生箇所と原因を具体的に記述する + +**チェック項目:** エラーの修正方法を説明する + +ユーザーの操作によってエラーが発生したとき、エラーについての十分な情報を提示してください。 + +具体例① エラーの原因を明記する + +✗ 悪い例: エラーの原因を明記しない + +なぜログインに失敗したかわからず、ユーザーは修正のためのアクションを起こせない。 + +✓ 良い例: エラーの原因を明記する + +なぜログインに失敗したかわかり、ユーザーは修正のためのアクションを起こせる。 + +具体例② エラーの修正方法を説明する + +✗ 悪い例: あいまいな修正指示 + +正しい形式とは何かが不明瞭で、どのように修正すればよいのかわからない。 + +✓ 良い例: 具体的で明快な修正指示 + +要求されている形式や文脈の情報を提供することで、正しい値に修正しやすくなる。 + +詳細: [エラーメッセージ](https://lifull.github.io/accessibility-guidelines/design/error-message) + +### エラーメッセージの提示 (レベル2) + +**チェック項目:** エラーメッセージを誰にでも見つけやすいようにする + +**チェック項目:** エラー箇所に容易にたどり着けるようにする + +視覚障害をもつユーザーは操作箇所から離れた場所に注意喚起が出ても気づかない可能性があります。エラー提示の際にページ遷移を伴う場合、ページの上部に目立つようにエラーメッセージを提示してください。動的にエラーを提示する場合、ポインターやフォーカスの付近にエラーを提示するようにしてください。 + +##### 気づきやすいエラーメッセージの提示パターン + +エラーメッセージを表示するタイミングには大別して2種類があります。エラーを含むHTMLをサーバーサイドから返却するものと、JavaScriptを使用してクライアントサイドで動的に行うものです。晴眼者にとってはあまり違いはありませんが、スクリーンリーダー利用者、拡大鏡利用者にとっての体験は大きく異なります。それぞれでの適切な提示方法について理解することが重要です。 + +エラーを含むHTMLをサーバーサイドから返却する場合、ページ遷移を伴う点に特徴があります。以下のような点に留意するとよいでしょう。 + +- エラーはページのできるだけ最初のほうに表示する +- エラー文言から当該箇所にジャンプできるようにする +- ページタイトルに「エラー」の文言を含めることでエラーの存在により気づきやすくなる + +クライアントサイドで動的にエラーを表示する場合、ユーザーが注目している場所から離れた場所にエラーを表示しても気づけないことがあります。以下のような点に留意するとよいでしょう。 + +- 送信ボタンが押されたとき、送信ボタンの付近にエラーを表示する +- または、エラーがあることをアラートダイアログで表示する。OKボタンを押すとエラーのあるフォームコントロールにフォーカスを移動する +- または、エラーが発生した箇所に自動的にフォーカスを移動し、フォームコントロールのすぐそばにエラー文言を表示する + +具体例①:エラーを含むHTMLをサーバーサイドから返却する + +✓ 良い例: エラーを含むHTMLをサーバーサイドから返却する + +エラーをページ上部に表示する。各箇所にもエラーの内容を表示する。ページ上部のエラーをクリックすると各箇所にジャンプできるようになっていると尚よい。 + +具体例②:クライアントサイドで動的にエラーを表示する + +✗ 悪い例: 送信ボタンを押せなくする + +送信ボタンのコントラストを落とし押せない状態にしていると、ユーザーは送信ボタンが見つけられなかったり、送信できない理由が理解できないことがある。 + +✗ 悪い例: ボタンから離れた位置にエラーを表示する + +送信ボタンから離れた位置で起きた変化は気づかれないことがある。 + +✓ 良い例: 送信ボタンの付近にエラーを表示する + +送信ボタンを常に表示し、送信ボタンが押されたらボタン付近にエラーを表示すると、多くのユーザーがエラーに気づくことができる。 + +詳細: [エラーメッセージの提示](https://lifull.github.io/accessibility-guidelines/design/error-presentation) + +### フォームコントロールのラベル (レベル2) + +**チェック項目:** すべてのフォームコントロールにラベルを設定する + +**チェック項目:** フォームコントロールの付近にラベルを表示する + +**チェック項目:** ラベルを常に表示する + +ユーザーの入力を受け付ける要素(フォームコントロール)のそばに常に表示されるラベルを配置することは、認知障害や弱視をもつユーザーの助けになります。 + +具体例:フォームコントロールのラベル + +✗ 悪い例: プレースホルダーでラベルを代用する + +✗ 悪い例: フォームコントロールとラベルが離れている + +✓ 良い例: フォームコントロールの付近にラベルを表示する + +詳細: [フォームコントロールのラベル](https://lifull.github.io/accessibility-guidelines/design/form-control-label) + +### シンプルなポインター操作 (レベル2) + +**チェック項目:** タッチジェスチャー・ドラッグ・マウスホイールで操作する機能は、シンプルなポインター操作だけでも利用できるようにする + +ピンチ操作やスワイプなどのタッチジェスチャー、ドラッグなどのマウス操作は、運動機能に障害があるユーザーには利用できないかもしれません。マウスホイールなどのデバイス依存の操作は、ユーザーのデバイスが対応していないかもしれません。シングルクリックやダブルクリックなどのシンプルなポインター操作だけでも機能を利用できるようにしてください。 + +この要件にはページ全体や、`overflow: scroll;`で表現されるスクロール領域のスクロール操作は含まれません。 + +詳細: [シンプルなポインター操作](https://lifull.github.io/accessibility-guidelines/design/simple-pointer) + +### デバイスの向き (レベル3) + +**チェック項目:** デバイスの向きを制限しない + +**チェック項目:** デバイスの回転なしですべてのコンテンツや機能を利用できるようにする + +運動機能障害を持つユーザーはデバイスの向きを固定して利用していることがあり、デバイスの向きが制限されたコンテンツや機能は利用できないかもしれません。 + +詳細: [デバイスの向き](https://lifull.github.io/accessibility-guidelines/design/device-orientation) + +### 新しいタブで開くリンク (レベル3) + +**チェック項目:** リンクやフォーム送信を同じタブで開く + +視覚的なコンテンツを知覚するのに困難を伴うユーザーをはじめとする一部のユーザーにとっては、リンクやフォーム送信を新しいタブで開くことは混乱の原因となりえます。 + +詳細: [新しいタブで開くリンク](https://lifull.github.io/accessibility-guidelines/design/link-opens-in-new-tab) + +### 予測可能なパターン (レベル3) + +**チェック項目:** UIがフォーカスを受取ったときや、フォームコントロール等の値を変更したとき、予測しづらいコンテンツの変化を起こさない + +**チェック項目:** コンテンツを変化させるための確定する操作をユーザーにゆだねる + +予測しづらいことが起きると、支援技術のユーザーや認知障害を持つユーザーは混乱したり、操作ができなくなることがあります。 + +具体例:プルダウンメニューによるナビゲーション + +

+ プルダウンメニューの値を変更する操作をしているイラスト +

+ +✗ 悪い例: 値を変更するとページ遷移する + +HTMLの`select`要素で実装された並び順選択UIでは、値が変更されるとページ遷移が行われる。値が変更されたタイミングでページ遷移が起きることは予測が難しい。キーボードユーザーは上下キーを使って値をひとつ変えただけでページ遷移してしまうため、目的の項目を選ぶことができない。 + +詳細: [予測可能なパターン](https://lifull.github.io/accessibility-guidelines/design/predictable-pattern) + +### 時間制限 (レベル3) + +**チェック項目:** 制限時間を設けない + +**チェック項目:** 制限時間が必要な場合、利用者が事前に制限時間を延長・無効化できるようにするか、制限時間を 20 時間以上とする + +認知障害をもつユーザーやコンテンツの言語に堪能でないユーザーは、コンテンツを利用するのに時間がかかることがあり、設けられた制限時間では足りないかもしれません。リアルタイムイベント等の制限時間を変更することが不可能な理由がある場合を除いて、制限時間は設けないようにしてください。 + +詳細: [時間制限](https://lifull.github.io/accessibility-guidelines/design/time-limit) + +### ユーザー認証 (レベル3) + +**チェック項目:** 認知機能テストによるユーザー認証に代替手段を用意する + +パズル認証や計算問題、記憶力を試す問題などは、認知障害を持つユーザーには利用できないことがあります。 + +詳細: [ユーザー認証](https://lifull.github.io/accessibility-guidelines/design/user-authentication) + +### フォーカスインジケーター (レベル1) + +**チェック項目:** フォーカスインジケーターを除去しない + +**チェック項目:** アウトライン型のフォーカスインジケーターは、APCA 45以上の色コントラストを確保する + +**チェック項目:** フォーカスインジケーターは十分な太さ(2px以上)である + +**チェック項目:** 背景色を用いたフォーカス表現は比較対象となる要素との距離を空けすぎない + +フォーカスインジケーターを非表示にすると、キーボードユーザーや弱視のユーザーはコンテンツや機能を利用できなくなってしまいます。 + +具体例:視認性に配慮したフォーカスインジケーター + +✗ 悪い例: コントラストが低すぎる + +✗ 悪い例: 色の変化のみ + +✓ 良い例: 十分なコントラストのあるフォーカスインジケーター + +✓ 良い例: 背景色を用いて表現されたフォーカスインジケーター + +詳細: [フォーカスインジケーター](https://lifull.github.io/accessibility-guidelines/design/focus-indicator) + +### リンクの判別 (レベル1) + +**チェック項目:** インラインリンクには下線を引いたり一貫したアイコンを添えたりしてリンクとわかるようにする + +本文のような複数行にわたるテキストブロックに含まれるインラインリンクは、リンクとしての手がかりを備えていないと、特にロービジョンや認知障害のあるユーザーは見逃してしまうかもしれません。 + +具体例:判別できるリンク + +✗ 悪い例: リンクを示すのに色だけを使用する + +本文中の一部のテキストがリンクのため青色で表現されている。 + +✓ 良い例: リンクを示すのに色と下線を使用する + +リンク色のほか下線をつかってリンクであることが表現されている。 + +詳細: [リンクの判別](https://lifull.github.io/accessibility-guidelines/design/link-identification) + +### 状態の判別 (レベル1) + +**チェック項目:** UIが状態を表現するとき、テキストや形状の変化で状態を判別できるようにする + +フォーカス・選択・ホバー・押下・展開・チェックなど、状態をもつUIコンポーネントは、状態の変化を視覚的に判別できるようにしてください。色に変化を持たせるだけでなく、色以外の手がかり(テキストや形状)を変化させることで状態を表現するようにしてください。ホバーのように状態の変化が装飾的なものであれば、テキストや形状の変化は必要ではありません。 + +具体例:状態が判別できるUIコンポーネント + +✓ 良い例: 押下状態がアイコンの変化でわかる + +✓ 良い例: チェック状態がアイコンの変化でわかる + +✓ 良い例: 展開状態がアイコンの変化でわかる + +✓ 良い例: フォーカス状態が枠線の変化でわかる + +✓ 良い例: 選択状態が塗りの変化でわかる + +詳細: [状態の判別](https://lifull.github.io/accessibility-guidelines/design/state-identification) + +### レスポンシブデザイン (レベル2) + +**チェック項目:** コンテンツ幅やレイアウトがビューポートの幅に応じるようにレスポンシブデザインを採用する + +レスポンシブデザインはブラウザービューポートサイズの変動にレイアウトが応じるため、ズーム時にも横スクロールを必要とせずコンテンツを利用できます。 + +詳細: [レスポンシブデザイン](https://lifull.github.io/accessibility-guidelines/design/responsive-design) + +### ターゲットサイズ (レベル2) + +**チェック項目:** リンク、ボタン、フォームコントロール等のポインター操作を受け付けるコンポーネントは、24px四方以上のサイズを確保する + +小さすぎるターゲットは、タッチ操作するユーザーや細かい操作を苦手とするユーザーにとって使いにくいことがあります。 + +具体例:ターゲットサイズ + +✗ 悪い例: 不十分なターゲットサイズ + +ターゲットサイズが24px未満であり、隣接するコンポーネントとの間隔も確保されていない。 + +✓ 良い例: 十分なターゲットサイズ + +ターゲットサイズを24px以上以上とする。ターゲットサイズが24px未満となる場合は、ターゲットサイズと隣接するコンポーネントとの間隔を足し合わせた値が24px以上になるようにする。 + +詳細: [ターゲットサイズ](https://lifull.github.io/accessibility-guidelines/design/target-size) + +### テキストの色コントラスト (レベル2) + +**チェック項目:** 記事本文以外の通常のテキストはAPCA 60以上の色コントラストを確保する + +**チェック項目:** 見出しなど大きいサイズのテキストはAPCA 45以上の色コントラストを確保する + +**チェック項目:** 記事本文など読みやすさが重要なテキストは[APCA](https://apcacontrast.com/) 75以上の色コントラストを確保する + +テキストの読みやすさには色のコントラストが重要です。特にロービジョンのユーザーには十分なコントラストが必要です。 + + APCA (Accessible Perceptual Contrast Algorithm) は2色のコントラスト比の計算アルゴリズムおよび評価手法です。WCAG 2.1で使われているコントラスト比計算の問題を解消するため開発されました。人間の知覚特性を加味した評価値を算出できることが特徴です。現在パブリックベータ版で、WCAGの次期バージョンにて取り入れられることが検討されています。 + + 白背景とLIFULLオレンジの文字色の組み合わせをAPCAでコントラストを計算すると59.7となりAPCA 60を満たしません。これを取りざたして問題視する必要はありません。ただし、文字色はそのままに、白でない色を背景にする場合、コントラストを確保するために文字の色を見直すことを推奨します。 + +詳細: [テキストの色コントラスト](https://lifull.github.io/accessibility-guidelines/design/text-contrast) + +### テキストの均等割付 (レベル2) + +**チェック項目:** 複数行にわたるテキストを均等割り付けにせず、左寄せもしくは右寄せにする + +特定の認知障害のあるユーザーは、均等割付されたテキストを読むことに苦労することがあります。 + +具体例:フォームコントロールのラベル + +✗ 悪い例: 均等割付する + +✓ 良い例: 均等割付しない + +詳細: [テキストの均等割付](https://lifull.github.io/accessibility-guidelines/design/text-justify) + +### アイコンやUIコンポーネントの色コントラスト (レベル2) + +**チェック項目** + +**チェック項目:** 必要とされた視覚的要素を、周囲の色に対して[APCA](https://apcacontrast.com/) 45以上の色コントラストを確保する + +フォームコントロールなどのユーザーが操作するUIコンポーネントや、アイコンやグラフなどの情報を持つグラフィックは、情報・機能・状態を判別し操作するために、充分なコントラストが必要です。 + +ただし、UIコンポーネントがアクティブではないときはコントラストの確保は不要です。 + +詳細: [アイコンやUIコンポーネントの色コントラスト](https://lifull.github.io/accessibility-guidelines/design/ui-contrast) + +## 実装のガイドライン + +### 背景画像 (レベル1) + +**チェック項目:** 情報を伝えている画像にはimg要素を使い、代替テキストを設定する + +情報を伝えている画像を CSS で背景画像として設定すると、テキストから情報を得ているユーザーに情報が伝わらなくなってしまいます。背景画像には代替テキストが設定できません。また背景画像は印刷に表示されません。 + +詳細: [背景画像](https://lifull.github.io/accessibility-guidelines/impl/background-image) + +### 見出し (レベル1) + +**チェック項目:** 設計資料に従い、階層構造の深さに応じた見出し要素(h1~h6)を使って見出しをマークアップする + +ページの情報構造を見出しを使って整理すると、ユーザーが情報を素早く把握できるようになります。特に、スクリーンリーダーの利用者は見出しを拾い読み※することでページの全体構造を把握しています。 + +詳細: [見出し](https://lifull.github.io/accessibility-guidelines/impl/heading) + +### 画像の代替テキスト (レベル1) + +**チェック項目:** img要素に代替テキストを指定するためにalt属性を使用する + +**チェック項目:** img要素を装飾画像とするために空のalt属性を使用する + +**チェック項目:** svg要素やアイコンフォントに代替テキストを指定するためにrole="img"とaria-label属性を使用する + +**チェック項目:** svg要素やアイコンフォントを装飾画像とするためにaria-hidden="true"を使用する + +**チェック項目:** イメージマップのarea要素に代替テキストを指定するためにalt属性を使用する + +画像やアイコンなどの非テキストコンテンツには、適した手段で代替テキストを指定してください。 + +##### 前工程で代替テキストが指定されなかった場合の対応 + +代替テキストはコンテンツの一部であるため、コンテンツ設計やデザインの段階で決められることが理想です。実装段階で代替テキストが決定していない場合、代替テキスト案を書き、コンテンツオーナーと相談してください。 + +- [altディシジョンツリー](https://www.w3.org/WAI/tutorials/images/decision-tree/ja)に沿って画像のタイプを判別する +- 画像が装飾的な画像ではない場合、[代替テキストの考え方](/accessibility-guidelines/alternative-text.html)を参考に代替テキスト案を書き、コンテンツオーナーと相談する +- 画像が複雑な情報を含みかつ本文やキャプションに同等の情報が含まれていなかったら、本文やキャプションに説明テキストを表示できないかどうかコンテンツオーナーと相談する + +具体例① 意味のある画像 + +✓ 良い例: `alt`属性を使用する + +```html +LIFULL HOME'S +``` + +意味のある`img`要素に代替テキストを指定するために`alt`属性を使用する。 + +✓ 良い例: `role="img"`と`aria-label`属性を使用する + +```html + +``` + +`svg`要素に代替テキストを指定するために`role="img"`と`aria-label`属性を使用する。 + +✓ 良い例: `role="img"`と`aria-label`属性を使用する + +```html + +``` + +アイコンが描画される要素に代替テキストを指定するために`role="img"`と`aria-label`属性を使用する。 + +具体例② 装飾的な画像 + +✓ 良い例: 空の`alt`属性を使用する + +```html + +``` + +`img`要素を装飾画像とするために空の`alt`属性を使用する。`alt`属性値そのものを省略すると、環境によって読み上げられ方が定まらない。 + +✓ 良い例: `aria-hidden="true"`を使用する + +```html + +``` + +`svg`要素やアイコンフォントを装飾画像とするために`aria-hidden="true"`を使用する。 + +具体例③ イメージマップ + +✓ 良い例: `area`要素に`alt`属性を使用する + +```html +日本地図 + + 北海道 + … + +``` + +イメージマップの`area`要素に代替テキストを指定するために`alt`属性を使用する。 + +詳細: [画像の代替テキスト](https://lifull.github.io/accessibility-guidelines/impl/image-alternative) + +### ページの言語 (レベル1) + +**チェック項目:** html要素にlang属性を使ってページ言語を明示する + +**チェック項目:** 部分的に別の言語のテキストが挿入される場合、その部分を囲む要素にlang属性を使って言語を明示する + +lang 属性を使うとページの全体や一部分に対して書かれている言語を明示することができます。機械がこれを読み取ることでテキストを適切に処理できるようになります。 + +詳細: [ページの言語](https://lifull.github.io/accessibility-guidelines/impl/language-of-page) + +### ページタイトル (レベル1) + +**チェック項目:** 設計資料に記載されたタイトルをtitle要素に指定する + +**チェック項目:** 設計資料にタイトルが記載されていない場合、サイト内で一意になるタイトル案を作成し、コンテンツオーナーと相談する + +ページタイトルはブラウザーのタブや検索結果に表示されたり、スクリーンリーダーによって読み上げられます。ページの内容を把握するための重要な手がかりとしてページタイトルは重要です。 + +詳細: [ページタイトル](https://lifull.github.io/accessibility-guidelines/impl/page-title) + +### 調整可能な文字サイズ (レベル2) + +**チェック項目:** ユーザー設定を反映できるようにルート要素への相対(rem)単位で文字サイズを指定する + +ユーザーの視力や認知特性に合わせるために、ブラウザーのデフォルトの文字サイズを大きく設定しているユーザーがいます。ウェブサイトが文字サイズをpx単位で指定していると、ブラウザーの文字サイズの設定が無視されてしまいます。 + +詳細: [調整可能な文字サイズ](https://lifull.github.io/accessibility-guidelines/impl/adjustable-text-size) + +### グループ化された画像 (レベル2) + +**チェック項目:** グループ化された画像に代替テキストを設定する + +複数の画像の並びがひとまとまりの情報を表現する場合、個別の画像に代替テキストを設定すると理解が難しくなる場合があります。 + +具体例①:評価メーター + +✗ 悪い例: 個別の画像に代替テキストを設定する + +```html +カスタマーレビュー + + 星1 + 星1 + 星1 + 星0.5 + + +``` + +連続して読み上げると「星1星1星1星0.5」となり、伝えたい情報が伝わらない。 + +✓ 良い例: 先頭の画像に代替テキストを設定する + +```html +カスタマーレビュー + + 5つ星のうち3.5 + + + + + +``` + +先頭の画像に数値の情報をまとめて設定し、残りの画像には空の文字列を設定する(=装飾画像とする)ことで、数値の情報を伝えられる。 + +✓ 良い例: 画像の並びをひとまとまりの画像として扱う + +```html +カスタマーレビュー + + + + + + + +``` + +`role="img"`属性を使用し、その要素をひとまとまりの画像として扱う。 + +詳細: [グループ化された画像](https://lifull.github.io/accessibility-guidelines/impl/grouped-images) + +### ランドマーク領域 (レベル2) + +**チェック項目:** サイト共通ヘッダーをheader要素を使ってマークアップする + +**チェック項目:** サイト共通フッターをfooter要素を使ってマークアップする + +**チェック項目:** ヘッダー、フッターを除く領域をmain要素を使ってマークアップする + +**チェック項目:** パンくずナビゲーションをnav要素を使ってマークアップする + +ページの主たる領域をランドマークとしてマークアップすると、スクリーンリーダー利用者の使い勝手を向上させることができます。 + +詳細: [ランドマーク領域](https://lifull.github.io/accessibility-guidelines/impl/landmark-region) + +### 意味のある順序 (レベル2) + +**チェック項目:** 意味の通る順序でコンテンツをマークアップし、スタイルシートでレイアウトする + +スクリーンリーダー等の支援技術は、DOM上の要素順に従ってコンテンツを読み上げます。スタイルシートやスクリプトを使ってレイアウトを調整していると、視覚的な順序と意味順序、読み上げの順序がバラバラになってしまうことがあり、混乱を招くかもしれません。 + +原則として、スタイルシートを無効化しても意味の通る順序でマークアップし、スタイルシートでレイアウトを実現するようにしてください。これによって視覚的な順序と読み上げの順序が一致しなくなり、支援技術による利用が著しく阻害されると思われる場合、デザイナーと話し合ってみてください。 + +詳細: [意味のある順序](https://lifull.github.io/accessibility-guidelines/impl/meaningful-sequence) + +### 改行と空白文字 (レベル2) + +**チェック項目:** 単語内の文字間隔を制御するために、CSS のletter-spacingを使用する + +ゆったりとした文字間隔を実現するためにスペース文字を文字間に挿入すると、スクリーンリーダーは語句を適切に検出できなくなり、うまく読み上げられなくなってしまいます。 + +詳細: [改行と空白文字](https://lifull.github.io/accessibility-guidelines/impl/whitespace-character) + +### 正しい構文と文法 (レベル3) + +**チェック項目:** 仕様に準拠したHTMLやCSSを記述する + +**チェック項目:** HTMLの構文と文法をチェックするために[Nu Html Checker](https://validator.w3.org/nu/)を使用する + +**チェック項目:** CSSの構文と文法をチェックするために[The W3C CSS Validation Service](https://jigsaw.w3.org/css-validator/)を使用する + +仕様に定められた構文と文法に準拠して HTML や CSS を記述することで、将来のウェブ技術の拡張や新しいユーザーエージェントに対して堅牢になります。 + +詳細: [正しい構文と文法](https://lifull.github.io/accessibility-guidelines/impl/syntax-and-grammar) + +### フォームコントロールのラベル (レベル1) + +**チェック項目:** label要素をつかってフォームコントロールとラベルを関連付ける + +テキスト入力欄やチェックボックス、`select`要素など、HTMLに定義されているユーザーの入力を受け付ける要素(フォームコントロール)は全て名前を持つ必要があります。名前は、スクリーンリーダー利用者がフォームコントロールにフォーカスを合わせたときに読み上げられます。 + +`label`要素を使うと、フォームコントロールに名前を付けられると同時にクリックやタップで選択できる範囲が広がるため、ユーザビリティ面にもメリットがあります。 + +具体例①:テキスト入力欄 + +✓ 良い例: ラベルとテキスト入力欄を一つの`label`要素に含める + +```html + +``` + +ラベルと入力欄を`label`要素で囲むと、入力欄の名前が「お名前」に設定されます。 + +✓ 良い例: テキスト入力欄に`id`を付与し、`label`要素の`for`属性で参照する + +```html + + +``` + +テキスト入力欄に`id`を付与し、`label`要素の`for`属性で参照することで、入力欄の名前が「お名前」に設定されます。ラベルとコントロールを隣同士に置けない場合などに便利です。 + +具体例②:チェックボックス + +✓ 良い例: チェックボックスとラベルを一つの`label`要素に含める + +```html + +``` + +チェックボックスラベルを`label`要素で囲むと、チェックボックスの名前が「メールを受取る」に設定されます。 + +✓ 良い例: チェックボックスに`id`を付与し、`label`要素の`for`属性で参照する + +```html + + +``` + +チェックボックスに`id`を付与し、`label`要素の`for`属性で参照することで、チェックボックスの名前が「メールを受取る」に設定されます。ラベルとコントロールを隣同士に置けない場合などに便利です。 + +詳細: [フォームコントロールのラベル](https://lifull.github.io/accessibility-guidelines/impl/label-for-control) + +### ラベルのないコントロール (レベル1) + +**チェック項目:** コントロールにラベルがなく、資料に名前の指定もない場合、名前の案を考え、コンテンツオーナーと認識をそろえる + +**チェック項目:** aria-labelledby属性もしくはaria-label属性をつかってフォームコントロールに名前を指定する + +デザイン上の理由でフォームコントロールのラベルが画面上から省略されている場合でも、フォームコントロールは名前を持つ必要があります。 + +ラベルのないコントロールに名前を付ける方法は2通りあります。`aria-labelledby`を使うと、ページ中の任意の要素のテキストをコントロールの名前として使用できます。`aria-label`属性はコントロールに直接名前を指定できます。 + +`aria-labelledby`属性を優先して使用するようにしてください。`aria-label`属性は利用状況によってはうまく機能しません。たとえばウェブページを機械翻訳にかけたときに翻訳されない場合があります。 + +具体例:ラベルのない検索フィールド + +✓ 良い例: `aria-labelledby`属性を使用する + +```html +
+ + + +
+``` + +コントロールに`aria-labelledby`属性を付与し、コントロールの名前を含む要素(不可視でも構わない)の`id`を指定する。 + +✓ 良い例: `aria-label`属性を使用する + +```html +
+ + +
+``` + +`aria-label`属性を使用し、コントロールに名前を直接指定する。 + +詳細: [ラベルのないコントロール](https://lifull.github.io/accessibility-guidelines/impl/labelless-control) + +### コピー&ペーストの許容 (レベル2) + +**チェック項目:** コピー&ペーストを禁止しない + +特定の認知障害を持つユーザーは、ユーザー名とパスワードの記憶に苦労することがあるため、コピー&ペーストが禁止されているログインフォームなどが利用できないことがあります。 + +詳細: [コピー&ペーストの許容](https://lifull.github.io/accessibility-guidelines/impl/allow-copy-paste) + +### フォームコントロールのグループ化 (レベル2) + +**チェック項目:** フォームコントロールをグルーピングするためにfieldset要素を使用する + +意味的にまとまりのある複数のフォームコントロールがある場合、それらをグループとしてマークアップしてください。スクリーンリーダーで当該フォームコントロールを操作するとき、所属するグループの名前も同時に読み上げられるようになり、文脈への理解が進みます。 + +具体例:性別の回答欄 + +✓ 良い例: `fieldset`要素で選択肢を囲う。 + +```html +
+ 性別 +

+ + + +

+
+``` + +詳細: [フォームコントロールのグループ化](https://lifull.github.io/accessibility-guidelines/impl/grouped-form-control) + +### 入力目的の特定 (レベル2) + +**チェック項目:** type属性とautocomplete属性を指定する + +**チェック項目:** 数字キーボードを表示するためにinputmode属性を使用する + +`input`要素の`type`属性や`autocomplete`属性を使用して入力フィールドの目的を特定しておくと、目的に応じたソフトウェアキーボードが表示されたり、ブラウザーに保存された値の補完機能を利用できるようになったりします。 + +補完機能を提供することで、入力時にかかる認知負荷を下げることができます。ログイン画面の入力項目のような記憶を要する場所に設定することは特に重要です。 + +具体例①:クレジットカード番号の入力欄 + +✗ 悪い例: 数字キーボードを表示するために`type="number"`を用いる + +```html + +``` + +数字キーボードを表示する目的のために`type="number"`や`type="tel"`を用いるのは誤り。 + +✓ 良い例: 補完種別を指定するために`autocomplete`属性を用いる。 + +```html + +``` + +✓ 良い例: 入力方式を指定するために`inputmode`を用いる。 + +```html + +``` + +詳細: [入力目的の特定](https://lifull.github.io/accessibility-guidelines/impl/identify-input-purpose) + +### フォームコントロールの説明文 (レベル3) + +**チェック項目:** aria-describedby属性をつかってフォームコントロールに説明文を指定する + +フォームコントロールと説明文をプログラムが解釈できるように紐づけると、スクリーンリーダー利用者は、フォーカスをフォームコントロールから移動することなく説明の内容を取得できるようになります。 + +具体例①:フォームコントロールの説明文 + +✓ 良い例: `aria-describedby`属性をつかってコントロールと説明文を紐づける + +``` + +具体例②:フォームコントロールのエラー + +✓ 良い例: `aria-describedby`属性をつかってコントロールとエラーメッセージを紐づける + +``` + + + 複数の`id`値をスペース区切りで指定することで、複数の要素にまたがる説明文およびエラーメッセージをコントロールに紐づけられる。 + +詳細: [フォームコントロールの説明文](https://lifull.github.io/accessibility-guidelines/impl/form-control-description) + +### ズームの許容 (レベル1) + +**チェック項目:** viewportメタデータにuser-scalable=noやmaximum-scaleを指定しない + +ロービジョンのユーザーを含む一部のユーザーにとっては、画面を任意の大きさにズームして表示できることは極めて重要です。そのような人にとって、ウェブサイトがズームを禁止しているとウェブサイトを利用できなくなることがあります。ズームされると大多数のユーザーに致命的な問題が起きない限り、ズームを許容してください。 + +##### ヒント:入力フィールドにフォーカスしたときに画面がズームしないようにする + +iOS Safariにおいて、ユーザーが入力フィールドにフォーカスすると画面が自動的にズームすることがあります。入力フィールドの文字サイズを16px以上にすることでこの挙動を抑止できます。 + +詳細: [ズームの許容](https://lifull.github.io/accessibility-guidelines/impl/allow-zoom) + +### フォーカスインジケーター (レベル1) + +**チェック項目:** フォーカスインジケーターを非表示にしない + +**チェック項目:** フォーカスインジケーターを抑制するためにHTMLElement.blur()を用いない + +**チェック項目:** フォーカスを受け取ったUIが他の要素によって完全に隠されてはいけない + +フォーカスインジケーターが不可視にされていると、キーボードユーザーはフォーカスの現在位置がわからず、操作を続けることができなくなってしまいます。 + +また、フォーカスインジケーターを取り除くために`HTMLElement.blur()`を使用すると、フォーカス位置が失われ、キーボード操作を再度先頭からやり直さなければいけなくなってしまうかもしれません。 + +##### ヒント:キーボード操作時だけフォーカスのスタイルを適用する + +`:focus`疑似クラスを使用すると、マウスやタッチ操作で選択したときにもスタイルが適用されます。キーボード操作時のみスタイルを適用したい場合、かわりに`:focus-visible`疑似クラスを使用します。 + +##### ヒント:フォーカスを受け取ったUIが追従コンテンツによって隠されないようにする + +ページをTabキーで下に移動すると、フォーカスの当たった要素が表示領域に収まるようにスクロールを伴いますが、追従ヘッダ等がある場合、それによってフォーカスされたUIが隠れてしまうケースがあります。このような場合、`scroll-padding`プロパティを設定することで要素の重なりを回避することができます。 + +##### ヒント:Cookieの利用同意を求めるモーダルダイアログにフォーカスを移動する + +Cookie利用同意のポップアップはしばしばダイアログとして実装され、ページの読み込み後すぐに表示されます。 このダイアログを背面と対話可能なモードレスダイアログとして実装すると、背面のUIがダイアログに完全に隠され、操作が困難になる可能性があります。この問題を回避するため、ダイアログはモーダルとして実装し、閉じるまでフォーカスが外側のコンテンツに移動しないようにすることが推奨されます。 + +詳細: [フォーカスインジケーター](https://lifull.github.io/accessibility-guidelines/impl/focus-indicator) + +### 挿入されるコンテンツ (レベル1) + +**チェック項目:** 動的に追加・表示されるコンテンツのDOMノードをトリガーの直後に配置する + +サブメニューやモーダルダイアログ、ディスクロージャー、「もっと見る」による追加読み込みなど、コンテンツが動的に追加・表示されるUIについて、そのコンテンツのDOMノードは操作の起点(トリガー)の直後になるようにしてください。トリガーの直後にコンテンツが挿入されることで、キーボードやスクリーンリーダーを使用したときに、トリガーの操作後に自然な流れで操作や読み上げを続けることができます。 + +JavaScriptによってフォーカス位置を制御できる場合、このガイドラインは必須ではありません。 + +詳細: [挿入されるコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/inserted-content) + +### ボタンの使用 (レベル1) + +**チェック項目:** トリガーとなるコンポーネントをbutton要素でマークアップする + +ユーザーのクリックやタップを起点にしてインタラクションが発生する場合、他に適切な要素がなければ、トリガーの要素には`button`要素を使用してください。 + +`button`要素はデフォルトでキーボードフォーカスを受け取り、スペースバーを含むキーボード操作によってクリックイベントを発生させることができます。`a`要素や`span`要素にJavaScriptでこれらの挙動を模倣して作るのは実装面・テスト面で大きなコストになりえます。 + +##### ヒント:ボタンが持つデフォルトスタイルをリセットする + +```css +button { + margin: 0; + padding: 0; + color: inherit; + background: none; + font-size: 100%; + line-height: inherit; + font-family: inherit; + font-weight: inherit; + text-transform: none; +} +``` + +詳細: [ボタンの使用](https://lifull.github.io/accessibility-guidelines/impl/use-button) + +### ホバーで表示されるコンテンツ (レベル2) + +**チェック項目:** ホバーやフォーカスで表示されるコンテンツは明示的に閉じられるまで表示し続ける + +ホバーやフォーカスがコンテンツ表示のトリガーとなっている場合、ユーザーによって明示的に閉じられるまでそのコンテンツを表示し続けてください。トリガーからポインターやフォーカスが外れた時点でコンテンツを非表示にしたり、時限式に閉じることは避けてください。 + +明示的に閉じる操作とは例えば次のような操作です。 + +- Escapeキーが押される +- 表示されたコンテンツの中の閉じるボタンが選択される +- トリガーとコンテンツの両方からポインターが外れ、0.5秒以上が経過する +- トリガーとコンテンツとは無関係の場所がクリックされる + +詳細: [ホバーで表示されるコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/content-on-hover) + +### 文脈に応じたフォーカス (レベル2) + +**チェック項目:** モーダルコンテンツを開いたとき、開かれたコンテンツにフォーカスを移動する + +**チェック項目:** モーダルコンテンツを閉じたとき、開くために使用したボタンにフォーカスを戻す + +**チェック項目:** コンテンツを追加で読み込む操作をしたとき、読み込まれたコンテンツにフォーカスを移動する + +**チェック項目:** ページ内リンクを模したスクロール操作をしたとき、スクロール先にフォーカスを移動する + +キーボードやスクリーンリーダーなどのシーケンシャルな操作体系の環境でスムーズに操作を行えるように、ユーザーの操作に応じた適切な位置にフォーカスを移動してください。 + +詳細: [文脈に応じたフォーカス](https://lifull.github.io/accessibility-guidelines/impl/contextual-focus) + +### Escapeキー操作 (レベル2) + +**チェック項目:** Escapeキーでポップアップメニューやダイアログ、ツールチップを閉じられるようにする + +Escapeキーは「現在のコンテキストから抜ける」ための慣習的に用いられているキー操作です。ポップアップメニューやダイアログ、ツールチップなど、一時的に、ほかのコンテンツより前面に表示されるコンテンツをEscapeキーで閉じられるようにしてください。 + +詳細: [Escapeキー操作](https://lifull.github.io/accessibility-guidelines/impl/escape-key) + +### 外部コンテンツおよびUIライブラリ (レベル2) + +**チェック項目:** iframeで埋め込まれる外部コンテンツがアクセシブルなことを確認する + +**チェック項目:** アクセシビリティに配慮された外部UIライブラリを採用する + +アクセシビリティはページ全体として評価されるため、採用している外部のUIライブラリや、iframeとして埋め込んでいる外部コンテンツもアクセシビリティに配慮したつくりになっている必要があります。 + +詳細: [外部コンテンツおよびUIライブラリ](https://lifull.github.io/accessibility-guidelines/impl/external-content-and-library) + +### 隠されているコンテンツ (レベル2) + +**チェック項目:** 非表示コンテンツにdisplay: noneやvisibility: hiddenも併用する + +**チェック項目:** display: noneやvisibility: hiddenが指定できない場合、非表示コンテンツにaria-hidden="true"を指定し、非表示コンテンツが含むインタラクティブ要素にtabindex="-1"を指定する + +DOMツリー上には存在するものの非表示にされているコンテンツは、意図的にそうしていない限り、キーボードや支援技術からもアクセスできないようにしてください。 + +キーボードや支援技術からもアクセスできてしまう非表示コンテンツとは、例えば次ような方法で非表示にしたコンテンツです。 + +- `opacity`を`0`にする +- `overflow: hidden`と`width`や`height`、`clip`を使ってコンテンツの一部分または全部を切り取る +- `position`や`transform`などを使って画面外ないし親要素の範囲外に追いやる + +詳細: [隠されているコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/hidden-content) + +### WAI-ARIA (レベル2) + +**チェック項目:** [APG](https://www.w3.org/WAI/ARIA/apg/)を参考に適切な役割、ステート、プロパティの値を設定する + +**チェック項目:** APGを参考に適切なキーボードインタラクションを実装する + +インタラクションを含むUIはHTMLのセマンティクスに加えてWAI-ARIAを実装することでアクセシビリティを高められることがあります。 + +WAI-ARIAに定義された役割やステート、プロパティは、想定されていない使われ方をすると逆にアクセシビリティを損なうことがあります。使い方について不明瞭な箇所があれば、社内のエキスパートに相談してください。 + +詳細: [WAI-ARIA](https://lifull.github.io/accessibility-guidelines/impl/wai-aria) + +### 背後のコンテンツ (レベル3) + +**チェック項目:** 背後に隠されたコンテンツにキーボードフォーカスを移せないようにする + +**チェック項目:** 背後に隠されたコンテンツを支援技術からも隠す + +ダイアログなどのモーダルコンテンツが表示されているとき、背後に隠されたコンテンツはキーボードや支援技術からアクセスされないようにしてください。 + +詳細: [背後のコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/backside-content) + +### ダウンイベントの使用 (レベル3) + +**チェック項目:** mousedown, pointerdown, touchstartを機能実行のトリガーにしない + +ダウンイベント(`mousedown`, `pointerdown`, `touchstart`)を機能実行のトリガーにしていると、細かい動作が苦手なユーザーは意図せず機能を実行してしまうことがあります。かわりにアップイベント(`mouseup`, `pointerup`, `touchend`)や、`click`, `input`, `change`のような、より意図に近いイベントを用いるようにしてください。 + +詳細: [ダウンイベントの使用](https://lifull.github.io/accessibility-guidelines/impl/down-event) + +### ドラッグ操作の中断 (レベル3) + +**チェック項目:** ドラッグ操作を中止できるようにする + +UIがドラッグアンドドロップ操作を求めるとき、ドラッグの途中で操作を中止する手段を設けてください。中止する手段の例としては次のようなものがあります。 + +- ドロップエリアは画面の中の一部分であり、それ以外の箇所にドロップしても何も起こらない +- ドロップ操作をした後、簡単な操作で元に戻すことができる +- ドラッグ中にEscapeキーを押すとドラッグ操作を中止できる + +詳細: [ドラッグ操作の中断](https://lifull.github.io/accessibility-guidelines/impl/drag-operation-interruption) + +### ステータスの通知 (レベル3) + +**チェック項目:** ARIAライブリージョンを適切に使用し、状態変化や通知メッセージを支援技術に伝える + +**チェック項目:** スクリーンリーダーを使用して動作検証を行う + +アプリケーションの状態変化や通知メッセージをUIに表示する場合、スクリーンリーダー等の支援技術にも同様の情報を伝える必要があります。WAI-ARIAのライブリージョンを使うことで実現可能です。 + +詳細: [ステータスの通知](https://lifull.github.io/accessibility-guidelines/impl/status-announcement) + diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..2394b64 --- /dev/null +++ b/llms.txt @@ -0,0 +1,103 @@ +# LIFULL Accessibility Guidelines + +> アクセシビリティに配慮したデザインと実装のためのガイドライン + +This file contains accessibility guidelines for designers and developers. + +## metadata +- url: https://lifull.github.io/accessibility-guidelines/ +- version: v3.0 +## ガイドラインについて + +- [はじめに](https://lifull.github.io/accessibility-guidelines/introduction.html) +- [利用方法](https://lifull.github.io/accessibility-guidelines/usage.html) + +## デザインのガイドライン + +### コンテンツ + +- [自動再生するコンテンツ](https://lifull.github.io/accessibility-guidelines/design/autostart-content) (レベル1: 必ず達成) +- [閃光](https://lifull.github.io/accessibility-guidelines/design/flash) (レベル1: 必ず達成) +- [見出し](https://lifull.github.io/accessibility-guidelines/design/heading) (レベル1: 必ず達成) +- [画像の代替テキスト](https://lifull.github.io/accessibility-guidelines/design/image-alternative) (レベル1: 必ず達成) +- [ページタイトル](https://lifull.github.io/accessibility-guidelines/design/page-title) (レベル1: 必ず達成) +- [動画の字幕](https://lifull.github.io/accessibility-guidelines/design/video-caption) (レベル1: 必ず達成) +- [音声のみのコンテンツ](https://lifull.github.io/accessibility-guidelines/design/audio-only) (レベル2: 可能な限り達成) +- [グラフや図](https://lifull.github.io/accessibility-guidelines/design/chart-and-diagram) (レベル2: 可能な限り達成) +- [データ可視化](https://lifull.github.io/accessibility-guidelines/design/data-visualization) (レベル2: 可能な限り達成) +- [テキスト画像](https://lifull.github.io/accessibility-guidelines/design/image-of-text) (レベル2: 可能な限り達成) +- [表](https://lifull.github.io/accessibility-guidelines/design/table) (レベル2: 可能な限り達成) +- [動画コンテンツ(音声を含む)](https://lifull.github.io/accessibility-guidelines/design/video-and-audio) (レベル2: 可能な限り達成) +- [リンクテキスト](https://lifull.github.io/accessibility-guidelines/design/link-text) (レベル3: できれば考慮) +- [コンテンツの順序](https://lifull.github.io/accessibility-guidelines/design/order-of-content) (レベル3: できれば考慮) +- [目次とサイトマップ](https://lifull.github.io/accessibility-guidelines/design/sitemap) (レベル3: できれば考慮) + +### フォーム・インタラクション + +- [カスタムUIのキーボード操作](https://lifull.github.io/accessibility-guidelines/design/keyboard) (レベル1: 必ず達成) +- [定番のパターン](https://lifull.github.io/accessibility-guidelines/design/established-pattern) (レベル1: 必ず達成) +- [キーボード操作](https://lifull.github.io/accessibility-guidelines/design/keyboard) (レベル1: 必ず達成) +- [ホバーで表示されるコンテンツ](https://lifull.github.io/accessibility-guidelines/design/content-on-hover) (レベル2: 可能な限り達成) +- [エラーメッセージ](https://lifull.github.io/accessibility-guidelines/design/error-message) (レベル2: 可能な限り達成) +- [エラーメッセージの提示](https://lifull.github.io/accessibility-guidelines/design/error-presentation) (レベル2: 可能な限り達成) +- [フォームコントロールのラベル](https://lifull.github.io/accessibility-guidelines/design/form-control-label) (レベル2: 可能な限り達成) +- [シンプルなポインター操作](https://lifull.github.io/accessibility-guidelines/design/simple-pointer) (レベル2: 可能な限り達成) +- [デバイスの向き](https://lifull.github.io/accessibility-guidelines/design/device-orientation) (レベル3: できれば考慮) +- [新しいタブで開くリンク](https://lifull.github.io/accessibility-guidelines/design/link-opens-in-new-tab) (レベル3: できれば考慮) +- [予測可能なパターン](https://lifull.github.io/accessibility-guidelines/design/predictable-pattern) (レベル3: できれば考慮) +- [時間制限](https://lifull.github.io/accessibility-guidelines/design/time-limit) (レベル3: できれば考慮) +- [ユーザー認証](https://lifull.github.io/accessibility-guidelines/design/user-authentication) (レベル3: できれば考慮) + +### ビジュアル + +- [フォーカスインジケーター](https://lifull.github.io/accessibility-guidelines/design/focus-indicator) (レベル1: 必ず達成) +- [リンクの判別](https://lifull.github.io/accessibility-guidelines/design/link-identification) (レベル1: 必ず達成) +- [状態の判別](https://lifull.github.io/accessibility-guidelines/design/state-identification) (レベル1: 必ず達成) +- [レスポンシブデザイン](https://lifull.github.io/accessibility-guidelines/design/responsive-design) (レベル2: 可能な限り達成) +- [ターゲットサイズ](https://lifull.github.io/accessibility-guidelines/design/target-size) (レベル2: 可能な限り達成) +- [テキストの色コントラスト](https://lifull.github.io/accessibility-guidelines/design/text-contrast) (レベル2: 可能な限り達成) +- [テキストの均等割付](https://lifull.github.io/accessibility-guidelines/design/text-justify) (レベル2: 可能な限り達成) +- [アイコンやUIコンポーネントの色コントラスト](https://lifull.github.io/accessibility-guidelines/design/ui-contrast) (レベル2: 可能な限り達成) + +## 実装のガイドライン + +### マークアップ + +- [背景画像](https://lifull.github.io/accessibility-guidelines/impl/background-image) (レベル1: 必ず達成) +- [見出し](https://lifull.github.io/accessibility-guidelines/impl/heading) (レベル1: 必ず達成) +- [画像の代替テキスト](https://lifull.github.io/accessibility-guidelines/impl/image-alternative) (レベル1: 必ず達成) +- [ページの言語](https://lifull.github.io/accessibility-guidelines/impl/language-of-page) (レベル1: 必ず達成) +- [ページタイトル](https://lifull.github.io/accessibility-guidelines/impl/page-title) (レベル1: 必ず達成) +- [調整可能な文字サイズ](https://lifull.github.io/accessibility-guidelines/impl/adjustable-text-size) (レベル2: 可能な限り達成) +- [グループ化された画像](https://lifull.github.io/accessibility-guidelines/impl/grouped-images) (レベル2: 可能な限り達成) +- [ランドマーク領域](https://lifull.github.io/accessibility-guidelines/impl/landmark-region) (レベル2: 可能な限り達成) +- [意味のある順序](https://lifull.github.io/accessibility-guidelines/impl/meaningful-sequence) (レベル2: 可能な限り達成) +- [改行と空白文字](https://lifull.github.io/accessibility-guidelines/impl/whitespace-character) (レベル2: 可能な限り達成) +- [正しい構文と文法](https://lifull.github.io/accessibility-guidelines/impl/syntax-and-grammar) (レベル3: できれば考慮) + +### フォーム + +- [フォームコントロールのラベル](https://lifull.github.io/accessibility-guidelines/impl/label-for-control) (レベル1: 必ず達成) +- [ラベルのないコントロール](https://lifull.github.io/accessibility-guidelines/impl/labelless-control) (レベル1: 必ず達成) +- [コピー&ペーストの許容](https://lifull.github.io/accessibility-guidelines/impl/allow-copy-paste) (レベル2: 可能な限り達成) +- [フォームコントロールのグループ化](https://lifull.github.io/accessibility-guidelines/impl/grouped-form-control) (レベル2: 可能な限り達成) +- [入力目的の特定](https://lifull.github.io/accessibility-guidelines/impl/identify-input-purpose) (レベル2: 可能な限り達成) +- [フォームコントロールの説明文](https://lifull.github.io/accessibility-guidelines/impl/form-control-description) (レベル3: できれば考慮) + +### インタラクション + +- [ズームの許容](https://lifull.github.io/accessibility-guidelines/impl/allow-zoom) (レベル1: 必ず達成) +- [フォーカスインジケーター](https://lifull.github.io/accessibility-guidelines/impl/focus-indicator) (レベル1: 必ず達成) +- [挿入されるコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/inserted-content) (レベル1: 必ず達成) +- [ボタンの使用](https://lifull.github.io/accessibility-guidelines/impl/use-button) (レベル1: 必ず達成) +- [ホバーで表示されるコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/content-on-hover) (レベル2: 可能な限り達成) +- [文脈に応じたフォーカス](https://lifull.github.io/accessibility-guidelines/impl/contextual-focus) (レベル2: 可能な限り達成) +- [Escapeキー操作](https://lifull.github.io/accessibility-guidelines/impl/escape-key) (レベル2: 可能な限り達成) +- [外部コンテンツおよびUIライブラリ](https://lifull.github.io/accessibility-guidelines/impl/external-content-and-library) (レベル2: 可能な限り達成) +- [隠されているコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/hidden-content) (レベル2: 可能な限り達成) +- [WAI-ARIA](https://lifull.github.io/accessibility-guidelines/impl/wai-aria) (レベル2: 可能な限り達成) +- [背後のコンテンツ](https://lifull.github.io/accessibility-guidelines/impl/backside-content) (レベル3: できれば考慮) +- [ダウンイベントの使用](https://lifull.github.io/accessibility-guidelines/impl/down-event) (レベル3: できれば考慮) +- [ドラッグ操作の中断](https://lifull.github.io/accessibility-guidelines/impl/drag-operation-interruption) (レベル3: できれば考慮) +- [ステータスの通知](https://lifull.github.io/accessibility-guidelines/impl/status-announcement) (レベル3: できれば考慮) + diff --git a/package-lock.json b/package-lock.json index 5d2c757..907bb12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "accessibility-guidelines", + "name": "lifull-accessibility-guidelines", "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.0.1", + "name": "lifull-accessibility-guidelines", "dependencies": { "@astrojs/markdown-remark": "^2.1.3", "@astrojs/mdx": "^0.18.3", @@ -2253,6 +2253,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", diff --git a/package.json b/package.json index 89f1c61..3972c57 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "dev": "astro dev", "start": "astro dev", "build": "astro build", - "preview": "astro preview" + "preview": "astro preview", + "generate-llms": "node ./scripts/generate-llms-txt.js" }, "dependencies": { "@astrojs/markdown-remark": "^2.1.3", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..6f237da --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,148 @@ +# llms.txt Generator + +このディレクトリには、LIFULL Accessibility Guidelines用のllms.txtファイルを生成するためのスクリプトが含まれています。 + +## 概要 + +llms.txtジェネレーターは、MDXファイルからアクセシビリティガイドラインの内容を抽出し、AI言語モデルが利用しやすい形式のファイルを生成します。 + +## ファイル構成 + +```text +scripts/ +├── generate-llms-txt.js # メインスクリプト(エントリーポイント) +├── llms-generator.js # メインジェネレータークラス +├── config.js # 設定ファイル +├── logger.js # ログ機能 +├── parsers/ # パーサーモジュール +│ ├── file-parser.js # ファイル操作ユーティリティ +│ ├── mdx-parser.js # MDXファイルパーサー +│ ├── component-parser.js # Astroコンポーネントパーサー +│ ├── code-block-parser.js # コードブロックパーサー +│ └── url-generator.js # URL生成ユーティリティ +└── generators/ # コンテンツ生成モジュール + └── content-generator.js # llms.txtコンテンツ生成 +``` + +## 使用方法 + +### 基本的な使用方法 + +```bash +# package.jsonで定義されたスクリプトを使用 +npm run generate-llms + +# または直接実行 +node scripts/generate-llms-txt.js +``` + +### プログラムからの使用 + +```javascript +const LlmsGenerator = require('./scripts/llms-generator'); + +// 基本的な使用 +const generator = new LlmsGenerator(); +await generator.generate(); + +// オプションを指定 +const generator = new LlmsGenerator({ + verbose: true, // 詳細なログを表示 + silent: false // 静寂モード +}); +await generator.generate(); +``` + +## 生成されるファイル + +- `llms.txt` - 簡易版(ガイドライン一覧とリンクのみ) +- `llms-full.txt` - 完全版(全コンテンツを含む) + +## 設定 + +設定は `config.js` で管理されています: + +```javascript +module.exports = { + // ファイルパス + paths: { + guidelines: '../src/content/guidelines', + pages: '../src/pages', + output: '../llms.txt', + outputFull: '../llms-full.txt' + }, + + // サイトメタデータ + metadata: { + title: 'LIFULL Accessibility Guidelines', + version: 'v3.0' + }, + + // その他の設定... +}; +``` + +## 機能 + +### パーサー機能 + +- **MDXファイル解析**: フロントマターとコンテンツの分離 +- **Astroコンポーネント処理**: Checkpoint、Cases、Case、Levelコンポーネントの解析 +- **コードブロック抽出**: HTMLコード例の正確な抽出 +- **日本語URL対応**: アンカーIDの生成で日本語文字をサポート + +### コンテンツ生成 + +- **階層構造**: エリア(デザイン/実装)とカテゴリーによる整理 +- **目次生成**: 完全版では自動的に目次を生成 +- **レベル表示**: ガイドラインの重要度レベルを表示 +- **URLリンク**: 各ガイドラインへの直接リンク + +### エラーハンドリング + +- **詳細なログ**: verbose モードで詳細な処理状況を表示 +- **例外処理**: ファイル読み込みやパース処理のエラーを適切に処理 +- **依存関係チェック**: 必要なnpmパッケージの自動インストール + +## 開発・メンテナンス + +### 新しいコンポーネントの追加 + +新しいAstroコンポーネントを処理に追加する場合: + +1. `parsers/component-parser.js` に解析ロジックを追加 +2. `config.js` に必要な設定を追加 +3. テストを実行して動作確認 + +### 設定の変更 + +設定変更は `config.js` を編集してください。変更後は以下をテスト: + +```bash +npm run generate-llms +``` + +### デバッグ + +詳細なログを確認したい場合: + +```javascript +const generator = new LlmsGenerator({ verbose: true }); +await generator.generate(); +``` + +## トラブルシューティング + +### よくある問題 + +1. **依存関係エラー**: `npm install` を実行 +2. **パスエラー**: `config.js` のパス設定を確認 +3. **MDXパースエラー**: ファイルの構文を確認 + +### ログレベル + +- **info**: 一般的な進行状況 +- **verbose**: 詳細な処理状況(verbose モード時のみ) +- **warn**: 警告メッセージ +- **error**: エラーメッセージ +- **success**: 成功メッセージ diff --git a/scripts/config.js b/scripts/config.js new file mode 100644 index 0000000..b299081 --- /dev/null +++ b/scripts/config.js @@ -0,0 +1,76 @@ +/** + * Configuration for llms.txt generation + */ + +module.exports = { + // File paths + paths: { + guidelines: '../src/content/guidelines', + pages: '../src/pages', + output: '../llms.txt', + outputFull: '../llms-full.txt' + }, + + // Base URL for the site + baseUrl: 'https://lifull.github.io/accessibility-guidelines', + + // Site metadata + metadata: { + title: 'LIFULL Accessibility Guidelines', + description: 'アクセシビリティに配慮したデザインと実装のためのガイドライン', + englishDescription: 'This file contains accessibility guidelines for designers and developers.', + version: 'v3.0' + }, + + // Pages to include in different versions + pages: { + full: ['introduction.mdx', 'usage.mdx', 'alternative-text.mdx', 'accessible-patterns.mdx'], + simplified: ['introduction.mdx', 'usage.mdx'] + }, + + // Category name mappings + categoryNames: { + contents: 'コンテンツ', + 'forms-and-interactions': 'フォーム・インタラクション', + visual: 'ビジュアル', + markup: 'マークアップ', + forms: 'フォーム', + interactions: 'インタラクション' + }, + + // Area name mappings + areaNames: { + design: 'デザイン', + impl: '実装' + }, + + // Level descriptions + levelDescriptions: { + 1: '必ず達成', + 2: '可能な限り達成', + 3: 'できれば考慮' + }, + + // Type labels for examples + typeLabels: { + good: '✓ 良い例', + bad: '✗ 悪い例' + }, + + // Regular expressions for content cleaning + regex: { + imports: /import[\s\S]*?from\s+["'].*?["'];?\s*/g, + jsxComments: /\{\/\*[\s\S]*?\*\/\}/g, + backticks: /`([^`]+)`/g, + frontmatterTitle: /#### 「\{frontmatter\.title\}」とは/g, + checkpointHeader: /#### チェック項目/g, + referenceSection: /##### 参考情報[\s\S]*$/g, + multipleNewlines: /\n\s*\n\s*\n/g, + htmlTags: /<[^>]*>/g, + markdownHeaders: /#{1,6}\s*/, + japaneseChars: /[^\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\w\s]/g, + spaces: /\s+/g, + dashes: /-+/g, + leadingTrailingDashes: /^-+|-+$/g + } +}; \ No newline at end of file diff --git a/scripts/generate-llms-txt.js b/scripts/generate-llms-txt.js new file mode 100755 index 0000000..e23297a --- /dev/null +++ b/scripts/generate-llms-txt.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +/** + * LIFULL Accessibility Guidelines - llms.txt Generator + * + * This script generates llms.txt files by parsing MDX files in the accessibility guidelines. + * It follows the format described at https://github.com/AnswerDotAI/llms-txt + * + * The script has been refactored into modular components for better maintainability: + * - config.js: Configuration and constants + * - parsers/: File parsing utilities + * - generators/: Content generation utilities + * - llms-generator.js: Main generator class + */ + +const LlmsGenerator = require('./llms-generator'); + +// Main execution +if (require.main === module) { + LlmsGenerator.run() + .then(() => { + process.exit(0); + }) + .catch(error => { + console.error('Error generating llms files:', error.message); + process.exit(1); + }); +} + +module.exports = LlmsGenerator; \ No newline at end of file diff --git a/scripts/generators/content-generator.js b/scripts/generators/content-generator.js new file mode 100644 index 0000000..48554a7 --- /dev/null +++ b/scripts/generators/content-generator.js @@ -0,0 +1,249 @@ +/** + * Content generation utilities for llms.txt files + */ + +const config = require('../config'); +const UrlGenerator = require('../parsers/url-generator'); + +class ContentGenerator { + /** + * Generate the header section for llms.txt files + * @returns {string} Header content + */ + static generateHeader() { + const { title, description, englishDescription, version } = config.metadata; + + return [ + `# ${title}`, + '', + `> ${description}`, + '', + englishDescription, + '', + '## metadata', + `- url: ${config.baseUrl}/`, + `- version: ${version}`, + '' + ].join('\n'); + } + + /** + * Generate table of contents for full version + * @param {Array} pages - Array of page data + * @param {Object} guidelines - Guidelines grouped by area and category + * @returns {string} Table of contents + */ + static generateTableOfContents(pages, guidelines) { + let toc = '## 目次\n\n'; + + // Add pages to TOC + if (pages.length > 0) { + toc += '- [ガイドラインについて](#ガイドラインについて)\n'; + pages.forEach(page => { + const anchorId = UrlGenerator.generateAnchorId(page.title); + toc += ` - [${page.title}](#${anchorId})\n`; + }); + } + + // Add guidelines to TOC + for (const area in guidelines) { + const areaName = UrlGenerator.getAreaName(area); + toc += `- [${areaName}のガイドライン](#${areaName.replace(/\s/g, '')}のガイドライン)\n`; + + for (const category in guidelines[area]) { + this._sortGuidelinesByLevel(guidelines[area][category]); + + guidelines[area][category].forEach(guideline => { + const anchorId = UrlGenerator.generateAnchorId(guideline.title, guideline.level); + toc += ` - [${guideline.title} (レベル${guideline.level})](#${anchorId})\n`; + }); + } + } + + return toc + '\n'; + } + + /** + * Generate pages section content + * @param {Array} pages - Array of page data + * @param {boolean} isFullVersion - Whether this is for the full version + * @returns {string} Pages content + */ + static generatePagesContent(pages, isFullVersion = true) { + if (pages.length === 0) return ''; + + if (isFullVersion) { + return this._generateFullPagesContent(pages); + } else { + return this._generateSimplifiedPagesContent(pages); + } + } + + /** + * Generate full pages content with details + * @private + * @param {Array} pages - Array of page data + * @returns {string} Full pages content + */ + static _generateFullPagesContent(pages) { + let content = '## ガイドラインについて\n\n'; + + pages.forEach(page => { + const anchorId = UrlGenerator.generateAnchorId(page.title); + content += `### ${page.title}\n\n`; + content += `${page.content}\n\n`; + }); + + return content; + } + + /** + * Generate simplified pages content with links only + * @private + * @param {Array} pages - Array of page data + * @returns {string} Simplified pages content + */ + static _generateSimplifiedPagesContent(pages) { + let content = '## ガイドラインについて\n\n'; + + // Sort pages to ensure consistent order + const sortedPages = this._sortPages(pages); + + sortedPages.forEach(page => { + const pageUrl = UrlGenerator.generatePageUrl(page.fileName); + content += `- [${page.title}](${pageUrl})\n`; + }); + + return content + '\n'; + } + + /** + * Generate guidelines section content + * @param {Object} guidelines - Guidelines grouped by area and category + * @param {boolean} isFullVersion - Whether this is for the full version + * @returns {string} Guidelines content + */ + static generateGuidelinesContent(guidelines, isFullVersion = true) { + let content = ''; + + for (const area in guidelines) { + const areaName = UrlGenerator.getAreaName(area); + content += `## ${areaName}のガイドライン\n\n`; + + if (isFullVersion) { + content += this._generateFullGuidelinesContent(area, guidelines[area]); + } else { + content += this._generateSimplifiedGuidelinesContent(area, guidelines[area]); + } + } + + return content; + } + + /** + * Generate full guidelines content with details + * @private + * @param {string} area - Area key + * @param {Object} categories - Categories within the area + * @returns {string} Full guidelines content + */ + static _generateFullGuidelinesContent(area, categories) { + let content = ''; + + for (const category in categories) { + this._sortGuidelinesByLevel(categories[category]); + + categories[category].forEach(guideline => { + const guidelineUrl = UrlGenerator.generateUrlPath(area, guideline.slug); + const anchorId = UrlGenerator.generateAnchorId(guideline.title, guideline.level); + + content += `### ${guideline.title} (レベル${guideline.level})\n\n`; + content += `${guideline.content}\n\n`; + content += `詳細: [${guideline.title}](${guidelineUrl})\n\n`; + }); + } + + return content; + } + + /** + * Generate simplified guidelines content with categories + * @private + * @param {string} area - Area key + * @param {Object} categories - Categories within the area + * @returns {string} Simplified guidelines content + */ + static _generateSimplifiedGuidelinesContent(area, categories) { + let content = ''; + + for (const category in categories) { + const categoryName = UrlGenerator.getCategoryName(category); + content += `### ${categoryName}\n\n`; + + this._sortGuidelinesByLevel(categories[category]); + + categories[category].forEach(guideline => { + const guidelineUrl = UrlGenerator.generateUrlPath(area, guideline.slug); + const levelText = UrlGenerator.getLevelDescription(guideline.level); + + content += `- [${guideline.title}](${guidelineUrl}) (レベル${guideline.level}: ${levelText})\n`; + }); + + content += '\n'; + } + + return content; + } + + /** + * Sort guidelines by level + * @private + * @param {Array} guidelines - Array of guidelines + */ + static _sortGuidelinesByLevel(guidelines) { + guidelines.sort((a, b) => a.level - b.level); + } + + /** + * Sort pages for consistent ordering + * @private + * @param {Array} pages - Array of page data + * @returns {Array} Sorted pages + */ + static _sortPages(pages) { + const order = { 'introduction.mdx': 0, 'usage.mdx': 1 }; + return pages.sort((a, b) => { + return (order[a.fileName] || 999) - (order[b.fileName] || 999); + }); + } + + /** + * Group guidelines by area and category + * @param {Array} guidelineFiles - Array of parsed guideline files + * @returns {Object} Guidelines grouped by area and category + */ + static groupGuidelines(guidelineFiles) { + const guidelines = {}; + + guidelineFiles.forEach(({ title, area, category, level, content, slug }) => { + if (!guidelines[area]) { + guidelines[area] = {}; + } + + if (!guidelines[area][category]) { + guidelines[area][category] = []; + } + + guidelines[area][category].push({ + title, + level, + content, + slug + }); + }); + + return guidelines; + } +} + +module.exports = ContentGenerator; \ No newline at end of file diff --git a/scripts/llms-generator.js b/scripts/llms-generator.js new file mode 100644 index 0000000..ba899b5 --- /dev/null +++ b/scripts/llms-generator.js @@ -0,0 +1,190 @@ +/** + * Main llms.txt generator class + */ + +const path = require('path'); +const config = require('./config'); +const MdxParser = require('./parsers/mdx-parser'); +const ContentGenerator = require('./generators/content-generator'); +const FileParser = require('./parsers/file-parser'); +const Logger = require('./logger'); + +class LlmsGenerator { + constructor(options = {}) { + this.guidelinesPath = path.resolve(__dirname, config.paths.guidelines); + this.pagesPath = path.resolve(__dirname, config.paths.pages); + this.outputPath = path.resolve(__dirname, config.paths.output); + this.outputFullPath = path.resolve(__dirname, config.paths.outputFull); + + // Initialize logger + this.logger = new Logger({ + verbose: options.verbose || false, + silent: options.silent || false + }); + } + + /** + * Generate both llms.txt and llms-full.txt files + * @returns {Promise} + */ + async generate() { + const timer = this.logger.timer('Generation process'); + + try { + this.logger.info('Generating llms.txt and llms-full.txt files...'); + + // Check dependencies + this._checkDependencies(); + + // Parse all content + const { pages, guidelines } = await this._parseAllContent(); + + // Generate and write simplified version + await this._generateSimplifiedVersion(pages, guidelines); + + // Generate and write full version + await this._generateFullVersion(pages, guidelines); + + timer(); + this.logger.success('Generation completed successfully!'); + } catch (error) { + this.logger.error('Error generating llms files:', error.message); + throw error; + } + } + + /** + * Parse all content from guidelines and pages + * @private + * @returns {Promise} Parsed content object + */ + async _parseAllContent() { + this.logger.info('Parsing content files...'); + const parseTimer = this.logger.timer('Content parsing'); + + try { + // Parse guidelines + console.log('[VERBOSE] Processing guidelines directory...'); + const guidelineFiles = MdxParser.processDirectory( + this.guidelinesPath, + MdxParser.parseGuidelineMdxFile + ); + + // Parse pages for full version + console.log('[VERBOSE] Processing pages for full version...'); + const fullPages = MdxParser.processDirectory( + this.pagesPath, + MdxParser.parsePagesMdxFile, + config.pages.full + ); + + // Parse pages for simplified version + console.log('[VERBOSE] Processing pages for simplified version...'); + const simplifiedPages = MdxParser.processDirectory( + this.pagesPath, + MdxParser.parsePagesMdxFile, + config.pages.simplified + ); + + // Group guidelines by area and category + console.log('[VERBOSE] Grouping guidelines by area and category...'); + const guidelines = ContentGenerator.groupGuidelines(guidelineFiles); + + parseTimer(); + this.logger.info(`Parsed ${guidelineFiles.length} guidelines and ${fullPages.length} pages`); + + return { + pages: { + full: fullPages, + simplified: simplifiedPages + }, + guidelines + }; + } catch (error) { + this.logger.error('Failed to parse content:', error.message); + throw error; + } + } + + /** + * Generate simplified version (llms.txt) + * @private + * @param {Object} pages - Pages object with full/simplified arrays + * @param {Object} guidelines - Guidelines grouped by area/category + * @returns {Promise} + */ + async _generateSimplifiedVersion(pages, guidelines) { + this.logger.info('Generating simplified version (llms.txt)...'); + + try { + const content = [ + ContentGenerator.generateHeader(), + ContentGenerator.generatePagesContent(pages.simplified, false), + ContentGenerator.generateGuidelinesContent(guidelines, false) + ].join(''); + + FileParser.writeFile(this.outputPath, content); + this.logger.success(`Successfully generated ${this.outputPath}`); + } catch (error) { + this.logger.error(`Failed to generate simplified version: ${error.message}`); + throw error; + } + } + + /** + * Generate full version (llms-full.txt) + * @private + * @param {Object} pages - Pages object with full/simplified arrays + * @param {Object} guidelines - Guidelines grouped by area/category + * @returns {Promise} + */ + async _generateFullVersion(pages, guidelines) { + this.logger.info('Generating full version (llms-full.txt)...'); + + try { + const content = [ + ContentGenerator.generateHeader(), + ContentGenerator.generateTableOfContents(pages.full, guidelines), + ContentGenerator.generatePagesContent(pages.full, true), + ContentGenerator.generateGuidelinesContent(guidelines, true) + ].join(''); + + FileParser.writeFile(this.outputFullPath, content); + this.logger.success(`Successfully generated ${this.outputFullPath}`); + } catch (error) { + this.logger.error(`Failed to generate full version: ${error.message}`); + throw error; + } + } + + /** + * Check if required dependencies are available + * @private + */ + _checkDependencies() { + try { + require.resolve('gray-matter'); + this.logger.verbose('Required dependencies are available'); + } catch (e) { + this.logger.info('Installing required dependencies...'); + try { + require('child_process').execSync('npm install --no-save gray-matter', { stdio: 'inherit' }); + this.logger.success('Dependencies installed successfully'); + } catch (installError) { + this.logger.error('Failed to install dependencies:', installError.message); + throw new Error('Could not install required dependencies'); + } + } + } + + /** + * Static method to run the generator + * @returns {Promise} + */ + static async run() { + const generator = new LlmsGenerator(); + await generator.generate(); + } +} + +module.exports = LlmsGenerator; \ No newline at end of file diff --git a/scripts/logger.js b/scripts/logger.js new file mode 100644 index 0000000..0ae9a74 --- /dev/null +++ b/scripts/logger.js @@ -0,0 +1,98 @@ +/** + * Logging utilities for the llms.txt generator + */ + +class Logger { + constructor(options = {}) { + this.verbose = options.verbose || false; + this.silent = options.silent || false; + } + + /** + * Log an info message + * @param {string} message - Message to log + * @param {...any} args - Additional arguments + */ + info(message, ...args) { + if (!this.silent) { + console.log(message, ...args); + } + } + + /** + * Log a verbose message (only shown if verbose mode is enabled) + * @param {string} message - Message to log + * @param {...any} args - Additional arguments + */ + verbose(message, ...args) { + if (this.verbose && !this.silent) { + console.log(`[VERBOSE] ${message}`, ...args); + } + } + + /** + * Log a warning message + * @param {string} message - Message to log + * @param {...any} args - Additional arguments + */ + warn(message, ...args) { + if (!this.silent) { + console.warn(`[WARNING] ${message}`, ...args); + } + } + + /** + * Log an error message + * @param {string} message - Message to log + * @param {...any} args - Additional arguments + */ + error(message, ...args) { + console.error(`[ERROR] ${message}`, ...args); + } + + /** + * Log a success message + * @param {string} message - Message to log + * @param {...any} args - Additional arguments + */ + success(message, ...args) { + if (!this.silent) { + console.log(`✓ ${message}`, ...args); + } + } + + /** + * Log progress information + * @param {string} message - Message to log + * @param {number} current - Current progress + * @param {number} total - Total items + */ + progress(message, current, total) { + if (!this.silent) { + const percentage = Math.round((current / total) * 100); + console.log(`[${current}/${total}] (${percentage}%) ${message}`); + } + } + + /** + * Create a timer for measuring execution time + * @param {string} label - Timer label + * @returns {Function} Function to stop the timer + */ + timer(label) { + const startTime = Date.now(); + const verbose = this.verbose; + const silent = this.silent; + + return () => { + const endTime = Date.now(); + const duration = endTime - startTime; + if (verbose && !silent) { + console.log(`[VERBOSE] ${label} completed in ${duration}ms`); + } + return duration; + }; + } +} + +module.exports = Logger; \ No newline at end of file diff --git a/scripts/parsers/code-block-parser.js b/scripts/parsers/code-block-parser.js new file mode 100644 index 0000000..dcb874a --- /dev/null +++ b/scripts/parsers/code-block-parser.js @@ -0,0 +1,70 @@ +/** + * Code block extraction utilities + */ + +class CodeBlockParser { + /** + * Extract code blocks robustly using string operations + * @param {string} content - Content to parse + * @returns {Array} Array of code block objects + */ + static parseCodeBlocks(content) { + const codeBlocks = []; + let startIndex = 0; + + while (true) { + // Find next code block start + const codeStart = content.indexOf('```', startIndex); + if (codeStart === -1) break; + + // Find the end of the opening ``` + const codeOpenEnd = codeStart + 3; + + // Extract language (if any) - read until newline + const newlineAfterOpen = content.indexOf('\n', codeOpenEnd); + const language = newlineAfterOpen !== -1 + ? content.substring(codeOpenEnd, newlineAfterOpen).trim() + : ''; + + // Find the closing ``` + const codeContentStart = newlineAfterOpen !== -1 ? newlineAfterOpen + 1 : codeOpenEnd; + const codeEnd = content.indexOf('```', codeContentStart); + + if (codeEnd === -1) { + // No closing ```, skip this one + startIndex = codeOpenEnd; + continue; + } + + // Extract code content + const codeContent = content.substring(codeContentStart, codeEnd).trim(); + + if (codeContent.length > 0) { + codeBlocks.push({ + fullMatch: content.substring(codeStart, codeEnd + 3), + language: language || 'html', + content: codeContent, + startIndex: codeStart, + endIndex: codeEnd + 3 + }); + } + + startIndex = codeEnd + 3; + } + + return codeBlocks; + } + + /** + * Format code blocks for markdown output + * @param {Array} codeBlocks - Array of code block objects + * @returns {string} Formatted markdown string + */ + static formatCodeBlocks(codeBlocks) { + return codeBlocks + .map(block => `\`\`\`${block.language}\n${block.content}\n\`\`\``) + .join('\n\n'); + } +} + +module.exports = CodeBlockParser; \ No newline at end of file diff --git a/scripts/parsers/component-parser.js b/scripts/parsers/component-parser.js new file mode 100644 index 0000000..2b7218f --- /dev/null +++ b/scripts/parsers/component-parser.js @@ -0,0 +1,189 @@ +/** + * Astro/MDX component parsing utilities + */ + +const config = require('../config'); +const CodeBlockParser = require('./code-block-parser'); + +class ComponentParser { + /** + * Parse Checkpoint components + * @param {string} content - Content containing Checkpoint components + * @returns {string} Parsed checkpoint content + */ + static parseCheckpoint(content) { + let result = ''; + + // Handle self-closing Checkpoint with title attribute + const selfClosingMatches = content.matchAll(/]*title=["']([^"']+)["'][^>]*\/>/g); + for (const match of selfClosingMatches) { + result += `**チェック項目:** ${match[1]}\n\n`; + } + + // Handle Checkpoint with slot title + const checkpointMatches = content.matchAll(/]*>([\s\S]*?)<\/Checkpoint>/g); + for (const match of checkpointMatches) { + const innerContent = match[1]; + + // Look for slot="title" span + const titleMatch = innerContent.match(/]*slot=["']title["'][^>]*>([\s\S]*?)<\/span>/); + if (titleMatch) { + // Clean up JSX comments and backticks + const title = titleMatch[1] + .replace(config.regex.jsxComments, '') // Remove JSX comments + .replace(config.regex.backticks, '$1') // Remove backticks but keep content + .trim(); + if (title) { + result += `**チェック項目:** ${title}\n\n`; + } + } else { + result += '**チェック項目**\n\n'; + } + } + + return result; + } + + /** + * Parse Cases and Case components + * @param {string} content - Content containing Cases components + * @returns {string} Parsed cases content + */ + static parseCases(content) { + let result = ''; + + const casesMatches = content.matchAll(/([\s\S]*?)<\/Cases>/g); + for (const casesMatch of casesMatches) { + const casesContent = casesMatch[1]; + + // Extract Cases title + const casesTitleMatch = casesContent.match(/]*slot=["']title["'][^>]*>([\s\S]*?)<\/div>/); + if (casesTitleMatch) { + const title = casesTitleMatch[1].replace(config.regex.markdownHeaders, '').trim(); + result += `${title}\n\n`; + } + + // Process individual Case components + result += this._parseCaseComponents(casesContent); + } + + return result; + } + + /** + * Parse individual Case components within Cases + * @private + * @param {string} casesContent - Content within Cases component + * @returns {string} Parsed case content + */ + static _parseCaseComponents(casesContent) { + let result = ''; + + const caseMatches = casesContent.matchAll(/]*>([\s\S]*?)<\/Case>/g); + for (const caseMatch of caseMatches) { + const caseFullMatch = caseMatch[0]; + const caseContent = caseMatch[1]; + + // Extract type and title from Case attributes + const { type, title } = this._extractCaseMetadata(caseFullMatch, caseContent); + + if (type && title) { + const typeLabel = config.typeLabels[type] || type; + result += `${typeLabel}: ${title}\n\n`; + } + + // Extract code blocks from figure slot + result += this._extractCaseCodeBlocks(caseContent); + + // Extract other text content + result += this._extractCaseTextContent(caseContent); + } + + return result; + } + + /** + * Extract metadata from Case component + * @private + * @param {string} caseFullMatch - Full Case component match + * @param {string} caseContent - Case component content + * @returns {Object} Type and title + */ + static _extractCaseMetadata(caseFullMatch, caseContent) { + const typeMatch = caseFullMatch.match(/type=["']([^"']+)["']/); + const attributeTitleMatch = caseFullMatch.match(/title=["']([^"']+)["']/); + const slotTitleMatch = caseContent.match(/]*slot=["']title["'][^>]*>([\s\S]*?)<\/span>/); + + const type = typeMatch ? typeMatch[1] : ''; + const title = attributeTitleMatch ? attributeTitleMatch[1].trim() : + slotTitleMatch ? slotTitleMatch[1].trim() : ''; + + return { type, title }; + } + + /** + * Extract code blocks from Case figure slot + * @private + * @param {string} caseContent - Case component content + * @returns {string} Formatted code blocks + */ + static _extractCaseCodeBlocks(caseContent) { + const figureMatch = caseContent.match(/]*slot=["']figure["'][^>]*>([\s\S]*?)<\/div>/); + if (!figureMatch) return ''; + + const figureContent = figureMatch[1]; + const codeBlocks = CodeBlockParser.parseCodeBlocks(figureContent); + + return codeBlocks.length > 0 ? + CodeBlockParser.formatCodeBlocks(codeBlocks) + '\n\n' : ''; + } + + /** + * Extract text content from Case component + * @private + * @param {string} caseContent - Case component content + * @returns {string} Extracted text content + */ + static _extractCaseTextContent(caseContent) { + const textContent = caseContent + .replace(/]*slot=["']figure["'][^>]*>[\s\S]*?<\/div>/g, '') + .replace(/]*slot=["']title["'][^>]*>[\s\S]*?<\/span>/g, '') + .replace(/<[^>]*slot=["'][^"']*["'][^>]*>[\s\S]*?<\/[^>]*>/g, '') + .replace(/]*>/g, '') + .replace(/```[\s\S]*?```/g, '') + .replace(config.regex.htmlTags, '') + .trim(); + + return textContent ? `${textContent}\n\n` : ''; + } + + /** + * Clean content by removing processed components + * @param {string} content - Original content + * @returns {string} Cleaned content + */ + static cleanProcessedComponents(content) { + return content + // Remove all Checkpoint components (already processed) + .replace(//g, '') + .replace(/]*\/>/g, '') + // Remove all Cases components (already processed) + .replace(/[\s\S]*?<\/Cases>/g, '') + // Remove Level components + .replace(/]*\/>/g, '') + // Remove Details components but keep content + .replace(/]*>([\s\S]*?)<\/Details>/g, '$1') + // Remove remaining HTML tags and components + .replace(config.regex.htmlTags, '') + // Clean up frontmatter references + .replace(config.regex.frontmatterTitle, '') + .replace(config.regex.checkpointHeader, '') + // Remove reference section + .replace(config.regex.referenceSection, '') + // Clean up extra whitespace + .replace(config.regex.multipleNewlines, '\n\n') + .trim(); + } +} + +module.exports = ComponentParser; \ No newline at end of file diff --git a/scripts/parsers/file-parser.js b/scripts/parsers/file-parser.js new file mode 100644 index 0000000..c5a845a --- /dev/null +++ b/scripts/parsers/file-parser.js @@ -0,0 +1,66 @@ +/** + * File system utilities for finding and reading MDX files + */ + +const fs = require('fs'); +const path = require('path'); + +class FileParser { + /** + * Recursively get all files with .mdx extension + * @param {string} dir - Directory to search in + * @returns {string[]} Array of file paths + */ + static getMdxFiles(dir) { + let results = []; + + try { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + results = results.concat(this.getMdxFiles(filePath)); + } else if (path.extname(file) === '.mdx') { + results.push(filePath); + } + } + } catch (error) { + console.warn(`Warning: Could not read directory ${dir}:`, error.message); + } + + return results; + } + + /** + * Read and validate file content + * @param {string} filePath - Path to the file + * @returns {string} File content + * @throws {Error} If file cannot be read + */ + static readFile(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error.message}`); + } + } + + /** + * Write content to file with error handling + * @param {string} filePath - Path to write to + * @param {string} content - Content to write + * @throws {Error} If file cannot be written + */ + static writeFile(filePath, content) { + try { + fs.writeFileSync(filePath, content, 'utf8'); + } catch (error) { + throw new Error(`Failed to write file ${filePath}: ${error.message}`); + } + } +} + +module.exports = FileParser; \ No newline at end of file diff --git a/scripts/parsers/mdx-parser.js b/scripts/parsers/mdx-parser.js new file mode 100644 index 0000000..d1295b5 --- /dev/null +++ b/scripts/parsers/mdx-parser.js @@ -0,0 +1,161 @@ +/** + * MDX file parsing utilities + */ + +const matter = require('gray-matter'); +const path = require('path'); +const config = require('../config'); +const FileParser = require('./file-parser'); +const ComponentParser = require('./component-parser'); + +class MdxParser { + /** + * Parse a guidelines MDX file and extract structured content + * @param {string} filePath - Path to the MDX file + * @returns {Object} Parsed guideline data + */ + static parseGuidelineMdxFile(filePath) { + const fileContent = FileParser.readFile(filePath); + const { data, content } = matter(fileContent); + + // Step 1: Remove imports + let cleanContent = content.replace(config.regex.imports, ''); + + // Step 2: Process components using dedicated functions + const checkpointContent = ComponentParser.parseCheckpoint(cleanContent); + const casesContent = ComponentParser.parseCases(cleanContent); + + // Step 3: Remove all components and HTML tags + cleanContent = ComponentParser.cleanProcessedComponents(cleanContent); + + // Step 4: Combine all content + const finalContent = MdxParser._combineContent(checkpointContent, cleanContent, casesContent); + + return { + title: data.title, + area: data.area, + category: data.category, + level: data.level, + content: finalContent, + slug: data.slug || MdxParser._generateSlugFromTitle(data.title) + }; + } + + /** + * Parse a pages MDX file (simpler structure) + * @param {string} filePath - Path to the MDX file + * @returns {Object} Parsed page data + */ + static parsePagesMdxFile(filePath) { + const fileContent = FileParser.readFile(filePath); + const { data, content } = matter(fileContent); + + // For pages, we use a simpler approach + const cleanContent = MdxParser._cleanPagesContent(content, data); + + return { + title: data.title, + content: cleanContent, + fileName: path.basename(filePath, '.mdx') + }; + } + + /** + * Clean pages content by removing components and processing templates + * @private + * @param {string} content - Raw content + * @param {Object} data - Frontmatter data + * @returns {string} Cleaned content + */ + static _cleanPagesContent(content, data) { + return content + // Remove imports + .replace(config.regex.imports, '') + // Remove component tags but keep content where appropriate + .replace(/]*>([\s\S]*?)<\/PageTitle>/g, '$1') + .replace(/]*>([\s\S]*?)<\/Prose>/g, '$1') + .replace(/]*\/>/g, '') + // Handle Details components - keep content but add summary + .replace(/]*summary=["']([^"']+)["'][^>]*>([\s\S]*?)<\/Details>/g, '**$1**\n\n$2') + .replace(/]*>([\s\S]*?)<\/Details>/g, '$1') + // Remove remaining component tags + .replace(config.regex.htmlTags, '') + // Clean up slot references + .replace(/\{frontmatter\.title\}/g, data.title || '') + // Clean up extra whitespace + .replace(config.regex.multipleNewlines, '\n\n') + .trim(); + } + + /** + * Combine different content sections + * @private + * @param {string} checkpointContent - Processed checkpoint content + * @param {string} cleanContent - Cleaned main content + * @param {string} casesContent - Processed cases content + * @returns {string} Combined content + */ + static _combineContent(checkpointContent, cleanContent, casesContent) { + let finalContent = ''; + + if (checkpointContent) { + finalContent += checkpointContent; + } + + if (cleanContent) { + finalContent += cleanContent + '\n\n'; + } + + if (casesContent) { + finalContent += casesContent; + } + + return finalContent.trim(); + } + + /** + * Generate slug from title + * @private + * @param {string} title - Page title + * @returns {string} Generated slug + */ + static _generateSlugFromTitle(title) { + return title + .toLowerCase() + .replace(config.regex.spaces, '-') + .replace(/[^\w\-]/g, ''); + } + + /** + * Process multiple MDX files in a directory + * @param {string} dirPath - Directory path + * @param {Function} parseFunction - Function to parse individual files + * @param {string[]} [fileFilter] - Optional array of filenames to include + * @returns {Array} Array of parsed file data + */ + static processDirectory(dirPath, parseFunction, fileFilter = null) { + const fullDirPath = path.resolve(__dirname, dirPath); + const mdxFiles = FileParser.getMdxFiles(fullDirPath); + const results = []; + + for (const filePath of mdxFiles) { + const fileName = path.basename(filePath); + + // Apply file filter if provided + if (fileFilter && !fileFilter.includes(fileName)) { + continue; + } + + try { + const parsedData = parseFunction(filePath); + results.push(parsedData); + } catch (error) { + console.error(`Error processing file ${filePath}:`, error.message); + } + } + + return results; + } +} + +module.exports = MdxParser; \ No newline at end of file diff --git a/scripts/parsers/url-generator.js b/scripts/parsers/url-generator.js new file mode 100644 index 0000000..1afdbd7 --- /dev/null +++ b/scripts/parsers/url-generator.js @@ -0,0 +1,74 @@ +/** + * URL and anchor ID generation utilities + */ + +const config = require('../config'); + +class UrlGenerator { + /** + * Generate URL path from area and slug + * @param {string} area - Area (design/impl) + * @param {string} slug - Page slug + * @returns {string} Full URL + */ + static generateUrlPath(area, slug) { + return `${config.baseUrl}/${area}/${slug}`; + } + + /** + * Generate page URL for pages directory files + * @param {string} fileName - File name (e.g., 'introduction.mdx') + * @returns {string} Full URL + */ + static generatePageUrl(fileName) { + const baseName = fileName.replace('.mdx', ''); + return `${config.baseUrl}/${baseName}.html`; + } + + /** + * Generate anchor ID for table of contents + * @param {string} title - Section title + * @param {number} [level] - Optional level for uniqueness + * @returns {string} Anchor ID + */ + static generateAnchorId(title, level) { + const id = title + .toLowerCase() + // Keep Japanese characters, alphanumeric, and spaces only + .replace(config.regex.japaneseChars, '') + .replace(config.regex.spaces, '-') + .replace(config.regex.dashes, '-') + .replace(config.regex.leadingTrailingDashes, ''); + + return id; + } + + /** + * Get level description text + * @param {number} level - Level number (1-3) + * @returns {string} Level description + */ + static getLevelDescription(level) { + return config.levelDescriptions[level] || 'Unknown level'; + } + + /** + * Get area name in Japanese + * @param {string} area - Area key (design/impl) + * @returns {string} Japanese area name + */ + static getAreaName(area) { + return config.areaNames[area] || area; + } + + /** + * Get category name in Japanese + * @param {string} category - Category key + * @returns {string} Japanese category name + */ + static getCategoryName(category) { + return config.categoryNames[category] || category; + } +} + +module.exports = UrlGenerator; \ No newline at end of file