mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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 4 of 6 in a stack that splits the library redesign into reviewable pieces. ## What is the current behavior? `BlockItem` renders the Open in v0 button, and `BlockItem` sits inside the Installation section — so the button appears partway down the page. On guides that install a client first it appears twice, once for the client and once for the block, and the MDX opts the extra one out with `showOpenInV0={false}`. So the page's markup already knows which registry item v0 should open. Only the component using it is in the wrong place. ## What is the new behavior? Velite reads that same signal at build time and records the item on the document, so the page header renders one icon button beside the framework selector, above the fold. `BlockItem` is left rendering only the install command. Guides with no installable item — the getting-started pages, the TanStack DB generator, and the starters — get no button. Guides that install a client first resolve to the block, not the client. ## Additional context 63 of 73 documents resolve to a registry item; the 10 that don't are exactly the ones that should have no button. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Documentation pages now display an “Open in v0” button when supported. * The button uses a compact, icon-only design alongside the framework selector. * **Improvements** * The applicable v0 destination is determined automatically from documentation content. * The page-level action replaces individual block-level v0 buttons, providing a more consistent experience. * The button includes an accessible label and appears only for documentation with a supported v0 destination. <!-- 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>
166 lines
5.2 KiB
JavaScript
166 lines
5.2 KiB
JavaScript
import path from 'path'
|
|
import { getHighlighter, loadTheme } from '@shikijs/compat'
|
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
|
import rehypePrettyCode from 'rehype-pretty-code'
|
|
import rehypeSlug from 'rehype-slug'
|
|
import { codeImport } from 'remark-code-import'
|
|
import remarkGfm from 'remark-gfm'
|
|
import { visit } from 'unist-util-visit'
|
|
import { defineConfig, s } from 'velite'
|
|
|
|
const LinksProperties = s.object({
|
|
doc: s.string().optional(),
|
|
api: s.string().optional(),
|
|
})
|
|
|
|
const NestedProperties = s.object({
|
|
radix: s.boolean().optional(),
|
|
shadcn: s.boolean().optional(),
|
|
vaul: s.boolean().optional(),
|
|
inputOtp: s.boolean().optional(),
|
|
reactAccessibleTreeview: s.boolean().optional(),
|
|
})
|
|
|
|
// The MDX is the only reliable source for which registry item v0 can open: doc
|
|
// slugs don't map onto registry names, and pages that install a client first
|
|
// list it as an extra BlockItem that opts out. Take the first item still opted in.
|
|
const getV0RegistryName = (raw) => {
|
|
for (const [, attributes] of raw.matchAll(/<BlockItem\b([^>]*?)\/?>/g)) {
|
|
if (/showOpenInV0=\{false\}/.test(attributes)) continue
|
|
const name = attributes.match(/name="([^"]+)"/)
|
|
if (name) return name[1]
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const docs = s
|
|
.object({
|
|
title: s.string(),
|
|
description: s.string(),
|
|
published: s.boolean().default(true),
|
|
links: LinksProperties.optional(),
|
|
featured: s.boolean().default(false),
|
|
component: s.boolean().default(false),
|
|
fragment: s.boolean().default(false),
|
|
toc: s.boolean().default(true),
|
|
source: NestedProperties.optional(),
|
|
// mirrors contentlayer2's `_raw.flattenedPath`: file path relative to the
|
|
// content dir, extension stripped, trailing `/index` dropped.
|
|
path: s.path(),
|
|
raw: s.raw(),
|
|
// internal doc cross-links (e.g. `[Button](components/button)`) aren't
|
|
// real files on disk — disable Velite's default asset-copying behavior,
|
|
// which otherwise treats every relative link as a local file to copy.
|
|
code: s.mdx({ copyLinkedFiles: false }),
|
|
})
|
|
.transform(({ path: flattenedPath, ...data }) => ({
|
|
...data,
|
|
slug: `/${flattenedPath}`,
|
|
slugAsParams: flattenedPath.split('/').slice(1).join('/'),
|
|
v0Name: getV0RegistryName(data.raw),
|
|
}))
|
|
|
|
export default defineConfig({
|
|
root: './content',
|
|
output: {
|
|
clean: true,
|
|
},
|
|
collections: {
|
|
allDocs: {
|
|
name: 'Doc',
|
|
pattern: 'docs/**/*.mdx',
|
|
schema: docs,
|
|
},
|
|
},
|
|
mdx: {
|
|
remarkPlugins: [remarkGfm, codeImport],
|
|
rehypePlugins: [
|
|
rehypeSlug,
|
|
() => (tree) => {
|
|
visit(tree, (node) => {
|
|
if (node?.type === 'element' && node?.tagName === 'pre') {
|
|
const [codeEl] = node.children
|
|
if (codeEl.tagName !== 'code') {
|
|
return
|
|
}
|
|
|
|
if (codeEl.data?.meta) {
|
|
// Extract event from meta and pass it down the tree.
|
|
const regex = /event="([^"]*)"/
|
|
const match = codeEl.data?.meta.match(regex)
|
|
if (match) {
|
|
node.__event__ = match ? match[1] : null
|
|
codeEl.data.meta = codeEl.data.meta.replace(regex, '')
|
|
}
|
|
}
|
|
|
|
node.__rawString__ = codeEl.children?.[0].value
|
|
node.__src__ = node.properties?.__src__
|
|
node.__style__ = node.properties?.__style__
|
|
}
|
|
})
|
|
},
|
|
[
|
|
rehypePrettyCode,
|
|
{
|
|
getHighlighter: async () => {
|
|
const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json'))
|
|
return await getHighlighter({ theme })
|
|
},
|
|
onVisitLine(node) {
|
|
// Prevent lines from collapsing in `display: grid` mode, and allow empty
|
|
// lines to be copy/pasted
|
|
if (node.children.length === 0) {
|
|
node.children = [{ type: 'text', value: ' ' }]
|
|
}
|
|
},
|
|
onVisitHighlightedLine(node) {
|
|
node.properties.className.push('line--highlighted')
|
|
},
|
|
onVisitHighlightedWord(node) {
|
|
node.properties.className = ['word--highlighted']
|
|
},
|
|
},
|
|
],
|
|
() => (tree) => {
|
|
visit(tree, (node) => {
|
|
if (node?.type === 'element' && node?.tagName === 'div') {
|
|
if (!('data-rehype-pretty-code-fragment' in node.properties)) {
|
|
return
|
|
}
|
|
|
|
const preElement = node.children.at(-1)
|
|
if (preElement.tagName !== 'pre') {
|
|
return
|
|
}
|
|
|
|
preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'div'
|
|
preElement.properties['__rawString__'] = node.__rawString__
|
|
|
|
if (node.__src__) {
|
|
preElement.properties['__src__'] = node.__src__
|
|
}
|
|
|
|
if (node.__event__) {
|
|
preElement.properties['__event__'] = node.__event__
|
|
}
|
|
|
|
if (node.__style__) {
|
|
preElement.properties['__style__'] = node.__style__
|
|
}
|
|
}
|
|
})
|
|
},
|
|
[
|
|
rehypeAutolinkHeadings,
|
|
{
|
|
properties: {
|
|
className: ['subheading-anchor'],
|
|
ariaLabel: 'Link to section',
|
|
},
|
|
},
|
|
],
|
|
],
|
|
},
|
|
})
|