feat(kb): Add topics pages support (#49959)

This commit is contained in:
Jeremias Menichelli authored and GitHub committed 2026-09-07 12:40:25 +02:00
1 parent 0ab0bb8efd
commit 7e19cbe140
6 files changed
+98 -11

No files matched your search

+7 -6
View File
@@ -9,15 +9,16 @@ import {
navigationMenuTriggerStyle,
} from 'ui'
import { TOPICS, topicToSlug } from '../lib/topics'
const topics = TOPICS.map((topic) => ({
label: topic.name,
href: `${import.meta.env.BASE_URL}/topics/${topicToSlug(topic.name)}`,
}))
/**
* Hard-codding links in here for now until we have actual content. Might be worth putting these arrays in their on data file too.
*/
const topics = [
{ label: 'Troubleshooting', href: '#' },
{ label: 'Migrations', href: '#' },
{ label: 'Comparisons', href: '#' },
]
const resources = [
{ label: 'Status', href: 'https://status.supabase.com' },
{ label: 'Changelog', href: 'https://supabase.com/changelog' },
+6 -2
View File
@@ -2,6 +2,8 @@ import { defineCollection } from 'astro:content'
import { glob } from 'astro/loaders'
import { z } from 'astro/zod'
import { TOPIC_NAMES } from './lib/topics'
// Every entry here is rendered through GuideLayout by
// src/pages/guides/[...slug].astro — dropping a new file in
// src/content/guides doesn't need any per-file layout wiring.
@@ -9,8 +11,10 @@ const guides = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/guides' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
topics: z.array(z.string()).optional(),
description: z.string(),
// z.enum (not z.string) so a guide referencing a topic outside TOPICS
// fails content validation instead of silently rendering an orphaned tag.
topics: z.array(z.enum(TOPIC_NAMES)),
github_url: z.string().optional(),
}),
})
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: 'Markdown elements sample'
description: 'A lorem ipsum sample guide exercising every base Markdown element supported by the guide layout.'
topics: ['example', 'markdown']
topics: ['Tutorial', 'Database']
github_url: 'https://github.com/supabase/supabase/discussions/0000000'
---
+10 -2
View File
@@ -3,11 +3,12 @@ import { Github } from 'lucide-react'
import { Badge } from 'ui'
import Layout from './Layout.astro'
import { getTopicDescription, type Topic, topicToSlug } from '../lib/topics'
interface Props {
title: string
description?: string
topics?: string[]
topics?: Topic[]
github_url?: string
}
@@ -21,7 +22,14 @@ const { title, description, topics = [], github_url } = Astro.props
{
topics.length > 0 && (
<div class="flex flex-wrap gap-2">
{topics.map((topic) => <Badge>{topic}</Badge>)}
{topics.map((topic) => (
<a
href={`${import.meta.env.BASE_URL}/topics/${topicToSlug(topic)}`}
title={getTopicDescription(topic)}
>
<Badge>{topic}</Badge>
</a>
))}
</div>
)
}
+31
View File
@@ -0,0 +1,31 @@
// Canonical list of guide topics. Single source of truth for the `topics`
// field in src/content.config.ts (so a guide with an unsupported topic fails
// content validation), the "Topics" nav menu, and the /topics/[topic] pages.
export const TOPICS = [
{ name: 'Migration', description: 'Moving data, schemas, or projects onto Supabase' },
{ name: 'Comparison', description: 'How Supabase compares to other databases and platforms' },
{ name: 'Troubleshooting', description: 'Common errors and how to resolve them' },
{ name: 'Tutorial', description: 'Step-by-step walkthroughs for building with Supabase' },
{ name: 'Storage', description: 'Uploading, managing, and serving files' },
{ name: 'Auth', description: 'Authentication, authorization, and user management' },
{ name: 'Database', description: 'Postgres schemas, queries, and performance' },
{ name: 'Edge Functions', description: 'Deploying and running serverless functions' },
{ name: 'Queues', description: 'Background jobs and message processing' },
{ name: 'Realtime', description: 'Broadcast, presence, and database changes' },
{ name: 'Supabase Platform', description: 'Project settings, billing, and infrastructure' },
] as const
export type Topic = (typeof TOPICS)[number]['name']
// zod's `enum()` needs a literal non-empty tuple of strings, which `.map()`
// can't preserve on its own — this cast is safe because TOPICS is `as const`.
export const TOPIC_NAMES = TOPICS.map((topic) => topic.name) as [Topic, ...Topic[]]
export function getTopicDescription(topic: Topic): string {
return TOPICS.find((t) => t.name === topic)!.description
}
// URL-safe slug for a topic, e.g. "Edge Functions" -> "edge-functions".
export function topicToSlug(topic: Topic): string {
return topic.toLowerCase().replace(/\s+/g, '-')
}
+43
View File
@@ -0,0 +1,43 @@
---
import { getCollection } from 'astro:content'
import Layout from '../../layouts/Layout.astro'
import { TOPICS, topicToSlug } from '../../lib/topics'
export async function getStaticPaths() {
const guides = await getCollection('guides')
// One page per topic in TOPICS — not just topics a guide already uses —
// so every link on the "Topics" nav resolves instead of 404ing.
return TOPICS.map((topic) => ({
params: { topic: topicToSlug(topic.name) },
props: {
topic: topic.name,
description: topic.description,
guides: guides.filter((guide) => guide.data.topics?.includes(topic.name)),
},
}))
}
const { topic, description, guides } = Astro.props
---
<Layout title={topic} description={description}>
<article class="prose max-w-3xl mx-auto px-6 py-16">
<h1>{topic}</h1>
<p class="lead">{description}</p>
{
guides.length === 0 ? (
<p>No guides for this topic</p>
) : (
<ul>
{guides.map((guide) => (
<li>
<a href={`${import.meta.env.BASE_URL}/guides/${guide.id}`}>{guide.data.title}</a>
</li>
))}
</ul>
)
}
</article>
</Layout>