Old-School Front-End / 2026 ・ No.12

いにしえのフロントエンド入門

第12回 承認の流れ ── ダイアログで確かめて、ボタン一つで状態を進める

公開

古井 一郎(承認者)の承認の画面。承認待ちの表の上に「申請を確認する EXP-2026-0012」のダイアログが開き、後ろは暗くなっている。ダイアログには申請者 新井 太郎、申請日 2026-10-17、件名 ニューヨーク出張(10月13日〜16日)、金額 46,851 円。コメント欄に「ニューヨーク出張、内容を確認しました。」と入力済みで、下に閉じる・差し戻す・承認するのボタン
今回のゴール。<dialog> で申請の中身を確かめ、コメントを添えて承認する(筆者のデモページを、練習用のサーバー経由でクラウド上の Chromium で撮影。Playwright で実際に入力・クリックしたところ。2026年10月17日。アカウントはデモ専用の架空のもの。ブラウザの枠は説明のために描き足したもの)

AI に頼まず、一行ずつ手で書く「いにしえの」フロントエンド入門。第12回は承認の流れです。
前回、古井さんの承認の画面に、新井さんのニューヨーク出張が並びました。でも押すボタンがありません。
今回は <dialog> で申請の中身を確かめる窓を作り、承認・差し戻しを JSON でサーバーへ送ります。
そして、二人の承認者が同じ申請を同時に押したらどうなるかを、実際に起こして確かめます。

一

今回のゴール

新井先輩、ニューヨーク出張の承認、まだですか? 一覧には出てるんですよね?

古井出てはいる。だが押すボタンがない。今日はそのボタンを作る。

新井「承認」ボタンを一つ置くだけですよね。AI なら3秒です。

古井置くのは3秒。困るのは、押したあとだ。押し間違えたら? 差し戻すなら理由は? 石田さんも同じ申請を押したら?

新井……石田さんって、だれでしたっけ。

古井今日から来た、もう一人の承認者だ。

いまここ:第12回

1 準備 2 HTML の骨組み 3 フォーム 4 CSS の基本 5 レイアウト 6 Bootstrap 7 JS の基本 8 DOM・イベント 9 入力チェック 10 通信 11 ログイン 12 承認の流れ 13 集計と印刷 14 jQuery 15 仕上げ いまここ 準備 HTML CSS JavaScript 実戦 仕上げ
  • 確かめてから押す ── 一覧の「確認する」で <dialog> を開き、申請の中身を見てから「承認する」か「差し戻す」
  • 差し戻しには理由 ── コメントが空なら送らない。サーバーでも同じことを確かめる
  • 同時に押されても壊れない ── 読んだときの「版」を添えて送り、先を越されていたらサーバーが 409 で断る
  • 申請者には履歴 ── だれが・いつ・なぜ差し戻したのかを、新井さんの一覧から開ける

相手は前回の練習用サーバーに、承認の API と履歴を足したものです。承認者は古井 一郎さんに、石田 美咲さん([email protected] / demo-ishida)が加わりました。どちらもデモ専用の架空のアカウントです。差し戻された申請を直して出し直す流れは、今回は省きます。

二

申請の状態と矢印

ボタンを作る前に、申請がどんな状態を通るのかを地図にします。申請のデータには、これまでも status(状態)がありました。下書き・申請中・承認済み・精算済み・差し戻し。今回作るのは、そのうち申請中から出る2本の矢印です。

状態は箱、操作は矢印。今回作るのは赤い2本

下書き draft 申請中 submitted 承認済み approved 精算済み paid 申請する 新井 承認する 古井 精算する 経理 差し戻し returned 差し戻す 古井・理由は必須 直して出し直す (今回は省略) ボタンを出すのは、いまの状態から行ける矢印だけ(申請中のものにだけ「確認する」) 矢印をたどってよいかを決めるのはサーバー(いまの状態と、だれが押したか)

この地図から、画面のきまりが2つ出てきます。

  • ボタンは、いまの状態から出ている矢印だけ ── 承認待ちの一覧に並ぶのは申請中のものだけなので、どの行にも「確認する」を付けてよい。承認済みの申請に「承認する」は出さない
  • 矢印を通してよいかは、サーバーが決める ── 画面にボタンがなくても、fetch は打てる(第11回の「隠すのと断るのは別」)。サーバーは「いまの状態」と「押した人」を見て、通すか断るかを決める

新井精算済みから「やっぱり差し戻し」はできないんですね。

古井お金が出たあとで差し戻されたら、経理が泣く。矢印がない、とはそういうことだ。

三

<dialog> で確かめる窓

「確認する」を押したら、画面の上に小さな窓を開いて、申請の中身とコメント欄とボタンを見せます。こういう窓をモーダルと呼びます。昔は div を重ねて自作したものですが、いまは HTML に <dialog> という専用の要素があります。

<dialog id="decide" aria-labelledby="decide-h">
  <h2 id="decide-h">申請を確認する <span id="d-no"></span></h2>
  <dl>
    <dt>申請者</dt><dd id="d-who"></dd>
    <dt>申請日</dt><dd id="d-date"></dd>
    <dt>件名</dt><dd id="d-title"></dd>
    <dt>金額</dt><dd id="d-amount"></dd>
  </dl>
  <label for="decide-comment">コメント(差し戻すときは必須)</label>
  <textarea id="decide-comment" rows="3"></textarea>
  <p id="decide-error" role="alert" hidden></p>
  <div>
    <button type="button" id="d-close">閉じる</button>
    <button type="button" id="d-return">差し戻す</button>
    <button type="button" id="d-approve">承認する</button>
  </div>
</dialog>

中身は、見出し・説明リスト(dl)・label と textarea・ボタン。どれも、ふつうの HTML です。ボタンはどれもフォームを送らないので type="button"。エラーの欄は第9回と同じく role="alert" にしておき、出たら読み上げられるようにします。

開くのは JavaScript です。一覧のボタンは行ごとに作り直されるので、第8回のイベントの委任で、tbody がまとめてクリックを受けます。

// ボタンは tbody で受ける(第8回のイベントの委任)
document.querySelector('#approvals').addEventListener('click', (event) => {
  const button = event.target.closest('button[data-id]');
  if (button) openDecision(claims.find((x) => x.id === button.dataset.id));
});

/** ダイアログに申請を入れて開く */
function openDecision(claim) {
  current = claim;
  document.querySelector('#d-no').textContent = claim.id;
  document.querySelector('#d-who').textContent = claim.ownerName;
  document.querySelector('#d-date').textContent = claim.date;
  document.querySelector('#d-title').textContent = claim.title;
  document.querySelector('#d-amount').textContent = formatYen(claim.amount);
  comment.value = '';
  decideError.hidden = true;
  dialog.showModal(); // showModal:後ろの画面は操作できなくなる
}

ダイアログの開き方は2つあります。show() はただ表示するだけ。showModal() はモーダルとして開き、ブラウザがいくつもの面倒を引き受けてくれます。

showModal() を呼ぶだけで、ブラウザがやってくれる4つのこと

1 後ろは触れない 暗くなり、クリックも Tab も届かない 2 フォーカスは中へ 最初の入力欄(コメント欄)が選ばれる 3 Esc で閉じる 閉じるボタンは自分で作る(close()) 4 閉じたら元のボタンへ キーボードの人が迷子にならない ここまで全部、showModal() を呼ぶだけでブラウザがやってくれる(実測は四で) 自作のモーダル(div を重ねる)では、この4つを全部自分で書くことになる

暗くするのは CSS の ::backdrop です。見た目は Bootstrap に合わせて、枠を消し、角を丸め、影を付けただけです。

.app-dialog {
  width: min(32rem, calc(100% - 2rem));
  border: 0;
  border-radius: .5rem;
  padding: 1.5rem;
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, .25);
}
.app-dialog::backdrop {  /* 後ろの画面を暗くする幕 */
  background: rgba(20, 30, 40, .45);
}
四

ダイアログを試す

「やってくれる」と書きましたが、本当でしょうか。Chromium で開いて、キーボードとマウスの代わりに Playwright で突いてみました。

