From 80c5459db5df54a80f4823db09f1251166fe704a Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:41:27 +0900 Subject: OGP画像を動的生成するエンドポイントを追加(Satori + resvg-js) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/pages/og/[slug].png.ts を追加 - 全ページに個別のOGP画像をビルド時に生成 - デザイン: faceicon + ページタイトル + サイト名 - 開発中は npm run preview でのみ確認可能(静的エンドポイントの制限) --- src/pages/og/[slug].png.ts | 114 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/pages/og/[slug].png.ts (limited to 'src/pages') diff --git a/src/pages/og/[slug].png.ts b/src/pages/og/[slug].png.ts new file mode 100644 index 0000000..52b790b --- /dev/null +++ b/src/pages/og/[slug].png.ts @@ -0,0 +1,114 @@ +import satori from "satori"; +import { Resvg } from "@resvg/resvg-js"; +import { readFileSync } from "node:fs"; + +const pages: Record = { + index: "だれもがしあわせに暮らせるまちへ", + jisseki: "実績", + policy: "私の方針", + support: "ご支援", + contact: "コンタクト", + "whisper-to-ai-moji-okoshi": + "無料・超高精度のWhisper + 生成AIで文字起こしする方法", + "koubunsyo-kanri": "公文書管理の不正追及の軌跡", + "ijime-judai-jitai": "いじめ重大事態への対応の軌跡", + "fukushi-shisetsu-gyakutai": "障害者福祉施設における虐待通報対応の軌跡", + "aiki-kouen": "合気公園の軌跡", + "joutyo-koteikyu": "情緒固定級の軌跡", + "kajo-seigen-kanwa": "過剰な制限緩和の軌跡", + "saresio-kaihatu": "東京サレジオ学園北側開発問題の軌跡", + "vaccine-kyuusai-tekiseika": "ワクチン副反応救済制度の適正化の軌跡", + "dislexia-taiou": "ディスレクシア(読み書き障害)対応の軌跡", + "ippan-situmon": "一般質問", +}; + +export async function getStaticPaths() { + return Object.keys(pages).map((slug) => ({ params: { slug } })); +} + +const fontBuffer = readFileSync("node_modules/.noto-sans-jp.otf"); +const faceiconBuffer = readFileSync("public/img/faceicon.jpg"); +const faceiconBase64 = `data:image/jpeg;base64,${faceiconBuffer.toString("base64")}`; + +export async function GET({ params }: { params: { slug: string } }) { + const slug = params.slug; + const title = pages[slug] ?? "小平市議 安竹洋平 公式サイト"; + + const svg = await satori( + { + type: "div", + props: { + style: { + display: "flex", + width: "1200px", + height: "630px", + background: "linear-gradient(135deg, #3730a3 0%, #4f46e5 100%)", + color: "#ffffff", + fontFamily: "sans-serif", + padding: "60px 80px", + alignItems: "center", + gap: "48px", + }, + children: [ + { + type: "img", + props: { + src: faceiconBase64, + width: 200, + height: 200, + style: { + borderRadius: "50%", + border: "4px solid rgba(255,255,255,0.3)", + flexShrink: "0", + }, + }, + }, + { + type: "div", + props: { + style: { display: "flex", flexDirection: "column", gap: "16px", flex: "1" }, + children: [ + { + type: "div", + props: { + style: { fontSize: "48px", fontWeight: "700", lineHeight: "1.3", letterSpacing: "-0.02em" }, + children: title, + }, + }, + { + type: "div", + props: { + style: { fontSize: "28px", fontWeight: "500", opacity: "0.8", color: "#e0e7ff" }, + children: "小平市議 安竹洋平", + }, + }, + { + type: "div", + props: { + style: { fontSize: "20px", fontWeight: "400", opacity: "0.5", color: "#e0e7ff", marginTop: "8px" }, + children: "yasutakeyohei.com", + }, + }, + ], + }, + }, + ], + }, + } as any, + { + width: 1200, + height: 630, + fonts: [{ name: "sans-serif", data: fontBuffer, weight: 400, style: "normal" }], + } + ); + + const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }); + const pngBuffer = resvg.render().asPng(); + + return new Response(pngBuffer, { + headers: { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); +} -- cgit v1.3.1