コンテンツにスキップ

ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を content/figures/ に置いて画像の記法で指し、ビルドのときにインラインにし、キャンバスには図の名前を渡す

Status

承認

Context

運営者がキャンバスで描いた図を、記事に埋め込む。
図をリポジトリに保存する形、記事への置き方、訪問者が記事の図をキャンバスで開く方法、図に入る第三者のアイコンの扱いを決める必要がある。
運営者がキャンバスから書き出した SVG を置くディレクトリ、記事の Markdown から図を指す書き方、キャンバスが図を読む URL、キャンバスに渡す識別子の形も決める必要がある。

Decision

1. リポジトリに置く形

運営者は、キャンバスから書き出した元データ入りの SVG を、リポジトリに置く。
図のファイルと SVG を別に持たず、SVG の 1 枚だけを置く。

  • ビルドにブラウザが要らない。Mermaid の図と同じ理由である。
  • 運営者は、置いた SVG をキャンバスで開いて、描き直せる。

2. 図を置くディレクトリ

運営者は、キャンバスから書き出した SVG を、apps/web/src/content/figures/ に置く。

  • ファイルの名前は、<名前>.drawio.svg か <名前>.excalidraw.svg にする。拡張子で、描いたツールを表す。
  • <名前> は、小文字の英数字とハイフンだけにする(^[a-z0-9][a-z0-9-]*$)。ディレクトリの入れ子は作らない。
  • ビルドは、SVG の中の元データが拡張子のツールと合うかを確認し、合わないときは失敗する。

content/ の直下の 1 つのディレクトリにする理由は、MIT License の対象から外す記述が 1 つのパスで済むことと、1 つの図を複数の記事で使えることである。

3. 記事の Markdown から図を指す書き方

運営者は、Markdown の画像の記法で、図のファイルを相対パスで指す。

![VPC の構成](../figures/vpc.drawio.svg "東京リージョンの VPC")
  • ビルドは、content/figures/ のファイルを指す画像を、インラインの SVG に置き換える。そのほかの画像は変えない。
  • 代替テキストは必須にする。空のときと、指したファイルが無いときは、ビルドが失敗する。
  • 画像の記法のタイトル(引用符の中の文)は、図のキャプションにする。
  • 全ての図の下に、「キャンバスで開く」のリンクを出す。

画像の記法にする理由は、.md のまま書けること、エディタのプレビューと GitHub で図が見えること、代替テキストとキャプションを書く場所が記法にあることである。

4. 記事への置き方

