New GuideTemplate with composable components (#36893)

* Add db wrappers open in dashboard cta

* Restore getLatestRelease return to null

* Add guide template components with flexible content positioning

* Add Guide components with composition pattern

* Remove default branch on tag value

* Add GuideTemplate component and do some clean up

* Restore original GuideTemplate

* Clean up code and unused props

* Remove displayName

* Remove unwanted export

* Refactor EditLink to its own helper file

* Apply several fixes

* Fix underline on cta button

* Remove default main tag

* More descriptive button cta label

* fix(docs): add iceberg_wrapper dashboard integration

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
Tomas PozoandCharis Lam authored and GitHub committed 2025-07-09 16:42:54 +00:00
1 parent 6e84bd1d72
commit ec1c534013
10 files changed
+293 -2

No files matched your search

@@ -3,8 +3,12 @@ import { readFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import emoji from 'remark-emoji'
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { Guide, GuideArticle, GuideHeader, GuideFooter, GuideMdxContent } from '~/features/ui/guide'
import { newEditLink } from '~/features/helpers.edit-link'
import {
genGuideMeta,
genGuidesStaticParams,
@@ -102,6 +106,7 @@ const pageMap = [
slug: 'airtable',
meta: {
title: 'Airtable',
dashboardIntegrationPath: 'airtable_wrapper',
},
remoteFile: 'airtable.md',
},
@@ -109,6 +114,7 @@ const pageMap = [
slug: 'auth0',
meta: {
title: 'Auth0',
dashboardIntegrationPath: 'auth0_wrapper',
},
remoteFile: 'auth0.md',
},
@@ -116,6 +122,7 @@ const pageMap = [
slug: 'bigquery',
meta: {
title: 'BigQuery',
dashboardIntegrationPath: 'bigquery_wrapper',
},
remoteFile: 'bigquery.md',
},
@@ -123,6 +130,7 @@ const pageMap = [
slug: 'clerk',
meta: {
title: 'Clerk',
dashboardIntegrationPath: 'clerk_wrapper',
},
remoteFile: 'clerk.md',
},
@@ -130,6 +138,7 @@ const pageMap = [
slug: 'clickhouse',
meta: {
title: 'ClickHouse',
dashboardIntegrationPath: 'clickhouse_wrapper',
},
remoteFile: 'clickhouse.md',
},
@@ -137,6 +146,7 @@ const pageMap = [
slug: 'cognito',
meta: {
title: 'AWS Cognito',
dashboardIntegrationPath: 'cognito_wrapper',
},
remoteFile: 'cognito.md',
},
@@ -151,6 +161,7 @@ const pageMap = [
slug: 'firebase',
meta: {
title: 'Firebase',
dashboardIntegrationPath: 'firebase_wrapper',
},
remoteFile: 'firebase.md',
},
@@ -158,6 +169,7 @@ const pageMap = [
slug: 'iceberg',
meta: {
title: 'Iceberg',
dashboardIntegrationPath: 'iceberg_wrapper',
},
remoteFile: 'iceberg.md',
},
@@ -165,6 +177,7 @@ const pageMap = [
slug: 'logflare',
meta: {
title: 'Logflare',
dashboardIntegrationPath: 'logflare_wrapper',
},
remoteFile: 'logflare.md',
},
@@ -172,6 +185,7 @@ const pageMap = [
slug: 'mssql',
meta: {
title: 'MSSQL',
dashboardIntegrationPath: 'mssql_wrapper',
},
remoteFile: 'mssql.md',
},
@@ -179,6 +193,7 @@ const pageMap = [
slug: 'notion',
meta: {
title: 'Notion',
dashboardIntegrationPath: 'notion_wrapper',
},
remoteFile: 'notion.md',
},
@@ -186,6 +201,7 @@ const pageMap = [
slug: 'paddle',
meta: {
title: 'Paddle',
dashboardIntegrationPath: 'paddle_wrapper',
},
remoteFile: 'paddle.md',
},
@@ -193,6 +209,7 @@ const pageMap = [
slug: 'redis',
meta: {
title: 'Redis',
dashboardIntegrationPath: 'redis_wrapper',
},
remoteFile: 'redis.md',
},
@@ -200,6 +217,7 @@ const pageMap = [
slug: 's3',
meta: {
title: 'AWS S3',
dashboardIntegrationPath: 's3_wrapper',
},
remoteFile: 's3.md',
},
@@ -207,6 +225,7 @@ const pageMap = [
slug: 'snowflake',
meta: {
title: 'Snowflake',
dashboardIntegrationPath: 'snowflake_wrapper',
},
remoteFile: 'snowflake.md',
},
@@ -214,6 +233,7 @@ const pageMap = [
slug: 'stripe',
meta: {
title: 'Stripe',
dashboardIntegrationPath: 'stripe_wrapper',
},
remoteFile: 'stripe.md',
},
@@ -254,7 +274,31 @@ const WrappersDocs = async (props: { params: Promise<Params> }) => {
} as SerializeOptions)
: undefined
return <GuideTemplate meta={meta} mdxOptions={options} {...data} />
const dashboardIntegrationURL = getDashboardIntegrationURL(meta.dashboardIntegrationPath)
return (
<Guide meta={meta}>
<GuideArticle>
<GuideHeader />
{dashboardIntegrationURL && (
<Admonition type="tip" className="mb-4">
<p>You can enable the {meta.title} wrapper right from the Supabase dashboard.</p>
<Button asChild>
<Link href={dashboardIntegrationURL} className="no-underline">
Open wrapper in dashboard
</Link>
</Button>
</Admonition>
)}
<GuideMdxContent content={data.content} mdxOptions={options} />
<GuideFooter editLink={data.editLink} />
</GuideArticle>
</Guide>
)
}
/**
@@ -323,6 +367,12 @@ const getContent = async (params: Params) => {
}
}
const getDashboardIntegrationURL = (wrapperPath?: string) => {
return wrapperPath
? `https://supabase.com/dashboard/project/_/integrations/${wrapperPath}/overview`
: null
}
const assetUrlTransform = (url: string, baseUrl: string): string => {
const assetPattern = /(\.\.\/)+assets\//
@@ -92,6 +92,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
{content && <MDXRemoteBase source={content} options={mdxOptions} />}
{children}
<footer className="mt-16 not-prose">
<a
href={
@@ -135,3 +136,4 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
}
export { GuideTemplate, newEditLink }
export type { EditLink }
+39
View File
@@ -0,0 +1,39 @@
const EDIT_LINK_SYMBOL = Symbol('edit link')
interface EditLink {
[EDIT_LINK_SYMBOL]: true
link: string
includesProtocol: boolean
}
/**
* Create an object representing a link where the original content can be
* edited.
*
* Takes either a relative path, which will be prefixed with
* `https://github.com/`, or a full URL including protocol.
*/
const newEditLink = (str: string): EditLink => {
if (str.startsWith('/')) {
throw Error(`Edit links cannot start with slashes. Received: ${str}`)
}
/**
* Catch strings that provide FQDNS without https?:
*
* At the start of a string, before the first slash, there is a dot
* surrounded by non-slash characters.
*/
if (/^[^\/]+\.[^\/]+\//.test(str)) {
throw Error(`Fully qualified domain names must start with 'https?'. Received: ${str}`)
}
return {
[EDIT_LINK_SYMBOL]: true,
link: str,
includesProtocol: str.startsWith('http://') || str.startsWith('https://'),
}
}
export { newEditLink }
export type { EditLink }
+35
View File
@@ -0,0 +1,35 @@
import { type ReactNode } from 'react'
import type { WithRequired } from '~/features/helpers.types'
import { EditLink } from '~/features/helpers.edit-link'
import { type GuideFrontmatter } from '~/lib/docs'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from './guide'
interface BaseGuideTemplateProps {
meta?: GuideFrontmatter
content?: string
children?: ReactNode
editLink: EditLink
mdxOptions?: SerializeOptions
}
type GuideTemplateProps =
| WithRequired<BaseGuideTemplateProps, 'children'>
| WithRequired<BaseGuideTemplateProps, 'content'>
const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideTemplateProps) => {
return (
<Guide meta={meta}>
<GuideArticle>
<GuideHeader />
<GuideMdxContent content={content} mdxOptions={mdxOptions}></GuideMdxContent>
{children}
<GuideFooter editLink={editLink} />
</GuideArticle>
</Guide>
)
}
export { GuideTemplate }
+69
View File
@@ -0,0 +1,69 @@
'use client'
import { createContext, useContext, type ReactNode } from 'react'
import { cn } from 'ui'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
import { type GuideFrontmatter } from '~/lib/docs'
interface GuideContextValue {
meta?: GuideFrontmatter
}
const GuideContext = createContext<GuideContextValue | undefined>(undefined)
export const useGuide = () => {
const context = useContext(GuideContext)
if (!context) {
throw new Error('useGuide must be used within a GuideProvider')
}
return context
}
interface GuideProps {
meta?: GuideFrontmatter
children?: ReactNode
className?: string
}
export function Guide({ meta, children, className }: GuideProps) {
const hideToc = meta?.hideToc || meta?.hide_table_of_contents
return (
<GuideContext.Provider value={{ meta }}>
<TocAnchorsProvider>
<div className={cn('grid grid-cols-12 relative gap-4', className)}>
<div
className={cn(
'relative',
'transition-all ease-out',
'duration-100',
hideToc ? 'col-span-12' : 'col-span-12 md:col-span-9'
)}
>
{children}
</div>
{!hideToc && (
<GuidesTableOfContents
video={meta?.tocVideo}
className={cn(
'hidden md:flex',
'col-span-3 self-start',
'sticky',
/**
* --header-height: height of nav
* 1px: height of nav border
* 2rem: content padding
*/
'top-[calc(var(--header-height)+1px+2rem)]',
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
'max-h-[calc(100vh-var(--header-height)-3rem)]'
)}
/>
)}
</div>
</TocAnchorsProvider>
</GuideContext.Provider>
)
}
@@ -0,0 +1,24 @@
import { type ReactNode } from 'react'
import { cn } from 'ui'
import Breadcrumbs from '~/components/Breadcrumbs'
interface GuideArticleProps {
children: ReactNode
className?: string
}
export function GuideArticle({ children, className }: GuideArticleProps) {
return (
<>
<Breadcrumbs className="mb-2" />
<article
// Used to get headings for the table of contents
id="sb-docs-guide-main-article"
className={cn('prose max-w-none', className)}
>
{children}
</article>
</>
)
}
@@ -0,0 +1,30 @@
import { ExternalLink } from 'lucide-react'
import { cn } from 'ui'
import { type EditLink } from '~/features/helpers.edit-link'
interface GuideFooterProps {
className?: string
editLink: EditLink
}
export function GuideFooter({ className, editLink }: GuideFooterProps) {
if (!editLink) return null
return (
<footer className={cn('mt-16 not-prose', className)}>
<a
href={editLink.includesProtocol ? editLink.link : `https://github.com/${editLink.link}`}
className={cn(
'w-fit',
'flex items-center gap-1',
'text-sm text-scale-1000 hover:text-scale-1200',
'transition-colors'
)}
target="_blank"
rel="noreferrer noopener edit"
>
Edit this page on GitHub <ExternalLink size={14} strokeWidth={1.5} />
</a>
</footer>
)
}
@@ -0,0 +1,26 @@
'use client'
import ReactMarkdown from 'react-markdown'
import { useGuide } from './Guide'
interface GuideHeaderProps {
className?: string
}
export function GuideHeader({ className }: GuideHeaderProps) {
const { meta } = useGuide()
return (
<div className={className}>
<h1 className="mb-0 [&>p]:m-0">
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
</h1>
{meta?.subtitle && (
<h2 className="mt-3 text-xl text-foreground-light">
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
</h2>
)}
<hr className="not-prose border-t-0 border-b my-8" />
</div>
)
}
@@ -0,0 +1,11 @@
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
interface GuideArticleProps {
content?: string
mdxOptions?: SerializeOptions
}
export function GuideMdxContent({ content, mdxOptions }: GuideArticleProps) {
return content ? <MDXRemoteBase source={content} options={mdxOptions} /> : null
}
+5
View File
@@ -0,0 +1,5 @@
export { Guide, useGuide } from './Guide'
export { GuideArticle } from './GuideArticle'
export { GuideMdxContent } from './GuideMdxContent'
export { GuideHeader } from './GuideHeader'
export { GuideFooter } from './GuideFooter'