// Cloudflare Pages Middleware for OGP injection // Injects OGP tags into index.html for all HTML requests interface Env { BLOG_BUCKET: R2Bucket ASSETS: Fetcher } interface BlogMeta { title: string description: string date: string tags?: string[] image?: string } function parseFrontmatter(content: string): { meta: BlogMeta content: string } { // Normalize line endings const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) if (!match) { return { meta: { title: "Untitled", description: "", date: "" }, content: normalized, } } const frontmatter = match[1] const meta: BlogMeta = { title: "Untitled", description: "", date: "" } for (const line of frontmatter.split("\n")) { const colonIndex = line.indexOf(":") if (colonIndex === -1) continue const key = line.slice(0, colonIndex).trim() let value = line.slice(colonIndex + 1).trim() // Remove quotes if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1) } if (key === "title") meta.title = value else if (key === "description") meta.description = value else if (key === "date") meta.date = value else if (key === "image") meta.image = value else if (key === "tags") { // Parse YAML array const tagsMatch = value.match(/\[(.*)\]/) if (tagsMatch) { meta.tags = tagsMatch[1] .split(",") .map((t) => t.trim().replace(/['"]/g, "")) } } } return { meta, content: match[2] } } // OGP metadata for static pages const staticPages: Record = { "/": { title: "c30.life", description: "c30's homepage" }, "/links": { title: "Links - c30.life", description: "c30のリンク集" }, "/timeline": { title: "Timeline - c30.life", description: "c30のタイムライン", }, "/info": { title: "Info - c30.life", description: "c30.lifeの情報" }, "/misskey": { title: "Misskey - c30.life", description: "c30のMisskeyアカウント一覧", }, "/mastodon": { title: "Mastodon - c30.life", description: "c30のMastodonアカウント一覧", }, "/environments": { title: "Environments - c30.life", description: "c30の開発環境", }, "/servers": { title: "Servers - c30.life", description: "c30が運営するサーバー一覧", }, "/pubkeys": { title: "Pubkeys - c30.life", description: "c30の公開鍵" }, "/watched-animes": { title: "Watched Animes - c30.life", description: "c30が観たアニメ・映画", }, "/downloads": { title: "Downloads - c30.life", description: "公開ファイルのダウンロード", }, "/blog": { title: "Blog - c30.life", description: "c30のブログ記事一覧" }, } function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") } function injectOgp( html: string, title: string, description: string, url: string, image = "https://c30.life/c30.png", ): string { // Replace title html = html.replace( /.*?<\/title>/, `<title>${escapeHtml(title)}`, ) // Replace meta tags html = html.replace( //, ``, ) html = html.replace( //, ``, ) html = html.replace( //, ``, ) html = html.replace( //, ``, ) // Add og:url if (html.includes('property="og:type"')) { html = html.replace( //, `\n `, ) } return html } export const onRequest: PagesFunction = async (context) => { const { request, next, env } = context const url = new URL(request.url) const pathname = url.pathname console.log("Middleware processing:", pathname) // Skip API routes and static assets - must call next() for API handlers if (pathname.startsWith("/api/")) { return next() } if ( pathname.match( /\.(js|css|png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|xml|txt|json)$/, ) ) { return next() } // Get the original response (index.html for SPA routes) let response: Response try { response = await next() } catch (e) { console.error("Middleware next() error:", e) return new Response("Internal Server Error", { status: 500 }) } // Only modify HTML responses const contentType = response.headers.get("content-type") if (!contentType?.includes("text/html")) { return response } const originalHtml = await response.text() let title = "c30.life" let description = "c30's homepage" let image = "https://c30.life/c30.png" // Check for blog post const blogMatch = pathname.match(/^\/blog\/([^/]+)$/) if (blogMatch && env.BLOG_BUCKET) { const slug = blogMatch[1] try { const object = await env.BLOG_BUCKET.get(`${slug}.md`) if (object) { const content = await object.text() const { meta } = parseFrontmatter(content) title = `${meta.title} - c30.life` description = meta.description || `${meta.title}の記事` if (meta.image) { image = meta.image } } } catch (e) { console.error("Failed to fetch blog post:", e) } } else if (pathname.startsWith("/downloads/") && pathname !== "/downloads/") { // Downloads subfolder const subPath = decodeURIComponent(pathname.replace("/downloads/", "")) const folderName = subPath.split("/").pop() || subPath title = `${folderName} - Downloads - c30.life` description = `${subPath} のファイル一覧` console.log("Downloads subfolder:", { pathname, subPath, folderName, title, description, }) } else if (staticPages[pathname]) { // Static page title = staticPages[pathname].title description = staticPages[pathname].description } const modifiedHtml = injectOgp( originalHtml, title, description, url.href, image, ) // Copy headers but exclude content-related headers that we'll set ourselves const newHeaders = new Headers() for (const [key, value] of response.headers.entries()) { const lowerKey = key.toLowerCase() if ( lowerKey !== "content-type" && lowerKey !== "content-length" && lowerKey !== "content-encoding" ) { newHeaders.set(key, value) } } newHeaders.set("Content-Type", "text/html; charset=utf-8") return new Response(modifiedHtml, { status: response.status, headers: newHeaders, }) }