mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Migrates client SDK References to App Router. (Management and CLI API references aren't migrated yet, nor are self-hosting config references.) Some notes: Big changes to the way crawler pages are built and individual section URLs (e.g., javascript/select) are served. All of these used to be SSG-generated pages, but the number of heavy pages was just too much to handle -- slow as molasses and my laptop sounded like it was taking off, and CI sometimes refuses to build it all at all. Tried various tricks with caching and pre-generating data but no dice. So I changed to only building one copy of each SDK+version page, then serving the sub-URLs through a response rewrite. That's for the actual user-visible pages. For the bot pages, each sub-URL needs to be its own page, but prebuilding it doesn't work, and rendering on demand from React components is too slow (looking for super-fast response here for SEO). Instead I changed to using an API route that serves very minimal, hand-crafted HTML. It looks ugly, but it's purely for the search bots. You can test what bots see by running curl --user-agent "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" <URL_OF_PAGE> Also added some smoke tests to run against prod for the crawler routes, since we don't keep an eye on those regularly, and Vercel config changes could surprise-break them. Tested the meta images on Open Graph and all seems to work fine. With this approach, full production builds are really fast: ~5 minutes Starts using the new type spec handling, which is better at finding params automatically, so I could remove some of the manually written ones from the spec files.
170 lines
4.7 KiB
TypeScript
170 lines
4.7 KiB
TypeScript
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
import { toMarkdown } from 'mdast-util-to-markdown'
|
|
import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx'
|
|
import { mdxjs } from 'micromark-extension-mdxjs'
|
|
import type { Metadata, ResolvingMetadata } from 'next'
|
|
import { redirect } from 'next/navigation'
|
|
import { visit } from 'unist-util-visit'
|
|
|
|
import { REFERENCES, clientSdkIds } from '~/content/navigation.references'
|
|
import { getFlattenedSections } from '~/features/docs/Reference.generated.singleton'
|
|
import { generateOpenGraphImageMeta } from '~/features/seo/openGraph'
|
|
import { BASE_PATH } from '~/lib/constants'
|
|
|
|
export interface AbbrevCommonClientLibSection {
|
|
id: string
|
|
type: string
|
|
title?: string
|
|
slug?: string
|
|
items?: Array<AbbrevCommonClientLibSection>
|
|
excludes?: Array<string>
|
|
meta?: {
|
|
shared?: boolean
|
|
}
|
|
}
|
|
|
|
export function parseReferencePath(slug: Array<string>) {
|
|
const isClientSdkReference = clientSdkIds.includes(slug[0])
|
|
|
|
if (isClientSdkReference) {
|
|
let [sdkId, maybeVersion, maybeCrawlers, ...path] = slug
|
|
if (!/v\d+/.test(maybeVersion)) {
|
|
maybeVersion = null
|
|
maybeCrawlers = maybeVersion
|
|
path = [maybeCrawlers, ...path]
|
|
}
|
|
if (maybeCrawlers !== 'crawlers') {
|
|
maybeCrawlers = null
|
|
path = [maybeCrawlers, ...path]
|
|
}
|
|
|
|
return {
|
|
__type: 'clientSdk' as const,
|
|
sdkId,
|
|
maybeVersion,
|
|
maybeCrawlers,
|
|
path,
|
|
}
|
|
} else {
|
|
return {
|
|
__type: 'UNIMPLEMENTED' as const,
|
|
}
|
|
}
|
|
}
|
|
|
|
async function generateStaticParamsForSdkVersion(sdkId: string, version: string) {
|
|
const flattenedSections = await getFlattenedSections(sdkId, version)
|
|
|
|
return flattenedSections
|
|
.filter((section) => section.type !== 'category' && !!section.slug)
|
|
.map((section) => ({
|
|
slug: [
|
|
sdkId,
|
|
version === REFERENCES[sdkId].versions[0] ? null : version,
|
|
'crawlers',
|
|
section.slug,
|
|
].filter(Boolean),
|
|
}))
|
|
}
|
|
|
|
export async function generateReferenceStaticParams() {
|
|
const nonCrawlerPages = clientSdkIds
|
|
.flatMap((sdkId) =>
|
|
REFERENCES[sdkId].versions.map((version) => ({
|
|
sdkId,
|
|
version,
|
|
}))
|
|
)
|
|
.map(({ sdkId, version }) => ({
|
|
slug: [sdkId, version === REFERENCES[sdkId].versions[0] ? null : version].filter(Boolean),
|
|
}))
|
|
|
|
return nonCrawlerPages
|
|
}
|
|
|
|
export async function generateReferenceMetadata(
|
|
{ params: { slug } }: { params: { slug: Array<string> } },
|
|
resolvingParent: ResolvingMetadata
|
|
): Promise<Metadata> {
|
|
const { alternates: parentAlternates, openGraph: parentOg } = await resolvingParent
|
|
|
|
const parsedPath = parseReferencePath(slug)
|
|
const isClientSdkReference = parsedPath.__type === 'clientSdk'
|
|
|
|
if (isClientSdkReference) {
|
|
const { sdkId, maybeVersion } = parsedPath
|
|
const version = maybeVersion ?? REFERENCES[sdkId].versions[0]
|
|
|
|
const flattenedSections = await getFlattenedSections(sdkId, version)
|
|
|
|
const displayName = REFERENCES[sdkId].name
|
|
const sectionTitle =
|
|
slug.length > 0
|
|
? flattenedSections.find((section) => section.slug === slug[0])?.title
|
|
: undefined
|
|
const url = [BASE_PATH, 'reference', sdkId, maybeVersion, slug[0]].filter(Boolean).join('/')
|
|
|
|
const images = generateOpenGraphImageMeta({
|
|
type: 'API Reference',
|
|
title: `${displayName}${sectionTitle ? `: ${sectionTitle}` : ''}`,
|
|
})
|
|
|
|
return {
|
|
title: `${displayName} API Reference | Supabase Docs`,
|
|
description: `API reference for the ${displayName} Supabase SDK`,
|
|
...(slug.length > 0
|
|
? {
|
|
alternates: {
|
|
...parentAlternates,
|
|
canonical: url,
|
|
},
|
|
}
|
|
: {}),
|
|
openGraph: {
|
|
...parentOg,
|
|
url,
|
|
images,
|
|
},
|
|
}
|
|
} else {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
export async function redirectNonexistentReferenceSection(
|
|
sdkId: string,
|
|
version: string,
|
|
path: Array<string>,
|
|
isLatestVersion: boolean
|
|
) {
|
|
const initialSelectedSection = path[0]
|
|
|
|
const validSlugs = await generateStaticParamsForSdkVersion(sdkId, version)
|
|
|
|
if (
|
|
initialSelectedSection &&
|
|
!validSlugs.some((params) => params.slug[0] === initialSelectedSection)
|
|
) {
|
|
redirect(`/reference/${sdkId}` + (!isLatestVersion ? '/' + version : ''))
|
|
}
|
|
}
|
|
|
|
export function normalizeMarkdown(markdownUnescaped: string): string {
|
|
const markdown = markdownUnescaped.replaceAll(/(?<!\\)\{/g, '\\{').replaceAll(/(?<!\\)\}/g, '\\}')
|
|
|
|
const mdxTree = fromMarkdown(markdown, {
|
|
extensions: [mdxjs()],
|
|
mdastExtensions: [mdxFromMarkdown()],
|
|
})
|
|
|
|
visit(mdxTree, 'text', (node) => {
|
|
node.value = node.value.replace(/\n/g, ' ')
|
|
})
|
|
|
|
const content = toMarkdown(mdxTree, {
|
|
extensions: [mdxToMarkdown()],
|
|
})
|
|
|
|
return content
|
|
}
|