はじめに
文部科学省の「日本食品標準成分表2023年版(八訂)」をもとに、全2,538食品・53栄養素を比較できる無料ツール、NutriGraphを公開しました。数値はすべて可食部100gあたりです。
この記事では、プロダクトの紹介と、ビルド不要のバニラJS構成、SVGで作る統計セル、プログラマティックSEOの実装ポイントをまとめます。
作った理由
栄養を紹介するサイトの多くはグラフがなく、文章か数値の羅列だけです。数値を出されても、それが多いのか少ないのか、ほかの食品と比べてどうなのかがわかりません。
さらに、「〇〇に良い」と書かれていても、実際にはほとんど入っていないこともあります。
例えば、「レタスは栄養が意外と豊富?!」と紹介しているサイトは結構あります。しかしながら、食品成分表のデータを野菜の中で見比べると、葉酸以外の全ての栄養素が全野菜中下位25%でした。
数値の大小と、ほかの食品の中での位置の両方がわかるツールがほしくて作りました。後述するマトリクスの統計セルは、このために用意した機能です。
できること
4つのタブで比較できます。
- 栄養素ランキング: 1つの栄養素で全食品を多い順・少ない順に並べる
- 食品プロファイル比較: 複数の食品を選んで相対比較する
- 食品群内で全比較: 食品群を選び、その群の全食品を栄養素ごとに比較する
- 食品×栄養素マトリクス: 行が食品、列が栄養素。各セルに統計を重ね、色でその食品群内の豊富さを表す
構成
| 項目 | 採用したもの |
|---|---|
| 言語・構成 | バニラJavaScript。ES Modules、依存パッケージなし |
| グラフ | Chart.js v4 |
| マトリクス描画 | インラインSVG |
| データ | data.json 約1.8MB |
| ホスティング | Vercel。環境変数なし |
データは2,538食品・53栄養素で固定です。公式データの更新は年単位なので、サーバーやDBを置く必要はありません。data.json を1回 fetch して、フィルタもソートもクライアント側で完結させています。
// src/store.js
export async function loadData() {
const url = new URL('../data.json', import.meta.url);
const res = await fetch(url);
if (!res.ok) throw new Error(`data.json の読み込みに失敗しました (${res.status})`);
const d = await res.json();
COLS = d.columns; FOODS = d.foods; GROUPS = d.groups;
return d;
}
ビルドステップがないので、ローカルは npx serve . で起動でき、デプロイはVercelに上げるだけです。フレームワークの更新やビルド設定のメンテが不要です。
なお、ES Modulesと fetch を使うため、file:// で直接開かず、簡易サーバー経由で表示します。
実装ポイント1: SVGで統計入りのセルを描く
マトリクスタブのポイントは、各セルが数値ではなく、その食品群の中でその値がどの位置かを表すところです。Chart.jsではなくインラインSVGで1セルずつ描画しています。
- 値の棒を5段階の色で塗り分ける。Q1未満はグレー、中央値以上で濃いティール、スケール超で赤
- Q1・Q3・平均の縦線と、中央値の点を重ねる
// src/tabs/matrix.js(抜粋)
function cellBox(v, st) {
if (v == null) return '<td><div class="cb empty">—</div></td>';
s += `<rect ... fill="${TIERS[tier].color}" />`;
s += `<line x1="${X(st.q1)}" ... stroke="${STATC.q1}"/>`;
s += `<line x1="${X(st.q3)}" ... stroke="${STATC.q3}"/>`;
s += `<line x1="${X(st.mean)}"... stroke-dasharray="2,1.5"/>`;
s += `<circle cx="${X(st.med)}" ... fill="${STATC.med}"/>`;
return `<td><div class="cb"><svg ...>${s}</svg></div></td>`;
}
統計は外れ値の影響を抑えるため、上位・下位5%を除外してから四分位を計算しています。
// src/utils.js
export function quantile(sorted, q) {
const p = (sorted.length - 1) * q, b = Math.floor(p), r = p - b;
return sorted[b + 1] !== undefined ? sorted[b] + r * (sorted[b + 1] - sorted[b]) : sorted[b];
}
実装ポイント2: プログラマティックSEOで約2,600ページ
SPAは検索流入が弱点になりやすいので、scripts/generate.mjs で約2,600ページの静的HTMLを生成しています。
- /foods/番号: 食品詳細。2,538ページ
- /nutrients/栄養素: 栄養素ランキング。53ページ
- /groups/番号: 食品群一覧。18ページ
- /collections/テーマ: 高たんぱく・低カロリーなどの特集
- /compare/a-b: 1対1比較
各ページに固有のtitle、description、canonical、OGP、パンくずJSON-LDを付け、sitemap.xmlに全URLを載せています。npm run generate で再生成できます。
実装ポイント3: 地味に工夫したUX
元データでは名称に( )が多く入っており、名称が長すぎます。これを表示しないようにしています。
// 表示名はかっこ書きを省略。検索は元の名前で効く
export const dispName = n =>
n.replace(/[((][^()()]*[))]/g, '').replace(/\s+/g, ' ').trim() || n;
// 数値は桁に応じて整形
export const fmt = n =>
n == null ? '—'
: (Math.abs(n) >= 100 ? n.toFixed(0)
: Math.abs(n) >= 10 ? n.toFixed(1)
: n.toFixed(2)).replace(/\.0+$/, '');
開発について
実装はAIコーディングエージェントのClaude Codeと相談しながら進めました。データ整形からSVG描画、SEOページの生成まで、設計判断を会話で詰めつつ短い期間で仕上げられました。
まとめ
- 公式の食品成分表を、全食品・全栄養素まとめて比較できる無料ツールを公開しました
- ビルド不要のバニラJSとChart.js、静的ホスティングで、運用コストはほぼゼロです
- SVGの統計セルと、約2,600ページのSEOページ生成が技術的な見どころです
よければ触ってみてください。
https://nutri-graph.vercel.app/