Teks Adalah Bagian Mudah: Pipeline yang Mengilustrasikan Artikel Tulisan AI

Menghasilkan teks sudah terpecahkan. Mengilustrasikannya belum. Pipeline end-to-end — prompt router, pencarian foto, diagram Mermaid, chart terverifikasi, serta alt text, kredit dan markup ImageObject yang mengubahnya menjadi SEO.

Seorang programmer mengetik di meja dengan dua monitor penuh kode, disinari lampu warna-warni.
Foto via Unsplash

Anda seorang developer dengan target konten: puluhan artikel per bulan, mungkin ratusan. Model penulisan menangani draf, outline, meta description, tautan internal. Lalu pipeline itu terhenti pada satu langkah yang tidak punya modelnya sendiri — gambar-gambarnya. Bukan karena gambar sulit didapat, tetapi karena tidak ada bagian mana pun dalam stack yang tahu gambar yang mana, dari mana, dengan lisensi apa, dideskripsikan bagaimana.

Artikel ini adalah langkah yang hilang itu, dirangkai dari ujung ke ujung: jenis visual apa yang harus dihasilkan untuk jenis bagian apa, prompt yang mengubah draf jadi menjadi brief pencarian, panggilan API yang mengembalikan foto-fotonya, model kedua yang menggambar apa yang tidak bisa disampaikan foto, dan assembler yang menerbitkan markup yang benar-benar bisa dibaca Google. Dua alur kerja lengkap di bagian akhir, siap untuk langsung dipakai.

Teks sudah terselesaikan. Ilustrasi adalah titik macetnya.

Coba lakukan audit jujur terhadap pipeline artikel otomatis. Outline: selesai. Draf: selesai. Judul, meta description, schema, tautan internal, terjemahan: selesai, semuanya oleh model yang sama, semuanya dalam bentuk teks. Lalu:

Langkah pipeline Status Apa yang sebenarnya menghambat
Outline & draf Selesai Satu panggilan model, satu prompt
Judul, meta, schema, tautan Selesai Teks masuk, teks keluar
Gambar hero Terhambat Membutuhkan berkas nyata, lisensi, dimensi, dan teks alt — tidak satu pun bisa dihasilkan model teks
Gambar per bagian Terhambat Tiga sampai lima per artikel, masing-masing berbeda, tidak ada yang berulang di seluruh situs
Grafik & diagram Terhambat Harus akurat — satu-satunya visual yang tidak bisa dikembalikan oleh pencarian dan tidak boleh dikarang oleh generator

Kegagalan ini bukan soal estetika, melainkan struktural: artikelnya terbit dengan placeholder stok, atau dengan foto yang sama seperti dua belas artikel sebelumnya, atau dengan gambar hasil generasi yang tangan berjari enamnya menjadi hal pertama yang dilihat pembaca. Dan gambar itu bukan dekorasi — dokumentasi gambar milik Google sendiri menyatakan dengan jelas: teks alt “adalah atribut paling penting dalam menyediakan metadata untuk sebuah gambar”, dan panduannya adalah menggunakan elemen <img> sungguhan dengan alt yang deskriptif, bukan latar belakang CSS, agar gambar bisa ditemukan dan dipahami sama sekali.1

Empat jenis visual, empat jenis model

Kesalahan desain terbesar adalah memperlakukan “gambar” sebagai satu masalah dengan satu penyedia. Padahal ada empat, dan yang merutekan di antaranya adalah satu baris prompt, bukan sebuah layanan:

Bagian ini membutuhkan… Hasilkan dengan Kenapa bukan yang lain
Adegan dari dunia nyatahero, situasi manusia, tempat, objek, gestur Pencarian foto semantik (Pexafy) Generator mengarang detailnya; grafik tidak punya apa-apa untuk diplot
Angka yang benar-benar Anda milikibenchmark, harga, hasil survei, latensi Model yang menulis kode plotting, dijalankan di sandbox Model gambar tidak bisa dipercaya untuk sebuah nilai; foto tidak bisa membawa data
Struktur atau alurarsitektur, urutan, mesin status Model yang menulis Mermaid / Graphviz, dirender secara deterministik Pustaka foto gratis tidak punya diagram sistem Anda
Produk Anda di layardokumentasi, changelog, tutorial Tangkapan layar browser dengan skrip (Playwright) Tidak ada yang lain bisa menampilkan UI yang hanya ada di build Anda

