Files
supabase/apps/ui-library/config/library.ts
3e79df3ece feat(library): serve the block catalog as Markdown and harden the exporter (#50370)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature, bug fix.

Part 3 of 6 in a stack that splits the library redesign into reviewable
pieces.

## What is the current behavior?

An agent can already fetch any guide as Markdown, but has no way to find
out what guides exist: the entry point is a rendered React page.

The exporter also fails quietly in ways that ship wrong output rather
than failing the build:

- An unknown component silently unwraps to its children, so a component
rename drops its rendered content.
- A registry item that cannot be read produces a page with no file
listing.
- A link to a missing page produces a 404 URL.
- An unrecognized install framework produces a plausible command for the
wrong CLI.
- Only absolute `/library/docs` links are rewritten, so in-page anchors
and sibling links break in the export.

## What is the new behavior?

`/library` negotiates Markdown the same way the guides do — `Accept:
text/markdown`, or an explicit `/library/index.md` — and returns a
categorized catalog with every block, its description, its framework
variants, and a link to each guide's Markdown.

`config/library.ts` is the single catalog description the generator
reads, and a test ties it to the content directory in both directions: a
guide cannot be added without a catalog entry, or listed without a
guide.

Each quiet failure above now throws, and links resolve against the page
they appear on and are checked against the set of published documents.

```bash
curl -H 'Accept: text/markdown' https://supabase.com/library
```

## Additional context

`config/library.ts` also carries the category and preview metadata the
redesigned homepage consumes in the last PR of the stack; here it is
exercised by the Markdown index and its test.


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

- **New Features**
- Added a browsable library catalog covering categories, blocks, starter
apps, and supported frameworks.
- Added Markdown versions of the library homepage and documentation for
compatible tools and workflows.
- Added framework-aware links and expanded registry information,
including dependencies and source details.
- Markdown requests now work for the homepage and documentation, while
browser requests continue receiving HTML.

- **Bug Fixes**
- Improved document link handling, metadata validation, slug
consistency, and detection of duplicate or missing documentation
entries.

- **Tests**
- Added coverage for catalog routes, Markdown generation, homepage
negotiation, document parsing, and framework-specific links.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
2026-09-22 14:14:04 +02:00

212 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { componentPages, mcpBlocks, oauthBlocks, platformBlocks } from './docs'
export const libraryCategories = [
{
name: 'Starter apps',
slug: 'starter-apps',
description: 'Complete starting points for your next product.',
},
{
name: 'Authentication',
slug: 'authentication',
description: 'Sign-in, sessions, and account management.',
},
{ name: 'Database', slug: 'database', description: 'Connect your interface to Postgres data.' },
{ name: 'Storage', slug: 'storage', description: 'Upload files with Supabase Storage.' },
{ name: 'Realtime', slug: 'realtime', description: 'Build experiences that stay in sync.' },
{
name: 'Messaging',
slug: 'messaging',
description: 'Bring conversations into your application.',
},
{ name: 'AI & APIs', slug: 'ai-apis', description: 'Connect agents to your application.' },
{
name: 'Application foundations',
slug: 'application-foundations',
description: 'Connect and extend your Supabase project.',
},
] as const
export type LibraryCategory = (typeof libraryCategories)[number]['name']
export type CatalogPreviewKind =
| 'auth'
| 'social'
| 'consent'
| 'avatar'
| 'table'
| 'storage'
| 'cursors'
| 'editor'
| 'flow'
| 'avatars'
| 'chat'
| 'mcp'
| 'agents'
| 'dashboard'
| 'client'
export type LibraryBlock = {
slug: string
title: string
description: string
category: LibraryCategory
preview: CatalogPreviewKind
tags: string[]
href: string
supportedFrameworks?: string[]
frameworkLabel?: string
external?: boolean
}
const blockMetadata: Record<string, Pick<LibraryBlock, 'description' | 'category' | 'preview'>> = {
'password-based-auth': {
description: 'Sign in and sign up with email and password.',
category: 'Authentication',
preview: 'auth',
},
'social-auth': {
description: 'OAuth sign-in flows for popular providers.',
category: 'Authentication',
preview: 'social',
},
'oauth-consent': {
description: 'Let users review and approve application access.',
category: 'Authentication',
preview: 'consent',
},
'current-user-avatar': {
description: 'Display the signed-in user’s avatar and profile.',
category: 'Authentication',
preview: 'avatar',
},
'infinite-query': {
description: 'Fetch and paginate Supabase data as users scroll.',
category: 'Database',
preview: 'table',
},
dropzone: {
description: 'Drag-and-drop file uploads with progress tracking.',
category: 'Storage',
preview: 'storage',
},
'realtime-cursor': {
description: 'Share live cursor positions across your application.',
category: 'Realtime',
preview: 'cursors',
},
'realtime-monaco': {
description: 'Edit code together with a collaborative Monaco editor.',
category: 'Realtime',
preview: 'editor',
},
'realtime-flow': {
description: 'Build collaborative diagrams with React Flow.',
category: 'Realtime',
preview: 'flow',
},
'realtime-avatar-stack': {
description: 'Show who is online with a live avatar stack.',
category: 'Realtime',
preview: 'avatars',
},
'realtime-chat': {
description: 'Send and receive messages in realtime.',
category: 'Messaging',
preview: 'chat',
},
'mcp-server': {
description: 'Add a user-scoped MCP server to your product.',
category: 'AI & APIs',
preview: 'mcp',
},
'headless-app': {
description: 'Combine auth, consent, and MCP tools into an agent-driven app.',
category: 'AI & APIs',
preview: 'agents',
},
client: {
description: 'Set up a Supabase client for your framework.',
category: 'Application foundations',
preview: 'client',
},
'platform-kit': {
description: 'Embed Supabase project management in your platform.',
category: 'Application foundations',
preview: 'dashboard',
},
}
// Starter guides share the same documentation routes and layout as individual blocks.
const starterApps: LibraryBlock[] = [
{
title: 'Next.js starter',
slug: 'nextjs-starter',
description: 'A Next.js app with cookie-based authentication, TypeScript, and Tailwind CSS.',
category: 'Starter apps',
preview: 'auth',
tags: ['Next.js', 'Authentication', 'TypeScript'],
href: '/docs/starters/nextjs-starter',
frameworkLabel: 'Next.js',
},
{
title: 'SaaS starter',
slug: 'saas-starter',
description: 'Subscription payments with Stripe, Supabase, and Next.js.',
category: 'Starter apps',
preview: 'dashboard',
tags: ['Next.js', 'Stripe', 'Subscriptions'],
href: '/docs/starters/saas-starter',
frameworkLabel: 'Next.js',
},
{
title: 'AI chat app',
slug: 'ai-chat-app',
description: 'A conversational app with Next.js, the Vercel AI SDK, and Supabase.',
category: 'Starter apps',
preview: 'chat',
tags: ['Next.js', 'AI', 'Messaging'],
href: '/docs/starters/ai-chat-app',
frameworkLabel: 'Next.js',
},
{
title: 'Flutter starter',
slug: 'flutter-starter',
description: 'A user management app with authentication, profiles, and file storage.',
category: 'Starter apps',
preview: 'avatar',
tags: ['Flutter', 'Authentication', 'Storage'],
href: '/docs/starters/flutter-starter',
frameworkLabel: 'Flutter',
},
]
export const libraryBlocks: LibraryBlock[] = [
...starterApps,
...[
...componentPages.items,
...oauthBlocks.items,
...mcpBlocks.items,
...platformBlocks.items,
].map((item) => {
const slug = item.href!.split('/').pop()!
return {
slug,
title: item.title,
href: item.href!,
supportedFrameworks: item.supportedFrameworks,
tags: item.supportedFrameworks ?? [],
...blockMetadata[slug],
}
}),
]
export function getLibraryBlockHref(block: LibraryBlock, framework?: string) {
return framework && block.supportedFrameworks?.includes(framework)
? `/docs/${framework}/${block.slug}`
: block.href
}
export function getLibraryCategoryHref(category: LibraryCategory) {
return `/?category=${encodeURIComponent(category)}`
}