「記事を書いた。でもアイキャッチ画像がない。FigmaもPhotoshopも使えないし、デザインのセンスにも自信がない」——そんな理由で無地のまま公開していませんか。この記事では、Pythonのコマンド1つでタイトル入りのアイキャッチ画像(1200×630)を作り、そのままOGP画像としても使う方法を解説します。使うのはグラフ描画で有名なmatplotlibと数値計算のnumpyだけで、画像編集ソフトは一切使いません。ノンデザイナーでも再現できるよう、デザインサンプルを3種類用意し、「どこをどう変えたら色や大きさが変わるのか」をコードと画像つきで1行単位で説明します。題材はこのブログ「DECODE」で実際に使っている生成スクリプト scripts/ogp_gen.py です。読み終わるころには、手元のタイトルを渡すだけでブログ全体の見た目が揃った画像を量産できるようになっているはずです。
この記事のポイントは3つです。1つ目はパッケージ選びの理由です。なぜPillowではなくmatplotlibなのか、導入コストと日本語表示の観点から整理します。2つ目は3種のデザイン比較です。標準のグラデーション、締まった印象のダーク帯、明るく軽いライト面を並べ、用途別の選び方を紹介します。3つ目はカスタマイズの特定手順です。色・文字サイズ・余白・ブランド表記の変え場所をファイル名と行番号レベルで示すので、迷わず改造できます。対象読者は、Pythonの基本操作ができ、コマンドラインからスクリプトを実行した経験がある方です。デザインの知識は不要です。逆に、すでにFigmaでブランドキットを運用している方には物足りないかもしれません。その場合は11章の改造ポイントだけ拾い読みしてください。
この記事の画像はすべてPythonで生成しています。デザインツールを開く工程はゼロです。タイトル文字列さえ決まれば、後はコマンド1行で量産できます。
完成イメージ:デザイン3種を見比べる
まずゴールを共有します。今回用意したデザインは3種類です。Aはこのブログの標準デザインで、ブランドカラーの対角グラデーションに白い太字タイトルと右下のDECODE表記を組み合わせたものです。記事一覧のサムネイルとしても、SNSでシェアされたときのOGPとしても、そのまま使える万能型です。Bは背景を紺色の帯色で引き締め、下部にだけグラデーションの細帯を残したタイプです。技術メモや硬めの解説記事と相性がよく、白文字が強く前に出ます。Cは背景を明るい面色にし、文字を濃紺、左端にだけグラデーションの縦帯を置いたタイプです。白背景のタイムラインに並んだときに軽やかに見え、女性向けや入門記事にも使いやすい印象です。
3種の違いは「背景・文字色・アクセントの置き方」だけです。サイズはすべて1200×630、タイトルの折り返しと自動サイズ調整の仕組みは共通です。つまり、好みのデザインを選んだ後も、タイトルを変えるだけで毎回同じ品質の画像が出ます。ここが手作業との大きな違いです。手作業では毎回文字の大きさや中央揃えが微妙にずれますが、スクリプト化すればブレがゼロになります。個人ブログのように記事数が増えるほど、この均一性が効いてきます。一覧ページに並んだときの統一感が、メディアとしての信頼感に直結するからです。
デザイン | 背景 | 文字色 | 向いている記事 |
|---|---|---|---|
A: 標準グラデーション | 青の対角グラデーション | 白 | 全部入り。迷ったらこれ |
B: ダーク帯 | 紺のベタ塗り+下部に細帯 | 白 | 技術解説・硬めの内容 |
C: ライト面 | 明るいグレー+左に縦帯 | 濃紺 | 入門・読み物・明るい話題 |