Dan ilustrasi hasil generate? Ilustrasi tetap menyimpan satu slot yang jujur: adegan yang tidak bisa difoto dan bukan data — mekanisme abstrak, produk yang belum ada, atau gaya ilustrasi rumahan milik Anda sendiri. (Argumen lengkap mengapa foto asli lebih unggul dari generasi — kecepatan pada skala besar, akurasi, masalah keseragaman — sudah dijabarkan di sini.) Harga bergerak searah tren: sejak 2 Agustus 2026, Pasal 50 EU AI Act mewajibkan penyedia sistem generatif untuk menandai keluaran sintetis dalam format yang dapat dibaca mesin.2 Itu adalah kewajiban bagi penyedia dan deployer AI, bukan aturan tentang apa yang boleh diterbitkan sebuah blog — tetapi itulah sebabnya asal-usul gambar di bagian atas artikel Anda semakin menjadi sesuatu yang bisa diperiksa pembaca, bukan sekadar dipercaya begitu saja.

Pipeline-nya, dari ujung ke ujung

Lima tahap. Hanya tahap 3 yang menyentuh API gambar, dan hanya tahap 4 yang opsional:

Gambaran besarnya — satu artikel masuk, satu artikel siap terbit keluar
┌─ 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
└───────────────────────────────────────────────────────────────┘

Ada dua sifat yang lebih penting daripada diagramnya. Tahap 2 adalah router: ia memutuskan per slot produser mana yang berjalan, sehingga Anda tidak pernah meminta grafik batang dari pustaka foto. Dan tahap 5 adalah tempat SEO-nya berada — semua yang didokumentasikan Google tentang gambar (alt deskriptif, metadata lisensi, hero yang aman untuk LCP) diterbitkan di sini, dari field yang sudah dibawa respons pencarian.

Tahap 2: mengubah draf menjadi brief visual

Satu panggilan model pada draf yang sudah selesai, dan yang dikembalikannya adalah sebuah rencana, bukan sekadar query. Cara menulis satu brief foto — mengapa judul artikel adalah input terburuk yang mungkin, seperti apa brief kamera 12-sampai-25-kata itu, dan cara-cara ia bisa gagal — dijelaskan lengkap di panduan mengilustrasikan satu artikel, dan tidak diulang di sini. Yang ditambahkan sebuah pipeline adalah routing: panggilan yang sama harus menentukan, slot demi slot, produser mana yang berjalan — dan entri chart membawa data sementara entri foto membawa adegan.

Prompt router — salin apa adanya
# 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:
Tulis brief kamera: sebuah adegan yang bisa diambil oleh kamera, 12 hingga 25 kata,
   dalam bahasa Inggris, sesuai dengan suasana bagian tersebut. Sebutkan apa yang ada
   di dalam bingkai, jangan pernah menyebut topiknya. Tanpa teks, logo, merek atau
   orang terkenal; tanpa metafora tak kasat mata. (Full rules, with examples and failure cases:
   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": "…" }
  ]
}

Baris kind mengerjakan sebagian besar pekerjaannya, dan instruksi data mengerjakan sisanya: entri chart hanya boleh membawa angka yang sudah muncul di draf, sehingga model men-transkripsi, bukan mengarang. Berikut ini router pada tiga bagian nyata dari sebuah artikel developer:

Bagian kind Apa yang dikembalikan router
Hero — “Kenapa build malam kami butuh 40 menit” photo “a developer working late at a desk with two monitors and a mechanical keyboard in a dark room lit by the screens”
“Ke mana sebenarnya waktu itu habis” chart data disalin dari paragraf: install 480 s, compile 1080 s, test 720 s, upload 120 s
“Bagaimana kami membagi graf-nya” diagram flowchart LR dari graf dependensi job
“Apa yang kami ubah, dan apa yang akan kami ulangi” photo “two engineers standing at a whiteboard covered in diagrams working through a problem together”

Tahap 3: foto-foto itu kembali dengan kredit lengkap

Setiap entri kind: "photo" adalah satu permintaan. Brief hero di atas, dijalankan terhadap API publik, mengembalikan ini dalam 147 ms:

GET /search/photos — brief hero · “a developer working late at a desk with two monitors…” · 147 ms
Mesin ini melakukan perankingan berdasarkan makna, sehingga kalimat panjang mempersempit kumpulan hasil, bukan mengosongkannya. Jalankan pencarian ini persis →

Brief bagian terakhir, sebuah adegan yang sama sekali berbeda, dalam 144 ms:

GET /search/photos — brief bagian · “two engineers standing at a whiteboard covered in diagrams…” · 144 ms
Artikel yang sama, sesi yang sama, sebuah adegan yang tidak akan tertukar dengan hero — karena briefnya ditulis per bagian, bukan per artikel. Jalankan yang ini juga →

