diff --git a/client/functions/[[path]].ts b/client/functions/[[path]].ts index f043acc..1180b30 100644 --- a/client/functions/[[path]].ts +++ b/client/functions/[[path]].ts @@ -17,7 +17,6 @@ interface ProjectData { }>; } -// og:title を書き換える HTMLRewriter class OGTitleRewriter { private title: string; @@ -32,7 +31,6 @@ class OGTitleRewriter { } } -// og:description を書き換える HTMLRewriter class OGDescriptionRewriter { private description: string; @@ -47,7 +45,20 @@ class OGDescriptionRewriter { } } -// 日付を YYYY/MM/DD 形式にフォーマット(日本時間) +class OGImageRewriter { + private imageUrl: string; + + constructor(imageUrl: string) { + this.imageUrl = imageUrl; + } + + element(element: Element) { + if (element.getAttribute("property") === "og:image") { + element.setAttribute("content", this.imageUrl); + } + } +} + function formatDate(dateString: string): string { const date = new Date(dateString); return new Intl.DateTimeFormat("ja-JP", { @@ -74,14 +85,12 @@ async function fetchProjectData(eventId: string, apiEndpoint: string): Promise

): Promise): Promise +import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api"; +// biome-ignore lint/correctness/noUnusedImports: React is needed for JSX transform +import React from "react"; + +interface Env { + API_ENDPOINT: string; +} + +interface ProjectData { + id: string; + name: string; + startDate: string; + endDate: string; +} + +async function fetchProjectData(eventId: string, apiEndpoint: string): Promise { + try { + const response = await fetch(`${apiEndpoint}/projects/${eventId}`); + if (!response.ok) { + return null; + } + return await response.json(); + } catch (error) { + console.error("Failed to fetch project data:", error); + return null; + } +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + return new Intl.DateTimeFormat("ja-JP", { + year: "numeric", + month: "2-digit", + day: "2-digit", + timeZone: "Asia/Tokyo", + }) + .format(date) + .replace(/-/g, "/"); +} + +function truncateByWidth(text: string, maxWidth = 60): string { + let width = 0; + let result = ""; + + for (const char of text) { + // ASCII なら 1、それ以外は 2 + const charWidth = char.charCodeAt(0) < 128 ? 1 : 2; + + if (width + charWidth > maxWidth) { + break; + } + + width += charWidth; + result += char; + } + + if (result.length < text.length) { + result += "…"; + } + + return result; +} + +export const onRequestGet: PagesFunction = async (context) => { + const { env, params } = context; + const eventId = params.eventId; + + if (typeof eventId !== "string") { + console.error("eventId is not a string"); + return new Response("Invalid event ID", { status: 400 }); + } + + if (!eventId || !/^[A-Za-z0-9_-]{21}$/.test(eventId)) { + return new Response("Invalid event ID", { status: 400 }); + } + + const projectData = await fetchProjectData(eventId, env.API_ENDPOINT); + + if (!projectData) { + // プロジェクトがない場合はデフォルト画像にリダイレクト + const url = new URL(context.request.url); + return Response.redirect(`${url.origin}/og-image.jpg`, 302); + } + + const fontData = await fetch( + "https://unpkg.com/@fontsource/m-plus-1p@5.2.8/files/m-plus-1p-japanese-700-normal.woff", + ).then((res) => res.arrayBuffer()); + + const projectName = truncateByWidth(projectData.name, 72); + + const startDate = formatDate(projectData.startDate); + const endDate = formatDate(projectData.endDate); + const dateRange = startDate === endDate ? startDate : `${startDate} 〜 ${endDate}`; + + const response = new ImageResponse( +

+
+
+ + {projectName} + + + {dateRange} + +
+
+ +
イツヒマ
+
+
+
, + { + width: 1200, + height: 630, + fonts: [ + { + name: "M PLUS 1p", + data: fontData, + weight: 700, + style: "normal", + }, + ], + }, + ); + + return response; +}; diff --git a/client/package.json b/client/package.json index 0c05ba8..87ee5c0 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "preview": "vite preview" }, "dependencies": { + "@cloudflare/pages-plugin-vercel-og": "^0.1.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fullcalendar/core": "^6.1.15", diff --git a/package-lock.json b/package-lock.json index b3bf946..60cba8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "client": { "version": "0.0.0", "dependencies": { + "@cloudflare/pages-plugin-vercel-og": "^0.1.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fullcalendar/core": "^6.1.15", @@ -533,6 +534,12 @@ "node": ">=14.21.3" } }, + "node_modules/@cloudflare/pages-plugin-vercel-og": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@cloudflare/pages-plugin-vercel-og/-/pages-plugin-vercel-og-0.1.2.tgz", + "integrity": "sha512-aWA2pHu1R2UbOJIcSASCuViyYVEhYkZ3tWTrTnr+yLosBfksOzjecHQkCkPv/jy75hGPrn1BbtsQR+oWVFkp1g==", + "license": "MIT" + }, "node_modules/@cloudflare/workers-types": { "version": "4.20250610.0", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250610.0.tgz",