diff --git a/apps/kb/src/components/Nav.tsx b/apps/kb/src/components/Nav.tsx index d28f765a059..16ff6977028 100644 --- a/apps/kb/src/components/Nav.tsx +++ b/apps/kb/src/components/Nav.tsx @@ -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' }, diff --git a/apps/kb/src/content.config.ts b/apps/kb/src/content.config.ts index 92612a16b79..fec50c46fb6 100644 --- a/apps/kb/src/content.config.ts +++ b/apps/kb/src/content.config.ts @@ -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(), }), }) diff --git a/apps/kb/src/content/guides/sample-guide.md b/apps/kb/src/content/guides/sample-guide.md index 6a69b4987bc..f1af5d55b00 100644 --- a/apps/kb/src/content/guides/sample-guide.md +++ b/apps/kb/src/content/guides/sample-guide.md @@ -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' --- diff --git a/apps/kb/src/layouts/GuideLayout.astro b/apps/kb/src/layouts/GuideLayout.astro index 49b82d04b1c..0362e842cfa 100644 --- a/apps/kb/src/layouts/GuideLayout.astro +++ b/apps/kb/src/layouts/GuideLayout.astro @@ -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 && (
- {topics.map((topic) => {topic})} + {topics.map((topic) => ( + + {topic} + + ))}
) } diff --git a/apps/kb/src/lib/topics.ts b/apps/kb/src/lib/topics.ts new file mode 100644 index 00000000000..c0725d4a303 --- /dev/null +++ b/apps/kb/src/lib/topics.ts @@ -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, '-') +} diff --git a/apps/kb/src/pages/topics/[topic].astro b/apps/kb/src/pages/topics/[topic].astro new file mode 100644 index 00000000000..aa741118dc0 --- /dev/null +++ b/apps/kb/src/pages/topics/[topic].astro @@ -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 +--- + + +
+

{topic}

+

{description}

+ { + guides.length === 0 ? ( +

No guides for this topic

+ ) : ( + + ) + } +
+