色の選び方にも理由があります。このブログでは色に役割を持たせており、操作系の青、注意の橙、分類の紫、細分のティール、帯の紺、装飾の黄緑という分担です。OGP画像ではこのうち青のグラデーションと帯の紺、面の明るいグレーを主に使います。記事の顔であるOGPに使う色をサイト本体と同じにすると、一覧から詳細、SNSシェアまで見た目が途切れません。詳しい配色の考え方は後述のカスタマイズ章で、16進数カラーコードつきで紹介します。
迷ったらAを選んでください。Aは背景がグラデーションなので、タイトルの長短や文字の濃淡に左右されにくく、失敗が最も少ないデザインです。
SNSで小さく表示されても読める?→ はい。タイトルは中央に大きく配置し、装飾は背景に抑えているため、スマートフォンのタイムライン程度の縮小でも判読できます。BとCも同じ配置思想です。
使うパッケージはmatplotlibとnumpyだけ
使うパッケージは2つだけです。グラフ描画ライブラリのmatplotlibと、数値計算のnumpyです。Pillow(画像処理の定番)ではなくmatplotlibを選んだ理由は3つあります。1つ目は日本語の折り返しと中央配置が楽なことです。matplotlibのテキスト描画は座標指定で中央揃えができ、フォントサイズの自動調整とも相性がよいです。2つ目はグラデーション背景が数行で書けることです。numpyで行列を作り、カラーマップに流し込むだけで対角グラデーションが描けます。3つ目は普段データ可視化で使っている人が多いことです。新しく画像編集ライブラリのAPIを覚えるより、既知の道具で済ませたほうが導入が速いという判断です。
バージョンの目安は、検証時点でmatplotlib 3.10系、numpyはmatplotlibに付随する版で動いています。厳密な固定はしていませんが、pipで最新を入れればまず動きます。日本語フォントはmacOS標準のヒラギノ、なければNoto Sans JPや游ゴシック系に自動でフォールバックします。WindowsやLinuxでも、Noto Sans JPさえ入っていれば同じコードで動きます。サーバー上で動かす場合も、フォントさえ用意すれば表示崩れはありません。フォント форматの違いを吸収する関数がスクリプト内にあるため、環境差を意識せずに済みます。
pip install matplotlib numpy
# 版の確認
python3 -c "import matplotlib; print(matplotlib.__version__)"
# 3.10.9 のように表示されればOKインストール後は、次の1行で動作確認ができます。タイトルに日本語を渡し、出力先を指定するだけです。オプションは --title と --out の2つしかなく、覚えることが少ないのも特徴です。生成にかかる時間は1〜2秒程度で、重い画像編集ソフトの起動を待つより圧倒的に速いです。タイトル違いで10枚作っても1分とかかりません。
python3 scripts/ogp_gen.py --title "はじめてのOGP画像生成" --out tmp-articles/ogp.png
# ogp -> tmp-articles/ogp.png (1200x630, font=Hiragino Sans, fs=64) と表示されれば成功Pillowでも作れますが、グラデーション+日本語中央配置+自動サイズ調整を全部書くとコードが長くなりがちです。matplotlibなら描画領域の正規化座標で直感的に配置できます。
商用利用は大丈夫?→ matplotlibとnumpyはどちらも商用利用可能なライセンスです。生成した画像の権利もあなたにあります。ただしフォント自体のライセンスは別なので、配布時は同梱フォントの規約を確認してください。
公式リポジトリとパッケージ構成を知る
使うパッケージの公式情報の置き場所を確認します。matplotlibはGitHubで開発されており、ソースコードやリリースノート、サンプル集はすべて公式リポジトリに集まっています。APIの仕様確認や版ごとの変更点は、個人ブログではなく公式を参照するのが確実です。とくにテキスト描画やカラーマップの仕様は版で微調整されるため、迷ったら公式のドキュメントを見る習慣が近道です。
公式リポジトリのリンクはこちらです。先に開いて更新頻度やサンプルを眺めておくと、枯れた安定ライブラリであることが実感できます。
matplotlib公式リポジトリ(github.com/matplotlib/matplotlib)が対象です。描画の本体は pyplot モジュールで、この記事の生成スクリプトも plt.figure による図の作成、ax.imshow によるグラデーション描画、ax.text によるタイトル描画という公式APIの組み合わせだけで書かれています。数値計算の numpy はグラデーション用の行列作りに使います。どちらもpipで導入でき、外部の画像編集ソフトは不要です。
# 公式パッケージの導入から確認までの最短手順
pip install matplotlib numpy
python3 -c "import matplotlib; print(matplotlib.__version__)"
# 仕様やサンプルは公式リポジトリのREADMEからたどれます今回の生成スクリプトは約137行の短いコードで、処理は「フォント解決→図の準備→グラデーション描画→装飾円→タイトル描画→ブランド描画→保存」という一直線の流れです。全文は下の付録に掲載しています。途中でわからなくなっても全体を眺めて雰囲気をつかめます。使っているのは公式APIだけなので、公式リポジトリのサンプルやドキュメントと見比べながら読めます。版を上げて動かなくなった場合も、公式のリリースノートで変更点を確認できます。出力サイズは1200×630に固定し、ブログ側への登録は管理画面から手動で行う運用です。
公式リポジトリをcloneする必要はありません。pipで入る版で十分です。リポジトリは仕様確認とサンプル参照用と割り切ってください。
最短手順:1コマンドでアイキャッチを作る
実際の操作はとても短いです。タイトルを決め、コマンドを実行し、出力PNGを確認するだけです。タイトルは記事タイトルをそのまま渡すのが基本です。長すぎるタイトルでも自動で折り返しと縮小が効くので、事前に短くする必要はありません。出力先は tmp-articles/ に置くと、後述のmicroCMS登録時に探しやすくなります。ファイル名は記事スラッグに合わせると管理が楽です。
python3 scripts/ogp_gen.py --title "Figma不要!Pythonでタイトル入りアイキャッチを作る方法" --out tmp-articles/ogp-design-a-standard.png
# ogp -> tmp-articles/ogp-design-a-standard.png (1200x630, font=Hiragino Sans, fs=64)実行結果の末尾に表示される fs=64 は、そのタイトルに選ばれたフォントサイズです。短いタイトルほど大きく、長いタイトルほど小さくなります。4行以内に収まる最大サイズを自動で選ぶ仕組みなので、毎回手で調整する必要がありません。5行以上になりそうな超長文タイトルは、4行目で省略記号に丸められます。OGPは一瞬で読まれるものなので、この割り切りは正解です。どうしても全文入れたい場合は、タイトル自体を短くするほうが画像側で頑張るより効果的です。
生成後は必ず実寸で目視します。プレビューの縮小表示ではきれいに見えても、1200×630の実寸では文字の詰まりや英語と日本語の混在による違和感が出ることがあります。特に英単語と日本語の境界は折り返し位置に影響します。スクリプトは英数境界での分割を優先するよう調整されていますが、固有名詞が途中で切れる場合はタイトルに読点を足すと切れ目を誘導できます。1〜2回試し生成して、一番読みやすい区切りを探すのがコツです。
タイトル末尾のスペースは自動で除去されます。コピー&ペースト時の余分な空白を気にせず渡せます。
タイトルは何文字まで入る?→ 目安は20文字前後で2〜3行、30文字超で3〜4行です。40文字を超えると省略される可能性が高まります。OGP用には30文字前後に整えるのがおすすめです。
コード解読1:サイズとDPIで1200×630を決める
ここからはスクリプトの中身を、改造に必要な範囲だけ読み解きます。最初のポイントは画像サイズの決め方です。matplotlibはインチとDPIの掛け算でピクセル数を決めます。このスクリプトでは幅12.0インチ、高さ6.3インチ、DPI100という設定で、1200×630ピクセルを作っています。OGPの推奨サイズが1200×630であるため、この数字は変える必要がありません。XやSNSのカード表示がこの比率を前提に作られているからです。
W_INCH, H_INCH = 12.0, 6.3
DPI = 100
W_PX, H_PX = int(W_INCH * DPI), int(H_INCH * DPI)
# 12.0*100=1200, 6.3*100=630 になる計算図の準備部分も見ておきます。fig.add_axes([0, 0, 1, 1]) は余白ゼロで全面を使う宣言です。通常のグラフでは軸や余白が入りますが、OGP画像では不要なので消しています。ax.set_xlim(0, 1) と ax.set_ylim(0, 1) で座標を0〜1に正規化し、ax.axis("off") で目盛りを消します。以降の配置はすべてこの0〜1座標で指定するため、「中央は0.5、右端は0.94」という直感的な指定ができます。ピクセル計算を意識せずに済むのが、この正規化の利点です。
fig = plt.figure(figsize=(W_INCH, H_INCH), dpi=DPI)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")保存時は fig.savefig(out_path, dpi=DPI) で同じDPIを指定します。ここを変えると出力サイズが変わってしまうので、触らないのが無難です。ファイル形式は拡張子で自動判定され、PNG指定なら透過なしの通常PNGになります。JPEGにしたい場合は拡張子を変えるだけですが、文字の輪郭がにじむためOGPにはPNGがおすすめです。容量が気になる場合は、生成後に画像圧縮ツールにかけるほうが画質と容量の両立ができます。
サイズを変えたい場合はDPIではなくインチ側を変えてください。DPIを変えると文字の相対サイズも変わりますが、インチ基準なら比率を保てます。ただしOGPは1200x630固定が推奨です。
コード解読2:グラデーション背景の作り方
次は見た目の核心であるグラデーション背景です。仕組みは、numpyで0〜1の行列を作り、matplotlibのカラーマップに流し込むだけです。コードは次の3行が本体です。X * 0.55 + Y * 0.45 という式が対角の傾きを決めており、サイト本体のCSS変数 --color-primary-gradient の135度相当を再現しています。数字を変えると傾きが変わりますが、初期値のままで十分きれいです。
C1, C2 = "#3b5ba9", "#4fa3c4"
GRAD = LinearSegmentedColormap.from_list("d2key", [C1, C2])
# 中略
Z = X * 0.55 + Y * 0.45
ax.imshow(Z, extent=[0, 1, 0, 1], aspect="auto", cmap=GRAD, zorder=0)色の指定はファイル上部の C1, C2 の2箇所だけです。左が開始色、右が終了色で、開始色を濃く、終了色を明るくすると立体感が出ます。このブログのブランドカラーでは開始色に操作系の青、終了色に明るい水色を使っています。サイトのボタンや強調と同じ組み合わせなので、一覧ページとの一体感が生まれます。後述のカスタマイズ章では、この2箇所を変えるだけで別ブランドの画像になることを示します。
背景の上には、白い円を薄く重ねた装飾があります。透明度を0.07〜0.10に抑えているため、主張しすぎずに単調さを避ける効果があります。位置は正規化座標で指定されており、右上・左下・上中央の3箇所です。装飾を消したい場合はこのループごと削除すればよく、濃くしたい場合は透明度の数字を上げます。ただし濃くしすぎると文字の邪魔になるので、0.15を超えないのが目安です。
for cx, cy, r, a in [(0.92, 0.95, 0.42, 0.10), (0.05, -0.05, 0.35, 0.08), (0.55, 1.05, 0.30, 0.07)]:
ax.add_patch(mpatches.Circle((cx, cy), r, transform=ax.transAxes,
fc="white", ec="none", alpha=a, zorder=1))グラデーションの向きを変えたい場合は Z の式を変えます。Xの係数を上げると横方向、Yを上げると縦方向の変化が強くなります。
べったりした単色にしたい場合は?→ imshowの代わりに Rectangle で1色を敷けばOKです。Bデザインがまさにその例で、紺のベタ塗りに細帯だけ残しています。
コード解読3:日本語の折り返しと自動サイズ調整
日本語タイトルをきれいに見せる工夫が、このスクリプトの一番の見どころです。英語と違って日本語には空白がないため、文字数で折り返す必要があります。単純に等分すると、開き括弧が行末に残ったり、英単語の途中で切れたりします。そこで wrap_jp 関数が、句読点・閉じ括弧・英数境界を優先して切れ目を探します。具体的には、最大文字数に達した位置から逆向きに走査し、読点や英数切り替わりを見つけたらそこで分割します。
def wrap_jp(text, max_chars):
"""日本語は空白が無いので文字数で折り返す。句読点・閉じ括弧・英数境界で切れるよう微調整。"""
lines, cur = [], ""
for ch in text:
cur += ch
if len(cur) >= max_chars:
# 「「」の直前では切らない、読点や英数境界では優先的に切る
cut = None
for i in range(len(cur) - 1, 0, -1):
c = cur[i]
if c in "、。!?」』)・::;;":
cut = i + 1
break
if c.isalnum() != cur[i - 1].isalnum() or c.isascii() != cur[i - 1].isascii():
cut = i
break
if cut:
lines.append(cur[:cut])
cur = cur[cut:]
else:
lines.append(cur)
cur = ""
if cur:
lines.append(cur)
return linesフォントサイズの自動調整も秀逸です。pick_fontsize 関数は64ポイントから30ポイントまで2刻みで試し、折り返し結果が4行以内に収まる最大サイズを返します。行あたりの文字数は 700 / フォントサイズ で概算しており、文字が大きいほど1行に入る字数が減る関係を再現しています。短いタイトルは大きく、長いタイトルは小さくという、デザイナーが無意識にやる調整を自動化したものです。
def pick_fontsize(title, usable_pt=700.0, max_lines=4, fs_max=64, fs_min=30):
"""折り返し結果がmax_lines行以内に収まる最大フォントサイズ(pt)を選ぶ。"""
for fs in range(fs_max, fs_min - 1, -2):
chars_per_line = max(6, int(usable_pt / fs))
if len(wrap_jp(title, chars_per_line)) <= max_lines:
return fs
return fs_min描画時は中央配置で複数行を一気に書きます。ha="center", va="center" が中央揃えの指定で、linespacing=1.4 が行間です。行間を詰めたい場合は1.2、ゆったりさせたい場合は1.6にします。4行を超える場合は4行目を省略記号に丸める処理が入っており、はみ出しを防ぎます。フォント解決は resolve_font が担当し、ヒラギノ→Noto Sans JP→游ゴシックの順に探します。見つからない場合はエラーメッセージを出して終了するため、文字化けした画像が出る心配がありません。
ax.text(0.5, 0.52, "\n".join(lines), transform=ax.transAxes, fontsize=fs,
fontweight="bold", color="#fff", va="center", ha="center", zorder=3,
linespacing=1.4)英語混じりのタイトルは英数境界で切れるため、意外ときれいに折り返します。逆にひらがな続きは区切りが見つからず等分されがちなので、適度に読点を入れると整います。
カスタマイズ1:色を変える場所は2行だけ
ここからは改造の実践です。まず色の変え方です。変える場所はファイル上部の C1, C2 の1行だけです。開始色と終了色の16進数カラーを書き換えれば、全体の印象が一変します。サイトのブランドカラーと合わせるのが基本ですが、記事カテゴリごとに変えるのも有効です。たとえば入門記事は明るめ、深掘り記事は濃いめという使い分けです。
# 標準(DECODEのキーグラデーション)
C1, C2 = "#3b5ba9", "#4fa3c4"
# 例:紫系にしたい場合(カテゴリ色に寄せる)
C1, C2 = "#5e3fa1", "#8b7ad8"
# 例:ティール系にしたい場合(タグ色に寄せる)
C1, C2 = "#2a6b5e", "#5fb3a3"
# 例:ダーク系にしたい場合(帯色ベース)
C1, C2 = "#1c2733", "#3b5ba9"このブログで定義している役割色の一覧がこちらです。OGPに使う場合は、背景と文字のコントラストを意識してください。白文字を使うなら背景は濃いめ、濃紺文字を使うなら背景は明るめが原則です。特に注意色の橙はNEWバッジ専用のため、背景全体には使わないのがルールです。ここぞという場所だけに残すことで、サイト全体の注意喚起が埋もれません。
役割 | 色 | OGPでの使い方 |
|---|---|---|
キー(操作系) | #3b5ba9 | グラデーション開始色の定番 |
キー明(グラデ終端) | #4fa3c4 | グラデーション終了色の定番 |
分類 | #5e3fa1 | 紫系バリエーションに |
細分 | #2a6b5e | ティール系バリエーションに |
帯 | #1c2733 | Bデザインの背景色に |
面 | #f0f3f5 | Cデザインの背景色に |
本文 | #17202b | Cデザインの文字色に |
文字色の変え場所はタイトル描画の color 引数です。標準は白の #fff ですが、明るい背景にする場合は濃紺の #17202b に変えます。ブランド表記の色も同様で、背景に合わせて白の半透明かグレー系に調整します。コントラスト比の目安は4.5倍以上です。白と青グラデーション、白と紺、濃紺と明るいグレーはいずれも基準を満たします。不安な場合は、生成後に目を細めて見て、文字が背景に溶けないか確認してください。
# 白文字(濃い背景用)
ax.text(0.5, 0.52, "\n".join(lines), color="#fff", fontsize=fs, fontweight="bold", va="center", ha="center")
# 濃紺文字(明るい背景用)
ax.text(0.5, 0.52, "\n".join(lines), color="#17202b", fontsize=fs, fontweight="bold", va="center", ha="center")色を変えたら必ず1枚試し生成してください。コード上の16進数と画面での見え方は光の加減で異なります。特にプロジェクターやスマホの夜間モードでは印象が変わります。
グラデーションを単色にしたい場合は?→ C1とC2を同じ値にすれば実質単色になります。コードを削らずに済む裏技です。
カスタマイズ2:大きさ・余白・文字サイズを変える
次に大きさの調整です。触る場所は3箇所です。1つ目は出力サイズの W_INCH, H_INCH, DPI ですが、OGP用途では変えないのが正解です。2つ目はフォントサイズの上限と下限である fs_max=64, fs_min=30 です。もっと大きく見せたい場合は上限を72に、長文でも小さくして詰め込みたい場合は下限を26にします。3つ目は行間の linespacing=1.4 です。行数が多いときは1.3に詰めると収まりがよくなります。
# 大きめにしたい場合
def pick_fontsize(title, usable_pt=700.0, max_lines=4, fs_max=72, fs_min=32):
for fs in range(fs_max, fs_min - 1, -2):
chars_per_line = max(6, int(usable_pt / fs))
if len(wrap_jp(title, chars_per_line)) <= max_lines:
return fs
return fs_min
# 行間を詰める場合(4行タイトル向け)
ax.text(0.5, 0.52, "\n".join(lines), fontsize=fs, linespacing=1.3, fontweight="bold", color="#fff", va="center", ha="center")配置の微調整も正規化座標で行います。タイトルの中央位置は ax.text(0.5, 0.52, ...) の2つの数字です。1つ目が横位置で0.5が中央、2つ目が縦位置で0.5が中央です。ブランド表記を入れる分、タイトルを少し上にずらしたい場合は0.52を0.55に上げます。ブランド表記の位置は右下の ax.text(0.94, 0.055, "DECODE", ...) で、右端から6%、下端から5.5%の場所です。左寄せにしたい場合は0.94を0.06にし、ha="right" を ha="left" に変えます。
# タイトルを少し上に、ブランドを左下にしたい場合
ax.text(0.5, 0.55, "\n".join(lines), fontsize=fs, fontweight="bold", color="#fff", va="center", ha="center", linespacing=1.4)
ax.text(0.06, 0.055, "DECODE", fontsize=21, fontweight="bold", color="#ffffff99", va="center", ha="left")ブランド文字の大きさは fontsize=21 で指定されています。OGP表示では小さく見えるため、20前後がちょうどよいです。太字の有無は fontweight="bold" で切り替えられ、細くしたい場合は削除します。色の半透明指定 #ffffff99 は末尾の99が透明度で、背景に溶け込ませる効果があります。不透明にしたい場合は #ffffff にします。細部の調整は1箇所ずつ変えて試し生成するのが、迷わないコツです。
一度に2箇所以上変えると、どれが効いたのか分からなくなります。色→大きさ→位置の順に1つずつ変えるのが近道です。
カスタマイズ3:BとCへの改造ポイント
3種デザインの違いをコード差分で見ます。AからBへの改造は、背景をベタ塗りにし、下部に細帯を足し、ラベル位置を変えるだけです。背景の描画を imshow の全面から Rectangle の紺ベタ塗りに替え、グラデーションの表示範囲を下部の10%に絞ります。タイトルは少し下げ、カテゴリラベルを上部中央に置きます。これだけで印象ががらりと変わります。
# B: ダーク帯(背景ベタ塗り+下部に細帯)
ax.add_patch(mpatches.Rectangle((0, 0), 1, 1, transform=ax.transAxes, fc="#1c2733", ec="none", zorder=0))
ax.imshow(Z, extent=[0, 1, 0.0, 0.10], aspect="auto", cmap=GRAD, zorder=1)
ax.text(0.5, 0.47, "\n".join(lines), fontsize=fs, fontweight="bold", color="#ffffff", va="center", ha="center", linespacing=1.4)
ax.text(0.5, 0.88, "DECODE | TECH MEMO", fontsize=19, fontweight="bold", color="#9fb0c3", va="center", ha="center")AからCへの改造は、背景を明色にし、文字を濃紺にし、左端に縦帯を足すだけです。背景色を面色の #f0f3f5 にし、グラデーションの表示範囲を左端の3.5%に絞ります。縦帯は extent=[0, 0.035, 0, 1] という指定で、横の0〜3.5%だけに描画します。タイトル色を濃紺に変え、ブランド表記も青系に変えると統一感が出ます。
# C: ライト面(明色背景+左に縦帯+濃紺文字)
ax.add_patch(mpatches.Rectangle((0, 0), 1, 1, transform=ax.transAxes, fc="#f0f3f5", ec="none", zorder=0))
ax.imshow(X, extent=[0, 0.035, 0, 1], aspect="auto", cmap=GRAD, zorder=1)
ax.text(0.52, 0.52, "\n".join(lines), fontsize=fs, fontweight="bold", color="#17202b", va="center", ha="center", linespacing=1.4)
ax.text(0.08, 0.82, "DECODE", fontsize=22, fontweight="bold", color="#3b5ba9", va="center", ha="left")3種の使い分けの目安をまとめます。連載ものはAで統一し、特集だけBにするという運用がおすすめです。Cは対談やエッセイなど柔らかい内容に使うと、読者の期待を裏切りません。1ブログで3種を混在させても、タイトルの書体とブランド表記が共通なら散らかりません。逆にフォントや配置まで変えると別メディアに見えるので、変えるのは色と帯だけに留めるのがコツです。今回のサンプル画像3枚は tmp-articles/ogp-design-a-standard.png ほかとして生成済みで、見比べながら選べます。
BとCのコードは記事用に簡略化しています。実運用ではAの関数を複製し、背景部分だけ差し替えるのが安全です。折り返し関数は共通のままにしてください。
3種以外の4つ目を作りたい場合は?→ Cの縦帯を上部の横帯に変えるだけで新デザインになります。extentの数字を入れ替えるだけなので5分でできます。
OGPとして使う:microCMS登録とSNS表示確認
画像ができたらOGPとして登録します。このブログではmicroCMSの記事フィールドにある thumbnail と img に画像を設定します。残念ながら画像のアップロード自体はAPI未対応のため、管理画面から手動で行います。手順は、管理画面のメディアで生成PNGをアップロードし、記事編集画面でサムネイルとして選択するだけです。一覧カードと詳細ヘッダー、SNSシェア時のOGPがいずれもこの画像を参照します。
OGPの仕組みを簡単に整理します。SNSや検索エンジンは、ページのHTMLにあるOGPメタタグを読み取ってカード表示を作ります。このブログでは記事詳細ページがタイトルとサムネイルURLからメタタグを出力するため、サムネイルを登録すれば自動でOGP対応になります。画像サイズが1200×630なのは、XやFacebookの large summary card がこの比率を推奨しているからです。小さすぎるとぼやけ、比率違いでは上下が切れます。生成スクリプトのサイズ固定は、この仕様に合わせたものです。
# OGP用に本番タイトルで生成する例
python3 scripts/ogp_gen.py --title "Figma不要!Pythonでタイトル入りアイキャッチを作る方法" --out tmp-articles/ogp-publish.png
# 管理画面での操作(手動)
# 1. メディアに ogp-publish.png をアップロード
# 2. 記事編集で thumbnail / img に設定
# 3. 下書き保存してプレビューで確認公開前の確認は3点です。1つ目は記事詳細の表示で、画像が切れずに表示されるかを見ます。2つ目はXやFacebookのカードバリデータでURLを検査し、キャッシュを更新します。特に初回シェア前にバリデータを通すと、古い画像が残る事故を防げます。3つ目はスマートフォンでの見え方です。PCでは大きく見えても、スマホのタイムラインではタイトルの細部が潰れます。生成PNGをスマホに送って実機確認するのが確実です。
OGP画像を差し替えたのにSNS表示が変わらない場合はキャッシュが原因です。各SNSのバリデータで再取得させてください。記事URL自体は変えなくて大丈夫です。
アイキャッチとOGPは別画像にすべき?→ 基本は同じでOKです。一覧・詳細・SNSで同じ顔が出るほうが覚えてもらえます。縦長サムネイルが必要な場合だけ別途用意します。
付録:ogp_gen.pyの全文
ogp_gen.pyは短いスクリプトなので全文を掲載します。コピーしてそのまま保存すれば動きます。必要なのはmatplotlibとnumpyと日本語フォントだけです。色を変えたい場合はファイル上部のC1とC2、大きさを変えたい場合はW_INCHとH_INCHとDPI、文字周りを変えたい場合はpick_fontsizeとax.textの指定を見てください。詳しい変え場所は前の章で解説した通りです。
#!/usr/bin/env python3
"""記事OGP画像生成 (matplotlib)
記事生成時にタイトルとキーカラーのグラデーション背景で
OGP画像(1200x630)を作る。アップロードはmicroCMS管理画面から手動で行う。
使い方:
python3 scripts/ogp_gen.py --title "記事タイトル" --out tmp-articles/ogp.png
デザイン: D2キーグラデーション (#3b5ba9 → #4fa3c4) + タイトル + DECODE表記
"""
import argparse
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.colors import LinearSegmentedColormap
import numpy as np
FONT = "Hiragino Sans" # macOS標準。無ければ Noto Sans JP にフォールバック
W_INCH, H_INCH = 12.0, 6.3
DPI = 100
W_PX, H_PX = int(W_INCH * DPI), int(H_INCH * DPI)
# D2キーグラデーション (globals.css の --color-primary-gradient と同期)
C1, C2 = "#3b5ba9", "#4fa3c4"
GRAD = LinearSegmentedColormap.from_list("d2key", [C1, C2])
def resolve_font():
from matplotlib import font_manager as fm
names = {f.name for f in fm.fontManager.ttflist}
for cand in (FONT, "Noto Sans JP", "Yu Gothic", "Hiragino Kaku Gothic ProN"):
if cand in names:
return cand
return None
def wrap_jp(text, max_chars):
"""日本語は空白が無いので文字数で折り返す。句読点・閉じ括弧・英数境界で切れるよう微調整。"""
lines, cur = [], ""
for ch in text:
cur += ch
if len(cur) >= max_chars:
if cur[-1] in "「『(【":
lines.append(cur[:-1])
cur = cur[-1]
continue
cut = None
for i in range(len(cur) - 1, 0, -1):
c = cur[i]
if c in "、。!?」』)・::;;":
cut = i + 1
break
if c.isalnum() != cur[i - 1].isalnum() or c.isascii() != cur[i - 1].isascii():
cut = i
break
if cut:
lines.append(cur[:cut])
cur = cur[cut:]
else:
lines.append(cur)
cur = ""
if cur:
lines.append(cur)
return lines
def pick_fontsize(title, usable_pt=700.0, max_lines=4, fs_max=64, fs_min=30):
"""折り返し結果がmax_lines行以内に収まる最大フォントサイズ(pt)を選ぶ。"""
for fs in range(fs_max, fs_min - 1, -2):
chars_per_line = max(6, int(usable_pt / fs))
if len(wrap_jp(title, chars_per_line)) <= max_lines:
return fs
return fs_min
def draw_ogp(title, out_path):
font = resolve_font()
if not font:
print("日本語フォントが見つかりません", file=sys.stderr)
sys.exit(1)
plt.rcParams["font.family"] = font
plt.rcParams["font.sans-serif"] = [font]
fig = plt.figure(figsize=(W_INCH, H_INCH), dpi=DPI)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
# 対角グラデーション背景 (135deg相当)
x = np.linspace(0, 1, 256)
y = np.linspace(0, 1, 256)
X, Y = np.meshgrid(x, y)
Z = X * 0.55 + Y * 0.45
ax.imshow(Z, extent=[0, 1, 0, 1], aspect="auto", cmap=GRAD, zorder=0)
# 装飾: 柔らかい円
for cx, cy, r, a in [(0.92, 0.95, 0.42, 0.10), (0.05, -0.05, 0.35, 0.08), (0.55, 1.05, 0.30, 0.07)]:
ax.add_patch(mpatches.Circle((cx, cy), r, transform=ax.transAxes,
fc="white", ec="none", alpha=a, zorder=1))
# タイトル (中央配置・自動折り返し・自動サイズ)
fs = pick_fontsize(title)
chars_per_line = max(6, int(700 / fs))
lines = wrap_jp(title, chars_per_line)
if len(lines) > 4:
lines = lines[:3] + [lines[3][:max(1, chars_per_line - 1)] + "…"]
ax.text(0.5, 0.52, "\n".join(lines), transform=ax.transAxes, fontsize=fs,
fontweight="bold", color="#fff", va="center", ha="center", zorder=3,
linespacing=1.4)
# ブランド (右下)
ax.text(0.94, 0.055, "DECODE", transform=ax.transAxes, fontsize=21,
fontweight="bold", color="#ffffff99", va="center", ha="right", zorder=3)
fig.savefig(out_path, dpi=DPI, facecolor=fig.get_facecolor())
plt.close(fig)
print(f"ogp -> {out_path} ({W_PX}x{H_PX}, font={font}, fs={fs})")
def main():
ap = argparse.ArgumentParser(description="記事OGP画像生成 (タイトル + D2グラデーション背景)")
ap.add_argument("--title", required=True, help="記事タイトル")
ap.add_argument("--out", default="ogp.png", help="出力PNGパス (既定: ogp.png)")
args = ap.parse_args()
draw_ogp(args.title.strip(), args.out)
if __name__ == "__main__":
main()全文を書き写すより、公式APIの使い方を追う読み方がおすすめです。plt.figureで図を作り、ax.imshowで背景を敷き、ax.textで文字を置くという3手順が全体の骨格です。
まとめと次のステップ
この記事では、Pythonのmatplotlibとnumpyだけでタイトル入りのアイキャッチ画像を作り、OGPとして使うまでの一連の流れを解説しました。振り返ると、やることは「コマンド1行で生成→3種から選ぶ→色と大きさを1箇所ずつ調整→microCMSに手動登録」という4ステップだけです。FigmaもPhotoshopも不要で、デザインの知識もいりません。必要なのはタイトル文字列と、変え場所を知っていることだけです。仕様の正本は matplotlib公式リポジトリにあり、生成スクリプトは公式APIだけを使った約137行の短いコードです。
次のステップとしては、3つの応用が考えられます。1つ目は連番生成の自動化です。タイトル一覧をテキストファイルに並べ、シェルのループで回せば全記事分を一気に作れます。2つ目はカテゴリ別色分けです。技術メモは青、読み物はティールというように、C1とC2を引数で切り替えられる改造です。3つ目はPillow版への移植です。サーバー上でフォント管理が難しい場合は、Pillowのほうが軽量なことがあります。ただし自動折り返しの関数だけは移植が必要です。まずはこの記事のAデザインで1枚作り、自分の記事に設定してみてください。その1枚ができたら、後は量産するだけです。迷ったら標準デザインに戻り、タイトルの言葉を磨くほうに時間を使ってください。画像の出来より言葉のほうが、クリック率に効きます。
最後に、この記事の順番をもう一度確認します。完成形の比較→パッケージの導入→リポジトリの把握→1コマンド生成→サイズ・背景・折り返しの読解→色・大きさ・派生デザインの改造→OGP登録と表示確認。この順番通りに進めれば、どこで躓いても「今どの段階か」が分かります。分からなくなったら3種のサンプルに戻り、好きな見た目を選んでください。道具が決まれば、デザインは怖くありません。






