首页
/ Supabase 官网(apps/www)内容工程指南:图片规范、OG 图动态生成与 /go/ 落地页系统

Supabase 官网(apps/www)内容工程指南:图片规范、OG 图动态生成与 /go/ 落地页系统

2026-09-06 16:06:22作者:何将鹤

Supabase 的营销站点(即 supabase.com 的源码,位于 apps/www)有一套成熟的内容工程规范:博客、活动、客户故事三类内容的图片字段约定,基于 Edge Function 的 Open Graph 图片动态生成,按 git diff 自动圈定范围的 Playwright 无障碍 E2E 检查,以及用 TypeScript 对象 + Zod 模式构建的 /go/* 独立营销活动落地页系统。读完本文,你可以掌握在这套站点上新增一篇带规范图片的博客/活动/客户故事、理解 OG 图的本地调试方式,以及如何按模板定义一个新的 /go/ 营销页并注册上线。

站点概览与本地启动

apps/www 是 Supabase 主站(营销官网)的 Next.js 应用,内容以目录形式组织:

  • 博客文章:apps/www/_blog/(MDX 文件)
  • 活动:apps/www/_events/(MDX 文件)
  • 客户故事(案例研究):apps/www/_customers/(MDX 文件)
  • 对比替代方案页:apps/www/_alternatives/(MDX 文件)
  • 营销活动落地页:apps/www/_go/(TypeScript 页面定义对象)

本地运行方式在根目录的开发指南 DEVELOPERS.md 中说明;启动前需要复制一份示例环境变量文件:

cp .env.local.example .env.local

后续章节聚焦该站点 README 中定义的四类最佳实践:图片处理、OG 图生成、内容 frontmatter 图片字段、端到端检查,以及 /go/ 页面系统。

图片最佳实践

站点 README 对新提交的图片有三条硬性要求:

  1. 缩放(Resize):所有新图片应只缩放到前端渲染所需的最大分辨率,不要上传比展示尺寸更大的图片。
  2. 压缩(Compress):提交前必须压缩,可使用 Clop、ImageOptim 等工具,在不产生明显画质损失的前提下减小文件体积。
  3. 存放位置(Locations):按内容类型分目录存放:
内容类型 目录
博客文章图片 apps/www/public/images/blog/
活动图片 apps/www/public/images/events/
客户 Logo(浅色底) apps/www/public/images/customers/logos/on-light/
客户 Logo(深色底) apps/www/public/images/customers/logos/on-dark/

OG 图生成:三类内容的不同策略

Open Graph 图(社交分享预览图)按内容类型采用不同处理方式:

  • 博客文章:使用静态图,通过 frontmatter 的 imgSocialimgThumb 字段指定(详见后文)。
  • 活动(Events):通过 og-images Edge Function 动态生成,可用可选的 og_image 字段覆盖。
  • 客户故事(Customer stories):一律动态生成,不提供静态图选项,frontmatter 中写 og_image 会被直接忽略。

动态生成由 supabase/functions/og-images/ 下的 Edge Function 承担。README 说明该函数在函数代码变更时通过 CI workflow(.github/workflows/og_images.yml)部署。

从源码看 og-images 的实现

handler.tsx 中的处理逻辑清晰展示了生成管线:

  • 参数解析handler.tsx#L28-L54):从 URL query 中读取 sitetitledescriptiontypeiconcustomerdateeventTypeduration。注意 getParamValue 兼容了 amp; 前缀的参数名(社交卡片 HTML 编码后 & 变成 & 的情况),且 site/icon/customer 会被转小写、title/description/date 会做 URI 解码。
  • 参数校验handler.tsx#L56-L61):缺少 sitetitle 时返回 404 和 { message: 'missing params' }site 无法识别时返回 404 和 site not found
  • 站点分发switch (site) 覆盖三种取值——docscustomersevents,分别渲染对应的 React 组件(DocsCustomerStoriesEvents),用 Satori(og_edgeImageResponse)合成 1200 × 630 的 PNG。
  • 字体加载handler.tsx#L14-L26):模块加载时异步 fetch 两个字体文件——Circular(正文)与 SourceCode(等宽),以 ArrayBuffer 形式注入 Satori 字体配置。
  • 长缓存handler.tsx#L89-L93):响应头带 cache-control: public, max-age=31536000, immutable 与 CDN 侧 max-age=31536000,即同一参数组合生成的图片在 CDN 上缓存一年——这解释了为什么 OG 图 URL 中把标题、日期等都编码进 query 参数。

本地调试流程见 og-images 的 README

supabase start
supabase functions serve og-images
# 然后访问 http://127.0.0.1:54321/functions/v1/og-images/?site=docs&title=...&description=...
supabase functions deploy og-images --no-verify-jwt   # 部署

主站 README 同时提醒:本地开发时该函数跑在 http://127.0.0.1:54321/functions/v1/og-images,前提是本地 Supabase 已通过 supabase start 启动。

内容 frontmatter 图片字段

不同内容类型使用不同的图片字段约定,核心目的是消除歧义。

博客文章:imgSocial 与 imgThumb

博客 frontmatter 支持两个图片字段:

  • imgSocial:用于 Open Graph / 社媒分享(X、LinkedIn 等)。由于在信息流中独立出现、没有伴随文字,应包含文字覆盖层(text overlay)
  • imgThumb:用于站内缩略图(博客列表页、精选文章)。它总是与文章标题和描述一起展示,不需要文字覆盖层

这套命名取代了旧版容易混淆的 thumbog 字段,语义更明确:imgSocial 为社交分享定制(需要文字),imgThumb 为站内展示优化(干净无文字)。

路径格式:frontmatter 中一律写相对路径(仅文件名,或 子目录/文件名),/images/blog/ 前缀由代码自动补上:

---
title: 'My Blog Post'
imgSocial: 2025-01-01-my-post/og.png    # 正确:相对路径,带文字覆盖层
imgThumb: 2025-01-01-my-post/thumb.png  # 正确:相对路径,干净图
---
  • ✅ 正确:imgSocial: my-post/og.pngimgSocial: og.png
  • ❌ 错误:imgSocial: /images/blog/my-post/og.png(会产生双重前缀)

上例中图片实际存放于 apps/www/public/images/blog/2025-01-01-my-post/og.png 和同目录的 thumb.png。也可以用同一张图同时填充两个字段:

---
title: 'My Blog Post'
imgSocial: my-image.png   # 存放于 apps/www/public/images/blog/my-image.png
imgThumb: my-image.png
---

回退(fallback)行为,两套优先级正好镜像:

场景 优先级 1 优先级 2 优先级 3
站内展示(访客看到的) imgThumb imgSocial 占位图 /images/blog/blog-placeholder.png
社交分享(OG meta 标签) imgSocial imgThumb 无(undefined)

字段缺失时的表现:

  • 只给 imgThumb:站内正常显示,社交分享用 imgThumb 兜底;
  • 只给 imgSocial:社交分享用它,站内展示用它兜底;
  • 两者都没有:站内显示占位图,社交分享无图;
  • 最佳实践:两个字段都提供。

源码侧可对照 blog-images.ts:站内缩略图取 imgThumb || imgSocial(见 blog-images.ts#L47-L50),社交元数据取 imgSocial || imgThumbblog-images.ts#L59-L60),且当文件缺少 imgThumb 时会输出类似 missing "imgThumb" 的构建期警告。回退逻辑有对应单测 blog-images.test.ts 覆盖"站内优先 imgThumb / 社交优先 imgSocial / 缺字段告警"等断言。

活动(Events)

活动为避免与博客混淆,采用另一套字段:

  • thumb:活动网格项缩略图(列表页小卡片)。
  • cover_url:精选活动横幅(活动页大展示位)。
  • og_image(可选):覆盖动态生成的 OG 图。

OG 图默认由 og-images 函数按以下信息动态生成:活动类型(conference、hackathon 等)、标题(有 meta_title 则优先)、描述(有 meta_description 则优先)、日期(按活动自身时区格式化为 "DD MMM YYYY")、时长(如提供)。需要自定义 OG 图时才写 og_image 字段覆盖:

---
title: 'Supabase Meetup'
thumb: /images/events/2025-01-meetup/thumbnail.png
cover_url: https://external-cdn.com/event-banner.jpg
og_image: /images/events/2025-01-meetup/custom-og.png   # 可选覆盖
---

og_image 为可选项,不提供时 OG 图自动走 Edge Function 生成。

客户故事(Case Studies)

客户故事定义在 apps/www/_customers/ 下的 MDX 文件中,与博客/活动的关键区别是:不使用静态 OG 图

  • 客户故事的 frontmatter 不要包含 og_image 字段——它会直接被忽略,OG 图永远由 og-images 函数按客户 slugtitle(有 meta_title 则优先)动态生成。
  • Logo 规范:客户 logo 必须是单色透明图,优先 SVG(PNG 也可)。按"置于何种背景上"分文件夹存放,对应不同 frontmatter 字段:
文件夹 图形颜色 frontmatter 字段 展示于
/images/customers/logos/on-light/ 深色/黑色 logo 浅色模式
/images/customers/logos/on-dark/ 浅色/白色 logo_inverse 深色模式

不得使用彩色品牌标识,合并前需在 /customers 页面同时以两种主题预览。图标类资产(如首页 chips)则保留在 /images/customers/logos/{slug}-icon.svg

一个容易被忽略的细节:OG 图生成时会拉取 https://supabase.com/images/customers/logos/on-dark/{slug}.png,而 Satori 无法栅格化 SVG——所以即使站点用 SVG,on-dark/ 目录下仍必须存在 {slug}.png

示例(apps/www/_customers/company-abc.mdx 风格):

---
name: Company ABC
title: Company ABC built their platform with Supabase
# 不要包含 og_image —— 它会被自动生成
logo: /images/customers/logos/on-light/company-abc.svg
logo_inverse: /images/customers/logos/on-dark/company-abc.svg
---

Legacy 案例研究data/CustomerStories.ts 中的旧数据)则用 imgUrl 字段指向源数据中的案例图;经 BlogGridItem 组件渲染时会映射为 imgThumb,旧数据只需一张站内展示图,不需要独立的社交分享图:

{
  type: 'Customer Story',
  title: 'Company ABC built their platform with Supabase',
  description: '...',
  organization: 'Company ABC',
  imgUrl: 'images/customers/logos/on-light/company-abc.svg', // 相对 public/ 的完整路径
  logo: '/images/customers/logos/on-light/company-abc.svg',
  logo_inverse: '/images/customers/logos/on-dark/company-abc.svg',
  url: '/customers/company-abc',
}

端到端检查:Playwright + axe-core

内容页由 e2e/www 下的 Playwright 套件加载并用 axe-core 扫描;PR 只测试你改动影响到的页面。该套件目前强制一条无障碍规则:page-has-heading-one(每个页面必须有且只有一个 H1)。

测试当前分支改动影响的页面:

PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:www

注意一个陷阱:命令会从你的分支解析出要测哪些页面,但实际是从生产环境读取页面内容——因此看不到你的编辑,新增页面会 404。要验证自己的内容,需把 PLAYWRIGHT_BASE_URL 指向该 PR 的预览环境。

e2e/www/README.md 给出了更多细节,要点如下:

  • 默认范围:git diff(相对 origin/master 的提交 + 工作区改动)映射到四类内容目录——apps/www/_blog/*.mdx/blog/<slug>_events/*.mdx/events/<slug>_customers/*.mdx/customers/<slug>_alternatives/*.mdx/alternatives/<slug>。博客与活动文件名会去掉 YYYY-MM-DD- 前缀取 slug,客户与 alternatives 直接用文件名。
  • 范围上限:解析结果最多 20 个页面,超出只测排序后的前 20 个;全量检查用 pnpm e2e:www:all(同时忽略 20 页上限与 WWW_E2E_PAGE_PATHS,并默认 --max-failures=0)。
  • 手动指定页面WWW_E2E_PAGE_PATHS=/blog/some-slug pnpm e2e:www(接受逗号或换行分隔的站点相对路径列表)。
  • 每个页面的断言:状态码成功 + axe 扫描无 page-has-heading-one 违规。README 特别说明 ENFORCED_RULES 与 docs 套件刻意分开——docs 还强制 heading-order,但 www 全站扫描发现大量内容页违反该规则(几乎都来自相关博文卡片和尾部 h6 两个共享组件),现在强制会挂掉几乎所有 PR,需先把违规清零再加规则。
  • CI 行为:PR 触碰 www 内容、e2e/wwwe2e/shared 时运行;会等待 Vercel www 预览就绪并把 PLAYWRIGHT_BASE_URL 指向预览(解析不到预览则跳过而不是退回生产);draft PR 一律跳过。
  • 调试pnpm -C e2e/www exec playwright show-report 查看 HTML 报告,test-results/ 下有 trace 与截图。

Go 页面系统:/go/* 营销活动落地页

/go/ 是一套用于构建独立营销活动落地页的系统(lead generation、法务页、thank-you 流程等)。命名刻意保持通用——这些页面通常从广告、邮件或合作活动链接进入,不属于主站导航。页面定义在 TypeScript 对象中(而非 MDX),构建期经 Zod schema 校验。

组成部分

位置 职责
apps/www/_go/ 页面定义。每个文件导出一个 page 对象,index.tsx 注册全部页面
apps/www/app/go/[slug]/page.tsx App Router 路由——按 slug 渲染页面,处理 404 与 metadata
apps/www/components/Go/GoPageRenderer.tsx www 专属包装器——加 Supabase logo 头部与页脚,注册自定义 section 渲染器
packages/marketing/src/go/ 框架无关的核心:schema、section 组件、模板、表单 server action
packages/marketing/src/crm/ 表单 server action 使用的 CRM 客户端抽象(HubSpot + Customer.io)

GoPageRenderer.tsx 的实现与 README 描述完全对应:它提供 SkipToContent 跳转链接、顶部 logo 导航、<main> 内调用 marketing 包的 MarketingPageRenderer 并传入 customRenderers,以及带隐私政策/服务条款链接的页脚。

模板与 Section 结构

每个页面指定一个 template 决定顶层布局:

  • lead-gen — hero + 任意 section(表单、指标、特性网格、推文、社交证明等)。
  • thank-you — hero + section + 彩带动画(Confetti)。
  • legal — hero + 目录侧边栏 + markdown 正文。

页面本质是一个带类型的 section 对象数组。marketing 包的 SectionRenderer 按 section 的 type 字段分发到对应组件。从 SectionRenderer.tsx#L40-L85 的 switch 可以看到当前支持的内置类型:single-columntwo-columnthree-columnformfeature-gridmetricstweetsfaqcode-blockstepsquotehubspot-meeting,并以 never 穷尽检查保证新增类型必须处理。

其中两处值得注意的实现细节:

  • form 类型的 CRM 配置不外泄SectionRenderer.tsx#L50-L56):section 进入客户端 bundle 前会剥离 crm 字段,server action 在提交时按 formRef 从注册表重新解析——CRM 凭据因此不会出现在前端代码中。
  • tweets 无默认渲染器SectionRenderer.tsx#L63-L66):因为它依赖 topTweets 数据和 Pages Router 的 basePath,marketing 包不该知道这些 www 专属依赖,所以走 customRenderers 扩展点。

自定义渲染器(Custom renderers)

marketing 包不感知 www 特有的数据与路由上下文,因此需要 www 专属依赖的 section 类型没有默认渲染器。GoPageRenderer.tsx#L11-L13 中注册了唯一一个:

const customRenderers: CustomSectionRenderers = {
  tweets: TweetsSection,
}

这就是任何需要 www 专属依赖的 section 的标准扩展方式:在 GoPageRenderer.tsxcustomRenderers 中登记即可,SectionRenderer 会优先检查自定义渲染器再落到 switch 分发。

新增一个 Go 页面的三步流程

  1. apps/www/_go/<category>/my-page.tsx 新建文件,导出一个 page 对象(templateslugmetadataherosections);
  2. apps/www/_go/index.tsx 中注册该页面对象;
  3. 页面自动通过静态生成暴露在 /go/<slug>

example-lead-gen.tsx 是仓库提供的样板:template: 'lead-gen'slug: 'example-ebook',hero 里含 CTA 按钮组,sections 依次演示了 single-column(嵌入 MediaBlock 视频)、feature-gridstepscode-block(多文件标签页)、metricsquotefaqhubspot-meeting(带 meetingSlug)、tweetsform(字段定义 + crm 配置)——可作为新页面的复制起点。

apps/www/_go/index.tsx 的实际注册表还能看到一个维护约定:每个页面导入后在 pages: GoPageInput[] 数组中登记,并用行内注释标注生命周期,例如 maintain forever(永久保留,如 contestRules)、remove after May 31, 2026(活动期过后删除)、maintain while ... is active(跟随功能上线状态)。目录结构也反映了这一约定:events/(按活动命名的子目录,含 contest 与 thank-you 页面)、lead-gen/legal/pre-release/webinar/thank-you/ 等。

小结

apps/www 的 README 把官网内容工程拆成了可执行的规则:图片"缩放—压缩—按类型存放"三原则;OG 图按内容类型区分静态(博客双字段)与动态(og-images Edge Function,Satori 渲染 1200×630、一年 CDN 缓存);frontmatter 字段语义明确且回退链有单测兜底(blog-images.test.ts);每次 PR 由 git-diff 驱动 Playwright + axe-core 做最小集无障碍校验(e2e/www/README.md);营销落地页则收敛为"TS 对象 + Zod 校验 + 模板 + SectionRenderer 分发 + www 侧自定义渲染器"的清晰分层(packages/marketing/src/go/)。这套规范对新贡献者的价值在于:每一类内容该放哪、写什么字段、如何本地验证,都有明确且可验证的答案。

登录后查看全文
热门项目推荐
相关项目推荐