はじめに bunのインストール 作業環境の作成 Prettierのインストール Remarkのインストール パーザのセットアップ RemarkとPrettierの整合性を取る Remarkの一部としてPrettierを動かす Linterのセットアップ ドキュメントのリンク切れを検証する Linter用の設定ファイルを外部化する まとめ はじめに みなさんこんにちは、XI本部エンジニアリングオフィスの佐藤太一です。 Amazon のKiroが話題になったあたりから、AIに Markdown で仕様書を書いてもらうことで成果物の品質や作業品質を高める取り組みが注目されていますね。 Amazon のKiroだけでなく、 GitHub の Spec Kit 、 Claude Code Spec Workflow や Claude Code Spec など様々な活動が公開されています。 このエントリでは、そういった活動の中で少し見過ごされがちな Markdown ファイルそのものの品質を底上げするための取り組みを紹介します。 具体的には、RemarkとPrettierを組み合わせて Markdown ファイルが所定のルールに沿った記述がされているか自動的に保証する方法を説明します。 bunのインストール 今回利用するツールは JavaScript やTypeScriptで記述されています。 これらのツールは、継続的に繰り返し実行するため、少しでも高速に動作することが望ましいので、今回はbunを使って実行します。 Windows 環境であれば以下のコマンドでインストールできます。 powershell -c "irm bun.sh/install.ps1 | iex" Linux や Mac 環境なら以下のコマンドでインストールできます。 curl -fsSL https://bun.sh/install | bash bunは高速に動作するうえに特別な設定をせずにTypeScriptを直接実行できるので、私は日常的な スクリプト もTypeScriptで書いています。 作業環境の作成 今回は筆者の作業環境が Windows なので、それを前提に環境構築を行います。 プラットフォーム依存性のある話は全体としてほぼ存在しませんので、 Linux や Mac を使っている方は適宜読み替えてください。 作業用の ディレクト リとして、 C:/dev/md-tutorial に新しい ディレクト リを作成しました。 今後は、この ディレクト リ内で作業しているものと考えてください。 この ディレクト リに、 bunfig.toml というファイルを以下の内容で作成します。 [install] exact = true これによって、bunでインストールするツールのバージョンが常に固定されたものになります。 Prettierのインストール Prettierは、htmlや JavaScript 、TypeScriptなど最近のフロントエンド開発で使われているテキストファイルのフォーマットを自動的に行う 意見の強い フォーマッタです。 私自身は、全ての ソースコード はフォーマットをすべきであると考えています。 ただし、一貫性さえあればフォーマットのルールについてはあまりこだわりがないので、Prettierを常用しています。 以下のコマンドを実行して、Prettierをbunでインストールしましょう。 bun add prettier 以下のように package.json が作成されます。 { " dependencies ": { " prettier ": " 3.6.2 " } } これにbunからPrettierを実行するためのコマンドを追加します。 { " dependencies ": { " prettier ": " 3.6.2 " } , " scripts ": { " lint:prettier ": " prettier --check docs/ ", " format:prettier ": " prettier --write docs/ " } } 今回はPrettierで検査対象にするファイルは docs/ ディレクト リに格納されるものとしてタスクを定義しました。 では、 docs/ ディレクト リに README.md を以下の内容で作成しましょう。 # Title Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Body Text Prettierの動作確認をしたいので、少し奇妙なファイルです。 タイトルの後ろに無意味な半角スペースがあったり、空行のみの行がスペースでインデントされていたりします。 この状態でPrettierを実行してみましょう。以下のコマンドを実行します。 bun lint:prettier 以下のように出力されました。 ファイルに問題があるようですね。しかし、今回の問題は自動的に対処できるものです。 では、以下のコマンドを実行しましょう。 bun format:prettier 実行結果は以下のように出力されます。 自動的な折り返しはされないようですね。しかし、余分な半角スペースは全て除去されています。 Prettierは基本的に設定の調整が不要なのですが、いくつか私の趣味に合わない部分があるので、そこだけは変えましょう。具体的には、タブと一行の最大幅と改行コードです。 タブは半角スペース2つに変換します。一行の幅はデフォルトの80文字から120文字に増やしましょう。改行コードはLFのみに固定します。 { " dependencies ": { " prettier ": " 3.6.2 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " scripts ": { " lint:prettier ": " prettier --check docs/ ", " format:prettier ": " prettier --write docs/ " } } あわせて、 .gitattributes ファイルを以下の内容で作成します。 text=auto eol=lf Remarkのインストール Remarkは JavaScript で実装された Markdown の処理ツールです。たくさんの プラグイン があり、処理ツールとしての機能を調節できるようになっています。 また、パーズしてASTになった Markdown をファイルに書き出せるような形式に永続化する機能もあります。 パーザのセットアップ まずは、Remarkのパーザ部分をセットアップしてみましょう。以下のコマンドで3つのモジュールをインストールします。 bun add remark-cli remark-frontmatter remark-gfm あわせて、bunから呼び出せるようにコマンドを追加します。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " scripts ": { " lint:md ": " remark --frail docs/ ", " lint:prettier ": " prettier --check docs/ ", " format:md ": " remark docs/ --frail --output ", " format:prettier ": " prettier --write docs/ " } } おっと、Remarkが追加したモジュールを動作するように構成していませんでした。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " plugins ": [ " remark-frontmatter ", " remark-gfm " ] } , " scripts ": { " lint:md ": " remark --frail docs/ ", " lint:prettier ": " prettier --check docs/ ", " format:md ": " remark docs/ --frail --output ", " format:prettier ": " prettier --write docs/ " } } remarkConfigにpluginsというプロパティがあるので、そこに追加したい プラグイン のモジュール名を列挙しています。今回は remark-frontmatter と remark-gfm ですね。 動作を確認していきましょう。とはいえ、既に用意した docs/README.md では何も起きないので新しいファイルを追加します。 docs/list.md というファイルを以下の内容で保存します。 --- author: taichi category: - example - sample --- # Title _foo bar baz_ ## Sub Title - [ ] one - [ ] two - [ ] three foo bar baz 1. first 1. second 1. third ---- frontmatterという YAML で メタデータ を付けた Markdown ですね。Listの部分では、 GitHub 固有の チェックボックス 記法を使っています。 Remarkの動作を確認してみましょう。以下のコマンドを実行します。 bun lint:md 特にエラーなく処理が終了するでしょう。 次は、フォーマットタスクを実行しましょう。以下のコマンドです。 bun format:md docs/list.md を開きなおしてみてください。興味深い変化が起きているはずです。 --- author: taichi category: - example - sample --- # Title *foo bar baz* ## Sub Title * [ ] one * [ ] two * [ ] three foo bar baz 1. first 2. second 3. third *** frontmatter部分に変化はありません。 次のTitleより下のイタリック体になっていた記号が変化しています。フォーマット前は _foo bar baz_ とアンダースコア記号を使っていましたが、フォーマット後は *foo bar baz* と アスタリスク が使われています。 リスト記法の - も * に変化しています。 同様に区切り線として、ハイフンを使っていたので ---- でしたが、フォーマット後は **** になっています。 最後の順序付きリストは、数字が昇順に調整されています。 これがRemarkに内蔵されている remark-stringify の正常な動作です。 Markdown をパーズしてASTにした後、メモリ上の表現を一定のルールで文字列に永続化するわけです。 このままでは、少し都合が悪いので調整しましょう。remarkConfigに settings プロパティを追加します。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm " ] } , " scripts ": { " lint:md ": " remark --frail docs/ ", " lint:prettier ": " prettier --check docs/ ", " format:md ": " remark docs/ --frail --output ", " format:prettier ": " prettier --write docs/ " } } ここでは、 リストに使う記号として - 斜体やボールドなどを強調する際に使う記号として _ 区切り線として使う記号として - を設定しています。 では、もう一度フォーマッタを動かしてみましょう。 bun format:md 処理は成功し以下のように設定に基づいた表現になっているはずです。順序付きリストだけが昇順のままですね。 --- author : taichi category : - example - sample --- # Title _foo bar baz_ ## Sub Title - [ ] one - [ ] two - [ ] three foo bar baz 1. first 2. second 3. third --- これで分かったように、Remarkを Markdown のフォーマッタとして使う場合には、remarkConfigオブジェクトのsettingsプロパティを調整します。その設定項目は、 remark-stringify#options に記載されています。 RemarkとPrettierの整合性を取る Prettierはテキストファイルのフォーマットを整えるツールですが、実はRemarkのLintルールの中には、Prettierの動作と矛盾するものや重複するものが含まれています。 Prettierとうまく組み合わさらないものは、あらかじめ無効にしてしまいましょう。そのためのモジュールを以下のコマンドでインストールします。 bun add remark-preset-prettier モジュールを追加したら、そのモジュール名を プラグイン として追加します。以下のようになるでしょう。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 ", " remark-preset-prettier ": " 2.0.2 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm ", " remark-preset-prettier " ] } , " scripts ": { " lint:md ": " remark --frail docs/ ", " lint:prettier ": " prettier --check docs/ ", " format:md ": " remark docs/ --frail --output ", " format:prettier ": " prettier --write docs/ " } } 次はフォーマットしがいのあるコンテンツを用意しましょう。docs/table.md として以下のようなファイルを作成します。 # Table | Column1 | Label | Description | | ----- | ----- | ----- | | val | aaaaaaa | Lorem ipsum dolor sit amet, consectetur adipiscing elit | | valval | aa | sed do eiusmod tempor incididunt ut labore | | foo | d | Duis aute irure dolor in reprehenderit in voluptate | | xxxxxxx | | deserunt mollit anim id est laborum | Prettierでフォーマットするコマンドは以下のとおりです。 bun format:prettier コマンドが正常に終了したら、どのようにフォーマットされるか確認してみましょう。 # Table | Column1 | Label | Description | | ------- | ------- | ------------------------------------------------------- | | val | aaaaaaa | Lorem ipsum dolor sit amet, consectetur adipiscing elit | | valval | aa | sed do eiusmod tempor incididunt ut labore | | foo | d | Duis aute irure dolor in reprehenderit in voluptate | | xxxxxxx | | deserunt mollit anim id est laborum | フォーマットする前のテーブルは正直言ってHTMLなりエディタの プレビュー機能 なりを使わなければ内容が把握できないものでしたが、Prettierのフォーマットによりテキスト表現だけでも読めるようになりましたね。 Remarkの一部としてPrettierを動かす RemarkによるフォーマットとPrettierによるフォーマットを毎回動かすのはやや面倒ですし、何かの拍子に抜けてしまいそうです。 というわけで、Remarkの処理の最後にPrettierを動かすための プラグイン を導入しましょう。 bun add unified-prettier これまで通り、pluginsに追加ですね。出力用の プラグイン なので remark-preset-prettier よりも後ろに配置します。 { "dependencies": { "prettier": "3.6.2", "remark-cli": "12.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-preset-prettier": "2.0.2", "unified-prettier": "2.0.1" }, "prettier": { "tabWidth": 2, "printWidth": 120, "endOfLine": "lf" }, "remarkConfig": { "settings": { "bullet": "-", "emphasis": "_", "rule": "-" }, "plugins": [ "remark-frontmatter", "remark-gfm", "remark-preset-prettier", "unified-prettier" ] }, "scripts": { "lint:md": "remark --frail docs/", "lint:prettier": "prettier --check docs/", "format": "remark docs/ --frail --output" } } PrettierとRemarkによるフォーマットは統合されたので、コマンド名はシンプルな format に変更しています。 Linterのセットアップ それでは、いよいよLinter機能をRemarkに追加しましょう。 Remarkが公式で提供しているLinterは、 remark-lint/packages に並んでいるのですが、この細かいルールを一つずつ選定していくのは面倒です。複数のルールを束にしたプリセットを公式から提供されていますので、それを使いましょう。 remark-preset-lint-recommended Markdown を使ううえで基本的なルールの集合です。今回はこれを採用します。 remark-preset-lint-consistent ドキュメント内で記法が一貫するように強制するルールの集合です。 それぞれの記法において、ファイル内で最初に登場した記法が一貫して使われているかどうかを検証できます。 今回は新規のプロジェクトを前提にしますので不採用です。 remark-preset-lint-markdown-style-guide Markdown Style Guide という極めて強い意見の Markdown に関するスタイルガイドを採用したルールです。 今回はそれほど多くのドキュメントがあるわけではないので不採用です。 まずは、モジュールをインストールします。 bun add remark-preset-lint-recommended モジュールをインストールしたら プラグイン として追加します。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 ", " remark-preset-lint-recommended ": " 7.0.1 ", " remark-preset-prettier ": " 2.0.2 ", " unified-prettier ": " 2.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm ", " remark-preset-lint-recommended ", " remark-preset-prettier ", " unified-prettier " ] } , " scripts ": { " lint:md ": " remark --frail docs/ ", " lint:prettier ": " prettier --check docs/ ", " format ": " remark docs/ --frail --output " } } Remarkの プラグイン システムは、配列の後ろ側になるほどルールとして優先されますので、今回追加するプリセットはちょうど中ほどに入るように追加します。 lintのルールを追加したのですから、エラーが発生するような Markdown を作りましょう。docs/link.md を以下の内容で作成します。 # Links https : //example.com リンク記法が使われずにリンクのようなものがテキストとして記述されていますので、これは警告されるはずです。 Linterを実行するコマンドは以下のとおりです。 bun lint : md 実行すると確かに警告が出力され、プロセスがエラーで終了します。 リンク記法を使うべきであるという警告が出力されていますね。これが自動的に改善されるかどうか確認してみましょう。 bun format Linterを実行したときと同じエラーが出力されていますね。 では、 Markdown ファイルはどうなっているのでしょうか。 # Links < https://example.com > リンクが <> でくくられていますね。自動修正は機能しているようです。ただ、AIを使った開発においては、この出力だとAIが標準出力されたメッセージを根拠に解決を試みます。 つまり、当該ファイルを開くものの実際には修正済みという状態になってしまいますので、一定の混乱が発生します。 通信した トーク ン量で課金されたり、制限される今のAI相手にはこういう無駄が起きないようにしなければなりません。 この問題に対応するために設定を調整してみましょう。pluginsプロパティにモジュールを列挙すると常に使われてしまうので、Linterを動かすときとフォーマッタを動かすときで使うモジュールを切り替える必要があります。 remarkコマンドの -u オプションを使うと個別にモジュールを指定できます。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 ", " remark-preset-lint-recommended ": " 7.0.1 ", " remark-preset-prettier ": " 2.0.2 ", " unified-prettier ": " 2.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm " ] } , " scripts ": { " lint:md ": " remark --frail docs/ -u remark-preset-lint-recommended -u remark-preset-prettier ", " lint:prettier ": " prettier --check docs/ ", " format ": " remark docs/ --frail --output -u remark-preset-prettier -u unified-prettier " } } この設定では、Linterとフォーマッタで共通して使う部分はremarkConfig以下に定義し、違う部分は各コマンドの引数として定義しています。 それでは、確認のため、補正済みのdocs/link.md を補正前に戻したうえで、Linterを動かしてみましょう。 エラーが出力されていますね。 では、フォーマッタを動かしてみましょう。 エラーや警告が出力されないので、プロセスは正常に終了します。 そして、 Markdown は自動補正されて正しくフォーマットされていますね。 # Links < https://example.com > ドキュメントのリンク切れを検証する 設計ドキュメントとして Markdown を使うということは、役割や責任範囲毎にファイルを分割していき、それらを ハイパーリンク で接続するという使い方が想定されます。 そうしたとき問題になるのが、ドキュメント間のリンク切れ問題です。ファイル名やファイルパスを変更することによって、リンク切れは頻繁に発生します。これをきちんとメンテナンスしていくのは、非常に面倒なタスクですよね。 そこで、Linterの一部としてリンク切れをチェックしエラーを標準出力することで、リンクのメンテナンスを生成AIに任せましょう。 Remarkにはリンク切れを検証する プラグイン がいくつかあるので、それらをまとめてインストールしましょう。 bun add remark-validate-links remark-lint-no-dead-urls remark-validate-links が同じ ディレクト リツリー内にあるファイルへのリンクを検証する プラグイン です。それに対して、 remark-lint-no-dead-urls はリンクとして記載されているURLにHTTPリク エス トを送信して機能しているか検証する プラグイン です。 これをLinter用のコマンドに組み込むとこうなります。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 ", " remark-lint-no-dead-urls ": " 2.0.1 ", " remark-preset-lint-recommended ": " 7.0.1 ", " remark-preset-prettier ": " 2.0.2 ", " remark-validate-links ": " 13.1.0 ", " unified-prettier ": " 2.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " remarkConfig ": { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm " ] } , " scripts ": { " lint:md ": " remark --frail docs/ -u remark-preset-lint-recommended -u remark-preset-prettier -u remark-validate-links=repository:false -u remark-lint-no-dead-urls=deadOrAliveOptions:{timeout:10000} ", " lint:prettier ": " prettier --check docs/ ", " format ": " remark docs/ --frail --output -u remark-preset-prettier -u unified-prettier " } } Linter用のコマンドが長くなり始めていますね。必要な所だけ抜粋して説明します。 内部リンクを検証する プラグイン の設定部分がこれです。 -u remark-validate-links=repository:false remarkコマンドのオプションとして、モジュール毎の設定を記述するには、モジュール名の最後に = を付けたうえで、最外周の {} がないJSON5を記述します。 remark-validate-linksは、検証対象がgit リポジトリ でかつoriginが設定されていることを前提に動作するので、その機能を無効化するために repository:false を設定しています。 普段ならこの設定は不要でしょう。 外部リンクを検証する プラグイン の設定部分がこれです。 -u remark-lint-no-dead-urls=deadOrAliveOptions:{timeout:10000} remark-lint-no-dead-urls は dead-or-alive というライブラリを内部的に使っています。その タイムアウト オプションをここでは設定しています。 これらの プラグイン が機能するか確認するために、先ほどの docs/links.md に手を加えて内部リンクと外部リンクを追加してみましょう。 # Links < https://example.com > ## Internal Links * [ Exists ]( #Links ) * [ NotExists ]( #likns ) ## External Links * [ External Example ]( https://example.com ) * [ NotExists External ]( https://example.com/fail ) では、リンク切れを検証してみましょう。以下のコマンドを実行します。 bun lint:md 実行結果はエラー終了になります。 タイポしているページ内のリンクと、存在しない外部ページに対するリンクが警告やエラーとして検出されていますね。 Linter用の設定ファイルを外部化する CLI オプションとしてJSON5を記述するというのは、分かり辛いので設定ファイルを外部化しましょう。 それではフォーマッタの設定から分離します。設定ファイルを config/remark-format.json に作成してください。 { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm ", " remark-preset-prettier ", " unified-prettier " ] } フォーマッタの設定はパーザとシ リアラ イザだけなのでシンプルですね。 次にLinterの設定ファイルを分離します。 config/remark-lint.json に作成します。 { " settings ": { " bullet ": " - ", " emphasis ": " _ ", " rule ": " - " } , " plugins ": [ " remark-frontmatter ", " remark-gfm ", " remark-preset-lint-recommended ", " remark-preset-prettier ", [ " remark-validate-links ", { " repository ": false } ] , [ " remark-lint-no-dead-urls ", { " deadOrAliveOptions ": { " timeout ": 10000 } } ] ] } このエントリではやりませんが、Linter側の設定はまだかなり調整の余地があるでしょう。 最後にpackage. json から不要になった設定を削除して、コマンドを調整します。 { " dependencies ": { " prettier ": " 3.6.2 ", " remark-cli ": " 12.0.1 ", " remark-frontmatter ": " 5.0.0 ", " remark-gfm ": " 4.0.1 ", " remark-lint-no-dead-urls ": " 2.0.1 ", " remark-preset-lint-recommended ": " 7.0.1 ", " remark-preset-prettier ": " 2.0.2 ", " remark-validate-links ": " 13.1.0 ", " unified-prettier ": " 2.0.1 " } , " prettier ": { " tabWidth ": 2 , " printWidth ": 120 , " endOfLine ": " lf " } , " scripts ": { " format ": " remark docs/ --frail --output -r config/remark-format.json ", " lint ": " remark --frail docs/ -r config/remark-lint.json " } } Prettierを直接利用するケースは基本的にありませんので、あわせてコマンドをシンプルにしておきました。 まとめ 今回のエントリでは、順を追ってPrettierの導入からRemarkによるフォーマット、その後Linterを導入し、最後は設定ファイルを切り出して調整しやすい状態にしました。 ここで紹介した手法を使えば、生成AIが出力する大量のテキストファイルのファイルフォーマットや構成を一定の水準に保てるようになります。 生成AIは出力した文章の中身について理解しておらず、それっぽいものを出力するのみですが、こうやって自動的に対処できる部分については、AIに対処してもらうことで私たちはより意味のあるレビューを行えるようになるはずです。 このエントリを読んだ皆さんが、より高品質なドキュメントを元にソフトウェア開発が実施できるようになることを願っています。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sato.taichi レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )