Claude Codeでテーブルコンポーネントを作る: React/TypeScript実装ガイド
Claude CodeでReactのテーブルを実装。ソート、検索、ページング、スマホ対応、アクセシビリティ、Playwright確認まで解説。
テーブルは「見た目」より先に意味を決める
管理画面、SaaSの顧客一覧、売上レポート、記事一覧、請求履歴。Webアプリを作ると、ほぼ必ずテーブルコンポーネントが出てきます。最初は<table>にデータを流し込むだけに見えますが、実務ではすぐに「名前で検索したい」「金額で並べ替えたい」「100件を分割したい」「スマホで横スクロールがつらい」「スクリーンリーダーで読めるか確認したい」という話になります。
Claude Codeは、こうした細かい実装を一気に進めるのに向いています。ただし、依頼文があいまいだと、見た目だけ整っていて意味の薄いdiv一覧になったり、ソート状態が画面には見えるのに支援技術へ伝わらなかったりします。テーブルは「データの関係」を伝えるUIなので、最初にHTMLの意味、操作、状態、検証コマンドをセットで渡すのが大切です。
この記事では、初心者でもそのまま試せるReact/TypeScriptのテーブルコンポーネントを作ります。扱う範囲は、セマンティックなtable、ソート、フィルタリング、ページネーション、レスポンシブ対応、アクセシビリティ、TanStack Tableを選ぶ判断、Playwrightでの確認です。React全体の進め方はClaude CodeでReact開発を爆速にする方法、a11yの基礎はClaude Codeでアクセシビリティ対応を効率化する方法もあわせて読むとつながります。
公式情報は必ず原典を確認します。HTMLテーブルの意味はMDNの<table>リファレンス、ソート状態はMDNのaria-sort、複雑なテーブル状態管理はTanStack Table公式ドキュメント、E2E確認はPlaywrightのWriting tests、Claude Codeの使い方はClaude Code overviewを起点にします。
セマンティックなテーブルの基本
テーブルは、行と列の交点に意味があるデータを表すためのHTMLです。単なるカード一覧を表に見せたいだけならulやdivのほうが自然な場合もあります。反対に、顧客名、プラン、月額売上、状態、登録日のように列ごとの比較が重要なら、tableを使う価値があります。
最低限、caption、thead、tbody、th scope="col"を使います。captionは表の目的を短く伝える説明です。th scope="col"は「この見出しは列見出しです」と示します。行の主語になるセルがある場合はth scope="row"を使うと、支援技術で表を読む時に関係が追いやすくなります。
<table>
<caption>顧客ごとの月額売上</caption>
<thead>
<tr>
<th scope="col">顧客名</th>
<th scope="col">プラン</th>
<th scope="col">月額売上</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Northwind</th>
<td>Pro</td>
<td>$1,200</td>
</tr>
</tbody>
</table>
Claude Codeに頼む時は「テーブルタグを使って」とだけ言うより、「captionを入れる」「列見出しにscope="col"を付ける」「顧客名は行見出しにする」と伝えるほうが再現性があります。アクセシビリティは後から追加するより、最初の構造で決めるほうが安く済みます。
flowchart TD
A["要件を決める"] --> B["意味のある table 構造"]
B --> C["ソート・検索・ページング状態"]
C --> D["モバイル表示"]
D --> E["aria-sort とキーボード操作"]
E --> F["Playwright で確認"]
Claude Codeに渡す依頼文テンプレート
Claude Codeへは、実装対象、触ってよい範囲、データ型、操作、アクセシビリティ、確認コマンドをまとめて渡します。これを省くと、ソートは動くけれどページネーションが検索後に戻らない、スマホCSSはあるけれどdata-labelが抜ける、という小さな事故が起きやすくなります。
React + TypeScriptで顧客一覧のテーブルコンポーネントを作ってください。
要件:
- src/components/DataTable.tsx と src/components/data-table.css だけを追加・変更する
- HTMLの table / caption / thead / tbody / th scope を使う
- 顧客データ型は id, name, plan, mrr, status, signedUpAt
- 全文フィルター、列ソート、5件ずつのページネーションを入れる
- ソート中の列だけ aria-sort="ascending|descending" を付ける
- 見出し内のボタンでソートできるようにする
- スマホでは横スクロールだけに頼らず、各セルに data-label を出す
- Playwrightでフィルター、ソート、ページング、モバイル表示を確認する
禁止:
- 既存のデザインシステムを無視して新しいUIライブラリを入れない
- role="grid" を安易に付けない
- 疑似コードではなく、コピーして動かせるコードを書く
ポイントは、Claude Codeを「コードを書く係」ではなく「要件を壊さず差分をそろえる係」として使うことです。特にテーブルは、UI、状態管理、CSS、テストが分かれて見えるため、依頼文に作業範囲を書いておくとレビューしやすくなります。
コピペで動くReact/TypeScript実装
以下はライブラリなしで動く基本実装です。Next.jsのクライアントコンポーネントでも、ViteのReactアプリでも使えます。Next.js以外なら先頭の"use client";は残っていても害はありませんが、気になる場合は削除してください。
// src/components/DataTable.tsx
"use client";
import { useMemo, useState, type ReactNode } from "react";
import "./data-table.css";
type SortDirection = "asc" | "desc";
type SortState<T> = {
key: keyof T;
direction: SortDirection;
} | null;
export type Customer = {
id: string;
name: string;
plan: "Free" | "Pro" | "Enterprise";
mrr: number;
status: "active" | "trial" | "paused";
signedUpAt: string;
};
type Column<T> = {
key: keyof T;
label: string;
numeric?: boolean;
render?: (value: T[keyof T], row: T) => ReactNode;
};
const pageSize = 5;
const sampleCustomers: Customer[] = [
{ id: "cus_001", name: "Northwind", plan: "Pro", mrr: 1200, status: "active", signedUpAt: "2026-01-15" },
{ id: "cus_002", name: "Blue Bottle", plan: "Free", mrr: 0, status: "trial", signedUpAt: "2026-02-02" },
{ id: "cus_003", name: "Kobayashi Studio", plan: "Enterprise", mrr: 8400, status: "active", signedUpAt: "2025-11-20" },
{ id: "cus_004", name: "Atlas Foods", plan: "Pro", mrr: 980, status: "paused", signedUpAt: "2025-12-09" },
{ id: "cus_005", name: "Green Lab", plan: "Pro", mrr: 1600, status: "active", signedUpAt: "2026-03-01" },
{ id: "cus_006", name: "Sakura Dental", plan: "Free", mrr: 0, status: "trial", signedUpAt: "2026-03-18" },
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
const columns: Column<Customer>[] = [
{ key: "name", label: "Customer" },
{ key: "plan", label: "Plan" },
{
key: "mrr",
label: "MRR",
numeric: true,
render: (_, row) => currency.format(row.mrr),
},
{
key: "status",
label: "Status",
render: (_, row) => <span className={`status status-${row.status}`}>{row.status}</span>,
},
{
key: "signedUpAt",
label: "Signed up",
render: (_, row) => new Date(row.signedUpAt).toLocaleDateString("en-US"),
},
];
function compareValues<T>(leftRow: T, rightRow: T, key: keyof T) {
const left = leftRow[key];
const right = rightRow[key];
if (typeof left === "number" && typeof right === "number") {
return left - right;
}
return String(left).localeCompare(String(right), undefined, {
numeric: true,
sensitivity: "base",
});
}
export function DataTable({ rows = sampleCustomers }: { rows?: Customer[] }) {
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [sort, setSort] = useState<SortState<Customer>>({
key: "name",
direction: "asc",
});
const filteredRows = useMemo(() => {
const keyword = query.trim().toLowerCase();
if (!keyword) return rows;
return rows.filter((row) =>
columns.some((column) =>
String(row[column.key]).toLowerCase().includes(keyword),
),
);
}, [query, rows]);
const sortedRows = useMemo(() => {
if (!sort) return filteredRows;
return [...filteredRows].sort((left, right) => {
const result = compareValues(left, right, sort.key);
return sort.direction === "asc" ? result : -result;
});
}, [filteredRows, sort]);
const totalPages = Math.max(1, Math.ceil(sortedRows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const pageRows = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return sortedRows.slice(start, start + pageSize);
}, [currentPage, sortedRows]);
function updateQuery(value: string) {
setQuery(value);
setPage(1);
}
function toggleSort(key: keyof Customer) {
setSort((current) => {
if (!current || current.key !== key) {
return { key, direction: "asc" };
}
return {
key,
direction: current.direction === "asc" ? "desc" : "asc",
};
});
}
return (
<section className="table-shell" aria-labelledby="customers-table-title">
<div className="table-actions">
<h2 id="customers-table-title">Customers</h2>
<label>
<span>Filter customers</span>
<input
type="search"
value={query}
onChange={(event) => updateQuery(event.target.value)}
placeholder="Search name, plan, or status"
/>
</label>
</div>
<div className="table-scroll" tabIndex={0}>
<table className="data-table">
<caption>Monthly recurring revenue by customer</caption>
<thead>
<tr>
{columns.map((column) => {
const isSorted = sort?.key === column.key;
const ariaSort = isSorted
? sort.direction === "asc"
? "ascending"
: "descending"
: undefined;
return (
<th
key={String(column.key)}
scope="col"
aria-sort={ariaSort}
className={column.numeric ? "numeric" : undefined}
>
<button type="button" onClick={() => toggleSort(column.key)}>
{column.label}
<span aria-hidden="true">
{isSorted ? (sort.direction === "asc" ? " ▲" : " ▼") : ""}
</span>
</button>
</th>
);
})}
</tr>
</thead>
<tbody>
{pageRows.length > 0 ? (
pageRows.map((row) => (
<tr key={row.id}>
{columns.map((column, index) => {
const content = column.render
? column.render(row[column.key], row)
: String(row[column.key]);
if (index === 0) {
return (
<th key={String(column.key)} scope="row" data-label={column.label}>
{content}
</th>
);
}
return (
<td
key={String(column.key)}
data-label={column.label}
className={column.numeric ? "numeric" : undefined}
>
{content}
</td>
);
})}
</tr>
))
) : (
<tr>
<td colSpan={columns.length}>No customers match this filter.</td>
</tr>
)}
</tbody>
</table>
</div>
<nav className="pagination" aria-label="Table pagination">
<button type="button" onClick={() => setPage((value) => value - 1)} disabled={currentPage === 1}>
Previous
</button>
<span aria-live="polite">
Page {currentPage} of {totalPages}
</span>
<button
type="button"
onClick={() => setPage((value) => value + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</nav>
</section>
);
}
この実装では、ソート対象の列だけにaria-sortを付けています。MDNの説明でも、aria-sortは現在ソートされている列または行のヘッダーにだけ置く属性です。未ソート列に全部aria-sort="none"を付ける実装も見かけますが、少なくとも「どの列が今ソートされているか」を1つに絞るほうが読み上げの意図が明確です。
レスポンシブとモバイル表示
テーブルのスマホ対応には大きく2つあります。1つ目は横スクロールを許す方法です。列の比較を保ちたい会計表や在庫表では、横スクロールが一番正直なこともあります。2つ目は、スマホ幅だけ行をカード風に積み、各セルの前にdata-labelを表示する方法です。この記事のCSSは後者を採用します。
/* src/components/data-table.css */
.table-shell {
display: grid;
gap: 1rem;
}
.table-actions {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.table-actions label {
display: grid;
gap: 0.35rem;
min-width: min(100%, 18rem);
}
.table-actions input {
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
padding: 0.55rem 0.75rem;
}
.table-scroll {
overflow-x: auto;
border: 1px solid #d8dee8;
border-radius: 0.5rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
.data-table caption {
padding: 0.75rem;
text-align: left;
font-weight: 600;
}
.data-table th,
.data-table td {
border-top: 1px solid #e5e7eb;
padding: 0.75rem;
text-align: left;
vertical-align: middle;
}
.data-table th.numeric,
.data-table td.numeric {
text-align: right;
}
.data-table th button {
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 0;
}
.data-table tbody tr:hover {
background: #f8fafc;
}
.status {
border-radius: 999px;
display: inline-block;
font-size: 0.8rem;
padding: 0.2rem 0.55rem;
}
.status-active {
background: #dcfce7;
color: #166534;
}
.status-trial {
background: #e0f2fe;
color: #075985;
}
.status-paused {
background: #fef3c7;
color: #92400e;
}
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
}
.pagination button {
border: 1px solid #cbd5e1;
border-radius: 0.45rem;
background: white;
padding: 0.45rem 0.75rem;
}
.pagination button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
@media (max-width: 640px) {
.table-scroll {
overflow: visible;
border: 0;
}
.data-table thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.data-table,
.data-table tbody,
.data-table tr,
.data-table th,
.data-table td {
display: block;
width: 100%;
}
.data-table tr {
border: 1px solid #d8dee8;
border-radius: 0.5rem;
margin-bottom: 0.75rem;
padding: 0.25rem 0;
}
.data-table th,
.data-table td,
.data-table th.numeric,
.data-table td.numeric {
display: grid;
grid-template-columns: 8.5rem 1fr;
gap: 0.75rem;
text-align: left;
}
.data-table th::before,
.data-table td::before {
color: #64748b;
content: attr(data-label);
font-weight: 700;
}
}
スマホで列見出しを視覚的に隠しても、DOM上にはtheadが残っています。画面上の読みやすさのためにdata-labelを出し、支援技術向けには本来のテーブル構造を維持する、という考え方です。表示がカードに見えてもデータの関係は表のままなので、CSSだけで変換しています。
アクセシビリティで確認する点
アクセシブルなテーブルでまず見るのは、captionがあるか、列見出しがthになっているか、ソート操作がbuttonになっているかです。クリック可能なthに直接onClickを付けるだけだと、キーボード操作やフォーカス表示が弱くなります。見出しセルの中にbuttonを置くと、EnterやSpaceで操作しやすくなります。
role="grid"は便利そうに見えますが、安易に付けないほうが安全です。スプレッドシートのようにセル単位の矢印キー移動や複雑な編集を実装するなら検討しますが、一覧表示のソート・検索・ページングなら、ネイティブのtableを保つほうが実装も説明も簡単です。
Claude Codeには、最後にこう確認させます。
このテーブルをアクセシビリティ観点でレビューしてください。
- caption は表の目的を説明しているか
- th scope は適切か
- ソート中の列だけ aria-sort が付いているか
- 見出しのソート操作は button でキーボード操作できるか
- フィルター input にラベルがあるか
- ページ番号の変更が aria-live で伝わるか
- role="grid" を使う必要が本当にあるか
TanStack Tableを選ぶタイミング
小さな一覧なら、上の自前実装で十分です。列数が増え、列の表示切り替え、固定列、サーバーサイドページング、仮想スクロール、列ごとのフィルター、行選択まで必要になるなら、TanStack Tableを検討します。TanStack Tableはヘッドレス、つまり見た目の部品ではなくテーブル状態を作るライブラリです。デザインは自分のCSSやデザインシステムで作れます。
| 選択肢 | 向いている場面 | 注意点 |
|---|---|---|
| 自前実装 | 5から8列程度、検索と単純ソートだけ | 仕様追加が続くと状態管理が散らばる |
| TanStack Table | 列設定、複数フィルター、行選択、サーバー連携が必要 | APIを理解し、UIは自分で作る必要がある |
| AG Gridなど重量級 | Excelに近い編集、巨大データ、業務専用機能 | 導入コストとライセンス確認が必要 |
TanStack Tableで状態をまとめる場合は、Claude Codeに「公式ドキュメントのReact Table Adapterに合わせる」「既存CSSを使う」「列定義を型安全にする」と指示します。
import {
type ColumnDef,
type PaginationState,
type SortingState,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useState } from "react";
const tanStackColumns: ColumnDef<Customer>[] = [
{ accessorKey: "name", header: "Customer" },
{ accessorKey: "plan", header: "Plan" },
{ accessorKey: "mrr", header: "MRR" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "signedUpAt", header: "Signed up" },
];
export function useCustomerTable(data: Customer[]) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
return useReactTable({
data,
columns: tanStackColumns,
state: { sorting, globalFilter, pagination },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
}
最初からライブラリを入れるべきか迷ったら、「半年後に列表示切り替えやサーバーサイドページングが来るか」で判断します。来ないなら自前で軽く始める。来るならTanStack Tableを選び、最初から列定義と状態を寄せておく。これがレビューしやすい分岐です。
Playwrightで壊れていないか確認する
テーブルのバグは、目視だけだと見逃します。検索後にページが2ページ目のままで空表示になる、ソートアイコンだけ変わって実データが並び替わらない、スマホで列名が消える、といった問題はPlaywrightで押さえます。Playwrightは操作して結果を検証するE2Eテストに向いており、公式ドキュメントでもアクションと期待値を組み合わせて書く流れが示されています。
// tests/customer-table.spec.ts
import { expect, test } from "@playwright/test";
test("customer table filters, sorts, paginates, and keeps mobile labels", async ({ page }) => {
await page.goto("/customers");
await expect(
page.getByRole("table", { name: /monthly recurring revenue/i }),
).toBeVisible();
await page.getByRole("button", { name: /MRR/ }).click();
await expect(page.getByRole("columnheader", { name: /MRR/ })).toHaveAttribute(
"aria-sort",
"ascending",
);
await page.getByLabel("Filter customers").fill("north");
await expect(page.getByRole("row", { name: /Northwind/ })).toBeVisible();
await expect(page.getByText("No customers match this filter.")).toBeHidden();
await page.getByLabel("Filter customers").fill("");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Page 2 of 2")).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.locator("td[data-label='Plan']").first()).toBeVisible();
});
Claude Codeへ修正を頼む時は、先にこのテストを渡して「失敗を再現してから直して」と書くと安定します。テスト戦略を広げるならClaude CodeでPlaywrightテストを自動化するも参考になります。
3つ以上のユースケース
| ユースケース | 追加したい機能 | 収益や運用への効き方 |
|---|---|---|
| SaaS顧客一覧 | プラン、MRR、状態、契約日でソート | 解約リスクやアップセル候補を探しやすい |
| EC商品管理 | 在庫、価格、公開状態、カテゴリで絞り込み | 欠品や価格ミスを早く見つけられる |
| 記事管理CMS | 公開日、カテゴリ、検索流入、CTAクリックで比較 | リライト優先度と広告収益改善を判断しやすい |
| 請求履歴 | 支払状態、金額、期限、請求先でページング | 未払い確認とサポート対応が速くなる |
どの用途でも、表は「読むだけの部品」ではなく意思決定の入口です。特にメディア運営では、記事一覧にPV、読了率、CTAクリック、収益を並べると、何をリライトすべきか判断しやすくなります。計測設計はClaude Codeでアナリティクス実装を効率化すると組み合わせると実務に落とし込みやすいです。
よくある失敗と落とし穴
1つ目は、divだけで表を作ることです。見た目は自由になりますが、列と行の関係をブラウザや支援技術が理解しにくくなります。データが表なら、まずtableを使います。
2つ目は、ソート状態をアイコンだけで表すことです。矢印は視覚的には分かりやすい一方、読み上げでは伝わりません。ソート中の列にaria-sortを付け、ボタン名や見出しから列名が分かるようにします。
3つ目は、検索時にページ番号を戻さないことです。2ページ目を見ている状態で検索すると、結果は1件あるのに2ページ目が空で「データがない」と見えることがあります。フィルター変更時はsetPage(1)を入れます。
4つ目は、モバイルで横スクロールだけに任せることです。会計表のように横比較が重要なら横スクロールでよいですが、顧客一覧や記事一覧ならカード風表示も検討します。ただしDOMは表のまま保ちます。
5つ目は、Claude Codeに新しいUIライブラリを勝手に入れさせることです。既存のButton、Input、デザイン変数があるなら、それを使うように明記します。テーブル1つのために依存関係を増やすと、後から保守コストが残ります。
収益導線とCTA
テーブルは収益導線とも相性が良いUIです。記事サイトなら、記事別のPV、CTAクリック、広告RPM、リライト日を並べるだけで、次に直すべき記事が見えます。SaaSなら、顧客別のMRR、利用状況、契約更新日を並べると、解約予兆やアップセル候補を見つけやすくなります。
Claude Codeでこうした管理画面を作る時は、単に一覧を表示するだけでなく「どの行を見たら次の行動に移れるか」を決めます。たとえば、低読了率の記事にはリライトボタン、MRRが高く利用率が落ちた顧客にはCSメモ、請求遅延には再送ボタンを置きます。操作列を増やす場合も、ボタン名、権限、確認ダイアログ、監査ログまで要件に入れてください。
Claude Codeをチーム導入して、管理画面や収益レポートを安全に作りたい場合はClaude Code研修・導入相談で相談できます。自分で学ぶ場合は教材一覧から、プロンプトテンプレートや実装チェックリストを確認してください。テーブルは地味ですが、広告、問い合わせ、継続課金の改善を支える実務UIです。
この記事で紹介した内容を実際に試した結果
Masaが小さな顧客一覧でこの構成を試した時、最初に効いたのは「ソート、検索、ページングを同じコンポーネントで閉じる」ことでした。検索時にページを1へ戻す処理を忘れて空表示になったことがありましたが、Playwrightでフィルター後の表示を確認するテストを入れるとすぐ検出できました。次に効いたのは、スマホCSSを最後に足すのではなく、最初からdata-labelを出す前提でセルを作ることです。Claude Codeには、見た目の調整だけでなくcaption、scope、aria-sort、モバイル表示、E2Eテストまで同じ依頼に含めたほうが、レビューしやすく壊れにくいテーブルになります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code権限セーフティラダー: 初心者がallowを広げる順番
Claude Codeの権限をread-onlyからbuild、限定編集、deploy確認まで段階的に広げる安全な運用手順。
Claude Code Small PR Proof Pack: 小さなPRをレビュー可能にする証拠セット
Claude Codeの小さなPRに、差分・検証・公開URL・CTA・rollbackを添える実務チェックリスト。
Claude Codeのコミット前レビューゲート: 差分、テスト、CTAをまとめて止める型
Claude Codeでcommit前に差分をレビューする実践手順。build、公開URL、CTA、Gumroadリンク、未翻訳本文を検知します。