fix(docs): split Management API reference into per-endpoint pages

The API reference rendered all ~172 endpoints on a single page regardless
of URL, so its pre-rendered size grew with the entire API surface. This
tripped Vercel's ISR page-size cap (INC-703) once the underlying OpenAPI
spec grew. Each endpoint now gets its own statically-generated page at its
existing slug URL, bounding page size to one endpoint instead of all of
them. Client SDK, CLI, and self-hosting reference pages are unaffected.
This commit is contained in:
Ali Waseem committed 2026-07-31 11:36:00 -06:00
1 parent e1ed157d9a
commit 0212c52606
9 files changed
+121 -46

No files matched your search

+2 -3
View File
@@ -1,5 +1,3 @@
import { notFound } from 'next/navigation'
import { REFERENCES } from '~/content/navigation.references'
import { ApiReferencePage } from '~/features/docs/Reference.apiPage'
import { CliReferencePage } from '~/features/docs/Reference.cliPage'
@@ -11,6 +9,7 @@ import {
parseReferencePath,
redirectNonexistentReferenceSection,
} from '~/features/docs/Reference.utils'
import { notFound } from 'next/navigation'
export const dynamicParams = false
@@ -46,7 +45,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
} else if (isCliReference) {
return <CliReferencePage />
} else if (isApiReference) {
return <ApiReferencePage />
return <ApiReferencePage sectionSlug={parsedPath.path[0]} />
} else if (isSelfHostingReference) {
return (
<SelfHostingReferencePage service={parsedPath.service} servicePath={parsedPath.servicePath} />
+15 -9
View File
@@ -3,11 +3,13 @@ import { reference_api } from '~/components/Navigation/NavigationMenu/Navigation
import { ClientLibIntroduction } from '~/features/docs/Reference.introduction'
import { ReferenceNavigation } from '~/features/docs/Reference.navigation'
import { ReferenceContentScrollHandler } from '~/features/docs/Reference.navigation.client'
import { RefSections } from '~/features/docs/Reference.sections'
import { RefSection } from '~/features/docs/Reference.sections'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
export async function ApiReferencePage() {
export async function ApiReferencePage({ sectionSlug }: { sectionSlug?: string }) {
const isIndexOrIntro = !sectionSlug || sectionSlug === 'introduction'
return (
<ReferenceContentScrollHandler libPath="api" version="latest" isLatestVersion={true}>
<SidebarSkeleton
@@ -20,18 +22,22 @@ export async function ApiReferencePage() {
libPath="api"
version="latest"
isLatestVersion={true}
realNavigation
/>
}
>
<LayoutMainContent>
<article className="@container/article">
<ClientLibIntroduction
libPath="api"
version="latest"
isLatestVersion={true}
className="max-w-[unset]"
/>
<RefSections libraryId="api" version="latest" />
{isIndexOrIntro ? (
<ClientLibIntroduction
libPath="api"
version="latest"
isLatestVersion={true}
className="max-w-[unset]"
/>
) : (
<RefSection libraryId="api" version="latest" slug={sectionSlug} />
)}
</article>
</LayoutMainContent>
</SidebarSkeleton>
@@ -239,11 +239,13 @@ export function RefLink({
section,
skipChildren = false,
className,
realNavigation,
}: {
basePath: string
section: AbbrevApiReferenceSection
skipChildren?: boolean
className?: string
realNavigation?: boolean
}) {
const ref = useRef<HTMLAnchorElement>(null)
@@ -272,16 +274,19 @@ export function RefLink({
return (
<>
{isCompoundSection ? (
<CompoundRefLink basePath={basePath} section={section} />
<CompoundRefLink basePath={basePath} section={section} realNavigation={realNavigation} />
) : (
<Link
ref={ref}
// We don't use these links because we never do real navigation, so
// prefetching just wastes egress
prefetch={false}
// Most reference "pages" are actually an agglomeration of many
// "pages", so by default we never do a real navigation and
// prefetching would just waste egress. Reference types that render
// one real page per section set `realNavigation` and genuinely
// benefit from prefetch instead.
prefetch={realNavigation ? undefined : false}
href={href}
className={getLinkStyles(isActive, className)}
onClick={onClick}
onClick={realNavigation ? undefined : onClick}
>
{section.title}
</Link>
@@ -321,9 +326,11 @@ function useCompoundRefLinkActive(basePath: string, section: AbbrevApiReferenceS
function CompoundRefLink({
basePath,
section,
realNavigation,
}: {
basePath: string
section: AbbrevApiReferenceSection
realNavigation?: boolean
}) {
const { open, setOpen, isActive } = useCompoundRefLinkActive(basePath, section)
@@ -357,7 +364,7 @@ function CompoundRefLink({
{(section.items || []).map((item, idx) => {
return (
<li key={`${section.id}-${idx}`}>
<RefLink basePath={basePath} section={item} />
<RefLink basePath={basePath} section={item} realNavigation={realNavigation} />
</li>
)
})}
@@ -1,16 +1,14 @@
import { isFeatureEnabled } from 'common'
import { type PropsWithChildren } from 'react'
import { cn } from 'ui'
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import RefVersionDropdown from '~/components/RefVersionDropdown'
import { getReferenceSections } from '~/features/docs/Reference.generated.singleton'
import {
RefLink,
ReferenceNavigationScrollHandler,
RefLink,
} from '~/features/docs/Reference.navigation.client'
import { type AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { isFeatureEnabled } from 'common'
import { type PropsWithChildren } from 'react'
import { cn } from 'ui'
interface ReferenceNavigationProps {
libraryId: string
@@ -19,6 +17,7 @@ interface ReferenceNavigationProps {
libPath: string
version: string
isLatestVersion: boolean
realNavigation?: boolean
}
export async function ReferenceNavigation({
@@ -28,6 +27,7 @@ export async function ReferenceNavigation({
libPath,
version,
isLatestVersion,
realNavigation,
}: ReferenceNavigationProps) {
const navSections = await getReferenceSections(libraryId, version)
const filteredNavSections = navSections?.filter((section) => section.title !== 'Auth')
@@ -46,11 +46,11 @@ export async function ReferenceNavigation({
{displayedNavSections?.map((section, index) =>
section.type === 'category' ? (
<li key={section.id ?? String(index)}>
<RefCategory basePath={basePath} section={section} />
<RefCategory basePath={basePath} section={section} realNavigation={realNavigation} />
</li>
) : (
<li key={section.id ?? String(index)} className={topLvlRefNavItemStyles}>
<RefLink basePath={basePath} section={section} />
<RefLink basePath={basePath} section={section} realNavigation={realNavigation} />
</li>
)
)}
@@ -64,9 +64,11 @@ const topLvlRefNavItemStyles = 'leading-5'
function RefCategory({
basePath,
section,
realNavigation,
}: {
basePath: string
section: AbbrevApiReferenceSection
realNavigation?: boolean
}) {
if (!('items' in section && section.items && section.items.length > 0)) return null
@@ -77,7 +79,7 @@ function RefCategory({
<ul className="space-y-2">
{section.items?.map((item) => (
<li key={item.id} className={topLvlRefNavItemStyles}>
<RefLink basePath={basePath} section={item} />
<RefLink basePath={basePath} section={item} realNavigation={realNavigation} />
</li>
))}
</ul>
+21 -1
View File
@@ -5,6 +5,7 @@ import {
getCliSpec,
getFlattenedSections,
getFunctionsList,
getSectionsBySlug,
getSelfHostedApiEndpointById,
getTypeSpec,
} from '~/features/docs/Reference.generated.singleton'
@@ -24,6 +25,7 @@ import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { normalizeMarkdown } from '~/features/docs/Reference.utils'
import { CodeBlock } from '~/features/ui/CodeBlock/CodeBlock'
import { isFeatureEnabled } from 'common'
import { notFound } from 'next/navigation'
import { Fragment } from 'react'
import ReactMarkdown from 'react-markdown'
import { Badge, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
@@ -64,6 +66,24 @@ async function RefSections({ libraryId, version }: RefSectionsProps) {
)
}
type RefSectionProps = {
libraryId: string
version: string
slug: string
}
async function RefSection({ libraryId, version, slug }: RefSectionProps) {
const sectionsBySlug = await getSectionsBySlug(libraryId, version)
const section = sectionsBySlug?.get(slug)
if (!section) notFound()
return (
<div className="flex flex-col my-16 gap-16">
<SectionSwitch libraryId={libraryId} version={version} section={section} />
</div>
)
}
function trimIntro(sections: Array<AbbrevApiReferenceSection>) {
const hasIntro = sections[0]?.type === 'markdown' && sections[0]?.slug === 'introduction'
if (hasIntro) {
@@ -573,4 +593,4 @@ async function FunctionSection({
)
}
export { RefSections }
export { RefSections, RefSection }
+20 -7
View File
@@ -1,5 +1,8 @@
import { clientSdkIds, REFERENCES, selfHostingServices } from '~/content/navigation.references'
import { getFlattenedSections } from '~/features/docs/Reference.generated.singleton'
import {
getFlattenedSections,
getSectionsBySlug,
} 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'
@@ -113,11 +116,11 @@ export async function generateReferenceStaticParams() {
},
]
const apiPages = [
{
slug: ['api'],
},
]
const apiFlattenedSections = await getFlattenedSections('api', 'latest')
const apiPages = (apiFlattenedSections ?? [])
.filter((section) => section.type !== 'category' && !!section.slug)
.map((section) => ({ slug: ['api', section.slug] }))
.concat([{ slug: ['api'] }])
const selfHostingPages = selfHostingServices.map((service) => ({
slug: [REFERENCES[service].libPath],
@@ -178,9 +181,19 @@ export async function generateReferenceMetadata(
description: 'CLI reference for the Supabase CLI',
}
} else if (isApiReference) {
const { path } = parsedPath
const sectionSlug = path[0] !== 'introduction' ? path[0] : undefined
const sectionsBySlug = await getSectionsBySlug('api', 'latest')
const sectionTitle = sectionSlug ? sectionsBySlug?.get(sectionSlug)?.title : undefined
const url = [BASE_PATH, 'reference', 'api', sectionSlug].filter(Boolean).join('/')
return {
title: 'Management API Reference | Supabase Docs',
title: `Management API Reference${sectionTitle ? `: ${sectionTitle}` : ''} | Supabase Docs`,
description: 'Management API reference for the Supabase API',
alternates: {
canonical: url,
},
}
} else if (isSelfHostingReference) {
return {
+8 -6
View File
@@ -29,12 +29,14 @@ export async function generateReferencePages() {
.map(async ({ sdkId, version, libPath, isLatestVersion }) => {
const flattenedSections = await getFlattenedSections(sdkId, version)
return (
flattenedSections?.map((section) => ({
link: isLatestVersion
? `reference/${libPath}/${section.slug}`
: `reference/${libPath}/${version}/${section.slug}`,
priority: 0.8,
})) ?? []
flattenedSections
?.filter((section) => !!section.slug)
.map((section) => ({
link: isLatestVersion
? `reference/${libPath}/${section.slug}`
: `reference/${libPath}/${version}/${section.slug}`,
priority: 0.8,
})) ?? []
)
})
)
+31
View File
@@ -163,3 +163,34 @@ describe('docs middleware — /guides/* content negotiation', () => {
expect(rewrite).not.toContain('/api/guides-md/')
})
})
describe('docs middleware — /reference/api/* routing', () => {
// Regression test for INC-703: /reference/api/* used to be collapsed to a
// single /reference/api page for every endpoint, which grew unboundedly
// with the OpenAPI spec until it exceeded Vercel's ISR page-size cap. Each
// endpoint now gets its own statically-generated page, so these paths must
// pass through untouched.
it('does not rewrite /reference/api/<endpoint-slug> to the bare index', () => {
const req = makeRequest('/docs/reference/api/v1-list-all-projects')
expect(middleware(req).headers.get(REWRITE_HEADER)).toBeNull()
})
it('does not rewrite the bare /reference/api index', () => {
const req = makeRequest('/docs/reference/api')
expect(middleware(req).headers.get(REWRITE_HEADER)).toBeNull()
})
it('still rewrites /reference/cli/* to the single CLI page', () => {
const req = makeRequest('/docs/reference/cli/some-command')
expect(middleware(req).headers.get(REWRITE_HEADER)).toBe(
'https://supabase.com/docs/reference/cli'
)
})
it('still rewrites /reference/self-hosting-<service>/* to its single service page', () => {
const req = makeRequest('/docs/reference/self-hosting-auth/some-endpoint')
expect(middleware(req).headers.get(REWRITE_HEADER)).toBe(
'https://supabase.com/docs/reference/self-hosting-auth'
)
})
})
-5
View File
@@ -63,11 +63,6 @@ export function middleware(request: NextRequest) {
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
if (lib === 'api') {
const rewritePath = [REFERENCE_PATH, 'api'].join('/')
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
if (lib?.startsWith('self-hosting-')) {
const rewritePath = [REFERENCE_PATH, lib].join('/')
return NextResponse.rewrite(new URL(rewritePath, request.url))