Supabase 官网(apps/www)内容工程指南:图片规范、OG 图动态生成与 /go/ 落地页系统
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 对新提交的图片有三条硬性要求:
- 缩放(Resize):所有新图片应只缩放到前端渲染所需的最大分辨率,不要上传比展示尺寸更大的图片。
- 压缩(Compress):提交前必须压缩,可使用 Clop、ImageOptim 等工具,在不产生明显画质损失的前提下减小文件体积。
- 存放位置(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 的
imgSocial与imgThumb字段指定(详见后文)。 - 活动(Events):通过
og-imagesEdge 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 中读取
site、title、description、type、icon、customer、date、eventType、duration。注意getParamValue兼容了amp;前缀的参数名(社交卡片 HTML 编码后&变成&的情况),且site/icon/customer会被转小写、title/description/date会做 URI 解码。 - 参数校验(handler.tsx#L56-L61):缺少
site或title时返回 404 和{ message: 'missing params' };site无法识别时返回 404 和site not found。 - 站点分发:
switch (site)覆盖三种取值——docs、customers、events,分别渲染对应的 React 组件(Docs、CustomerStories、Events),用 Satori(og_edge的ImageResponse)合成 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:用于站内缩略图(博客列表页、精选文章)。它总是与文章标题和描述一起展示,不需要文字覆盖层。
这套命名取代了旧版容易混淆的 thumb 和 og 字段,语义更明确: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.png或imgSocial: 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 || imgThumb(blog-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函数按客户slug和title(有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/www或e2e/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-column、two-column、three-column、form、feature-grid、metrics、tweets、faq、code-block、steps、quote、hubspot-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.tsx 的 customRenderers 中登记即可,SectionRenderer 会优先检查自定义渲染器再落到 switch 分发。
新增一个 Go 页面的三步流程
- 在
apps/www/_go/<category>/my-page.tsx新建文件,导出一个 page 对象(template、slug、metadata、hero、sections); - 在
apps/www/_go/index.tsx中注册该页面对象; - 页面自动通过静态生成暴露在
/go/<slug>。
example-lead-gen.tsx 是仓库提供的样板:template: 'lead-gen'、slug: 'example-ebook',hero 里含 CTA 按钮组,sections 依次演示了 single-column(嵌入 MediaBlock 视频)、feature-grid、steps、code-block(多文件标签页)、metrics、quote、faq、hubspot-meeting(带 meetingSlug)、tweets、form(字段定义 + 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/)。这套规范对新贡献者的价值在于:每一类内容该放哪、写什么字段、如何本地验证,都有明确且可验证的答案。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00