Yang dikembalikan per foto adalah bagian yang membuat tahap 5 memungkinkan — bukan sekadar sebuah berkas:

Satu hasil, dipangkas ke field yang dikonsumsi assembler
{
  "photo_id":  "019e1c7f-0063-759e-b498-33ce1714e6c9",   // simpan: tidak ada pengulangan
  "urls": { "small": "…?w=400", "regular": "…?w=1080",
             "large": "…?w=1920" },
  "width": 3000, "height": 1688,          // → tidak ada pergeseran tata letak
  "blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na",   // → placeholder yang nyata
  "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 (…)" }
}

Tahap 4: apa yang tidak bisa disampaikan sebuah foto

Dua slot dalam rencana ini tidak bisa dicari, dan justru di sinilah model kedua membuktikan kegunaannya — bukan untuk menggambar sebuah gambar, tetapi untuk menulis kode yang menggambarkannya. Perbedaan ini penting: kode bisa ditinjau, deterministik, dan tidak bisa mengarang-ngarang tinggi sebuah batang grafik.

Diagram: teks masuk, SVG keluar

Mermaid merender diagram dari sebuah definisi teks polos,3 yang menjadikannya target paling aman untuk sebuah model: keluarannya bisa diperiksa, bisa dibedakan di git, dan dirender dengan cara yang sama setiap kali. Router sudah mengembalikan spec-nya.

diagram.sh — model menulis spec, CLI merendernya
# field "spec" dari sebuah entri diagram, ditulis ke 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
# → sebuah SVG yang bisa ditinjau di PR, bukan gambar yang harus dipercaya begitu saja

Grafik: hanya angka yang sudah ada di artikel

Prinsip yang sama, satu pengaman tambahan. Router menyalin nilai-nilainya dari draf; model menulis kode plotting-nya; kodenya dijalankan di sandbox; assembler memeriksa ulang nilai yang dirender terhadap angka sumbernya sebelum grafik itu diizinkan mendekati sebuah halaman.

chart.py — plot data yang sudah disalin, lalu verifikasi
import re, matplotlib
matplotlib.use("Agg")                # headless: tidak ada tampilan di CI
import matplotlib.pyplot as plt

def assert_in_draft(value, draft: str) -> None:
    """Angka yang diplot harus muncul di artikel SEBAGAI SEBUAH ANGKA."""
    # Pencocokan substring adalah jebakannya di sini: "120" ada di dalam "1200", dan
    # di dalam "?w=1200" — `str(v) in draft` yang naif akan lolos pada apa pun.
    # Cocokkan pada batas kata, dan terima 1 234 / 1,234 / 1234.
    body = re.sub(r"(?<=\d)[  ,](?=\d{3}\b)", "", draft)   # hapus pemisah
    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)       # nilai halusinasi → tidak ada grafik

    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)                    # artefak deterministik, bisa ditinjau
    return out

Pengamannya singkat, tetapi tulis dengan hati-hati: pemeriksaan substring tidak bekerja. "120" in draft bernilai benar untuk artikel yang mengandung 1200 atau ?w=1200, sehingga versi naifnya lolos pada segalanya dan tidak melindungi apa pun. Jangkarkan pada batas digit, normalisasi pemisah ribuan, dan kelas kesalahan yang bisa membuat sebuah artikel teknis dibongkar habis-habisan di kolom komentar — sebuah grafik yang bertentangan dengan paragrafnya sendiri — tidak akan pernah sampai ke produksi. Tangkapan layar mengikuti prinsip yang sama: sebuah page.screenshot() berskrip terhadap build nyata Anda adalah satu-satunya sumber kebenaran untuk UI Anda sendiri, dan itu tetap benar seiring UI-nya berubah.

Tahap 5: perakitan adalah tempat SEO-nya berada

Semua yang sejauh ini menghasilkan berkas dan field. Tahap ini mengubahnya menjadi markup — dan penting untuk teliti di sini, karena tiga perilaku terdokumentasi diputuskan dalam beberapa baris ini.

Hero-nya, diterbitkan dari respons pencarian — tidak ada yang dikarang
<!-- Byte-nya berasal dari origin lain: bayar handshake-nya lebih awal -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>

<!-- Elemen LCP: jangan pernah lazy, selalu prioritas tinggi -->
<figure>
  <img src="{urls.regular}"
       width="{width}" height="{height}"        <!-- menghilangkan pergeseran tata letak -->
       alt="{alt}"                                <!-- ditulis SETELAH pemilihan -->
       fetchpriority="high" decoding="async"
       style="background:{color_hex}">   <!-- warna dominan, 1 field -->
  <figcaption>{attribution.html}</figcaption>
