seo-blog-images について
ブログ記事向けに SEO を考慮した画像を作成し、組み込むワークフロー用スキルです。用途に応じてスクリーンショット、決定的なグラフ、生成画像を選び、ファイルとメタデータを確認します。
主な機能
- 01画像枠ごとにビジュアルを選択
- 02画像ファイルの生成と圧縮
- 03実データからグラフを作成
- 04画像内の文字ミスを確認
- 05SEO メタデータと寸法属性を追加
活用シーン
- 01ブログ記事のカバー画像を用意
- 02チュートリアルに製品画面を追加
- 03ベンチマーク数値をグラフ化
- 04画像名と読み込み設定を監査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ illyism/ seo-skills --skill seo-blog-imagesインストール後、Claude Code で/seo-blog-imagesを入力して読み込まれたか確認します。
詳細
ブログ画像と画像 SEO
デフォルトの AI ブログ画像は、記事の印象を損ないます。暗い背景に、光るワイヤーフレームアイコンが5つ、判読できないインフォグラフィックのラベル、2.4 MB の image-1.png、そして width/height がないため、読み込み時にレイアウトが跳ねます。このワークフローでは、各スロットに適したビジュアルの種類を選び(実際のキャプチャ、決定論的なチャート、または短いプロンプトによる生成)、画像を作成し、文字を一つ残らず読み返して、SEO に必要なタグをすべて記事に組み込みます。
ステップ 0:画像の保存場所を確認する
Which framework is the repo?
├── `next.config.*`, `astro.config.*`, `vite.config.*`, Remix -> files in `public/blog/<slug>/`, src `/blog/<slug>/<name>.webp`
├── Hugo (`hugo.toml` / `config.toml`) -> page bundle next to `index.md`, or `static/blog/<slug>/`
├── Jekyll (`_config.yml`) -> `assets/blog/<slug>/`
└── Other -> match the path pattern of an existing image in a published post
リポジトリに既存の画像ディレクトリがある場合は、新しいディレクトリを勝手に作成しないでください。保存場所が混在すると、サイズ監査と CDN キャッシュのルールが機能しなくなります。
ステップ 1:スロットごとにビジュアルの種類を選ぶ
カバー画像と、ビジュアルが必要な各 <h2> について、次の手順を実行します。
What is this slot for?
├── Software product review or UI tutorial (`### 1. ToolName`)?
│ └── Path B: real Playwright screenshot or short `<video>` loop.
│ Never ask an image model to draw a real product's UI; readers spot fake dashboards and it misrepresents the product.
├── Benchmark, dataset, or any chart with numbers?
│ └── Path C: deterministic SVG chart from the real numbers.
│ Never ask an image model for charts or tables; it invents bar heights and misspells values.
├── Cover / OpenGraph (`og:image`)?
│ └── Path A: 16:9, 12-22 word prompt, 1 focal scene + a 3-6 word headline badge.
└── Inline concept (workflow, pricing contrast, architecture)?
├── Does a 2-4 word contrast label make the point in half a second?
│ └── Path A: topic-literal scene + 2-4 quoted labels (`"MANUAL CSV"` vs `"LIVE WEBHOOK"`). No invented numbers.
└── Otherwise -> Path A: zero-text close-up, 12-20 words, tight framing.
ステップ 2:プロンプトのルール(パス A のみ)
- 12〜22語にします。 長いプロンプトでは、画像モデルがフレームに小さなサブパネルや判読できないほど小さな文字を詰め込みがちです。短いプロンプトなら、主題が一つに絞られます。
- 暗いアイコンボックスのテンプレートは使いません。
"cards with glowing wireframe icons"、"holographic brain"、"dark monochrome desk with ice-blue highlights"、トロフィーの比喩は、絶対にプロンプトに含めないでください。明るくコントラストの高い光の下にある、実体のある物体を描写します。 - カバーの見出しバッジは、検索クエリと年または形式のバッジを使い、引用符で囲んだ3〜6語にします。 X と LinkedIn の 1.91:1 の OG クロップで切れないよう、フレーム中央の80%の範囲内に収めます。
querybench.dev:"Crisp daylight 3D studio, cobalt Postgres server rack splitting into instant branches, bold headline 'POSTGRES CI BRANCHING', badge '2026 BENCHMARK'."deployfast.dev:"High-contrast isometric shipping container launching from a GitHub terminal into orbit, bold text '11 BEST NEXT.JS HOSTS', badge '2026 TESTED'."
- インラインの比較ラベルは、実在する数値のみを使った、引用符で囲んだ2〜4個のラベルにします。 ラベル内の数値はすべて、記事または
seo/kb/に記載されている必要があります。invoicekit.io:"Split studio scene: tangled red paper receipts labeled '0.8% BILLING FEE' versus a clean cobalt API card labeled '$19 FLAT', crisp daylight."
ステップ 3:ファイルを作成する
画像スクリプトを準備する(リポジトリごとに1回)
scripts/seo-image.mjs が存在しない場合は、作成します。このスクリプトは、生成(パス A)と既存 PNG の圧縮(パス B と C)の両方に対応しているため、すべての画像に同じサイズ処理パイプラインを適用できます。
// scripts/seo-image.mjs
// Generate: node scripts/seo-image.mjs --prompt "<12-22 words>" --out public/blog/<slug>/<keyword-name>.webp
// Compress: node scripts/seo-image.mjs --input /tmp/raw.png --out public/blog/<slug>/<keyword-name>.webp
// Options: --quality 82 (lower it if the file is over budget), --model <openrouter-image-model>
import fs from "node:fs/promises";
import path from "node:path";
import { parseArgs } from "node:util";
import sharp from "sharp";
const { values: a } = parseArgs({
options: {
prompt: { type: "string" },
input: { type: "string" },
out: { type: "string" },
quality: { type: "string", default: "82" },
model: { type: "string", default: "google/gemini-2.5-flash-image" },
},
});
if (!a.out || (!a.prompt && !a.input)) {
console.error("Usage: --prompt <text> | --input <file>, plus --out <path.webp|.jpg>");
process.exit(1);
}
let raw;
if (a.input) {
raw = await fs.readFile(a.input);
} else {
if (!process.env.OPENROUTER_API_KEY) {
console.error("OPENROUTER_API_KEY is not set");
process.exit(1);
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: a.model,
messages: [{ role: "user", content: a.prompt }],
modalities: ["image", "text"],
image_config: { aspect_ratio: "16:9" },
}),
});
const data = await res.json();
const url = data?.choices?.[0]?.message?.images?.[0]?.image_url?.url;
if (!res.ok || !url) {
console.error(`Generation failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
process.exit(1);
}
raw = Buffer.from(url.replace(/^data:image\/\w+;base64,/, ""), "base64");
}
await fs.mkdir(path.dirname(a.out), { recursive: true });
const q = Number(a.quality);
const img = sharp(raw).resize({ width: 1536, withoutEnlargement: true });
const buf = /\.jpe?g$/i.test(a.out)
? await img.jpeg({ quality: q, mozjpeg: true }).toBuffer()
: await img.webp({ quality: q }).toBuffer();
await fs.writeFile(a.out, buf);
const { width, height } = await sharp(buf).metadata();
console.log(`${a.out} ${width}x${height} ${Math.round(buf.length / 1024)} KB`);
リポジトリのパッケージマネージャーで sharp をインストールします。pnpm add -D sharp(pnpm-lock.yaml)、bun add -d sharp(bun.lock)、yarn add -D sharp(yarn.lock)、それ以外の場合は npm i -D sharp を実行します。リポジトリに package.json がない場合(Hugo、Jekyll)は、スクリプトを /tmp/seo-image/ に配置し、そこで npm init -y && npm i sharp を実行して、絶対パスの --out を渡します。これにより、リポジトリに未使用の Node ツールが追加されません。
OPENROUTER_API_KEY が設定されていない場合は、エージェント内蔵の画像ツールで生成し、PNG を /tmp/ に保存して、スクリプトを --input モードで実行します。
パス A:生成する
node scripts/seo-image.mjs --prompt "<prompt from Step 2>" --out public/blog/<slug>/<keyword-name>.webp
サイトの OG パイプラインが WebP を受け付けない場合、カバーには .jpg を使い、それ以外の場所では .webp を使います。
パス B:実際のスクリーンショット
npx playwright install chromium # first run only
npx playwright screenshot --viewport-size "1440, 900" --wait-for-timeout 1500 \
"https://querybench.dev" /tmp/raw-shot.png
node scripts/seo-image.mjs --input /tmp/raw-shot.png --out public/blog/postgres-tools/querybench-explain-analyze-ui.webp
パス C:決定論的なチャート
記事または seo/kb/proof/benchmarks/*.md に記載された数値を使い、自己完結した 1536x864 の SVG を作成します。棒の長さは数値から計算し、最も長い横棒は 760px を上限とします。これにより、右側の値ラベルが 1536px の端で切れません。すべての値をラベルとして表示し、font-family="system-ui, -apple-system, sans-serif" を指定して、明るい背景に暗い文字を配置します。/tmp/<name>.svg に保存し、--input /tmp/<name>.svg で変換します。これにより、チャート上のすべての数値がソースと正確に一致します。
ステップ 4:すべての画像を見返す
保存したらすぐに、エージェントのファイル表示ツールで生成したファイルをすべて開きます。画像モデルはスペルミス("BENCHMMARK")や文字の重複を起こしたり、端でテキストを切り取ったりします。
- 引用された単語を一文字ずつ確認し、フレーム内に完全に収まっていることを確かめます。
- テキストの文字化け、ぼやけ、重複、切り取りがある場合、または暗いアイコンボックスのレイアウトになっている場合は、ファイルを削除し、12〜16語のプロンプトで再生成します。2回試しても失敗したら、その枠はテキストなしモードに切り替えます。
ステップ 5:画像 SEO を徹底して組み込む
- キーワードを含むケバブケースのファイル名で、対象を表します(
postgres-zfs-branch-spinup-benchmark.webp)。image1.png、hero.webp、screenshot-2026.png、UUID は使いません。ファイル名は画像検索が読み取る数少ないテキストシグナルの一つだからです。 - 80〜160 文字の
altテキストで、記事内の用語を使って画像の内容を説明します。"Image of"、"Picture of"、"An infographic showing"で始めないでください。スクリーンリーダーはすでに画像であることを読み上げます。- 悪い例:
"An infographic of database branching speed"(43 文字、汎用的)。 - 良い例:
"Comparison of 50GB Postgres CI branch creation times showing ZFS copy-on-write cloning in 1.8 seconds versus 14 minutes for pg_restore"(134 文字)。
- 悪い例:
- **明示的な
widthとheight**を、スクリプトが出力した固有のピクセル寸法に設定します。ブラウザーがスペースを確保し、レイアウトがずれないよう、すべての<img>と<Image>に設定します。 - 読み込み優先度:
- カバー画像/スクロールせずに見える範囲のヒーロー画像のみ:LCP 要素であるため、
priority(Next.js)またはfetchpriority="high"+loading="eager"を指定します。 - スクロールしないと見えない位置にあるその他すべての画像:
loading="lazy"とdecoding="async"を指定します。本文内の画像にpriorityを指定しないでください。ヒーロー画像と帯域幅を取り合います。
- カバー画像/スクロールせずに見える範囲のヒーロー画像のみ:LCP 要素であるため、
- サイズ上限:
- カバー画像:幅 1200px 以上、350 KB 以下にします。また、X、LinkedIn、Slack、Google Discover で大きなカードとして表示されるよう、frontmatter(
image/cover/openGraph.images)にも設定します。 - インライン画像:220 KB 以下、
.webpにします。上限を超える場合は、--quality 72、続いて--quality 62を指定して再実行します。
- カバー画像:幅 1200px 以上、350 KB 以下にします。また、X、LinkedIn、Slack、Google Discover で大きなカードとして表示されるよう、frontmatter(
- **配置:**インライン画像は、
<h2>セクションの最初の段落の後に配置します。見出しと最初の文の間には配置しません。強調スニペットの抽出でも読者にとっても、見出しの直下にテキストで回答があることが望ましいためです。
## How much faster is ZFS copy-on-write branching than pg_restore in CI?
Cloning a 50.4 GB Postgres 16 database with `pg_restore -j 4` takes a median of 14 minutes and 22 seconds in GitHub Actions. A ZFS copy-on-write snapshot boots the same 180-million-row dataset in 1.8 seconds.
<Image
src="/blog/postgres-ci-testing/postgres-zfs-branch-spinup-benchmark.webp"
alt="Benchmark comparison showing QueryBench ZFS copy-on-write booting a 50GB Postgres branch in 1.8 seconds compared to 14 minutes for pg_restore"
width={1536}
height={864}
loading="lazy"
decoding="async"
/>
ステップ 6:完了前に監査する
dir=public/blog/<slug>; post=content/blog/<slug>.mdx
for f in "$dir"/*; do
n=$(basename "$f"); kb=$(( $(wc -c < "$f") / 1024 ))
echo "$kb KB $n"
[ "$kb" -gt 350 ] && echo " FAIL: over 350 KB"
[ "$kb" -gt 220 ] && echo " CHECK: only the cover may exceed 220 KB"
echo "$n" | grep -qiE '^(image|img|hero|screenshot|untitled)[-_.0-9]|^[0-9a-f]{8}-' && echo " FAIL: generic filename"
done
grep -oE 'alt="[^"]*"' "$post" | awk '{ l = length($0) - 6; s = (l < 80 || l > 160) ? "FAIL" : "ok"; print s, l, $0 }'
完了の条件:
- 画像ファイルがフレームワークの画像ディレクトリに存在し、記事ファイルから参照されている。
- 各枠で適切なパスを使用している:製品にはスクリーンショット、数値には SVG チャート、カバーと概念図には短いプロンプト。
- 生成したすべての画像を確認し、スペルミスや切り取りがない。
- 監査で
FAILが出力されず、220 KB を超えるファイルはカバー画像のみ。 - カバー画像が frontmatter/OG メタデータに設定されている。インライン画像は
width、height、loading="lazy"とともに、各<h2>の最初の段落の後に配置されている。