# <dialog> の振る舞い(Chromium で実測)
したこと 結果
showModal() で開く フォーカスはコメント欄(textarea)へ
ログアウトのボタンの位置を調べる 当たるのは DIALOG。後ろの画面には触れない
Esc キーを押す ダイアログが閉じた(open = false)
閉じたあとのフォーカス 開いたボタン(EXP-2026-0007 の「確認する」)へ戻った
理由なしで「差し戻す」 「差し戻すときは、理由をコメントに書いてください。」
  • 開いた瞬間、フォーカスはコメント欄へ。すぐに書き始められる
  • ヘッダーの「ログアウト」の位置を document.elementFromPoint() で調べると、当たったのはダイアログ(の後ろの幕)。後ろのボタンはクリックできない
  • Esc で閉じ、フォーカスは開いたときの「確認する」(0007 の行)に戻った。キーボードで操作している人が、ページの先頭まで飛ばされない
POINT

div を重ねる自作モーダルでは、この「後ろを触らせない」「フォーカスを中へ」「Esc で閉じる」「元へ戻す」を、全部自分で書くことになります。いにしえの人々は、これで何度も泣きました。ブラウザに専用の部品があるなら、まずそれを使う。手で書くのは、部品にないところだけです。

五

承認・差し戻しを送る

「承認する」「差し戻す」を押したら、サーバーに伝えます。送り先は POST /api/expenses/EXP-2026-0012/decision。中身は、決めたこと・コメント・読んだときの版(六で使います)の3つです。

第10回では、申請フォームを FormData で送りました。今回はフォームではなく、こちらで組み立てた小さなデータなので、JSON にして送ります。ここに一つ、落とし穴があります。

FormData と JSON では、Content-Type の扱いが逆

new FormData(form) 第10回 自動 Content-Type: multipart/form-data; boundary=… 区切りの印も自動。自分で書くと壊れる(第10回) JSON.stringify({…}) 今回 自動 Content-Type: text/plain;charset=UTF-8 ただの文字として送られる。サーバーは 415 で断った(実測) JSON を送るときは、Content-Type を自分で書く(FormData のときとは逆) headers: { 'Content-Type': 'application/json' }
# body に何を渡すと、Content-Type はどうなるか(Chromium で実測)
body に渡したもの ブラウザが付けた Content-Type
JSON.stringify(…)(ただの文字列) text/plain;charset=UTF-8
 → 練習用サーバーは 415 で断った(JSON 以外は受け付けない)
new FormData(…)(第10回) multipart/form-data; boundary=…

第10回では「Content-Type は自分で書くな」でした。区切りの印(boundary)をブラウザが決めるからです。JSON は逆で、JSON.stringify() が作るのはただの文字列なので、ブラウザは text/plain を付けます。JSON だと伝えたければ、自分で書く。

/** 承認・差し戻しをサーバーへ送る */
async function decide(decision) {
  decideError.hidden = true;
  if (decision === 'return' && comment.value.trim() === '') { // 差し戻しは理由が必須(サーバーでも同じチェック)
    decideError.textContent = '差し戻すときは、理由をコメントに書いてください。';
    decideError.hidden = false;
    comment.focus();
    return;
  }
  const buttons = dialog.querySelectorAll('button');
  for (const b of buttons) b.disabled = true;
  try {
    const res = await fetch(`/api/expenses/${current.id}/decision`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }, // JSON を送るときは Content-Type を自分で書く(FormData のときとは逆)
      body: JSON.stringify({ decision, comment: comment.value, version: current.version }), // 読んだときの版。ほかの人が先に変えていたら 409
    });
    const no = current.id.slice(-4);
    if (res.status === 409) { // 409 = ほかの人が先に処理した
      const body = await res.json();
      const by = body.by, what = WHAT[body.status];
      dialog.close();
      pageStatus.className = 'alert alert-warning';
      pageStatus.textContent = `${no} は、${by}さんが先に${what}していました。一覧を新しくしました。`;
      await loadApprovals();
      pageStatus.focus(); // 押したボタンは一覧ごと消えたので、結果のメッセージへフォーカスを移す
      return;
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    dialog.close();
    pageStatus.className = 'alert alert-success';
    pageStatus.textContent = decision === 'approve' ? `${no} を承認しました。` : `${no} を差し戻しました。`;
    await loadApprovals();
    pageStatus.focus(); // 押したボタンは一覧ごと消えたので、結果のメッセージへフォーカスを移す
  } catch (error) {
    decideError.textContent = '送れませんでした。通信の状態を確かめて、もう一度お試しください。';
    decideError.hidden = false;
  } finally {
    for (const b of buttons) b.disabled = false; // うまくいっても、失敗しても、ボタンを戻す
  }
}
  • 差し戻しで理由が空なら、送らずにダイアログの中で知らせる(第9回の入力チェックと同じ考え)
  • 送っている間は、ダイアログのボタンを全部 disabled に。二度押しで2回送らないため(第10回)。finally で必ず戻す
  • うまくいったら、ダイアログを閉じて、緑の帯で知らせて、一覧を読み直す