</figure>

<!-- Gambar bagian, di bawah lipatan: pengaturan yang berlawanan -->
<img src="{urls.regular}" width="{width}" height="{height}"
     alt="{alt}" loading="lazy" decoding="async">

Hotlink atau re-hosting? Cuplikan di atas melakukan hotlink, yang merupakan cara tercepat untuk diterbitkan dan alasan adanya preconnect: hero jarak jauh memakan biaya pencarian DNS dan handshake TLS di jalur kritis, dan itu bisa menghabiskan keuntungan yang baru saja Anda beli dengan fetchpriority. Re-hosting menghilangkan origin pihak ketiga sepenuhnya, memungkinkan Anda menyajikan AVIF/WebP pada breakpoint Anda sendiri, dan tetap bertahan saat URL upstream berubah — dengan biaya penyimpanan, sebuah langkah fetch dalam pipeline, dan tagihan CDN Anda sendiri. Apa pun yang Anda pilih, color_hex memberi Anda placeholder untuk satu field (blur hash lebih rapi, tetapi harus didekode dulu menjadi data URI — itu bukan warna CSS). Menyalin hero kasar langsung ke background: adalah tempat kebanyakan pipeline diam-diam menerbitkan kotak abu-abu kosong.

  1. Jangan pernah lazy-load hero-nya. web.dev jelas tegas: “Jangan pernah lazy-load gambar LCP Anda, karena itu selalu menyebabkan penundaan pemuatan sumber daya yang tidak perlu, dan akan berdampak negatif pada LCP”, dan menyarankan fetchpriority="high" pada elemen yang kemungkinan menjadi LCP — digunakan secara hemat, pada satu gambar.4 Pipeline yang mencap loading="lazy" pada setiap gambar, termasuk hero-nya, adalah luka Core Web Vitals paling umum yang tidak disengaja dalam penerbitan otomatis.
  2. Selalu terbitkan width dan height — keduanya sudah kembali dalam respons, jadi tidak ada alasan untuk tidak melakukannya; pasangan atribut tunggal itulah yang memungkinkan browser mencadangkan ruangnya dan menghentikan lompatan tata letak. Gunakan blur_hash sebagai placeholder saat berkasnya dimuat.
  3. Tulis alt setelah pemilihan, jangan pernah sebelumnya. Ini yang halus. Field alt dalam rencana menggambarkan adegan yang Anda minta; foto yang Anda dapatkan adalah kecocokan terdekat, bukan adegan itu. Menerbitkan teks brief sebagai alt justru adalah kegagalan aksesibilitas yang seharusnya dihindari pipeline ini — sebuah deskripsi gambar yang tidak ada di halaman. Bangun alt dari alt_description milik foto yang dipilih, diperhalus sesuai paragraf tempatnya berada. Panduan Google adalah “fokus pada pembuatan konten yang berguna dan kaya informasi yang menggunakan kata kunci secara tepat dan sesuai konteks isi halaman”, dan memperingatkan bahwa mengisi atribut alt dengan kata kunci “menghasilkan pengalaman pengguna yang negatif dan bisa membuat situs Anda dianggap spam”.1

Bagian yang hampir tidak pernah diotomatisasi siapa pun: metadata lisensi

Google mendukung data terstruktur ImageObject untuk lisensi gambar. Ia mensyaratkan contentUrl ditambah setidaknya salah satu dari creator, creditText, copyrightNotice, atau license, merekomendasikan acquireLicensePage, dan gambar dengan informasi lisensi menjadi memenuhi syarat untuk lencana Licensable di Google Images.5 Setiap field itu sudah ada dalam respons pencarian — jadi menerbitkannya cukup soal template, bukan sebuah proyek:

ImageObject JSON-LD, diisi dari respons API
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "ImageObject",
  "contentUrl": "{urls.large}",                 // wajib
  "creator": { "@type": "Person",
                "name": "{photographer_full_name}" },
  "creditText": "{photographer_full_name} on {source}",
  "license": "{LICENSE_URL[source]}",           // halaman lisensi milik pustaka itu sendiri
  "acquireLicensePage": "{source_image_url}"     // halaman foto tersebut
}
</script>

# LICENSE_URL memetakan field `source` ke lisensi yang sebenarnya mengatur
# foto tersebut — unsplash.com/license, pexels.com/license, pixabay.com/…

