mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
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:
1 parent
e1ed157d9a
commit
0212c52606
9 files changed
+121
-46
No files matched your search
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})) ?? []
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user