Skip to content

Commit ee8cb33

Browse files
authored
new go blocks (#43771)
### FAQ (`type: 'faq'`) Accordion-style FAQ with expand/collapse. Each item has a `question` and `answer`. Click to toggle — only one open at a time. <img width="1309" height="610" alt="Screenshot 2026-03-13 at 17 31 31" src="https://github.com/user-attachments/assets/289c8a12-3835-4f64-bbe6-fb7095df4e7c" /> ### Code Block (`type: 'code-block'`) Syntax-highlighted code display using shiki with custom Supabase dark/light themes. Supports: - **Single file** — `code` + optional `filename` + `language` - **Multi-file** — `files: [{ filename, code, language }]` with clickable tabs - Line numbers via CSS counters - All highlighting runs at build time (server component), only tab switching is client-side <img width="1283" height="415" alt="Screenshot 2026-03-13 at 17 32 07" src="https://github.com/user-attachments/assets/9cc9a215-d5c9-47c9-8e21-c1dd3beca4ba" /> ### Steps (`type: 'steps'`) Numbered step-by-step guide with a vertical timeline connector. Each item has `title` and either a plain `description` string or a `content` slot accepting any React node (e.g. images, code blocks). <img width="1119" height="810" alt="Screenshot 2026-03-13 at 17 32 20" src="https://github.com/user-attachments/assets/cb67aaab-9ed4-42e2-bf1c-8d836024e469" /> ### Quote (`type: 'quote'`) Centered testimonial block with `quote`, `author`, optional `role`, and optional `avatar` image. <img width="1095" height="238" alt="Screenshot 2026-03-13 at 17 32 37" src="https://github.com/user-attachments/assets/356a39ca-9f65-4414-bf77-6060993594a4" />
1 parent 25036af commit ee8cb33

13 files changed

Lines changed: 691 additions & 4 deletions

File tree

apps/www/_go/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import pgconfDevContestThankYou from './events/pgconf-dev-2026/contest-thank-you
2222
import aiEngineerEuropeContest from './events/ai-engineer-europe-2026/contest'
2323
import aiEngineerEuropeContestThankYou from './events/ai-engineer-europe-2026/contest-thank-you'
2424
import startupGrindContest from './events/startup-grind-2026/contest'
25+
import exampleLeadGen from './lead-gen/example-lead-gen'
2526

2627
const pages: GoPageInput[] = [
28+
exampleLeadGen,
2729
byocEarlyAccess,
2830
amoe,
2931
amoeThankYou,

apps/www/_go/lead-gen/example-lead-gen.tsx

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const page: GoPageInput = {
1414
title: 'Building Modern Applications with Supabase',
1515
subtitle: 'Sample ebook landing page',
1616
description:
17-
'This is a sample lead generation page. The content below demonstrates the template layout. Replace it with your real ebook title, description, and cover image.',
17+
'This is a sample lead generation page. The content below demonstrates the template layout and all available section components. Replace it with your real ebook title, description, and cover image.',
1818
image: {
1919
src: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=announcement&layout=vertical&copy=Modern+applications&icon=supabase.svg',
2020
alt: 'Ebook cover: Building Modern Applications with Supabase',
@@ -72,6 +72,92 @@ const page: GoPageInput = {
7272
},
7373
],
7474
},
75+
{
76+
type: 'steps',
77+
title: 'Get started in minutes',
78+
description: 'Three simple steps to launch your project with Supabase.',
79+
items: [
80+
{
81+
title: 'Create a project',
82+
description:
83+
'Sign up for a free Supabase account and create a new project from the dashboard. Your database, auth, and storage are provisioned instantly.',
84+
},
85+
{
86+
title: 'Build your schema',
87+
description:
88+
'Use the Table Editor or write SQL directly to define your tables, relationships, and row-level security policies.',
89+
},
90+
{
91+
title: 'Connect your app',
92+
description:
93+
'Install the Supabase client library for your framework and start querying your database with auto-generated APIs.',
94+
},
95+
{
96+
title: 'Deploy to production',
97+
content: (
98+
<div className="mt-2 space-y-3">
99+
<p className="text-foreground-lighter text-sm leading-relaxed">
100+
Push your project live with a single click. Supabase handles scaling, backups, and
101+
monitoring automatically.
102+
</p>
103+
<img
104+
src="https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=announcement&layout=vertical&copy=Deploy+to+production&icon=supabase.svg"
105+
alt="Deploy to production"
106+
className="rounded-lg border border-muted w-full max-w-lg"
107+
/>
108+
</div>
109+
),
110+
},
111+
],
112+
},
113+
{
114+
type: 'code-block',
115+
title: 'Simple, powerful APIs',
116+
description: 'Interact with your database using the auto-generated client library.',
117+
files: [
118+
{
119+
filename: 'app/page.tsx',
120+
language: 'typescript',
121+
code: `import { createClient } from '@supabase/supabase-js'
122+
123+
const supabase = createClient(
124+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
125+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
126+
)
127+
128+
// Fetch all published posts
129+
const { data: posts } = await supabase
130+
.from('posts')
131+
.select('id, title, content, author(name)')
132+
.eq('published', true)
133+
.order('created_at', { ascending: false })`,
134+
},
135+
{
136+
filename: 'lib/supabase.ts',
137+
language: 'typescript',
138+
code: `import { createClient } from '@supabase/supabase-js'
139+
140+
export const supabase = createClient(
141+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
142+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
143+
)`,
144+
},
145+
{
146+
filename: 'schema.sql',
147+
language: 'sql',
148+
code: `create table posts (
149+
id bigint generated always as identity primary key,
150+
title text not null,
151+
content text,
152+
author_id bigint references authors(id),
153+
published boolean default false,
154+
created_at timestamptz default now()
155+
);
156+
157+
alter table posts enable row level security;`,
158+
},
159+
],
160+
},
75161
{
76162
type: 'metrics',
77163
items: [
@@ -80,6 +166,44 @@ const page: GoPageInput = {
80166
{ label: 'GitHub stars', value: '80,000+' },
81167
],
82168
},
169+
{
170+
type: 'quote',
171+
quote:
172+
'Supabase has completely transformed how we build products. What used to take weeks now takes hours.',
173+
author: 'Jane Smith',
174+
role: 'CTO, Acme Corp',
175+
avatar: {
176+
src: 'https://i.pravatar.cc/80?u=jane-smith',
177+
alt: 'Jane Smith',
178+
},
179+
},
180+
{
181+
type: 'faq',
182+
title: 'Frequently asked questions',
183+
description: 'Everything you need to know about getting started with Supabase.',
184+
items: [
185+
{
186+
question: 'What is Supabase?',
187+
answer:
188+
'Supabase is an open-source Firebase alternative that provides a Postgres database, authentication, instant APIs, edge functions, real-time subscriptions, and storage. It gives you all the backend services you need to build a product.',
189+
},
190+
{
191+
question: 'How much does Supabase cost?',
192+
answer:
193+
'Supabase has a generous free tier that includes 500MB of database space, 1GB of storage, and 50,000 monthly active users. Paid plans start at $25/month for additional resources and features like daily backups and priority support.',
194+
},
195+
{
196+
question: 'Can I self-host Supabase?',
197+
answer:
198+
'Yes! Supabase is fully open-source and can be self-hosted using Docker. The official documentation provides detailed guides for deploying Supabase on your own infrastructure.',
199+
},
200+
{
201+
question: 'What frameworks does Supabase support?',
202+
answer:
203+
'Supabase provides client libraries for JavaScript/TypeScript, Python, Dart (Flutter), Swift, and Kotlin. It works with any framework including Next.js, React, Vue, Svelte, and more.',
204+
},
205+
],
206+
},
83207
{
84208
type: 'tweets',
85209
title: 'Loved by developers',

packages/marketing/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
"typescript": "catalog:"
1818
},
1919
"dependencies": {
20-
"ui": "workspace:*",
21-
"zod": "catalog:",
20+
"@types/react": "catalog:",
2221
"react": "catalog:",
2322
"react-markdown": "^8.0.3",
2423
"server-only": "^0.0.1",
25-
"@types/react": "catalog:"
24+
"shiki": "^4.0.1",
25+
"ui": "workspace:*",
26+
"zod": "catalog:"
2627
},
2728
"license": "MIT"
2829
}

packages/marketing/src/go/schemas.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,62 @@ export const tweetsSectionSchema = z.object({
233233
ctas: z.array(ctaSchema).optional(),
234234
})
235235

236+
export const faqItemSchema = z.object({
237+
question: z.string().min(1),
238+
answer: z.string().min(1),
239+
})
240+
241+
export const faqSectionSchema = z.object({
242+
...sectionBase,
243+
type: z.literal('faq'),
244+
title: z.string().optional(),
245+
description: z.string().optional(),
246+
items: z.array(faqItemSchema).min(1),
247+
})
248+
249+
export const codeFileSchema = z.object({
250+
filename: z.string().min(1),
251+
code: z.string().min(1),
252+
language: z.string().optional().default('sql'),
253+
})
254+
255+
export const codeBlockSectionSchema = z.object({
256+
...sectionBase,
257+
type: z.literal('code-block'),
258+
title: z.string().optional(),
259+
description: z.string().optional(),
260+
/** Single-file mode */
261+
code: z.string().optional(),
262+
language: z.string().optional().default('sql'),
263+
filename: z.string().optional(),
264+
/** Multi-file mode — takes precedence over code/filename/language when set */
265+
files: z.array(codeFileSchema).optional(),
266+
})
267+
268+
export const stepItemSchema = z.object({
269+
title: z.string().min(1),
270+
description: z.string().optional(),
271+
content: z.any().optional(),
272+
icon: z.string().optional(),
273+
})
274+
275+
export const stepsSectionSchema = z.object({
276+
...sectionBase,
277+
type: z.literal('steps'),
278+
title: z.string().optional(),
279+
description: z.string().optional(),
280+
items: z.array(stepItemSchema).min(1),
281+
})
282+
283+
export const quoteSectionSchema = z.object({
284+
...sectionBase,
285+
type: z.literal('quote'),
286+
quote: z.string().min(1),
287+
author: z.string().min(1),
288+
role: z.string().optional(),
289+
avatar: imageSchema.optional(),
290+
})
291+
236292
// ----- Dynamic sections -----
237293

238294
export const goSectionSchema = z.discriminatedUnion('type', [
@@ -243,6 +299,10 @@ export const goSectionSchema = z.discriminatedUnion('type', [
243299
featureGridSectionSchema,
244300
metricsSectionSchema,
245301
tweetsSectionSchema,
302+
faqSectionSchema,
303+
codeBlockSectionSchema,
304+
stepsSectionSchema,
305+
quoteSectionSchema,
246306
])
247307

248308
// ----- Page-level schemas -----
@@ -307,6 +367,13 @@ export type GoFeatureGridSection = z.infer<typeof featureGridSectionSchema>
307367
export type GoMetricItem = z.infer<typeof metricItemSchema>
308368
export type GoMetricsSection = z.infer<typeof metricsSectionSchema>
309369
export type GoTweetsSection = z.infer<typeof tweetsSectionSchema>
370+
export type GoFaqItem = z.infer<typeof faqItemSchema>
371+
export type GoFaqSection = z.infer<typeof faqSectionSchema>
372+
export type GoCodeFile = z.infer<typeof codeFileSchema>
373+
export type GoCodeBlockSection = z.infer<typeof codeBlockSectionSchema>
374+
export type GoStepItem = z.infer<typeof stepItemSchema>
375+
export type GoStepsSection = z.infer<typeof stepsSectionSchema>
376+
export type GoQuoteSection = z.infer<typeof quoteSectionSchema>
310377
export type GoSection = z.infer<typeof goSectionSchema>
311378
export type GoMetadata = z.infer<typeof metadataSchema>
312379

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { codeToHtml } from 'shiki'
2+
3+
import type { GoCodeBlockSection } from '../schemas'
4+
import CodeBlockTabs from './CodeBlockTabs'
5+
import { supabaseDark, supabaseLight } from './codeThemes'
6+
7+
const lineNumberStyles = `
8+
.go-code code { counter-reset: line; }
9+
.go-code code .line { counter-increment: line; }
10+
.go-code code .line::before {
11+
content: counter(line);
12+
display: inline-block;
13+
width: 2ch;
14+
margin-right: 1.5ch;
15+
text-align: right;
16+
color: hsl(var(--foreground-light));
17+
opacity: 0.35;
18+
}
19+
`
20+
21+
async function highlightCode(code: string, language: string) {
22+
const [darkHtml, lightHtml] = await Promise.all([
23+
codeToHtml(code, { lang: language, theme: supabaseDark }),
24+
codeToHtml(code, { lang: language, theme: supabaseLight }),
25+
])
26+
return { darkHtml, lightHtml }
27+
}
28+
29+
export default async function CodeBlockSection({ section }: { section: GoCodeBlockSection }) {
30+
const isMultiFile = section.files && section.files.length > 0
31+
const isSingleFileWithName = !isMultiFile && section.filename && section.code
32+
33+
// Build the highlighted files array
34+
const highlightedFiles = isMultiFile
35+
? await Promise.all(
36+
section.files!.map(async (file) => {
37+
const { darkHtml, lightHtml } = await highlightCode(file.code, file.language ?? 'sql')
38+
return { filename: file.filename, darkHtml, lightHtml }
39+
})
40+
)
41+
: section.code
42+
? [
43+
{
44+
filename: section.filename ?? '',
45+
...(await highlightCode(section.code, section.language ?? 'sql')),
46+
},
47+
]
48+
: []
49+
50+
const showTabs =
51+
highlightedFiles.length > 1 || (highlightedFiles.length === 1 && isSingleFileWithName)
52+
53+
return (
54+
<div className="max-w-[80rem] w-full min-w-0 mx-auto px-8">
55+
{(section.title || section.description) && (
56+
<div className="mb-12">
57+
{section.title && (
58+
<h2 className="text-2xl sm:text-3xl font-medium text-foreground">{section.title}</h2>
59+
)}
60+
{section.description && (
61+
<p className="text-foreground-lighter mt-3 text-lg">{section.description}</p>
62+
)}
63+
</div>
64+
)}
65+
<style dangerouslySetInnerHTML={{ __html: lineNumberStyles }} />
66+
<div className="go-code border border-muted rounded-xl overflow-hidden w-full">
67+
{showTabs ? (
68+
<CodeBlockTabs files={highlightedFiles} />
69+
) : highlightedFiles.length === 1 ? (
70+
<>
71+
<div
72+
className="hidden dark:block px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:!bg-transparent [&_pre]:!m-0 [&_code]:font-mono"
73+
dangerouslySetInnerHTML={{ __html: highlightedFiles[0].darkHtml }}
74+
/>
75+
<div
76+
className="block dark:hidden px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:!bg-transparent [&_pre]:!m-0 [&_code]:font-mono"
77+
dangerouslySetInnerHTML={{ __html: highlightedFiles[0].lightHtml }}
78+
/>
79+
</>
80+
) : null}
81+
</div>
82+
</div>
83+
)
84+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { cn } from 'ui'
5+
6+
interface CodeBlockTabsProps {
7+
files: { filename: string; darkHtml: string; lightHtml: string }[]
8+
}
9+
10+
export default function CodeBlockTabs({ files }: CodeBlockTabsProps) {
11+
const [activeIndex, setActiveIndex] = useState(0)
12+
13+
return (
14+
<>
15+
<div className="flex border-b border-muted bg-surface-200/50">
16+
{files.map((file, i) => (
17+
<button
18+
key={file.filename}
19+
type="button"
20+
onClick={() => setActiveIndex(i)}
21+
className={cn(
22+
'px-5 py-3 text-xs font-mono transition-colors',
23+
i === activeIndex
24+
? 'text-foreground border-b border-foreground -mb-px'
25+
: 'text-foreground-lighter hover:text-foreground-light'
26+
)}
27+
>
28+
{file.filename}
29+
</button>
30+
))}
31+
</div>
32+
<div
33+
className="hidden dark:block px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:!bg-transparent [&_pre]:!m-0 [&_code]:font-mono"
34+
dangerouslySetInnerHTML={{ __html: files[activeIndex].darkHtml }}
35+
/>
36+
<div
37+
className="block dark:hidden px-5 py-4 sm:px-6 sm:py-5 overflow-x-auto text-[13px] leading-[1.6] [&_pre]:!bg-transparent [&_pre]:!m-0 [&_code]:font-mono"
38+
dangerouslySetInnerHTML={{ __html: files[activeIndex].lightHtml }}
39+
/>
40+
</>
41+
)
42+
}

0 commit comments

Comments
 (0)