Files
supabase/apps/ui-library/scripts/build-markdown-index.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

110 lines
4.2 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { libraryBlocks, libraryCategories } from '../config/library'
import {
collectMdxFiles,
getDocSlug,
markdownLink,
parseLibraryDocument,
} from '../lib/library-documents'
const DOCS_BASE_URL = 'https://supabase.com/library/docs'
const LIBRARY_BASE_URL = 'https://supabase.com/library'
interface DocMeta {
title: string
description?: string
path: string
}
export function getDocFiles(docsDirectory: string): DocMeta[] {
return collectMdxFiles(docsDirectory).map((fullPath) => {
const { data } = parseLibraryDocument(fs.readFileSync(fullPath, 'utf8'))
if (!data.title) throw new Error(`Missing document title: ${fullPath}`)
return {
title: data.title,
description: data.description,
path: getDocSlug(path.relative(docsDirectory, fullPath)),
}
})
}
export function buildLlmsTxt(docs: DocMeta[], generatedAt = new Date()): string {
const entries = docs.map((doc) => {
const description = doc.description?.replace(/\s+/g, ' ').trim()
return [
`- ${markdownLink(doc.title, `${DOCS_BASE_URL}/${doc.path}.md`)}`,
description ? ` - ${description}` : '',
]
.filter(Boolean)
.join('\n')
})
return `# Supabase Library
Last updated: ${generatedAt.toISOString()}
## Overview
Library of components for your project. The components integrate with Supabase and are shadcn compatible. Each docs page is also available as markdown for agents (append .md to the URL).
Block catalog: https://supabase.com/library/index.md
## Docs
${entries.join('\n')}
`
}
/**
* The homepage catalog as markdown, so an agent can list every block without
* rendering the page. Mirrors the categories and blocks in `config/library.ts`.
*/
export function buildIndexMarkdown(generatedAt = new Date()): string {
const sections = libraryCategories
.map((category) => {
const blocks = libraryBlocks.filter((block) => block.category === category.name)
const entries = blocks.map((block) => {
const description = block.description.replace(/\s+/g, ' ').trim()
// Framework variants follow the URL pattern spelled out in the overview,
// so listing the slugs beats repeating a near-identical link per framework.
const frameworks = block.supportedFrameworks?.length
? ` Frameworks: ${block.supportedFrameworks.join(', ')}.`
: block.frameworkLabel
? ` Framework: ${block.frameworkLabel}.`
: ''
return `- ${markdownLink(block.title, `${LIBRARY_BASE_URL}${block.href}.md`)} — ${description}${frameworks}`
})
return [`## ${category.name}`, category.description, '', entries.join('\n')].join('\n')
})
.filter(Boolean)
return `# Supabase Library
Last updated: ${generatedAt.toISOString()}
## Overview
Building blocks for your next backend. Every block is shadcn compatible and integrates with Supabase, and each one ships with a guide you can install from.
Every docs page is also available as markdown for agents (append .md to the URL). Blocks that support several frameworks share one guide per framework at ${LIBRARY_BASE_URL}/docs/<framework>/<block>.md.
Start here: ${markdownLink('Quick Start', `${LIBRARY_BASE_URL}/docs/getting-started/quickstart.md`)}, ${markdownLink('Introduction', `${LIBRARY_BASE_URL}/docs/getting-started/introduction.md`)}, ${markdownLink('FAQ', `${LIBRARY_BASE_URL}/docs/getting-started/faq.md`)}.
Full page index: ${LIBRARY_BASE_URL}/llms.txt
${sections.join('\n\n')}
`
}
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
const publicDirectory = path.join(process.cwd(), 'public')
fs.mkdirSync(publicDirectory, { recursive: true })
fs.writeFileSync(
path.join(publicDirectory, 'llms.txt'),
buildLlmsTxt(getDocFiles(path.join(process.cwd(), 'content', 'docs')))
)
console.log('Generated llms.txt in public/')
const indexOutputPath = path.join(publicDirectory, 'markdown', 'index.md')
fs.mkdirSync(path.dirname(indexOutputPath), { recursive: true })
fs.writeFileSync(indexOutputPath, buildIndexMarkdown())
console.log('Generated index.md in public/markdown/')
}