ビルドのときに、SVG を記事の HTML にインラインで入れる。
ページに、図を描くための JavaScript を配らない。

  • draw.io の SVG は、ルートの color-scheme の指定を外し、背景と線の色の変数(--ge-dark-color、--ge-light-color)にトークンを渡す。
  • Excalidraw の SVG は、線と文字のデフォルトの色(#1e1e1e)を、トークンに置き換える。
  • 運営者が選んだ色は、置き換えない。

インラインにする理由は、図の背景と線の色が、ページのトークンと揃うことである。

5. 図の URL

apps/web のビルドは、置いた SVG を、変えずに https://fukuemon.dev/figures/<名前>.<ツール>.svg に出力する。
fukuemon.dev は、/figures/* に、play.fukuemon.dev を許す Access-Control-Allow-Origin を返す。

変えずに出力する理由は、キャンバスのツールが、運営者の書き出した元データを、そのまま読めるためである。

サイトの URL に、次の 1 つを足す。ADR-0030 の Decision 2 のほかの URL は変えない。

URL 中身
/figures/<名前>.<ツール>.svg 記事の図の、元データ入りの SVG。play.fukuemon.dev のキャンバスのツールが読む

6. 訪問者が記事の図をキャンバスで開く方法

記事の図に「キャンバスで開く」のリンクを置き、キャンバスに図の名前だけを渡す。

https://play.fukuemon.dev/drawio/?figure=vpc
https://play.fukuemon.dev/excalidraw/?figure=vpc
  • キャンバスのツールが、名前から図の URL(https://fukuemon.dev/figures/<名前>.<自分のツール>.svg)を組み立てて、fetch する。
  • キャンバスは、fukuemon.dev の図のパスだけを読み込む。任意の URL は読み込まない。
  • キャンバスのツールは、名前が Decision 2 の規則に合わないときは、fetch しない。
  • キャンバスのツールは、リダイレクトを辿らずに fetch する。
  • 運営者が置いた図だけを開けるので、ADR-0031 の Decision 4 の「運営者が用意した初期ファイル」と同じ扱いにする。Decision 4 は決め直さない。

名前だけを渡す理由は、URL の値で、/figures/ のほかのパスと、ほかの origin を指せなくなるためである。
ツールは URL のパスで決まるので、図を、描いたツールと別のツールに渡すリンクは作れない。

7. 図の名前の規則と URL の形を置く package

図の名前の規則と、ファイルの名前と URL のパスの形は、新しい package(packages/figures、@fukuemon/figures)に置く。

  • package は、次の 6 つを持つ。
    • 図を描いたツールの型(drawio、excalidraw)
    • 名前が Decision 2 の規則に合うかを返す関数
    • 名前とツールから、ファイルの名前(<名前>.<ツール>.svg)と、URL のパス(/figures/<名前>.<ツール>.svg)を組み立てる関数
    • ファイルの名前を、名前とツールに分ける関数
    • 名前とツールから、「キャンバスで開く」のリンクのパスとクエリ(/<ツール>/?figure=<名前>)を組み立てる関数
    • クエリから名前を読む関数。クエリのキー(figure)は、値としても公開する
  • package は、SVG を読む処理、fetch、2 つのアプリの origin の値を持たない。
  • apps/web のビルドは、この package で、ファイルの名前を検証し、出力するパスと、「キャンバスで開く」のリンクを組み立てる。apps/playground は、同じ package で、クエリから figure の値を読んで検証し、fetch する URL のパスを組み立てる。
  • package は、workspace のほかの package にも、React にも依存しない。

package にする理由は、図を出力する側(apps/web)と、読む側(apps/playground)の 2 つが、同じ規則と同じパスの形を使うためである。

8. 第三者のアイコンと書体が入った図

図のファイルを置くディレクトリは、Decision 2 の apps/web/src/content/figures/ だけである。
LICENSE か README に、そのディレクトリの図に入っている第三者のアイコンを、MIT License の対象から外すと書く。
アイコンの利用条件の原文は、運営者が確認する。

Excalidraw の SVG に入る書体(Excalifont、Xiaolai。OFL-1.1)も、同じ記述で、MIT License の対象から外す。

Verification

端末、ブラウザ、バージョンは、ADR-0039 の Verification と同じである。
エディタから書き出した SVG を、記事の形のページに 7 通りの置き方で並べ、JavaScript を無効にして、3 つのブラウザで見た。

置き方 ライト OS がダーク data-theme="dark"
draw.io を <img> で置く 読める 読める 読める
draw.io をインラインで置く 読める 読める 読めない
draw.io をインラインで置き、color-scheme の指定を外して、トークンを渡す 読める 読める 読める
Excalidraw を <img> で置く(ライトの 1 枚) 読める 読めない 読めない
Excalidraw を <picture> で置く(ライトとダークの 2 枚) 読める 読める 読めない
Excalidraw をインラインで置き、線と文字のデフォルトの色をトークンに置き換える 読める 読める 読める
Excalidraw をインラインで置き、ダークで色を反転する(CSS の filter) 読める 読める 読める
  • draw.io を <img> で置いた行の data-theme="dark" は、Chromium と Firefox で見た。WebKit の画面は見ていない。
  • draw.io の SVG は、色を light-dark() で持ち、明暗の両方の色を持つ。大きさは 15.6 KB(図形 4 つ、線 3 本)で、アイコンはパスで入る。
  • Excalidraw の SVG は、ライトの色だけを持つ。大きさは 26.2 KB(図形 3 つ、矢印 2 本、文字 1 つ)で、使った文字の書体が data URI で入る。
  • 記事のページのリクエストは、ページと図の 3〜4 件だけで、キャンバスのコードは読み込まれなかった。
  • キャンバスのページに ?src=<URL> を渡し、別の origin に置いた元データ入りの SVG を fetch で読んで開いた。draw.io と Excalidraw の両方で、図が開いた(Chromium)。図を返す側に、Access-Control-Allow-Origin のヘッダが要った。
  • draw.io で、?src= で開いただけでは、IndexedDB に保存してあった図は変わらなかった。開いた図を編集すると、保存してあった図が、開いた図に上書きされた(Chromium)。試作は、図を 1 つだけ保存する作りである。
  • Excalidraw の scene から、Node だけで SVG を作る形は、1 回試して失敗した(ERR_IMPORT_ATTRIBUTE_MISSING)。動く条件は未確認である。

Considered Options

選択肢 強み 弱み
採用: 元データ入りの SVG を置き、ビルドのときにインラインにして、色をトークンに渡す ビルドにブラウザが要らない。JavaScript なしで読める。図の背景と線の色が、ページのトークンと揃う ビルドに、SVG の文字列を置き換える処理が要る。Excalidraw の、運営者が選んだ色は、ダークで変わらない
インラインにし、Excalidraw はダークで色を反転する Excalidraw の、運営者が選んだ色も、ダークで読める色になる 反転した色は、運営者が選んだ色と違う色になる
<img> で置く。Excalidraw は <picture> で 2 枚置く ビルドの処理が要らない。HTML が小さい 図の背景の色が、ページのトークンと揃わない。Excalidraw は、運営者がダークの SVG をもう 1 枚書き出す
図のファイルを置き、ビルドのときに SVG にする リポジトリに置くファイルが、図の元データだけになる draw.io の図を、ブラウザを使わずに SVG にする手段を確認していない。Excalidraw は、Node だけで SVG を作る形が動かなかった
キャンバスが、任意の URL の図を読み込む fukuemon.dev のほかに置いた図も開ける 他人が用意した図を、URL を開かせるだけで訪問者のキャンバスに読み込める。ADR-0031 の Decision 4 を決め直すことになる
訪問者が記事の図を開く機能を、最初の公開に入れない 作るものが少ない 訪問者が、記事の図を描き変えて試せない
第三者のアイコンが入った図を、何も書かずに置く 文書を直さない アイコンが、MIT License の対象に見える
クラウドのアイコンを使う図を、記事に埋め込まない リポジトリに、第三者のアイコンが入らない 構成図を記事で見せられない

図を置くディレクトリの選択肢は次のとおり。

選択肢 強み 弱み
採用: apps/web/src/content/figures/ に置く スライドの Markdown と同じ並びになる。MIT License の対象から外す記述が 1 つのパスで済む。図を複数の記事で使える 図と記事が、別のディレクトリになる。図の名前を、ツールごとに、サイトの全体で重ならないようにする
apps/web/public/figures/ に置く ビルドが、図を出力する処理を持たずに済む 運営者が書く原稿が、content/ と public/ に分かれる
記事のディレクトリの下(articles/<記事>/figures/) 図が、記事と同じ場所にまとまる。実行環境のプロジェクトと同じ並びになる 図を置くディレクトリを 1 つにする決定(Decision 2)と合わない

Markdown の書き方の選択肢は次のとおり。

選択肢 強み 弱み
採用: 画像の記法で、図のファイルを相対パスで指す .md のまま書ける。プレビューと GitHub で図が見える。代替テキストとキャプションを書ける シリーズの中の記事は、../../figures/ と書く。ビルドに、図を指す画像を置き換える処理が要る
コードフェンス(figure)に、図の名前を書く Mermaid の図と同じ形になる 代替テキストとキャプションを書く場所を、別に決める。プレビューで図が見えない
ディレクティブ(::figure{name="…" alt="…"})で書く 属性を足しやすい ディレクティブのプラグインが要る。プレビューで図が見えない
MDX のコンポーネント(<Figure name="…" alt="…" />)で書く 属性を型で検証できる .mdx の記事でしか使えない

キャンバスに渡す識別子の選択肢は次のとおり。

選択肢 強み 弱み
採用: 図の名前だけを渡す(?figure=vpc) 埋め込みの ID(ADR-0049)と同じ形になる。ほかのパスと、ほかの origin を指せない。拡張子と、ツールのページが合わないリンクを作れない キャンバスのツールのコードが、fukuemon.dev の図のパスの形に依存する。パスの形を、2 つのアプリが使う package に置く(Decision 7)
図のパスを渡す(?figure=/figures/vpc.drawio.svg) キャンバスのツールが、パスの組み立てを持たずに済む キャンバスのツールが、パスの先頭、..、拡張子とツールの一致を検証する。検証の抜けが、ほかのパスの読み込みになる

図の名前の規則と URL の形を置く場所の選択肢は次のとおり。

選択肢 強み 弱み
採用: 依存を持たない新しい package に置く 規則とパスの形が 1 か所になる。プロジェクトの JSON の形を @fukuemon/playground-project に置いた ADR-0049 と、同じ分け方になる package が 1 つ増える
apps/web と apps/playground の 2 か所に、同じ形を書く package を増やさずに済む 片方だけを直すと、ビルドが通ったまま、「キャンバスで開く」のリンクの先で、図を読めなくなる

Consequences

Positive

  • 運営者は、キャンバスで描いた図を、SVG の 1 枚を置くだけで記事に埋め込める。
  • 訪問者は、JavaScript を無効にしても、記事の図を読める。図は、ライトとダークのどちらでも読める。
  • キャンバスの読み込みが、記事の初回の表示を遅らせない。
  • 訪問者は、記事の図をキャンバスで開いて、描き変えられる。
  • 運営者は、SVG の 1 枚を content/figures/ に置き、画像の記法の 1 行を書くだけで、図を記事に埋め込める。
  • 運営者は、ファイルの名前で、図を描いたツールを見分けられる。
  • 他人が、自分の用意した図を読み込ませる URL を作れない。

Negative

  • インラインにした SVG の分だけ、記事の HTML が大きくなる。
  • ビルドの置き換えは、draw.io と Excalidraw が書き出す SVG の形に依る。エディタのバージョンが上がったときに、置き換えを確認し直す。
  • Excalidraw の図で、運営者が選んだ色は、ダークでもライトの色のままである。運営者は、両方の背景で読める色を選ぶ。
  • Excalidraw の SVG には、OFL-1.1 の書体の一部が入る。
  • 第三者のアイコンの利用条件を、運営者が確認するまで、アイコンの入った図を公開のリポジトリに置けるかが決まらない。
  • サイトの URL と、Access-Control-Allow-Origin を返すパスが、1 つずつ増える。
  • 図の名前を変えると、その図を指す記事の Markdown と、訪問者が保存した「キャンバスで開く」のリンクが、古い名前を指す。
  • workspace の package が、1 つ増える。
  • 図を描いたツールの型(package)と、保存する図のツールの種類(apps/playground)が、同じ文字列を別々に持つ。ツールのページが、同じ文字列を両方に渡す。
  • SVG の中の元データが、どのツールのものかを見分ける処理は、apps/web のビルドと、apps/playground のツールのページの 2 か所が持つ。2 か所が別々に持つ理由は、ビルドは見分けるだけで、ツールのページは元データを取り出すまで行うためである。
  • ビルドに、画像の記法を置き換える処理と、拡張子と元データの一致を確認する処理が増える。
  • 1 つの記事に複数の図をインラインで入れたときに、SVG の中の id が重なるかは、確認していない。