Files
supabase/apps/ui-library/lib/library-documents.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

64 lines
2.3 KiB
TypeScript

import { readdirSync } from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
export function parseLibraryDocument(raw: string) {
const { content, data } = matter(raw)
for (const field of ['title', 'description', 'preview'] as const) {
if (data[field] !== undefined && typeof data[field] !== 'string') {
throw new Error(`Document ${field} must be a string`)
}
}
return {
content,
data: data as { title?: string; description?: string; preview?: string },
}
}
export function collectMdxFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const entryPath = path.join(directory, entry.name)
if (entry.isDirectory()) return collectMdxFiles(entryPath)
return entry.name.endsWith('.mdx') ? [entryPath] : []
})
.sort((a, b) => a.localeCompare(b))
}
// Match Velite's flattened path, relative to content/docs.
export function getDocSlug(relativePath: string): string {
return relativePath
.replace(/\\/g, '/')
.replace(/\.mdx$/, '')
.replace(/\/index$/, '')
}
export function toAgentHref(
href: string,
documentSlugs?: ReadonlySet<string>,
documentBasePath?: string
): string {
if (!href || href.startsWith('#') || href.startsWith('//')) return href
const isRelative = !/^[a-z][a-z\d+.-]*:/i.test(href)
if (!isRelative && !href.startsWith('https://supabase.com/library/docs/')) return href
if (isRelative && !href.startsWith('/') && !documentBasePath) return href
// documentBasePath is the source-relative path (e.g. "foo/index"), not the
// flattened doc slug ("foo") — that keeps relative links from foo/index.mdx
// resolving inside foo/, instead of jumping to foo's parent directory.
const url = new URL(href, `https://supabase.com/library/docs/${documentBasePath ?? ''}`)
if (url.pathname.startsWith('/library/docs/')) {
const slug = url.pathname.slice('/library/docs/'.length).replace(/\.md$/, '')
if (documentSlugs && !documentSlugs.has(slug)) {
throw new Error(`Missing library document: ${slug}`)
}
url.pathname = `/library/docs/${slug}.md`
}
return url.href
}
export function markdownLink(title: string, url: string): string {
const escaped = title.replace(/\s+/g, ' ').replace(/([\\\[\]])/g, '\\$1')
return `[${escaped}](${url})`
}