Satu detail yang layak diperhatikan: license harus menunjuk ke lisensi yang mengatur foto itu — halaman lisensi pustaka sumbernya sendiri — bukan ke halaman ringkasan di domain Anda. Google membacanya untuk menentukan kelayakan lencana, dan URL yang merujuk ke diri sendiri lebih lemah sebagai sinyal dan sulit dibela sebagai apa pun selain tautan balik ke diri sendiri. Simpan ringkasan lisensi Anda sendiri sebagai halaman internal untuk pembaca; letakkan yang kanonik dalam markup.

Dan selagi berada di assembler: beri nama berkas yang singkat namun deskriptif alih-alih IMG_0042.jpg, dan tambahkan gambarnya ke sitemap — format image sitemap Google menerima hingga 1.000 gambar per URL halaman.6 Keduanya masing-masing hanya satu baris dalam sebuah pipeline dan tidak pernah dilakukan secara manual.

Dua pipeline yang siap dicontek

Lima tahap yang sama, dua bentuk yang sangat berbeda — satu untuk artikel yang Anda tulis, satu untuk dokumentasi yang harus sesuai dengan produk yang sedang berjalan. Pilih yang mode kegagalannya Anda kenali.

1 · Blog developer di CI — Markdown di repo

Artikel berada sebagai Markdown, gambar di-commit di sebelahnya, dan semuanya berjalan saat push. Deterministik, bisa ditinjau di PR, tanpa ketergantungan runtime pada API apa pun:

.github/workflows/illustrate.yml
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   # gambar mendarat di PR
        with: { commit-message: "chore(content): illustrate" }

Seorang manusia masih menyetujui PR-nya, dan itulah intinya: pipeline mengusulkan, peninjau memutuskan, dan front-matter yang ditulisnya bisa diperbandingkan.

Bagian pencariannya adalah satu GET — request-nya, score_threshold-nya, dan field yang dikembalikannya dijelaskan baris demi baris di panduan satu artikel, jadi bagian itu diimpor di sini, bukan dicetak ulang. Yang ditambahkan file ini adalah semua hal yang dibutuhkan sebuah rencana namun tidak dibutuhkan satu foto: percobaan ulang pada brief yang tidak mengembalikan apa pun, klaim atas photo_id agar tidak ada dua halaman berbagi gambar yang sama, dan alt yang ditulis dari foto yang berhasil didapat.

plan_to_pr.py — perekat: rencana visual masuk, front-matter keluar
import frontmatter
from photo_search import search   # satu GET /search/photos; melewati id yang ada di `used`

def find_photo(entry: dict, used: set) -> dict | None:
    """Cari; jika brief terlalu spesifik, longgarkan sekali, lalu menyerah."""
    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"])   # klaim: tidak ada pengulangan di seluruh situs
            return photo
    return None                        # pemanggil memutuskan: lewati slot, atau gagal