最後の pageStatus.focus() は、試していて見つけたものです。最初はこの行がありませんでした。承認すると一覧が作り直され、押した「確認する」のボタンは表ごと消えます。ダイアログはフォーカスを開いたボタンへ戻そうとしますが、そのボタンはもうありません。調べると、フォーカスは BODY、つまりページの先頭に落ちていました。キーボードで操作している人は、どこにいるのか分からなくなります。そこで、結果のメッセージ(<p id="page-status" role="status" tabindex="-1">)にフォーカスを移しました。直したあとは page-status にいることを確かめています。

新井マウスで使ってたら、一生気づかないやつですね。

古井だから Tab キーで一度は使ってみる。いにしえの作法だ。

六

二人が同時に押したら

承認者が二人になりました。古井さんが承認の画面を開いたまま、お茶を淹れに行きます。その間に、石田さんが 0007 を承認しました。戻ってきた古井さんの画面には、まだ 0007 が「申請中」で並んでいます。古井さんがそれを差し戻したら……?

何も考えずに作ると、サーバーは後から来た「差し戻し」で上書きします。石田さんの承認は、だれにも知られずに消えます。新井さんから見ると、承認されたはずの申請が、いつの間にか差し戻しに。これは困る。

読んだときの版を添えて送る。違っていたら、サーバーは断る

古井 サーバー 石田 0007 · submitted · version 1 GET /api/approvals GET /api/approvals version: 1 version: 1 approve, version: 1 200 version → 2 return, version: 1 409 Conflict by: 石田 美咲 0007 · approved · version 2 「先に承認していました」 送るときに「読んだときの版」を添える。いまの版と違えば、だれかが先に変えた サーバーは上書きせず 409 で断る。画面は理由を伝えて、一覧を読み直す

そこで、申請のデータに版(version)を持たせます。変わるたびに 1 増える番号です。画面は一覧を読んだときの版を覚えておき、送るときに添えます。サーバーは、届いた版が今の版と違えば、「だれかが先に変えた」と分かるので、上書きせずに 409 Conflict で断ります。返事には、いまの状態と、だれが変えたかを入れておきます。

こうしたやり方を楽観的ロックと呼びます。「たぶんだれともぶつからない」と楽観して、ぶつかったときだけ断る。開いた瞬間に鍵をかけて、ほかの人を待たせるやり方(悲観的ロック)もありますが、お茶を淹れに行った人がいると、全員が待たされます。

サーバーに、いろいろ送ってみました。

# 承認の API に、いろいろ送ってみる(古井さんで。Chromium の fetch で実測)
送ったもの 状態コード サーバーの返事
JSON の文字列。Content-Type なし 415 send JSON
{decision: approve}(JSON ではない) 400 bad JSON
decision が maybe 400 bad decision
差し戻し。コメントは空白だけ 400 a reason is required
版が古い(version 0) 409 changed by someone else
承認(version 1) 200 status: approved, version: 2
もう一度、同じものを送る 409 changed by someone else
申請者(新井さん)が送る 403 approvers only
  • Content-Type がないと 415、JSON として読めないと 400、決めたことが変なら 400、理由が空白だけでも 400。画面のチェックを抜けてきても、サーバーで止まる
  • 版が古いと 409。同じものを2回送っても、2回目は 409(版が上がっているから)。二度押しの守りにもなっている
  • 申請者の新井さんが送ると 403(第11回)

409 が返ってきたら、画面はダイアログを閉じ、黄色い帯で「0007 は、石田 美咲さんが先に承認していました。一覧を新しくしました。」と知らせて、一覧を読み直します。だれが、どうしたのかを言うのがコツです。「エラーが発生しました」だけでは、古井さんはもう一度押してしまいます。

新井お茶を淹れてる間に、世界は変わってるんですね。

