文章は簡単な部分:AIが書いた記事に挿絵を入れるパイプライン
テキスト生成は解決済みだ。挿絵はそうではない。ルータープロンプト、写真検索、Mermaid図、検証済みグラフ、そしてそれをSEOに変えるalt テキスト・クレジット・ImageObjectマークアップまで——エンドツーエンドのパイプライン。
あなたは開発者で、コンテンツ目標を抱えている——月に数十本、場合によっては数百本の記事だ。執筆モデルが下書き、アウトライン、メタディスクリプション、内部リンクを処理する。そしてパイプラインは、それ自体にモデルを持たない唯一の工程——画像——に突き当たり、止まる。画像の入手が難しいからではなく、どの画像を、どこから、どんなライセンスで、どのように記述するかを、スタックの中の何一つ知らないからだ。
この記事は、その欠けている工程をエンドツーエンドで組み上げるものだ。どの種類のセクションにどの種類のビジュアルを生成すべきか、完成した原稿を検索ブリーフに変えるプロンプト、写真を返すAPI呼び出し、写真では表現できないものを描く第二のモデル、そしてGoogleが実際に読み取れるマークアップを出力する組み立て工程まで。最後には、そのまま流用できる2つの完全なワークフローを示す。
テキストは解決済み。詰まるのはイラストレーションだ。
自動化された記事パイプラインを正直に監査してみよう。アウトライン:解決済み。下書き:解決済み。タイトル、メタディスクリプション、スキーマ、内部リンク、翻訳:すべて解決済みで、すべて同じモデルによって、すべてテキストの形で処理される。そして次に:
| パイプラインの工程 | 状況 | 実際に何がブロックしているか |
|---|---|---|
| アウトラインと下書き | 解決済み | モデル呼び出し1回、プロンプト1つ |
| タイトル、メタ、スキーマ、リンク | 解決済み | テキストを入力し、テキストが出力される |
| ヒーロー画像 | ブロック中 | 実ファイル、ライセンス、寸法、alt テキストが必要——いずれもテキストモデルでは生成できない |
| セクション画像 | ブロック中 | 記事あたり3〜5枚、それぞれ異なり、サイト内で重複しない |
| チャートと図 | ブロック中 | 正確でなければならない——検索では返せず、生成モデルが創作してはならない唯一のビジュアル |
この失敗は美的なものではなく、構造的なものだ。記事はストックのプレースホルダー画像とともに公開されるか、直近12本の記事と同じ写真を使うか、あるいは指が6本ある手が真っ先に目に入る生成画像とともに公開される。そして画像は装飾ではない——Google自身の画像に関するドキュメントはこう明言している。alt テキストは「画像のメタデータを提供する際に最も重要な属性」であり、画像を発見・理解可能にするためには、CSS背景ではなく、説明的なaltを伴う実際の<img>要素を使うべきだとしている。1
4種類のビジュアル、4種類のモデル
最大の設計上の誤りは、「画像」を単一のプロバイダーで解決する単一の問題として扱うことだ。実際には4つの問題であり、それらの間を振り分けるルーターは、サービスではなく1行のプロンプトにすぎない。
| そのセクションに必要なのは… | 生成手段 | 他の手段ではだめな理由 |
|---|---|---|
| 現実世界の場面ヒーロー画像、人物の状況、場所、物、しぐさ | セマンティック写真検索(Pexafy) | 生成モデルは細部を創作してしまい、チャートには描画するデータがない |
| 実際に持っている数値ベンチマーク、価格、調査結果、レイテンシ | サンドボックスで実行される、プロットコードを書くモデル | 画像モデルに数値を任せることはできず、写真ではデータを表現できない |
| 構造やフローアーキテクチャ、シーケンス、状態遷移 | Mermaid/Graphvizを書き、決定的にレンダリングするモデル | 無料の写真ライブラリには自社システムの図など存在しない |
| 画面上の自社プロダクトドキュメント、変更履歴、チュートリアル | スクリプト化されたブラウザスクリーンショット(Playwright) | 自社のビルドにしか存在しないUIを表示できるものは他にない |
では生成イラストは? それは一つの正直な役割を担う。写真に撮れず、データでもないもの—— 抽象的な仕組み、まだ存在しない製品、あなた自身が所有する家のイラストスタイル。 (生成に対して実写を推す完全な議論——量における速度、正確さ、同質化問題——は こちらで展開している。) 流れの方向に沿って価格を捉えるなら:2026年8月2日以降、 EU AI法の第50条は生成システムの提供者に対し、合成された出力を機械可読な形式で マークすることを義務付けている。2 これはAIの提供者・導入者に 対する義務であって、ブログが何を公開してよいかを定めるルールではない——しかし、まさにそれゆえに、 記事冒頭の画像の出自は、読者が信頼するしかないものから、確認できるものへと変わりつつある。
パイプライン全体像
5つの段階がある。画像APIに触れるのは第3段階だけで、第4段階だけが任意だ:
┌─ 1. WRITE ────────────────────────────────────────────────────┐
topic ──▶ LLM ──▶ draft.md (h2 sections, front-matter)
└───────────────────────────────┬───────────────────────────────┘
┌─ 2. BRIEF ────────────────────┴───────────────────────────────┐
draft.md ──▶ LLM ──▶ { hero: {...}, sections: [ {...} ] }
one JSON object: per slot, a kind +
either a camera brief or a data/diagram spec
└──────────┬──────────────────────────────────┬─────────────────┘
│ kind = "photo" │ kind = "chart" | "diagram"
▼ ▼
┌─ 3. SEARCH ───────────────┐ ┌─ 4. DRAW (optional) ─────────┐
GET /search/photos LLM ──▶ mermaid | plotting code
← credited photo + ──▶ sandbox ──▶ .svg / .png
w/h, blur_hash, alt, (deterministic render, no
licence, source URL invented numbers)
└──────────┬────────────────┘ └──────────────┬───────────────┘
└───────────────┬──────────────────┘
┌─ 5. ASSEMBLE ─────────────┴───────────────────────────────────┐
<img> with width/height + fetchpriority | loading
alt written for a human · visible credit · ImageObject JSON-LD
photo_id stored so no two pages share a hero
└───────────────────────────────────────────────────────────────┘
この図よりも重要な性質が2つある。第2段階はルーターであるということ——スロットごとにどの生成手段を使うかを決定するので、写真ライブラリに棒グラフを求めるようなことは決して起こらない。そしてSEOが宿るのは第5段階だ——Googleが画像について文書化している事項(説明的なalt、ライセンスメタデータ、LCPに安全なヒーロー画像)はすべてここで出力され、その材料はすでに検索レスポンスに含まれていたフィールドだ。
第2段階:下書きをビジュアルブリーフに変換する
完成した原稿に対する1回のモデル呼び出しが返すのは、クエリではなく計画である。 1枚の写真ブリーフをどう書くか——記事タイトルが最悪の入力である理由、12〜25語のカメラブリーフが どのようなものか、そしてそれがどう失敗するか——は 1記事へのイラスト付けガイド で詳しく述べており、ここでは繰り返さない。パイプラインが追加するのはルーティングである。 同じ呼び出しがスロットごとに、どのプロデューサーを動かすかを決めなければならず、 チャートのエントリはシーンではなくデータを運ぶ。
# system prompt — run once per finished draft
You are the art director of a technical publication. Read the article and
return the visual plan: one entry for the hero, one per H2 section.
For each entry choose exactly one kind:
"photo" a real scene: someone doing something somewhere, a place,
an object, a gesture. The default for heroes.
"chart" the section states numbers that are IN the article. Never
invent values: copy them into data, verbatim.
"diagram" the section describes a structure, a flow or a sequence.
"none" the section is short, or already carries a code block.
Rules for "photo" entries — the field is query:
カメラブリーフを書いてください。カメラが撮影できるであろうシーンを、
英語で12語から25語で、そのセクションの雰囲気に合わせて記述します。
フレームの中に写っているものを名指しし、トピックそのものは書かない
こと。文字、ロゴ、ブランド、有名人は不可。目に見えない比喩も不可。
(完全なルール、例と失敗例付き:
pexafy.com/blog/illustrate-blog-articles-at-scale/)
Do NOT write the alt text of a photo entry: the picture you get back is the
closest match to the brief, not the scene you described, so its alt has to be
written from the chosen photo. Chart and diagram entries DO carry an alt —
there you control exactly what is rendered.
Return JSON only:
{
"hero": { "kind": "photo", "query": "…", "orientation": "landscape" },
"sections": [
{ "h2": "…", "kind": "photo", "query": "…" },
{ "h2": "…", "kind": "chart", "title": "…", "unit": "ms",
"data": [ {"label": "…", "value": 0} ], "alt": "…" },
{ "h2": "…", "kind": "diagram", "spec": "flowchart LR; …", "alt": "…" }
]
}
kind の行が仕事の大半を担い、data の指示が残りを担う。チャートのエントリは
原稿に既に登場している数値しか持てないため、モデルは書き起こすのであって、創作するのではない。
以下は、開発者向け記事の3つの実際のセクションに対するルーターの出力である。
| セクション | kind | ルーターが返した内容 |
|---|---|---|
| ヒーロー——「夜間ビルドが40分かかる理由」 | photo |
「暗い部屋で画面の光に照らされながら、2台のモニターとメカニカルキーボードのあるデスクで夜遅くまで作業する開発者」 |
| 「実際に時間がどこに消えているのか」 | chart |
段落から転記されたdata:インストール480秒、コンパイル1080秒、テスト720秒、アップロード120秒 |
| 「グラフをどう分割したか」 | diagram |
ジョブ依存関係グラフのflowchart LR |
| 「変更した内容と、次回もそうする理由」 | photo |
「図でいっぱいのホワイトボードの前に立ち、一緒に問題に取り組む2人のエンジニア」 |
第3段階:写真はクレジット付きで返ってくる
kind: "photo"のエントリはそれぞれ1回のリクエストになる。上記のヒーローブリーフを公開APIに対して実行すると、147ミリ秒でこれが返ってくる:
記事の最後のセクションのブリーフ——まったく異なる場面——は144ミリ秒で返ってきた:
写真ごとに返ってくる内容こそが、第5段階を可能にする部分だ——単なるファイルではない:
{
"photo_id": "019e1c7f-0063-759e-b498-33ce1714e6c9", // store it: no repeats
"urls": { "small": "…?w=400", "regular": "…?w=1080",
"large": "…?w=1920" },
"width": 3000, "height": 1688, // → no layout shift
"blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na", // → real placeholder
"alt_description": "Person types on keyboard in front of dual monitors…",
"photographer_full_name": "Jakub Żerdzicki", // → ImageObject.creator
"source": "Unsplash", "license_type": "free",
"source_image_url": "https://unsplash.com/photos/…",
"attribution": { "html": "<span…>Photo by …</span>",
"plain": "Photo by Jakub Żerdzicki on Unsplash (…)" }
}
第4段階:写真では表現できないもの
プランの中の2つのスロットは検索できるものではなく、これはまさに第2のモデルがその存在価値を発揮する場面だ——絵を描くためではなく、それを描くコードを書くためだ。この違いは重要だ。コードはレビュー可能で決定的であり、棒グラフの高さを幻覚することはない。
図:テキストを入力すると、SVGが出力される
Mermaidはプレーンテキストの定義から図をレンダリングする。3これによって、モデルにとって最も安全な出力先になる——検査可能で、gitでdiffが取れ、常に同じようにレンダリングされるからだ。ルーターはすでにspecを返している。
# the "spec" field of a diagram entry, written to build/graph.mmd
cat build/graph.mmd
flowchart LR
install["install deps · 480s"] --> compile["compile · 1080s"]
compile --> test["test suite · 720s"]
compile --> upload["upload artifacts · 120s"]
npx -y @mermaid-js/mermaid-cli -i build/graph.mmd -o static/img/graph.svg
# → an SVG you can review in the PR, not a picture you have to trust
チャート:記事にすでに存在する数値のみ
原則は同じだが、追加のガードが1つある。ルーターは下書きから値を転記し、モデルはプロットコードを書き、コードはサンドボックスで実行され、組み立て工程はチャートがページに近づくことを許可する前にレンダリングされた値を元の数値と照合し直す。
import re, matplotlib
matplotlib.use("Agg") # headless: no display in CI
import matplotlib.pyplot as plt
def assert_in_draft(value, draft: str) -> None:
"""A plotted number must appear in the article AS A NUMBER."""
# Substring matching is the trap here: "120" is inside "1200", and
# inside "?w=1200" — a naive `str(v) in draft` passes on anything.
# Match on word boundaries, and accept 1 234 / 1,234 / 1234.
body = re.sub(r"(?<=\d)[ ,](?=\d{3}\b)", "", draft) # strip separators
if not re.search(rf"(?<![\d.]){re.escape(str(value))}(?![\d.])", body):
raise ValueError(f"{value} is not stated in the article — refusing to plot")
def render_chart(entry: dict, draft: str, out: str) -> str:
labels = [d["label"] for d in entry["data"]]
values = [d["value"] for d in entry["data"]]
for v in values:
assert_in_draft(v, draft) # hallucinated value → no chart
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=160)
ax.barh(labels, values)
ax.set_xlabel(entry["unit"])
ax.set_title(entry["title"])
fig.tight_layout()
fig.savefig(out) # deterministic, reviewable artefact
return out
このガードは短いが、慎重に書く必要がある。部分文字列チェックはうまくいかない。"120" in draftは、1200や?w=1200を含む記事に対しても真になる。つまり素朴な実装はすべてを通過させてしまい、何も守らない。数字の境界で固定し、桁区切りを正規化すれば、コメント欄で技術記事が槍玉に上がるような種類のエラー——チャートが自分自身の段落と矛盾すること——は本番環境に到達できなくなる。スクリーンショットも同じ原則に従う。実際のビルドに対してスクリプト化されたpage.screenshot()を実行することが、自社UIについての唯一の真実の情報源であり、UIが変わってもその原則は成立し続ける。
第5段階:組み立て工程こそがSEOを左右する
ここまでの工程はファイルとフィールドを生成しただけだ。この段階でそれらをマークアップに変換する——ここは正確に説明する価値がある。というのも、文書化された3つの挙動がこのわずか数行で決まるからだ。
<!-- The bytes come from another origin: pay the handshake early -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>
<!-- LCP element: never lazy, always high priority -->
<figure>
<img src="{urls.regular}"
width="{width}" height="{height}" <!-- kills layout shift -->
alt="{alt}" <!-- written AFTER the pick -->
fetchpriority="high" decoding="async"
style="background:{color_hex}"> <!-- dominant colour, 1 field -->
<figcaption>{attribution.html}</figcaption>
</figure>
<!-- Section images, below the fold: the opposite settings -->
<img src="{urls.regular}" width="{width}" height="{height}"
alt="{alt}" loading="lazy" decoding="async">
ホットリンクか、再ホストか?上記のスニペットはホットリンクを使っており、これは公開までの最速の方法であり、preconnectを使う理由でもある——リモートのヒーロー画像は、クリティカルパス上でDNSルックアップとTLSハンドシェイクのコストを伴い、それがfetchpriorityで得た効果を食いつぶすこともある。再ホストすればサードパーティのオリジンを完全に排除でき、自社のブレークポイントでAVIF/WebPを配信でき、上流のURLが変わっても影響を受けない——ただし、その代償としてストレージ、パイプライン内のフェッチ工程、自社のCDN料金が発生する。どちらを選ぶにせよ、color_hexは1つのフィールドとしてプレースホルダーを提供する(ブラーハッシュの方が見栄えは良いが、まずデータURIにデコードする必要があり、CSSの色ではない)。ざっくりしたヒーロー画像をそのままbackground:にコピーしてしまうと、多くのパイプラインが静かに空のグレーのボックスを公開してしまう。
-
ヒーロー画像は決して遅延読み込みしない。web.devは明確にこう述べている。「LCP画像を遅延読み込みしてはならない。それは常に不要なリソース読み込み遅延につながり、LCPに悪影響を及ぼす」。そして、LCPになりそうな要素には
fetchpriority="high"を推奨している——ただし、控えめに、1つの画像にのみ使うべきだとしている。4すべての画像に、ヒーロー画像も含めてloading="lazy"を貼り付けるパイプラインは、自動化された公開作業において最もよくある自傷的なCore Web Vitalsの傷だ。 -
widthとheightを必ず出力すること——これらはレスポンスに含まれているので、出力しない言い訳はない。この属性のペアだけで、ブラウザがスペースを確保でき、レイアウトの跳ねを止められる。ファイルが読み込まれている間のプレースホルダーにはblur_hashを使う。 -
altは選定後に書く。選定前ではない。これは微妙な点だ。プランの
altフィールドは、あなたが依頼した場面を記述しているが、実際に得られた写真は、その場面に最も近いだけで、その場面そのものではない。ブリーフのテキストをそのままaltとして出荷することは、まさにこのパイプラインが避けようとしているアクセシビリティの失敗そのものだ——ページ上に存在しない写真の説明文になってしまう。選ばれた写真のalt_descriptionを土台にaltを構築し、それが置かれる段落に照らして精緻化すること。Googleのガイダンスは「キーワードを適切に使用し、ページの内容の文脈に沿った、有用で情報豊富なコンテンツの作成に注力する」ことを求めており、altの属性にキーワードを詰め込むと「ユーザー体験を損ない、サイトがスパムと見なされる原因になり得る」と警告している。1
ほとんど誰も自動化していない部分:ライセンスメタデータ
Googleは画像ライセンスのためのImageObject構造化データをサポートしている。これにはcontentUrlに加え、creator、creditText、copyrightNotice、licenseのうち少なくとも1つが必要で、acquireLicensePageが推奨されている。ライセンス情報を持つ画像は、Google画像検索のLicensableバッジの対象になる。5これらのフィールドはすべてすでに検索レスポンスに含まれている——つまり、これを出力することはプロジェクトではなく、テンプレートの問題にすぎない。
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "ImageObject",
"contentUrl": "{urls.large}", // required
"creator": { "@type": "Person",
"name": "{photographer_full_name}" },
"creditText": "{photographer_full_name} on {source}",
"license": "{LICENSE_URL[source]}", // the library's own page
"acquireLicensePage": "{source_image_url}" // the photo's page
}
</script>
# LICENSE_URL maps the `source` field to the licence that actually governs
# the photo — unsplash.com/license, pexels.com/license, pixabay.com/…
正しくしておく価値のある細部が1つある。licenseは、その写真を実際に統治するライセンス——ソースライブラリ自身のライセンスページ——を指すべきであり、自社ドメイン上の要約ページを指すべきではない。Googleはこれを読んでバッジの対象かどうかを判断するため、自己参照的なURLはシグナルとして弱いだけでなく、自社への単なるリンクバック以外の何物でもないと弁明することも難しい。読者向けの内部ページとして自社のライセンス要約は残しておき、マークアップには正典となるURLを入れること。
組み立て工程に手を入れている間に、もう1つ——ファイルにはIMG_0042.jpgではなく短く説明的な名前を付け、画像をサイトマップに追加すること。Googleの画像サイトマップ形式は、1つのページURLあたり最大1,000枚の画像を受け付ける。6どちらもパイプライン内では1行の作業だが、手作業では決して実施されない。
流用できる2つのパイプライン
同じ5段階だが、形は大きく異なる2種類——1つは自分で書く記事向け、もう1つは稼働中のプロダクトと一致していなければならないドキュメント向けだ。自分が見覚えのある失敗モードの方を選んでほしい。
1・CI内の開発者ブログ——リポジトリ内のMarkdown
記事はMarkdownとして存在し、画像はその隣にコミットされ、全体がpush時に実行される。決定的で、PR内でレビュー可能、実行時にどのAPIにも依存しない:
on: { pull_request: { paths: ["content/**.md"] } }
jobs:
illustrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python plan_to_pr.py $(git diff --name-only origin/main -- 'content/*.md')
env:
PEXAFY_API_KEY: ${{ secrets.PEXAFY_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- uses: peter-evans/create-pull-request@v6 # images land in the PR
with: { commit-message: "chore(content): illustrate" }
それでも人間がPRを承認する——これが要点だ。パイプラインは提案し、レビュアーが判断する。生成されたフロントマターはdiff可能だ。
検索部分は1回の GET であり——そのリクエスト、score_threshold、そして
返されるフィールドは
単一記事向けガイド
に1行ずつ書き出してあるため、ここでは再掲せず取り込むだけにする。このファイルが追加するのは、
計画が必要とし、1枚の写真が必要としないすべてのもの——何も返らなかったブリーフに対する
リトライ、2ページが同じ写真を共有しないようにする photo_id の確保、そして返ってきた
写真から書かれるalt——である。
import frontmatter
from photo_search import search # GET /search/photos を1回呼ぶ。`used` にあるIDはスキップ
def find_photo(entry: dict, used: set) -> dict | None:
"""Search; if the brief was too specific, widen it once, then give up."""
orientation = entry.get("orientation", "landscape")
for query in (entry["query"], widen(entry["query"])):
photo = search(query, orientation, used)
if photo:
used.add(photo["photo_id"]) # claim it: no repeats site-wide
return photo
return None # caller decides: skip the slot, or fail
def widen(query: str) -> str:
"""Drop the last clause — usually the over-specific one."""
return query.rsplit(" in ", 1)[0] if " in " in query else query
def illustrate(path: str, plan: dict, used: set) -> None:
post = frontmatter.load(path)
hero = find_photo(plan["hero"], used)
if hero is None: # no hero is better than a bad one
raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")
post["hero"] = { # everything the template needs
"src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
# The alt describes the photo we GOT, never the scene we asked for.
"alt": alt_for(hero, section=post.get("title", "")),
"bg": hero["color_hex"], "credit": hero["attribution"]["html"],
"creator": hero["photographer_full_name"], "id": hero["photo_id"],
"source": hero["source"],
"source_url": hero["source_image_url"], # → acquireLicensePage
}
open(path, "w").write(frontmatter.dumps(post))
def alt_for(photo: dict, section: str) -> str:
"""alt_description as the base, trimmed to ~125 chars for screen readers.
Send it back through the model with the paragraph if you want better."""
base = photo.get("alt_description") or photo.get("description", "")
return base[:125].rstrip(" ,;")
2・ドキュメントと変更履歴——スクリーンショットが先、写真は最後
ルーターのデフォルトを反転させる。プロダクトドキュメントにおいては、正直なビジュアルとはほぼ常に自社のUIそのものだ——実際のビルドを開き、固定のビューポートを設定し、段落が説明している状態を正確にキャプチャするPlaywrightスクリプトだ。図はアーキテクチャページを担当し、写真はコンセプチュアルページやランディングページにのみ登場する——そこではスクリーンショットは何も語らないからだ。
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900},
device_scale_factor=2) # retina-crisp
page.goto("http://localhost:3000/dashboard")
page.get_by_role("button", name="New API key").click()
page.screenshot(path="static/img/docs/new-api-key.png")
browser.close()
# Runs in the same CI job as the docs build → the screenshot can never
# describe a version of the UI that no longer exists.
名前を挙げておく価値はあるが、それ専用のレシピを立てるほどではない2つのバリエーションがある。プログラマティックSEOはループを反転させる——データベースから生成される数百ページでは、ページごとに検索するのではなく、100枚まで返す1回のリクエストを使い、トピッククラスターごとに検索し、プールから割り当てる。一意性の制約が重複排除を担う(そのアーキテクチャの詳細はこちら)。そしてニュースレターとソーシャルカードは同じ写真を4種類の切り抜きで必要とする——photo_idを保持し、必要なサイズをurlsから取得すればよく、その切り抜きが本当に機能しなかったときにのみ再検索する。
同じパイプラインを、1つのエージェントとして
すでにモデルが下書きを書いているのであれば、最短経路はプロセス間でJSONを受け渡しすることではなく、検索ツールをモデルに直接渡すことです。Pexafyは
mcp.pexafy.com/mcpでホスト型のModel Context
Protocolサーバーを稼働させています。コネクタのセットアップとそこで公開されるツールについては別記事で扱っています——デスクトップおよびエディタでのセットアップは
単一記事向けガイドで、
CIランナー向けのヘッドレス版は
大規模コンテンツに関する記事で、
そしてより広いコネクタ市場の全体像——誰がどのような条件でMCP画像サーバーを提供しているか——については
AIエージェント向け画像検索インフラの調査記事で解説しています。
ここで示す価値があるのは、エージェントがツールを保持した時点でこのパイプラインに何が起きるかです。5段階だったものが、1つの指示に変わります。
You Here is the draft. Build the visual plan, illustrate it, and open a PR.
Charts only from numbers already in the text.
Agent → plan: hero=photo · §2=chart · §3=diagram · §4=photo
→ search_photos(q="a developer working late at a desk with two
monitors and a mechanical keyboard in a dark room…")
← 16 photos · 147 ms · picked #1, 3000×1688, credited
→ mermaid-cli build/graph.mmd → static/img/graph.svg
→ chart.py §2 → 4 values checked against the draft ✓
→ search_photos(q="two engineers standing at a whiteboard…")
← 16 photos · 144 ms · picked #1
✓ 4 slots filled · 2 API calls · alt + credit + ImageObject written
⚠ §5 returned nothing above 0.5 — brief too abstract, rewritten as
"a person at a kitchen table checking figures on a laptop"
最後の1行こそが、単なるスクリプトではなく、このループにエージェントを組み込む価値がある理由だ。自動化されたイラストレーションの失敗モードとはブリーフが悪いことであり、悪いブリーフを書き直すこと自体が、まさに言語モデルの得意分野だ。割り当て、重複排除、数値ガードといった決定的な部分はコードのまま残しておく。
イラスト付き記事1本あたりのコスト
記事あたり3つの写真スロットは3回の検索リクエストを意味する。したがって無料プラン(月間5,000リクエスト)は、支払いの問題が生じるよりも先に月間1,666本の記事をカバーする——そして、記事ごとではなくトピッククラスターごとに検索をプールすれば、この上限はさらに1桁動く。プラン別の内訳と、それを無意味にするプーリングアーキテクチャについては大規模コンテンツのイラスト作成に関する併載記事に書かれている。
記事あたり2回のモデル呼び出し——1回は下書き、もう1回はビジュアルプランのため——は数千トークンで済み、パイプライン中で最も安い部分になるだろう。MermaidとMatplotlibのレンダリングはCIの秒数以外何もコストがかからない。人々を驚かせる数字は、どの行が安くないかということだ——記事あたり4枚の画像、月間400枚の画像、加えて没になった試行分——そして、その出力にはいまだに撮影者もなく、日付もなく、ソースURLもない。
このパイプラインが解決しないこと
正直に述べるべきセクションだ。というのも、これが防ぐ失敗にはコストがかかるからだ。よくイラストレーションされた記事は、それでも記事にすぎない。実際の写真、正確なチャート、正しいマークアップは、存在する価値のあるページを改善する。しかし、薄く量産されたコンテンツをランクさせるわけではない。Googleのスパムポリシーはスケールされたコンテンツの悪用を名指ししている——ランキングの操作を主目的として多数のページを生成し、ユーザーにほとんど価値を提供しないこと、自動化が関与しているかどうかを問わず——そしてイラストレーションの品質は、その判断における要素ではない。7
したがって、成立する枠組みはこうだ。このパイプラインは、すでに公開される理由を持つページに適用される自分でコントロールできる品質の下限だ。これが明らかに報われる場面は:
- 読者が検証できる出所。実在の写真家とソースURLを伴うクレジット表記は、確認可能な主張であり、同じフィールドがGoogleが読み取る
ImageObjectマークアップにも供給される。 - アクセシビリティとCore Web Vitals。本物のaltテキスト、すべての画像に対する寸法情報、決して遅延読み込みされないヒーロー画像。これを公開するすべての記事にかけ合わせれば、それこそがサイトの画像品質の物語になる。
- 検証可能な範囲での正確さ。数値が記事に対して検証されるチャート、稼働中のビルドから生成されるスクリーンショット、PR内でテキストとしてレビュー可能な図——これら3つのビジュアルは、テストが失敗しない限り真実からずれることがない。
帰属表示について、このパイプライン特有の論点は狭い。
クレジットを常に
表示すべき理由は他で論じており、自動アセンブラが追加するのは、それが出力する
attribution 文字列が、構造化データが必要とする creditText と
同一である、という点だ。1つのフィールドが、2つの場所に、同じテンプレートの1回の処理で出力される——
だからこそ、パイプラインには、人間よりもさらに省く理由がない。
どこから始めるか
- ルータープロンプトを追加する——すでに下書きを書いているものに組み込み、それに基づいて動作させることなくJSONを出力させてみる。10件のプランを読む。ブリーフが場面ではなくトピックを名指ししているなら、統合コードを書く前にプロンプトを直すこと。
- 写真スロットだけを配線する。ブリーフごとに1回の
GET /search/photosを行い、初日からphoto_idを保存すること——3,000ページに及ぶ稼働中のサイトに後から重複排除を組み込むのは、1つのカラムを追加する程度の話ではなく、マイグレーションになってしまう。 - width、height、altを同じコミットで出力する。これはあなたが行うCore Web Vitals対応の中で最も安いものだ。
- 次に第4段階を追加する。チャートより先に図から始めること——Mermaidはテキストなので、構文エラー以外の失敗モードを持たない唯一のものだ。
- 数値ガードを追加する——最初のチャートが読者に届く前に、届いた後ではなく。
出典と脚注
1 Google Search Central、画像SEOのベストプラクティス:altテキストは「画像のメタデータを提供する際に最も重要な属性」であり、ガイダンスは「キーワードを適切に使用し、ページの内容の文脈に沿った、有用で情報豊富なコンテンツ」を作成すること、キーワードを詰め込んだalt属性を避けること、CSS画像ではなくHTMLの<img>要素を使うこと、ファイルには短くも説明的な名前を付けることを求めている。
2 EU AI法第50条——2026年8月2日から適用される透明性義務:合成画像、音声、動画、テキストを生成するシステムのプロバイダーは、出力を機械可読な形式でマークし、人工的に生成されたものとして検出可能にしなければならない。これはAIプロバイダーとデプロイヤーを拘束するものであり、ウェブサイトがどの画像を公開してよいかを定めるルールではない。
3 Mermaidは、Markdownに着想を得たテキスト定義から図やチャートをレンダリングする。これにより、出力がレビュー可能かつ決定的になる。
4 web.dev、Largest Contentful Paintの最適化:「ページのLCP要素になりそうな<img>要素にはfetchpriority="high"を設定するのがよい」——ただし控えめに使うべきだとしている。そして「LCP画像を遅延読み込みしてはならない。それは常に不要なリソース読み込み遅延につながり、LCPに悪影響を及ぼす」。
5 Google Search Central、画像メタデータ(構造化データ):ImageObjectはcontentUrlに加え、creator、creditText、copyrightNotice、licenseのうち少なくとも1つを必要とする。acquireLicensePageが推奨されており、ライセンス情報を持つ画像はGoogle画像検索のLicensableバッジの対象になり得る。
6 Google Search Central、画像サイトマップ:画像サイトマップは、JavaScriptによって発見された画像を含め、サイト上の画像についてGoogleに通知するものであり、1つのページURLあたり最大1,000枚の画像を受け付ける。
7 Google検索のスパムポリシー——スケールされたコンテンツの悪用:自動化、人間の作業、またはその組み合わせのいずれによって作成されたかを問わず、ランキングの操作を主目的として多数のページを生成し、ユーザーにほとんど価値を提供しないこと。
出典の確認日:2026年8月17日: 画像SEOのベストプラクティス · 画像メタデータ · 画像サイトマップ · LCPの最適化 · 検索スパムポリシー · AI法第50条 · Mermaid · Pexafy API&MCPドキュメント。 検索のタイミング(147ミリ秒、144ミリ秒)と表示されているすべての写真は、同日に取得された実際のAPIレスポンスだ。
よくある質問
LLMが書いた記事に自動で挿絵を入れるにはどうすればいいですか?
GET /api/v1/search/photos、約150ミリ秒)に送ります。グラフと図のエントリは、プロットコードやMermaidを書くモデルに送られ、決定論的にレンダリングされます。記事タイトルを画像検索に入力してはいけません。タイトルは抽象的で、それを描写した写真は存在しないからです。ドキュメントサイトはスクリーンショットとストック写真のどちらを使うべきか?
AIパイプラインがグラフに誤った数値を入れるのを防ぐにはどうすればいいですか?
dataフィールドにコピーできるようにします。そしてレンダリング前にコードでそれをアサートします——各値について、その文字列が記事内に存在するかを確認し、なければ例外を発生させます。9行のコードで、自分自身の段落と矛盾するグラフが読者に届くことは決してなくなります。自動化パイプラインはSEOのためにどんな画像マークアップを出力すべきですか?
alt——Googleはaltテキストを最も重要な画像メタデータと呼び、キーワードの詰め込みを避けるよう警告しています。すべての画像にwidthとheightを設定し、ヒーロー画像にはfetchpriority="high"を付け、決してloading="lazy"を付けないこと。LCP画像は遅延読み込みしてはいけないからです。そしてcontentUrl、creator、creditText、licenseを含むImageObject構造化データ——これがGoogle画像検索のLicensableバッジの対象資格を与えます。AIが書いた記事に挿絵を入れることは、順位向上に役立ちますか?
編集上のコントロールを失わずに、画像挿入ステップをCIで実行するにはどうすればよいか?
photo_idによる重複排除、プロットされたすべての数値が記事内に登場することの検証、そしてスコアしきい値を超える写真が1枚もない場合のハードエラー。誤ったヒーロー画像を使うくらいなら、何も使わない方がよい。