def widen(query: str) -> str:
    """Buang klausa terakhir — biasanya yang terlalu spesifik."""
    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:                    # tanpa hero lebih baik daripada hero yang buruk
        raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")

    post["hero"] = {                    # semua yang dibutuhkan template
        "src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
        # Alt-nya menggambarkan foto yang KITA DAPATKAN, bukan pernah adegan yang kita minta.
        "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 sebagai basis, dipangkas ke ~125 karakter untuk pembaca layar.
    Kirim kembali lewat model bersama paragrafnya jika ingin hasil yang lebih baik."""
    base = photo.get("alt_description") or photo.get("description", "")
    return base[:125].rstrip(" ,;")

2 · Dokumentasi & changelog — tangkapan layar dulu, foto terakhir

Balikkan default router-nya. Dalam dokumentasi produk, visual yang jujur hampir selalu adalah UI Anda sendiri: sebuah skrip Playwright yang membuka build nyata, mengatur viewport tetap, dan menangkap persis keadaan yang dijelaskan paragrafnya. Diagram mencakup halaman-halaman arsitektur, dan foto hanya muncul di halaman konseptual dan landing page — tempat tangkapan layar tidak akan menjelaskan apa-apa.

shots.py — tangkapan layarnya dihasilkan, tidak pernah dideskripsikan
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)   # setajam retina
    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()
# Berjalan di job CI yang sama dengan build dokumentasi → tangkapan layar tidak akan
# pernah menggambarkan versi UI yang sudah tidak ada lagi.

Ada dua varian yang layak disebut meski tidak sampai butuh resepnya sendiri. Programmatic SEO membalikkan alurnya: dengan ratusan halaman yang dihasilkan dari sebuah basis data, Anda tidak mencari per halaman — satu permintaan mengembalikan hingga 100 foto, jadi Anda mencari per klaster topik dan menugaskan dari sebuah kumpulan, dengan batasan keunikan yang melakukan deduplikasinya (arsitektur itu, secara lengkap). Dan newsletter serta kartu media sosial membutuhkan foto yang sama dalam empat crop: simpan photo_id, minta ukuran yang Anda butuhkan dari urls, dan hanya cari lagi ketika sebuah crop benar-benar gagal.

Pipeline yang sama, sebagai satu agen

Jika sebuah model sudah menulis draf, jalan tersingkat adalah memberinya alat pencarian secara langsung daripada mengoper JSON antarproses. Pexafy menjalankan server Model Context Protocol ter-hosting di mcp.pexafy.com/mcp. Pengaturan konektor dan alat yang diekspos sudah dibahas di tempat lain — pengaturan desktop dan editor di panduan artikel tunggal, varian headless untuk CI runner di artikel tentang konten dalam skala besar, dan gambaran pasar konektor yang lebih luas — siapa yang menyediakan server gambar MCP, dengan syarat apa — dalam studi infrastruktur pencarian gambar untuk agen AI. Yang layak ditunjukkan di sini adalah apa yang terjadi pada pipeline ini setelah agen memegang alat tersebut: pipeline berhenti menjadi lima tahap dan menjadi satu instruksi.

Satu instruksi, seluruh rencana dieksekusi
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 slot terisi · 2 panggilan API · alt + kredit + ImageObject ditulis
      ⚠ §5 tidak mengembalikan apa pun di atas 0.5 — brief terlalu abstrak, ditulis ulang menjadi
        "a person at a kitchen table checking figures on a laptop"

Baris terakhir itulah alasan mengapa layak memiliki seorang agen dalam loop ini alih-alih hanya sebuah skrip murni: mode kegagalan ilustrasi otomatis adalah brief yang buruk, dan menulis ulang brief yang buruk itu persis untuk apa sebuah model bahasa berguna. Simpan bagian-bagian deterministiknya — penugasan, deduplikasi, pengaman numerik — dalam kode.

Biaya satu artikel yang diilustrasikan

Tiga slot foto per artikel berarti tiga permintaan pencarian, jadi paket gratis (5,000 permintaan/bulan) mencakup 1,666 artikel per bulan sebelum muncul pertanyaan soal membayar — dan jika Anda mengumpulkan pencarian per klaster topik alih-alih per artikel, plafon itu bergeser satu orde besaran lagi. Rincian per paket, dan arsitektur pengumpulan yang membuatnya tidak relevan, ada di artikel pendamping tentang mengilustrasikan konten dalam skala besar.

Dua panggilan model per artikel — satu untuk drafnya, satu untuk rencana visualnya — hanya beberapa ribu token dan akan menjadi baris termurah dalam pipeline; render Mermaid dan matplotlib tidak memakan biaya apa pun selain detik-detik CI. Angka yang mengejutkan orang adalah baris mana yang tidak murah: menghasilkan empat gambar per artikel, empat ratus gambar per bulan, ditambah percobaan-percobaan yang tidak lolos — dan hasilnya masih tidak membawa fotografer, tidak ada tanggal, dan tidak ada URL sumber.

Apa yang tidak diperbaiki pipeline ini

Sebuah bagian yang jujur, karena kegagalan yang dicegahnya itu mahal. Artikel yang diilustrasikan dengan baik tetaplah sebuah artikel: fotografi yang nyata, grafik yang akurat, dan markup yang benar memperbaiki halaman yang memang layak ada. Semua itu tidak membuat konten tipis yang diproduksi massal ikut naik peringkat. Kebijakan spam Google menyebut penyalahgunaan konten berskala (scaled content abuse) — menghasilkan banyak halaman terutama untuk memanipulasi peringkat dan hanya memberi sedikit nilai bagi pengguna, terlepas dari apakah otomasi terlibat atau tidak — dan kualitas ilustrasinya bukan faktor dalam penilaian itu.7

Jadi kerangka yang tetap berlaku: pipeline ini adalah batas kualitas yang Anda kendalikan, diterapkan pada halaman-halaman yang sudah punya alasan untuk diterbitkan. Manfaatnya yang terbukti nyata:

  • Provenans yang bisa diverifikasi pembaca. Baris kredit dengan fotografer nyata dan URL sumber adalah klaim yang bisa diperiksa — dan field yang sama memberi makan markup ImageObject yang dibaca Google.
  • Aksesibilitas dan Core Web Vitals. Teks alt yang nyata, dimensi pada setiap gambar, hero yang tidak pernah lazy-load. Kalikan dengan setiap artikel yang Anda terbitkan dan inilah persis cerita kualitas-gambar dari situs Anda.
  • Akurasi di tempat yang bisa diperiksa. Grafik yang angkanya divalidasi terhadap artikelnya, tangkapan layar yang dihasilkan dari build yang berjalan, diagram yang bisa ditinjau sebagai teks di PR — tiga visual yang tidak bisa menyimpang dari kebenaran tanpa sebuah tes gagal.

Soal atribusi, poin yang spesifik untuk pipeline ini sempit cakupannya: alasan untuk selalu menampilkan kredit sudah dijabarkan di tempat lain, dan yang ditambahkan oleh assembler otomatis adalah bahwa string attribution yang sama yang dicetaknya adalah creditText yang dibutuhkan structured data. Satu field, dua tempat, dikeluarkan dalam satu proses template yang sama — itulah sebabnya sebuah pipeline punya lebih sedikit alasan lagi daripada manusia untuk mengabaikannya.

Mulai dari mana

  1. Tambahkan prompt router ke apa pun yang sudah menulis draf Anda, dan cetak JSON-nya tanpa menindaklanjutinya. Baca sepuluh rencana. Jika briefnya menyebut topik alih-alih adegan, perbaiki prompt-nya sebelum menulis integrasi apa pun.
  2. Sambungkan hanya slot foto. Satu GET /search/photos per brief, dan simpan photo_id sejak hari pertama — deduplikasi yang dipasang belakangan di 3.000 halaman aktif adalah sebuah migrasi, bukan sekadar sebuah kolom.
  3. Terbitkan width, height, dan alt dalam commit yang sama. Ini adalah pekerjaan Core Web Vitals termurah yang akan pernah Anda lakukan.
  4. Baru kemudian tambahkan tahap 4, diagram sebelum grafik — Mermaid adalah teks, jadi itulah satu-satunya yang tidak punya mode kegagalan selain kesalahan sintaksis.
  5. Tambahkan pengaman numerik sebelum grafik pertama sampai ke pembaca, bukan sesudahnya.

Referensi & catatan kaki

1 Google Search Central, Image SEO best practices: teks alt adalah “atribut paling penting dalam menyediakan metadata untuk sebuah gambar”; panduannya adalah membuat “konten yang berguna dan kaya informasi yang menggunakan kata kunci secara tepat dan sesuai konteks isi halaman”, menghindari atribut alt yang dijejali kata kunci, menggunakan elemen HTML <img> alih-alih gambar CSS, dan memberi nama berkas yang singkat namun deskriptif.

2 EU AI Act, Pasal 50 — kewajiban transparansi yang berlaku sejak 2 Agustus 2026: penyedia sistem yang menghasilkan gambar, audio, video, atau teks sintetis harus menandai keluarannya dalam format yang dapat dibaca mesin dan membuatnya terdeteksi sebagai buatan AI. Ini mengikat penyedia dan pengguna AI; bukan aturan tentang gambar mana yang boleh diterbitkan sebuah situs web.

3 Mermaid merender diagram dan grafik dari definisi teks bergaya Markdown, dan itulah yang membuat keluarannya bisa ditinjau dan deterministik.

4 web.dev, Optimize Largest Contentful Paint: “Sebaiknya Anda menyetel fetchpriority="high" pada sebuah elemen <img> jika Anda merasa elemen itu kemungkinan menjadi elemen LCP halaman Anda”, digunakan secara hemat; dan “Jangan pernah lazy-load gambar LCP Anda, karena itu selalu menyebabkan penundaan pemuatan sumber daya yang tidak perlu, dan akan berdampak negatif pada LCP.”

5 Google Search Central, Image metadata (structured data): ImageObject mensyaratkan contentUrl ditambah setidaknya salah satu dari creator, creditText, copyrightNotice, atau license; acquireLicensePage direkomendasikan, dan gambar yang membawa informasi lisensi bisa memenuhi syarat untuk lencana Licensable di Google Images.

6 Google Search Central, Image sitemaps: image sitemap memberi tahu Google tentang gambar di sebuah situs, termasuk yang ditemukan lewat JavaScript, dan menerima hingga 1.000 gambar per URL halaman.

7 Kebijakan spam Google Search — scaled content abuse: menghasilkan banyak halaman terutama untuk memanipulasi peringkat dan hanya memberi sedikit nilai bagi pengguna, baik dibuat lewat otomasi, upaya manusia, maupun kombinasi keduanya.

Pertanyaan yang sering diajukan

Bagaimana cara mengilustrasikan artikel tulisan LLM secara otomatis?
Tambahkan satu panggilan model di antara proses menulis dan publikasi: minta model mengembalikan rencana visual — satu entri per slot gambar, masing-masing ditandai sebagai foto, chart, diagram, atau tidak ada. Entri foto membawa deskripsi 12 hingga 25 kata tentang sebuah adegan yang bisa saja diambil oleh kamera, yang kamu kirim ke API pencarian gambar semantik (GET /api/v1/search/photos, sekitar 150 ms). Entri chart dan diagram dikirim ke model yang menulis kode plotting atau Mermaid, dirender secara deterministik. Jangan pernah memasukkan judul artikel ke pencarian gambar: judul bersifat abstrak dan tidak ada foto yang menggambarkannya.
Haruskah situs dokumentasi menggunakan screenshot atau foto stok?
Balik default router: dalam dokumentasi produk, visual yang jujur hampir selalu adalah UI Anda sendiri. Skrip Playwright yang membuka build asli, mengunci viewport, dan menangkap kondisi persis yang dijelaskan paragraf berjalan dalam job CI yang sama dengan build dokumentasi, sehingga screenshot tidak akan pernah menampilkan versi antarmuka yang sudah tidak ada lagi. Diagram mendukung halaman arsitektur, dan fotografi hanya muncul di halaman konseptual dan landing page, di mana screenshot tidak akan menyampaikan apa-apa.
Bagaimana cara mencegah pipeline AI memasukkan angka yang salah ke dalam chart?
Buat rencana tersebut menyalin, bukan mengarang: router hanya boleh menyalin nilai yang sudah muncul di draf ke dalam field data entri chart. Lalu periksa (assert) di kode sebelum rendering — untuk setiap nilai, periksa apakah string-nya muncul di artikel, dan lempar error jika tidak. Sembilan baris kode, dan sebuah chart yang bertentangan dengan paragrafnya sendiri tidak akan pernah sampai ke pembaca.
Markup gambar apa yang harus dihasilkan pipeline otomatis untuk SEO?
Tiga hal, semuanya berasal dari field yang sudah ada di respons pencarian. alt deskriptif yang ditulis sesuai konteks paragraf — Google menyebut alt text sebagai metadata gambar paling penting dan memperingatkan agar tidak menumpuk kata kunci. width dan height pada setiap gambar, dengan fetchpriority="high" pada hero image dan jangan pernah loading="lazy" pada hero, karena gambar LCP tidak boleh dimuat secara lazy. Serta data terstruktur ImageObject dengan contentUrl, creator, creditText, dan license, yang membuat sebuah gambar memenuhi syarat untuk lencana Licensable di Google Images.
Apakah mengilustrasikan artikel tulisan AI membantu peringkatnya?
Tidak dengan sendirinya, dan penting untuk lebih spesifik di sini. Kebijakan spam Google mendefinisikan scaled content abuse sebagai menghasilkan banyak halaman terutama untuk memanipulasi peringkat dengan nilai yang sedikit bagi pengguna, terlepas dari apakah otomatisasi terlibat atau tidak; ilustrasi tidak mengubah penilaian tersebut. Yang didapatkan dari pipeline yang baik adalah standar kualitas minimum pada halaman yang memang layak ada: asal-usul yang bisa diverifikasi, alt text yang aksesibel, Core Web Vitals yang tetap baik meski diotomatisasi, serta chart dan screenshot yang tidak bisa menyimpang dari kebenaran.
Bagaimana cara menjalankan tahap ilustrasi di CI tanpa kehilangan kendali editorial?
Picu job tersebut pada pull request yang menyentuh file konten Anda, biarkan ia menulis gambar-gambar dan front-matter, lalu buat ia membuka pull request alih-alih melakukan commit langsung ke branch — pipeline mengajukan, manusia menyetujui, dan setiap field yang ditulisnya bisa diperiksa lewat diff. Jaga tiga hal tetap deterministik dalam kode, bukan dalam model: deduplikasi berdasarkan photo_id, pemastian bahwa setiap angka yang diplot muncul dalam artikel, dan kegagalan total ketika tidak ada foto yang melewati ambang skor. Tidak ada hero image lebih baik daripada hero image yang salah.

Berhenti memburu kata kunci. Deskripsikan apa yang Anda maksud.

Cari 9M+ gambar bebas pakai berdasarkan makna — dalam bahasa apa pun, dalam kurang dari 100 md.