古井画面に出ているのは、読んだときの世界だ。今の世界じゃない。それを忘れないことだ。

七

履歴を残して見せる

差し戻された新井さんが知りたいのは、「なぜ?」です。そこで、申請ごとに出来事の記録(history)を残します。状態は書きかえますが、記録は消さずに1行ずつ書き足します。

状態は書きかえ、履歴は書き足す

申請(いまの状態) id: 'EXP-2026-0005' status: 'returned' version: 2 history: [ … ] 書きかえる history(出来事の記録) at: …T09:30… submitted 新井 at: …T16:40… returned 古井 「領収書の画像が途中で切れています。…」 次の出来事は、ここに書き足す 時刻はサーバーが付ける(ブラウザの時計は当てにしない) 見せるときに、日本時間で、その言語の書き方にする 消さずに足していくから、「だれが・いつ・なぜ」をあとからたどれる

時刻はサーバーが付けます。ブラウザの時計は、人によってずれていることがあるからです。サーバーは 2026-10-17T01:06:00.000Z(日本時間の 10:06)のような、時差まで書いた世界共通の書き方(ISO 8601)で保存し、画面が見せるときに、日本時間で、その言語の書き方に直します。

/** 時刻は日本時間で、その言語の書き方に */
function formatTime(iso) {
  return new Date(iso).toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo', dateStyle: 'medium', timeStyle: 'short' });
}

