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",