Supabase Edge Functions 生成 Open Graph 图片并用 Storage CDN 做缓存:解析 supabase 仓库中的 lwx-og 示例 Supabase Edge Functions 生成 Open Graph 图片并用 Storage CDN 做缓存解析 supabase 仓库中的 lwx-og 示例【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase本篇以 lwx-og 函数的说明文档 为主体讲清「在 Supabase Edge Function 里用 Deno React 服务端渲染实时生成 Open Graph 图片再把产物上传到 Supabase Storage 公共桶、由 Storage CDN 托管缓存」这一完整方案。读完后你可以复现本地运行与部署流程并理解从请求解析、数据库取数、ImageResponse渲染到 CDN 缓存穿透的每一步实现以及它与lwx-ticket、lwx-ticket-og两个伴生函数的协作关系。方案要解决什么问题Open GraphOG图片决定了链接分享到社交平台时的卡片预览效果。传统做法是预生成静态图但 lwx-og 这类「个性化分享卡」Supabase Launch Week 的门票卡片需要为每个用户动态生成一张独一无二的图片且会随用户资料、分享行为例如同时在 Twitter 和 LinkedIn 分享后升级 platinum 版而变化。该 README 给出的一行式方案概括是用 Deno 和 Supabase Edge Functions 生成 Open Graph 图片并用 Supabase Storage 的 CDN 缓存生成后的图片。即把「生成」放在边缘函数、「存储 分发缓存」交给 Storage 公共桶两者各取所长。整体架构与请求流程从源码结构看整个链路如下社交爬虫或浏览器请求GET /functions/v1/lwx-og?username用户名函数用 service role 客户端查 Postgres实际查的是物化视图lwx_tickets_golden拿到姓名、票号、分享状态等数据根据票种regular / platinum / secret选择配色与背景素材用ImageResponse渲染一张 1200×628 的 PNG把 PNG 以upsert方式上传到 Storage 的images桶路径为lwx/og/票种/用户名.png触发伴生函数lwx-ticket-ogfire-and-forget让它再合成一张「嵌入门票」的复合 OG 卡片最后return await fetch(公共 Storage URL 时间戳)——响应实际是 Storage 公共对象经 Storage CDN 回源并缓存后续同一路径请求直接由 CDN 命中。关键文件只有两个入口 apps/www/supabase/functions/lwx-og/index.ts基于 Deno 标准库的 HTTP server 挂载 handler核心 apps/www/supabase/functions/lwx-og/handler.tsx全部业务逻辑取数、渲染、上传、联动。入口文件非常薄仅做依赖声明和挂载import { serve } from https://deno.land/std0.168.0/http/server.ts import { handler } from ./handler.tsx console.log(Function lwx-og up and running) serve(handler)本地运行与部署README 原文操作README 给出的本地运行步骤supabase start supabase functions serve lwx-og --no-verify-jwt --env-file ./supabase/.env.local然后访问本地函数端点Supabase CLI 的本地端点路径与函数名一致http://localhost:54321/functions/v1/lwx-og?usernamethorwebdev说明README 原文 中「Navigate to」与 Demo 一行写的是同目录lwx-ticket函数的端点/functions/v1/lwx-ticket?usernamethorwebdev这是姊妹函数的门票图端点本文函数lwx-og对应的本地路径为/functions/v1/lwx-og。两个参数值得注意--no-verify-jwt关闭 JWT 校验。OG 图片是由社交平台爬虫抓取的爬虫不会携带 Supabase JWT所以必须允许匿名访问--env-file ./supabase/.env.local本地显式注入环境变量如函数用到的MISC_USE_URL、MISC_USE_SERVICE_ROLE_KEY部署时则由 Supabase 平台侧提供。部署命令只有一条supabase functions deploy lwx-og --no-verify-jwtREADME 还列出了参考资源og_edge 库的官方文档deno.land/x/og_edgeREADME 中写的是 0.0.2 版以及 Vercel 文档中的 OG Image Generation 示例章节——og_edge 正是 Vercel OG 方案在 Deno 环境的移植实现。核心实现详解handler.tsx下面按 handler 的执行顺序拆解 handler.tsx。依赖与模块级初始化文件头导入三样核心依赖handler.tsx#L1-L3import { ImageResponse } from https://deno.land/x/og_edge0.0.4/mod.ts import React from https://esm.sh/react18.2.0?deno-std0.140.0 import { createClient } from jsr:supabase/supabase-js2注意源码实际锁定的og_edge版本是0.0.4而 README 参考链接写的是 0.0.2以源码为准。模块加载时就发起了字体预取handler.tsx#L10-L17——字体和背景素材都放在同一个 Storage 公共桶里由 Storage CDN 分发const STORAGE_URL https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lwx // Load custom font const FONT_URL ${STORAGE_URL}/font/CircularStd-Book.otf const MONO_FONT_URL ${STORAGE_URL}/font/SourceCodePro-Regular.ttf?t2023-07-18T13%3A03%3A29.474Z const font fetch(new URL(FONT_URL, import.meta.url)).then((res) res.arrayBuffer()) const mono_font fetch(new URL(MONO_FONT_URL, import.meta.url)).then((res) res.arrayBuffer())两个字体 fetch 被写成模块级 Promise函数冷启动即开始下载CircularStd-Book.otf正文与SourceCodePro-Regular.ttf等宽渲染时await复用避免每次请求重复拉字体。URL 里的?t...时间戳参数是典型的缓存穿透手段。此外源码定义了 CORS 头允许任意来源与授权头handler.tsx#L5-L8仓库中另有一个共享版 cors.ts 可供其他函数复用。参数解析与数据库客户端handler 从 URL 解析参数handler.tsx#L44-L46const username url.searchParams.get(username) ?? url.searchParams.get(amp;username) const assumePlatinum url.searchParams.get(platinum) ?? url.searchParams.get(amp;platinum)amp;username是 HTML 实体转义残留被转义成amp;的兜底解析说明该端点曾被以 HTML 属性形式内嵌过。缺少username时直接抛错并走 400 分支。数据库客户端使用service role服务端特权密钥const supabaseAdminClient createClient( Deno.env.get(MISC_USE_URL) ?? , Deno.env.get(MISC_USE_SERVICE_ROLE_KEY) ?? )源码注释说明这两个环境变量在部署时由平台侧导出service role 能绕过 RLS仅适合放在服务端函数中不能出现在浏览器代码里。社交分享追踪与票种判定函数利用请求头user-agent判断来源平台首次被 Twitter / LinkedIn 抓取时把对应字段置为nowhandler.tsx#L60-L72if (userAgent?.toLocaleLowerCase().includes(twitter)) { await supabaseAdminClient .from(LW_TABLE) // lwx_tickets .update({ sharedOnTwitter: now }) .eq(username, username) .is(sharedOnTwitter, null) // 只在首次分享时写入 } else if (userAgent?.toLocaleLowerCase().includes(linkedin)) { // 同理写 sharedOnLinkedIn }.is(sharedOnTwitter, null)保证只有「尚未分享过」的行才会被更新实现幂等埋点。随后从物化视图读取展示数据handler.tsx#L75-L87const LW_TABLE lwx_tickets const LW_MATERIALIZED_VIEW lwx_tickets_golden const { data, error } await supabaseAdminClient .from(LW_MATERIALIZED_VIEW) .select(name, ticketNumber, sharedOnTwitter, sharedOnLinkedIn, metadata) .eq(username, username) .maybeSingle()票种判定规则handler.tsx#L85-L87metadata.hasSecretTicket为真 →secret否则 Twitter 与 LinkedIn都分享过 →platinum其余 →regular。若请求显式带platinum参数但用户实际不是 platinum函数会直接返回桶内预置的趣味图assets/golden_no_meme.png而不是生成正式卡片——一个轻量的「防装」降级设计。三种票种各有独立的背景图、Logo 与配色STYLING_CONGIFhandler.tsx#L22-L41如 regular 是深灰底#303030 浅色字platinum/secret 是浅灰底#f1f1f1 深色字。说明lwx_tickets表与lwx_tickets_golden物化视图定义在 www 项目的 Supabase 数据库中仓库里检入了它们的引用上述两个函数以及部分 Launch Week 相关迁移如 20240723155310_add_lw12_ticketing_schema.sql但从函数中select的字段列表name、ticketNumber、sharedOnTwitter、sharedOnLinkedIn、metadata可以完整推知视图对外暴露的列。用 React JSX 渲染 1200×628 的 PNG渲染部分把整张卡片写成一个 React 函数组件树传给ImageResponsehandler.tsx#L128-L294。布局常量handler.tsx#L119-L126const OG_WIDTH 1200 const OG_HEIGHT 628 const TICKET_WIDTH 1000 const TICKET_HEIGHT TICKET_WIDTH / 2 const TICKET_PADDING_X 60 const TICKET_PADDING_Y 60 const OG_PADDING_X (OG_WIDTH - TICKET_WIDTH) / 2 const OG_PADDING_Y (OG_HEIGHT - TICKET_HEIGHT) / 2卡片由四层构成position: absolute铺满的 OG 背景图zIndex 0、居中圆角带阴影的票面背景图zIndex 1、左上角的姓名 头衔/公司名区块无元数据时回退显示username、左下角的 Logo 补零后的票号NO 00000001风格「Launch Week X / DEC 11-15 / 8AM PT」文字区块。字体通过fonts选项以ArrayBuffer注入渲染引擎{ width: OG_WIDTH, height: OG_HEIGHT, fonts: [ { name: Circular, data: fontData, style: normal }, { name: SourceCodePro, data: monoFontData, style: normal }, ], headers: { content-type: image/png, cache-control: public, max-age31536000, s-maxage31536000, no-transform, immutable, cdn-cache-control: max-age31536000, }, }这里cache-control声明了「一年、immutable」的激进缓存策略票卡内容稳定后几乎不变适合长缓存真正需要刷新时靠下面上传 时间戳机制解决。上传 Storage 并借 CDN 完成缓存README 主题的核心渲染完成后立即上传handler.tsx#L296-L307const { error: storageError } await supabaseAdminClient.storage .from(images) .upload(lwx/og/${ticketType}/${username}.png, generatedTicketImage.body!, { contentType: image/png, cacheControl: 0, // Update cached og image, people might need to update info upsert: true, })三个细节是缓存策略的关键对象路径按「票种 用户名」确定性命名同一用户同一票种永远命中同一路径天然形成内容寻址upsert: truecacheControl: 0源码注释写明「用户可能会更新自己的资料」所以允许覆盖写入、对象本身不设长缓存靠下一次访问时重新生成来纠正内容返回的是公共 Storage URL 而不是函数直出 PNGconst NEW_TIMESTAMP new Date() return await fetch(${STORAGE_URL}/og/${ticketType}/${username}.png?t${NEW_TIMESTAMP})函数fetch桶内刚写入的对象并把其响应体流式返回给调用方。这一跳让首次请求就经过 Storage CDN回源拉取并缓存而后续所有爬虫/浏览器的请求只要不带时间戳就会直接命中 CDN 上已缓存的对象——这就是 README 标题「cache the generated image with Supabase Storage CDN」的含义生成逻辑在边缘函数但分发与缓存完全由 Storage CDN 承担边缘函数不再成为热点。?t${NEW_TIMESTAMP}仅用于强制本次读取拿到最新写入的对象避免上传后立即读旧缓存。伴生函数联动lwx-ticket-og上传成功后handler 还会以 fire-and-forget 方式触发另一个边缘函数handler.tsx#L309-L321// Generate og image fetch(https://obuldanrptloktxcffvn.supabase.co/functions/v1/lwx-ticket-og, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer anon key, // 源码中为内联的 anon JWT }, body: JSON.stringify({ username, platinum }), }).catch((err) console.log(generate og err, err))被触发的 lwx-ticket-og/handler.tsx 接收{ username, platinum }从 Storage 取出已生成的门票图lwx/tickets/票种/v1/用户名.png套上og_bg_regular.png/og_bg_platinum.png背景再渲染一张 1200×628 的复合卡片同样以upsert: true写回lwx/og/...最后返回 JSON{ success: true }。由于lwx-og主链路不等待这个 POST.catch吞掉异常门票卡的首次可用性不受复合卡生成的影响两条产物路径门票图与复合 OG 卡解耦。同目录下的 lwx-ticket/handler.tsx 则是门票图本身的生成端渲染 1200×628 门票、上传到lwx/tickets/票种/用户名.pngREADME 里的 Demo 链接指向的就是它再往前还有 Launch Week 11 时期的同构实现 lw11-og/README.md其运行/部署命令与本文完全一致可见这是该仓库沉淀下来的一套标准打法。错误处理与响应约定整段 handler 被try/catch包裹任何失败缺参数、查无此用户、Storage 上传失败等统一返回 400 JSON 并带上 CORS 头handler.tsx#L326-L331return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, Content-Type: application/json }, status: 400, })对爬虫而言拿到 4xx 时社交平台会优雅地放弃卡片预览因此「出错就返回结构化 JSON 400」是安全的降级。可复用的工程要点小结生成与分发解耦动态图片在 Edge Function 里一次性渲染产物落 Storage 公共桶由 CDN 承接后续流量确定性路径 upsert用业务主键票种/用户名命名对象路径允许覆盖以实现「内容可更新、路径不变」缓存策略分层对象cacheControl: 0保证内容可纠偏响应头immutable一年缓存 时间戳查询参数做定向穿透--no-verify-jwt只用于面向爬虫的端点需要鉴权的业务端点不应照搬service role 密钥只存在于函数环境静态素材字体、背景、Logo也放进同一个 Storage 公共桶让函数素材同样享受 CDN 分发并用?t时间戳管理素材版本。按 README 的命令本地serve验证、deploy上线之后这套「Edge Function 渲染 Storage CDN 缓存」的 OG 图片方案即可直接迁移到自己的 Supabase 项目中使用。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考