/** 申請の履歴をダイアログで見せる */
async function openHistory(id) {
  const res = await fetch(`/api/expenses/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const claim = await res.json();
  document.querySelector('#h-claim').textContent = `${claim.id}  ${claim.title}`;
  const items = claim.history.map((h) => {
    const li = document.createElement('li');
    li.className = 'mb-2';
    const head = document.createElement('div');
    head.textContent = `${formatTime(h.at)}  ${ACT[h.action]}  ${h.byName}`;
    li.append(head);
    if (h.comment) {
      const note = document.createElement('div');
      note.className = 'small text-body-secondary';
      note.textContent = h.comment;
      li.append(note);
    }
    return li;
  });
  document.querySelector('#h-list').replaceChildren(...items);
  historyDialog.showModal();
}

コメントは新井さんではなく承認者が書いたものなので、innerHTML ではなく textContent で入れます(第8回の XSS の話)。timeZone: 'Asia/Tokyo' を指定しているので、パソコンがどの国の時間になっていても、日本時間で出ます。

# 同じ時刻を、3つの言語で(toLocaleString。Chromium で実測)
言語 0012 の承認 0005 の申請
ja-JP 2026/10/17 10:06 2026/10/09 9:30
zh-CN 2026年10月17日 10:06 2026年10月9日 09:30
en-US Oct 17, 2026, 10:06 AM Oct 9, 2026, 9:30 AM

同じ瞬間でも、言語によって書き方が違います。日本語は時の前に 0 を付けず「9:30」、中国語は「09:30」、英語は午前・午後を付けて「9:30 AM」。自分で getHours() をつないで作ると、こういう違いを全部覚えておくことになります。ここもブラウザに任せます。

新井 太郎(申請者)の申請一覧。今月のまとめは申請5件・承認待ちの金額 0 円・差し戻し1件。表は 0012 ニューヨーク出張 46,851 円 承認済み、0007 大阪出張 32,400 円 承認済み、0006 客先打ち合わせの電車代 3,830 円 承認済み、0005 技術書の購入 4,180 円 差し戻し、0004 勉強会の参加費 3,000 円 精算済み。どの行にも「履歴」のリンク風ボタン
承認が終わったあとの、新井さんの申請一覧。行ごとに「履歴」が開ける(筆者のデモページを、練習用のサーバー経由でクラウド上の Chromium で撮影。Playwright で実際に入力・クリックしたところ。2026年10月17日。アカウントはデモ専用の架空のもの。ブラウザの枠は説明のために描き足したもの)
八

動かしてみる

練習用サーバーを立て、古井さん・石田さん・新井さんの3つのブラウザで、Playwright で実際に操作しました。古井さんの画面は、石田さんが承認する前に開いたままにしてあります。

3つの場面。1 コメントを書かずに「差し戻す」を押すと、ダイアログの中に「差し戻すときは、理由をコメントに書いてください。」と出る。2 石田さんが先に 0007 を承認したあと、古井さんが古い一覧のまま 0007 を差し戻そうとすると、黄色い帯で「0007 は、石田 美咲さんが先に承認していました。一覧を新しくしました。」と出て、表には 0010 だけが残る。3 新井さんが 0005 の履歴を開くと、2026/10/09 9:30 申請 新井 太郎、2026/10/09 16:40 差し戻し 古井 一郎、その下に理由「領収書の画像が途中で切れています。撮り直して添付してください。」
①理由なしの差し戻し、②二人の承認者の取り合い、③申請者が履歴を開く(筆者のデモページを、練習用のサーバー経由でクラウド上の Chromium で撮影。Playwright で実際に入力・クリックしたところ。2026年10月17日。アカウントはデモ専用の架空のもの。ブラウザの枠は説明のために描き足したもの)
# 実際に操作して記録した値(Chromium。サーバーは練習用)
だれが したこと 画面に出たこと
古井 承認の画面を開く 承認待ち 3 件:0007, 0010, 0012
古井 0012 を理由なしで差し戻す 差し戻すときは、理由をコメントに書いてください。
古井 0012 を承認 0012 を承認しました。(残り 2 件)
石田 0007 を承認 0007 を承認しました。(残り 1 件)
古井 (古い一覧のまま)0007 を差し戻す 0007 は、石田 美咲さんが先に承認していました。(409)
新井 申請一覧を開く 0012 承認済み / 0007 承認済み / 0006 承認済み / 0005 差し戻し …
新井 0005 の履歴を開く 2 行。最後は「2026/10/09 16:40 差し戻し 古井 一郎」
  • 理由なしの差し戻しは、ダイアログの中で止まった。サーバーには何も送っていない
  • 古井さんが 0012 を承認すると、一覧は2件に。石田さんが 0007 を承認すると、石田さんの一覧は1件に
  • 古井さんが古い一覧のまま 0007 を差し戻すと、サーバーが 409 で断り、石田さんの名前つきで知らせた。一覧は読み直されて 0010 だけに
  • 新井さんの一覧では 0012 と 0007 が承認済み。0005 の履歴を開くと、古井さんの差し戻しの理由が読めた
POINT

承認の流れで、画面がすることは多くありません。確かめてから押させる、押せないものは押させない、断られたら何が起きたかを言う。状態を進めてよいか、先を越されていないかを決めるのは、ここでもサーバーです。

結 — 一枚まとめと次回予告

  1. 申請は状態を持つ。ボタンは、いまの状態から出ている矢印だけ。通してよいかはサーバーが決める。
  2. 確かめる窓は <dialog> と showModal()。後ろを触らせない、フォーカスを中へ、Esc で閉じる、元へ戻す、をブラウザがやってくれる。
  3. 一覧を作り直すと、押したボタンは消える。フォーカスの行き先を自分で決める。
  4. JSON を送るときは Content-Type: application/json を自分で書く。FormData のときとは逆。
  5. 版を添えて送り、違えば 409。断られたら、だれが何をしたかを伝えて、一覧を読み直す。
  6. 履歴は消さずに書き足す。時刻はサーバーが付け、見せるときに toLocaleString で。

新井ニューヨーク出張、承認済みになりました! 46,851 円、いつ振り込まれます?

古井それは経理に聞け。次回は集計と印刷。月ごと・人ごとの合計を出して、紙に印刷しても見やすい画面にする。経理の人が喜ぶ回だ。

新井経理の人に喜ばれたら、振り込みも早くなりますかね。

参考

ページの写真は、筆者が作ったデモページを、練習用のサーバー(Node.js)経由で2026年10月17日にクラウド上の Chromium(Playwright)で撮影したものです。撮影環境には Hiragino Sans がないため、日本語は Noto Sans CJK JP で表示されています。アカウント・申請・コメントは、練習用サーバーの架空のデータです。ダイアログの振る舞い、画面に出たメッセージ、状態コード、Content-Type、履歴の時刻の書き方、フォーカスの位置は、同じ Chromium を Playwright で操作・実行して記録した実際の値です。各ページの HTML は Nu Html Checker(vnu)でエラーがないことを確かめています。登場人物は架空の人物です。

いにしえのフロントエンド入門 第12回 — 承認の流れ / 2026.10

← 記事一覧へ