Skip to content

Commit 679f735

Browse files
authored
Merge pull request #36 from ut-code/main
relase: og 出し分け
2 parents 299c92a + 8f6a66a commit 679f735

File tree

9 files changed

+213
-2
lines changed

9 files changed

+213
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
22
.direnv
3+
CLAUDE.local.md

biome.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"files": {
99
"ignoreUnknown": false,
10-
"ignore": []
10+
"ignore": ["dist", "node_modules"]
1111
},
1212
"formatter": {
1313
"enabled": true,

bun.lock

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"zod": "^3.24.2",
3434
},
3535
"devDependencies": {
36+
"@cloudflare/workers-types": "^4.20250610.0",
3637
"@eslint/js": "^9.21.0",
3738
"@types/react": "^19.0.10",
3839
"@types/react-dom": "^19.0.4",
@@ -136,6 +137,8 @@
136137

137138
"@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
138139

140+
"@cloudflare/workers-types": ["@cloudflare/[email protected]", "", {}, "sha512-HxnUoey3QxCEfy07pUm7J42jBi9YPHq/hA3fw6JmOqYLHdviHI28OA8lup+2RUaHwDzh6q1DSfrBvvDqde645A=="],
141+
139142
"@emotion/babel-plugin": ["@emotion/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
140143

141144
"@emotion/cache": ["@emotion/[email protected]", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
@@ -1062,6 +1065,22 @@
10621065

10631066
"@eslint/eslintrc/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
10641067

1068+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
1069+
1070+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1071+
1072+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1073+
1074+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1075+
1076+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
1077+
1078+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
1079+
1080+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
1081+
1082+
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1083+
10651084
"@typescript-eslint/parser/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
10661085

10671086
"@typescript-eslint/type-utils/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1073,5 +1092,17 @@
10731092
"esbuild-register/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
10741093

10751094
"eslint/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
1095+
1096+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1097+
1098+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
1099+
1100+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1101+
1102+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1103+
1104+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1105+
1106+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["[email protected]", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
10761107
}
10771108
}

client/functions/[[path]].ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/// <reference types="@cloudflare/workers-types" />
2+
3+
interface Env {
4+
API_ENDPOINT: string;
5+
}
6+
7+
interface ProjectData {
8+
id: string;
9+
name: string;
10+
startDate: string;
11+
endDate: string;
12+
allowedRanges: Array<{
13+
id: string;
14+
projectId: string;
15+
startTime: string;
16+
endTime: string;
17+
}>;
18+
}
19+
20+
// og:title を書き換える HTMLRewriter
21+
class OGTitleRewriter {
22+
private title: string;
23+
24+
constructor(title: string) {
25+
this.title = title;
26+
}
27+
28+
element(element: Element) {
29+
if (element.getAttribute("property") === "og:title") {
30+
element.setAttribute("content", this.title);
31+
}
32+
}
33+
}
34+
35+
// og:description を書き換える HTMLRewriter
36+
class OGDescriptionRewriter {
37+
private description: string;
38+
39+
constructor(description: string) {
40+
this.description = description;
41+
}
42+
43+
element(element: Element) {
44+
if (element.getAttribute("property") === "og:description") {
45+
element.setAttribute("content", this.description);
46+
}
47+
}
48+
}
49+
50+
// 日付を YYYY/MM/DD 形式にフォーマット
51+
function formatDate(dateString: string): string {
52+
const date = new Date(dateString);
53+
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`;
54+
}
55+
56+
// 時刻を HH:MM 形式にフォーマット
57+
function formatTime(dateString: string): string {
58+
const date = new Date(dateString);
59+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
60+
}
61+
62+
// プロジェクト情報を取得
63+
async function fetchProjectData(eventId: string, apiEndpoint: string): Promise<ProjectData | null> {
64+
try {
65+
const response = await fetch(`${apiEndpoint}/projects/${eventId}`);
66+
if (!response.ok) {
67+
return null;
68+
}
69+
return await response.json();
70+
} catch (error) {
71+
console.error("Failed to fetch project data:", error);
72+
return null;
73+
}
74+
}
75+
76+
// eventId が有効かチェック(21文字のnanoid)
77+
function isValidEventId(path: string): boolean {
78+
// パスが /eventId の形式で、eventId が21文字の英数字・ハイフン・アンダースコア
79+
const match = path.match(/^\/([A-Za-z0-9_-]{21})$/);
80+
return !!match;
81+
}
82+
83+
// パスから eventId を抽出
84+
function extractEventId(path: string): string | null {
85+
const match = path.match(/^\/([A-Za-z0-9_-]{21})$/);
86+
return match ? match[1] : null;
87+
}
88+
89+
// biome-ignore lint/suspicious/noExplicitAny: This is a Cloudflare Worker context
90+
export async function onRequest(context: EventContext<Env, any, any>): Promise<Response> {
91+
const { request, env, next } = context;
92+
const url = new URL(request.url);
93+
const path = url.pathname;
94+
95+
// 静的アセットや API ルートは通常通り処理
96+
if (
97+
path.startsWith("/assets/") ||
98+
path.startsWith("/api/") ||
99+
path.startsWith("/public/") ||
100+
path.includes(".") // ファイル拡張子がある場合
101+
) {
102+
return await next();
103+
}
104+
105+
// eventId パターンをチェック
106+
if (!isValidEventId(path)) {
107+
return await next();
108+
}
109+
110+
const eventId = extractEventId(path);
111+
if (!eventId) {
112+
return await next();
113+
}
114+
115+
// 元のHTMLレスポンスを取得
116+
const response = await next();
117+
118+
// HTMLでない場合はそのまま返す
119+
const contentType = response.headers.get("content-type");
120+
121+
// 304 Not Modified の場合やHTMLでない場合はスキップ
122+
if (response.status === 304 || (!contentType?.includes("text/html") && contentType !== null)) {
123+
return response;
124+
}
125+
126+
// プロジェクト情報を取得
127+
const projectData = await fetchProjectData(eventId, env.API_ENDPOINT);
128+
129+
if (!projectData) {
130+
return response;
131+
}
132+
133+
// og:title を書き換え
134+
const ogTitle = `${projectData.name} - イツヒマ`;
135+
136+
// og:description を作成
137+
const startDate = formatDate(projectData.startDate);
138+
const endDate = formatDate(projectData.endDate);
139+
const dateRange = startDate === endDate ? startDate : `${startDate} - ${endDate}`;
140+
141+
let timeRange = "";
142+
if (projectData.allowedRanges && projectData.allowedRanges.length > 0) {
143+
const range = projectData.allowedRanges[0];
144+
const startTime = formatTime(range.startTime);
145+
const endTime = formatTime(range.endTime);
146+
timeRange = `${startTime} - ${endTime}`;
147+
}
148+
149+
const ogDescription = timeRange ? `日程: ${dateRange} | 時間: ${timeRange}` : `日程: ${dateRange}`;
150+
151+
return new HTMLRewriter()
152+
.on('meta[property="og:title"]', new OGTitleRewriter(ogTitle))
153+
.on('meta[property="og:description"]', new OGDescriptionRewriter(ogDescription))
154+
.transform(response);
155+
}

client/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
88
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
99
<meta property="og:type" content="website" />
10-
<meta property="og:title" content="イツヒマ - 「いつ暇?」で日程調整しよう" />
10+
<meta property="og:title" content="イツヒマ" />
11+
<meta property="og:description" content="「いつ暇?」で日程調整しよう" />
1112
<meta property="og:image" content="https://itsuhima.utcode.net/og-image.webp" />
1213
<meta property="og:site_name" content="イツヒマ" />
1314
<meta property="og:locale" content="ja_JP" />

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"zod": "^3.24.2"
3434
},
3535
"devDependencies": {
36+
"@cloudflare/workers-types": "^4.20250610.0",
3637
"@eslint/js": "^9.21.0",
3738
"@types/react": "^19.0.10",
3839
"@types/react-dom": "^19.0.4",

client/wrangler.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name = "itsuhima"
2+
compatibility_date = "2024-12-01"
3+
4+
[env.production.vars]
5+
API_ENDPOINT = "https://api.itsuhima.utcode.net"
6+
7+
[env.preview.vars]
8+
API_ENDPOINT = ""
9+
10+
[vars]
11+
API_ENDPOINT = "http://localhost:3000"
12+
13+
[env.development.vars]
14+
API_ENDPOINT = "http://localhost:3000"

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/routes/projects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-nocheck FIXME: 駄目すぎる
12
import { type Response, Router } from "express";
23
import { nanoid } from "nanoid";
34
import { z } from "zod";

0 commit comments

Comments
 (0)