mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
I made the crawler renderer resolve legacy JavaScript and Dart reference slugs to their current sections, and updated authored guide and SDK spec links to use them. Exact slugs still win, ambiguous bare slugs still return 404, and `file-buckets-listv2` remains a section slug in canonical links. I kept the www redirect work in a separate draft PR because the apps deploy independently. ## To test - [x] On the Docs preview, request `reference/javascript/order` and `reference/dart/get-user` with a bot user agent. Expect the intended heading and canonical URL. - [x] Request `reference/javascript/file-buckets-listv2` with bot and browser user agents. Expect it to open the list v2 section. - [x] Request `reference/swift/get-user` and the Kotlin reference root with a bot user agent. Expect the intended heading. - [x] Open the Storage quickstart guide and follow its upload reference link. Expect the current JavaScript upload section. ## Linear refs GROWTH-1293 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Reference pages now resolve legacy aliases and ambiguous slugs more accurately, with canonical links that preserve explicit SDK versions. * SDK version paths are recognized only when the full path segment matches the version format, improving reference-page routing. * **Documentation** * Updated API reference links across authentication, storage, security, and SDK guides to point to current pages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
316 lines
9.6 KiB
TypeScript
316 lines
9.6 KiB
TypeScript
import { clientSdkIds, REFERENCES, selfHostingServices } from '~/content/navigation.references'
|
|
import { getFlattenedSections } from '~/features/docs/Reference.generated.singleton'
|
|
import { generateOpenGraphImageMeta } from '~/features/seo/openGraph'
|
|
import { BASE_PATH } from '~/lib/constants'
|
|
import { getCustomContent } from '~/lib/custom-content/getCustomContent'
|
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx'
|
|
import { toMarkdown } from 'mdast-util-to-markdown'
|
|
import { mdxjs } from 'micromark-extension-mdxjs'
|
|
import type { Metadata, ResolvingMetadata } from 'next'
|
|
import { redirect } from 'next/navigation'
|
|
import { visit } from 'unist-util-visit'
|
|
|
|
const { metadataTitle } = getCustomContent(['metadata:title'])
|
|
|
|
export interface AbbrevApiReferenceSection {
|
|
id: string
|
|
type: string
|
|
title?: string
|
|
slug?: string
|
|
items?: Array<AbbrevApiReferenceSection>
|
|
excludes?: Array<string>
|
|
meta?: {
|
|
shared?: boolean
|
|
}
|
|
}
|
|
|
|
export function parseReferencePath(slug: Array<string>) {
|
|
const isClientSdkReference = clientSdkIds.includes(slug[0])
|
|
const isCliReference = slug[0] === 'cli'
|
|
const isApiReference = slug[0] === 'api'
|
|
const isSelfHostingReference = slug[0].startsWith('self-hosting-')
|
|
|
|
if (isClientSdkReference) {
|
|
let sdkId: string
|
|
let maybeVersion: string | null
|
|
let maybeCrawlers: string | null
|
|
let path: string[]
|
|
;[sdkId, maybeVersion, maybeCrawlers, ...path] = slug
|
|
if (!/^v\d+$/.test(maybeVersion)) {
|
|
maybeVersion = null
|
|
path = [maybeCrawlers, ...path]
|
|
maybeCrawlers = maybeVersion
|
|
}
|
|
if (maybeCrawlers !== 'crawlers') {
|
|
if (typeof maybeCrawlers === 'string') {
|
|
path = [maybeCrawlers, ...path]
|
|
}
|
|
maybeCrawlers = null
|
|
}
|
|
|
|
return {
|
|
__type: 'clientSdk' as const,
|
|
sdkId,
|
|
maybeVersion,
|
|
maybeCrawlers,
|
|
path,
|
|
}
|
|
} else if (isCliReference) {
|
|
return {
|
|
__type: 'cli' as const,
|
|
path: slug.slice(1),
|
|
}
|
|
} else if (isApiReference) {
|
|
return {
|
|
__type: 'api' as const,
|
|
path: slug.slice(1),
|
|
}
|
|
} else if (isSelfHostingReference) {
|
|
return {
|
|
__type: 'self-hosting' as const,
|
|
service: slug[0].replace('self-hosting-', ''),
|
|
servicePath: slug[0],
|
|
path: slug.slice(1),
|
|
}
|
|
} 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),
|
|
}))
|
|
}
|
|
|
|
// Spike (DOCS-1268): one static page per Management API endpoint, in addition
|
|
// to the existing bare `/reference/api` monolith. Deliberately does not reuse
|
|
// generateStaticParamsForSdkVersion's output shape — that function bakes in a
|
|
// 'crawlers' path segment for a separate crawler-only mechanism unrelated to
|
|
// these human-facing per-operation URLs.
|
|
async function generateStaticParamsForApi() {
|
|
const flattenedSections = await getFlattenedSections('api', 'latest')
|
|
|
|
return (flattenedSections || [])
|
|
.filter((section) => section.type !== 'category' && !!section.slug)
|
|
.map((section) => ({
|
|
slug: ['api', section.slug],
|
|
}))
|
|
}
|
|
|
|
export async function generateReferenceStaticParams() {
|
|
const sdkPages = clientSdkIds
|
|
.flatMap((sdkId) =>
|
|
REFERENCES[sdkId].versions.map((version) => ({
|
|
sdkId,
|
|
version,
|
|
}))
|
|
)
|
|
.map(({ sdkId, version }) => ({
|
|
slug: [sdkId, version === REFERENCES[sdkId].versions[0] ? null : version].filter(Boolean),
|
|
}))
|
|
|
|
const cliPages = [
|
|
{
|
|
slug: ['cli'],
|
|
},
|
|
]
|
|
|
|
const apiPages = [
|
|
{
|
|
slug: ['api'],
|
|
},
|
|
...(await generateStaticParamsForApi()),
|
|
]
|
|
|
|
const selfHostingPages = selfHostingServices.map((service) => ({
|
|
slug: [REFERENCES[service].libPath],
|
|
}))
|
|
|
|
return [...sdkPages, ...cliPages, ...apiPages, ...selfHostingPages]
|
|
}
|
|
|
|
export async function generateReferenceMetadata(
|
|
props: { params: Promise<{ slug: Array<string> }> },
|
|
resolvingParent: ResolvingMetadata
|
|
): Promise<Metadata> {
|
|
const { slug } = await props.params
|
|
const { alternates: parentAlternates, openGraph: parentOg } = await resolvingParent
|
|
|
|
const parsedPath = parseReferencePath(slug)
|
|
const isClientSdkReference = parsedPath.__type === 'clientSdk'
|
|
const isCliReference = parsedPath.__type === 'cli'
|
|
const isApiReference = parsedPath.__type === 'api'
|
|
const isSelfHostingReference = parsedPath.__type === 'self-hosting'
|
|
if (isClientSdkReference) {
|
|
const { sdkId, maybeVersion, path } = 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, path[0]].filter(Boolean).join('/')
|
|
|
|
const images = generateOpenGraphImageMeta({
|
|
type: 'API Reference',
|
|
title: `${displayName}${sectionTitle ? `: ${sectionTitle}` : ''}`,
|
|
})
|
|
|
|
return {
|
|
title: `${displayName} API Reference | ${metadataTitle || 'Supabase'}`,
|
|
description: `API reference for the ${displayName} Supabase SDK`,
|
|
...(slug.length > 0
|
|
? {
|
|
alternates: {
|
|
canonical: url,
|
|
},
|
|
}
|
|
: {}),
|
|
openGraph: {
|
|
...parentOg,
|
|
url,
|
|
images,
|
|
},
|
|
}
|
|
} else if (isCliReference) {
|
|
return {
|
|
title: 'CLI Reference | Supabase Docs',
|
|
description: 'CLI reference for the Supabase CLI',
|
|
}
|
|
} else if (isApiReference) {
|
|
const { path } = parsedPath
|
|
const operationSlug = path[0]
|
|
|
|
const flattenedSections = operationSlug
|
|
? await getFlattenedSections('api', 'latest')
|
|
: undefined
|
|
const sectionTitle = flattenedSections?.find((section) => section.slug === operationSlug)?.title
|
|
|
|
const url = [BASE_PATH, 'reference', 'api', operationSlug].filter(Boolean).join('/')
|
|
const images = generateOpenGraphImageMeta({
|
|
type: 'API Reference',
|
|
title: `Management API${sectionTitle ? `: ${sectionTitle}` : ''}`,
|
|
})
|
|
|
|
return {
|
|
title: `${sectionTitle ? `${sectionTitle} | ` : ''}Management API Reference | Supabase Docs`,
|
|
description: `Management API reference for the Supabase API${sectionTitle ? `: ${sectionTitle}` : ''}`,
|
|
...(operationSlug
|
|
? {
|
|
alternates: {
|
|
canonical: url,
|
|
},
|
|
}
|
|
: {}),
|
|
openGraph: {
|
|
...parentOg,
|
|
url,
|
|
images,
|
|
},
|
|
}
|
|
} else if (isSelfHostingReference) {
|
|
return {
|
|
title: 'Self-Hosting | Supabase Docs',
|
|
}
|
|
} 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 {
|
|
/**
|
|
* Need to first escape the braces so that the MDX parser doesn't choke on
|
|
* them. Unlike the MDX parser, the regular Markdown parser handles braces
|
|
* gracefully, so we use it to find the positions of the code blocks, then
|
|
* escape all other braces before the final conversion with the MDX parser.
|
|
*/
|
|
const markdownTree = fromMarkdown(markdownUnescaped)
|
|
|
|
const codeBlocks = [] as Array<{
|
|
type: string
|
|
start: number
|
|
end: number
|
|
}>
|
|
visit(markdownTree, ['code', 'inlineCode'], (node) => {
|
|
codeBlocks.push({
|
|
type: node.type,
|
|
start: node.position?.start?.offset || 0,
|
|
end: node.position?.end?.offset || 0,
|
|
})
|
|
})
|
|
// Sort code blocks by start offset in descending order
|
|
codeBlocks.sort((a, b) => b.start - a.start)
|
|
|
|
let markdown = markdownUnescaped
|
|
let lastIndex = markdown.length
|
|
|
|
// Iterate through the sorted code blocks
|
|
for (const block of codeBlocks) {
|
|
// Escape braces in the text between the current code block and the last processed position
|
|
const textBetween = markdown.slice(block.end, lastIndex)
|
|
const escapedTextBetween = textBetween.replace(/(?<!\\)([{}])/g, '\\$1')
|
|
|
|
// Replace the original text with the escaped version
|
|
markdown = markdown.slice(0, block.end) + escapedTextBetween + markdown.slice(lastIndex)
|
|
|
|
// Update the last processed position
|
|
lastIndex = block.start
|
|
}
|
|
|
|
// Escape braces in the remaining text before the first code block
|
|
if (lastIndex > 0) {
|
|
const remainingText = markdown.slice(0, lastIndex)
|
|
const escapedRemainingText = remainingText.replace(/(?<!\\)([{}])/g, '\\$1')
|
|
markdown = escapedRemainingText + markdown.slice(lastIndex)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
export { SUPPORTS_NEW_REFERENCE_PROCESS } from '~/features/docs/Reference.constants'
|