Files
supabase/apps/www/README.md
Alaister YoungandAlaister Young 27af9ca162 chore(www): run production builds through Turbo (#50713)
www builds currently bypass Turbo. This caches Next.js compilation and
sitemap generation while keeping content refreshes and asset uploads on
every build, including cache hits.

**Changed:**

- Refresh content before Turbo hashes its inputs, and upload assets
after Turbo saves or restores the build output.
- Include generated content, shared code, environment settings,
sitemaps, and the customer RSS feed in the cache configuration.
- Remove the redundant `vercel.json` build override and consolidate
public environment settings into `NEXT_PUBLIC_*`.

Cache reuse requires the same commit and build inputs because production
CDN URLs include the commit SHA.

**Added:**

- Build lifecycle tests covering cache restoration, input invalidation,
root/app commands, and upload ordering and failure handling.

## To test

- From `apps/www`, run `pnpm exec vitest run turbo-build.test.ts
generate-sitemap.test.ts scripts/lib/githubStars.test.ts`.
- Check the Vercel preview build runs content preparation before
`build:next`, and verify the homepage, `/sitemap.xml`, and
`/customers-rss.xml` load.
- Rebuild with identical prepared content and environment settings to
check compilation is cached. Asset uploads should still run when
enabled.

Validation: 63 tests passed, along with typecheck, ESLint for the new
test, formatting, and a full local build. The local build used public
example settings and placeholder survey configuration, with asset
uploads disabled; Vercel deployment validation is still pending.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Build & Deployment**
- Production builds now reuse cached outputs and restore generated
assets when a cache is available.
- Static assets upload after a successful build, and changes to content,
documentation, configuration, or shared components trigger a fresh
build.

- **Documentation**
- Added production build guidance covering caching, CDN uploads,
overrides, and direct-build limitations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-09-28 12:14:33 -07:00

269 lines
12 KiB
Markdown

# supabase.com
## Overview
Refer to the [Development Guide](../../DEVELOPERS.md) to learn how to run this site locally.
To get started copy the example env file using `cp .env.local.example .env.local`.
## Production builds
Run `pnpm build` from `apps/www`, or `pnpm build --filter=www` from the repository
root. Vercel uses the app's `build` script by default.
`prebuild` refreshes remote content before Turbo hashes it. Turbo caches
`build:next` (Next.js, sitemaps, and the customer RSS feed), then `postbuild`
uploads assets even on cache hits. The outer `build` task stays uncached so
neither preparation nor upload can be skipped. Uploads run after caching because
the upload script removes `.next/static`.
Production cache reuse requires the same commit and inputs: CDN URLs include
`VERCEL_GIT_COMMIT_SHA`. Set `FORCE_ASSET_CDN=-1` to disable local uploads.
Running `build:next` directly skips content preparation and uploads.
Run `pnpm exec vitest run turbo-build.test.ts` to check cache invalidation and
asset restoration using real pnpm, Turbo, and the upload script with fixture
generators, a stub compiler, and a fake AWS CLI.
## Best practices
### Images
- **Resize images**: All new images should be resized to only the maximum resolution needed when rendering on the frontend. Don't upload images larger than what will be displayed.
- **Compress images**: All new images should be compressed before committing. Use tools like Clop or ImageOptim to reduce file size without noticeable quality loss.
- **Image locations**: Store blog post images in `apps/www/public/images/blog/`. Event images go in `apps/www/public/images/events/`. Customer logos go in `apps/www/public/images/customers/logos/on-light/` and `on-dark/` (see Customer Stories below).
### OG image generation
Open Graph (OG) images for social sharing are handled differently across content types:
- **Blog posts**: Use static images via `imgSocial` and `imgThumb` fields (see _Blog posts_ section below)
- **Events**: Use dynamic generation via Edge Function (with optional `og_image` override)
- **Customer stories**: Use dynamic generation via Edge Function (no static option)
The `og-images` Edge Function (`supabase/functions/og-images/`) automatically generates OG images for events and customer stories. It's deployed via `.github/workflows/og_images.yml` when changes are made to the function code.
**Development**: In local development, the function runs at `http://127.0.0.1:54321/functions/v1/og-images`. Ensure Supabase is running locally (`supabase start`).
### Content frontmatter image fields
Different content types use different image field conventions:
#### Blog posts
Blog posts support two image fields in their frontmatter:
- **`imgSocial`**: Used for Open Graph and social media sharing (X, LinkedIn, etc.). These images should include text overlays since they appear standalone in social feeds without accompanying text.
- **`imgThumb`**: Used for internal thumbnails displayed on the blog listing pages and featured posts. These images don't need text overlays since they're always displayed alongside the post title and description.
This naming convention was introduced to replace the previously confusing `thumb` and `og` fields, which were often mixed up. The new names clearly indicate:
- **`imgSocial`**: Purpose-built for social media sharing (needs text overlays)
- **`imgThumb`**: Optimized for site display (clean, no text overlays)
This separation allows you to optimize images for their specific use case while maintaining a clear, unambiguous naming convention.
**Image path format**
Always use **relative paths** (just the filename or subfolder/filename). The `/images/blog/` prefix is added automatically by the code.
- ✅ Correct: `imgSocial: my-post/og.png` or `imgSocial: og.png`
- ❌ Wrong: `imgSocial: /images/blog/my-post/og.png` (creates double prefix)
**Image fallback behavior**
For site display (what visitors see):
- Priority 1: `imgThumb`
- Priority 2: `imgSocial` (if `imgThumb` is missing)
- Priority 3: `/images/blog/blog-placeholder.png` (if both missing)
For social sharing (Open Graph meta tags):
- Priority 1: `imgSocial`
- Priority 2: `imgThumb` (if `imgSocial` is missing)
- Priority 3: No fallback (undefined)
What happens if fields are not provided?
- If only `imgThumb` is provided: Site displays the image correctly, social sharing uses `imgThumb` as fallback
- If only `imgSocial` is provided: Social sharing uses it, site display uses it as fallback
- If neither is provided: Site shows placeholder image, social sharing has no image
- Best practice: Provide both fields for optimal display and social sharing
**Example**
```yaml
---
title: 'My Blog Post'
imgSocial: 2025-01-01-my-post/og.png # Relative path - with text overlay for social sharing
imgThumb: 2025-01-01-my-post/thumb.png # Relative path - without text, clean image
---
```
The images would be stored at:
- `apps/www/public/images/blog/2025-01-01-my-post/og.png`
- `apps/www/public/images/blog/2025-01-01-my-post/thumb.png`
Or if using the same image for both:
```yaml
---
title: 'My Blog Post'
imgSocial: my-image.png # Stored at: apps/www/public/images/blog/my-image.png
imgThumb: my-image.png
---
```
#### Blog post dates
Use quoted `YYYY-MM-DD` values. `date` is the publication date. Set optional `updated` for substantive content revisions, excluding typo, link, or image fixes. It must be on or after `date` and supplies the sitemap and structured-data modification date. When absent, `date` supplies both.
#### Events
Events use different image fields to avoid confusion with their display patterns:
- **`thumb`**: Used for event grid item thumbnails (small cards in listing)
- **`cover_url`**: Used for the featured event banner (large display on events page)
- **`og_image`** (optional): Used to override the dynamically generated OG image for social sharing
**OG image generation**
Events automatically generate Open Graph images using the `og-images` Supabase Edge Function. The function creates images dynamically based on:
- Event type (conference, hackathon, etc.)
- Title (or `meta_title` if provided)
- Description (or `meta_description` if provided)
- Date (formatted as "DD MMM YYYY" using the event's timezone)
- Duration (if provided)
If you need a custom OG image that differs from the auto-generated one, you can provide an `og_image` field in the frontmatter. This will override the dynamic generation.
**Example:**
```yaml
---
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 # Optional override
---
```
**Note**: The `og_image` field is optional. If not provided, OG images are generated automatically via the Edge Function.
## End-to-end checks
Content pages are loaded and scanned with axe-core by the Playwright suite in
`e2e/www`. Pull requests test the pages your change affects. Today the suite
enforces one accessibility rule, `page-has-heading-one`.
To test the pages your current branch changes:
```bash
PLAYWRIGHT_BASE_URL=https://supabase.com pnpm e2e:www
```
That resolves which pages to test from your branch, but reads them from
production, so it won't see your edits and will 404 on a page you just added.
Point `PLAYWRIGHT_BASE_URL` at your pull request's preview to test your own
content.
See [`e2e/www/README.md`](https://github.com/supabase/supabase/blob/master/e2e/www/README.md)
for coverage and limits.
## Go pages (`/go/*`)
`/go/` is a system for building standalone campaign landing pages (lead generation, legal, thank-you flows). The name is intentionally generic — these pages are typically linked from ads, emails, or partner campaigns and are not part of the main site navigation.
Pages are defined as TypeScript objects (not MDX files) and validated against Zod schemas at build time.
### Parts
| Location | Purpose |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `apps/www/_go/` | Page definitions. Each file exports a page object. `index.tsx` registers all pages. |
| `apps/www/app/go/[slug]/page.tsx` | App Router route — renders the page for a given slug, handles 404s and metadata. |
| `apps/www/components/Go/GoPageRenderer.tsx` | www-specific wrapper — adds the Supabase logo header and footer, registers custom section renderers. |
| `packages/marketing/src/go/` | Framework-agnostic core: schemas, section components, templates, form server action. |
| `packages/marketing/src/crm/` | CRM client abstraction (HubSpot + Customer.io) used by the form server action. |
### Page structure
Each page specifies a `template` which determines its top-level layout:
- **`lead-gen`** — hero + arbitrary sections (form, metrics, feature grid, tweets, social proof, etc.)
- **`thank-you`** — hero + sections + confetti animation
- **`legal`** — hero + table-of-contents sidebar + markdown body
Pages are arrays of typed section objects. The `SectionRenderer` in `packages/marketing` dispatches each section to the right component based on its `type` field.
### Custom renderers
The `marketing` package doesn't know about `topTweets` data or the Pages Router `basePath`, so the `tweets` section type has no default renderer. `GoPageRenderer.tsx` in www registers `TweetsSection` as a custom renderer for that type. This is the extension point for any section that requires www-specific dependencies.
### Adding a new page
1. Create a new file in `apps/www/_go/<category>/my-page.tsx` exporting a page object.
2. Register it in `apps/www/_go/index.tsx`.
3. The page will be available at `/go/<slug>` automatically via static generation.
#### Customer Stories (Case Studies)
Customer stories are defined in MDX files (`apps/www/_customers/*.mdx`) and use a different approach:
- **No `og_image` field**: Customer stories do NOT use static OG images
- **Dynamic OG generation**: All customer story OG images are automatically generated using the `og-images` Supabase Edge Function
- The function creates images based on the customer `slug` and `title` (or `meta_title` if provided)
Do not include an `og_image` field in customer story frontmatter. It will be ignored. OG images are always generated dynamically.
**Logos**
Customer logos are monochrome and transparent. Prefer SVG; PNG is also fine. Put each theme variant in the folder named for the background it sits on:
| Folder | Mark | Frontmatter | Shown in |
| ----------------------------------- | ----------- | -------------- | ---------- |
| `/images/customers/logos/on-light/` | Dark/black | `logo` | Light mode |
| `/images/customers/logos/on-dark/` | Light/white | `logo_inverse` | Dark mode |
Do not use coloured brand marks. Preview `/customers` in both themes before merging.
Icon-only assets (homepage chips, etc.) stay at `/images/customers/logos/{slug}-icon.svg`.
Customer story OG images are generated by the `og-images` Edge Function, which fetches `https://supabase.com/images/customers/logos/on-dark/{slug}.png`. Satori cannot rasterise SVG, so stories still need an `on-dark/{slug}.png` even if the site uses SVG.
**Example** (in `apps/www/_customers/company-abc.mdx`):
```yaml
---
name: Company ABC
title: Company ABC built their platform with Supabase
# DO NOT include og_image - it's generated automatically
logo: /images/customers/logos/on-light/company-abc.svg
logo_inverse: /images/customers/logos/on-dark/company-abc.svg
---
```
**Legacy Case Studies** (in `data/CustomerStories.ts`):
- **`imgUrl`**: Path to the case study image in the source data
- This gets mapped to `imgThumb` when rendered via `BlogGridItem` component
- Case studies only need one image for site display (no separate social sharing image)
**Example** (in `data/CustomerStories.ts`):
```typescript
{
type: 'Customer Story',
title: 'Company ABC built their platform with Supabase',
description: '...',
organization: 'Company ABC',
imgUrl: 'images/customers/logos/on-light/company-abc.svg', // Full path from public/
logo: '/images/customers/logos/on-light/company-abc.svg',
logo_inverse: '/images/customers/logos/on-dark/company-abc.svg',
url: '/customers/company-abc',
}
```