Reference Page Generation
This system converts structured API definition files into rendered MDX reference pages. Each reference (e.g. the JavaScript Client Library) lives in a self-contained folder under spec/reference/ and is processed via a language-specific adapter.
Table of Contents
- How it works
- Adding a new reference page
- Adding a new language processor
- Running the script
- File map
How it works
spec/reference/<slug>/ ← source folder (one per reference page)
config.json ← metadata and processing options
*.json ← API definition data (any name, any number)
introduction.partial.mdx ← optional prose injected after the frontmatter
<category-slug>.partial.mdx ← optional prose injected under each category
↓ pnpm generate:mdx-reference ↓
content/reference/<slug>/
index.mdx ← generated MDX page
data.json ← sections tree + config properties (used by the page route)
The script scripts/reference/generate-mdx-reference.ts discovers every spec folder, selects the right language processor based on config.json, and writes the output.
Adding a new reference page
1. Create the spec folder
Create a folder under spec/reference/ whose name matches the URL slug you want:
spec/reference/dart/ ← page will live at /reference/dart
spec/reference/dart/v2/ ← versioned page at /reference/dart/v2
The folder path relative to spec/reference/ is mirrored exactly into content/reference/.
2. Write the config
Create config.json in the folder. All fields except language are optional but recommended.
{
// Required: selects the language processor (see scripts/reference/languages/)
"language": "typescript",
// Displayed in the page header and browser tab
"title": "Dart Client library",
"subtitle": "The official Dart/Flutter client for Supabase.",
// Shown as a link next to the title
"referenceLink": "https://github.com/supabase/supabase-dart",
"referenceLinkLabel": "supabase-dart",
// Navigation metadata (consumed by the page route and layout)
"name": "Dart",
"menuTitle": "Dart",
"icon": "reference-dart",
"version": "v2",
"versions": ["v2", "v1"],
"isLatestVersion": true,
// Optional: list categories in a specific order.
// Categories not listed appear after those that are, in insertion order.
"categoryOrder": ["Database", "Auth", "Functions"],
// Optional: hide definitions globally or within a specific category.
// String form hides the definition in every category.
// Object form hides it only in the named category.
// Important: It is still recommended to use a @hidden tag or similar directly on the code.
"ignoreDefinitions": ["constructor", { "category": "Database", "definition": "internalHelper" }],
// Optional: rename or reorder a definition within a category.
// `definition` is the original name in the source data.
// `name` replaces it in the output.
// `order` is a 0-based position within the category (other items fill remaining slots).
"overrideDefinitions": [
{ "category": "Database", "definition": "constructor", "name": "createClient", "order": 0 },
],
}
3. Add source data files
Place any number of .json files (not counting the config.json one) in the spec folder. The language processor reads all of them and merges the results in a single definition object.
The exact format depends on the processor — for typescript, they are TypeDoc JSON outputs.
spec/reference/dart/
config.json
dart_core.json ← processed by the dart processor
dart_storage.json ← also processed
4. Add optional MDX partials
Partials are authored MDX fragments injected into the generated page at specific positions.
| File | Inserted |
|---|---|
introduction.partial.mdx |
Immediately after the frontmatter, before any category |
<category-slug>.partial.mdx |
Under the matching category heading, before its definitions |
The category slug is the category name lowercased with spaces and special characters replaced by hyphens — e.g. File Buckets → file-buckets.partial.mdx.
## Heading lines inside partials are automatically converted to <Heading> components with a stable anchor id (<category-slug>-<heading-slug>), so they appear correctly in the navigation sidebar.
Example: spec/reference/dart/auth.partial.mdx
## Social Login
Supabase supports OAuth providers out of the box. See the [OAuth guide](/docs/guides/auth/social-login) for setup instructions.
5. Create a dedicated page route
Each reference needs an App Router page at app/reference/<slug>/page.tsx. Copy the structure from the existing JavaScript page:
// app/reference/dart/page.tsx
import { ReferencePageLayout } from '~/features/docs/Reference.layout.new'
import { REFERENCE_DIRECTORY, type GuideFrontmatter } from '~/lib/docs'
import { join } from 'path'
import matter from 'gray-matter'
import { promises as fs } from 'node:fs'
import { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
interface ReferenceData {
title?: string
subtitle?: string
referenceLink?: string
referenceLinkLabel?: string
sections: AbbrevApiReferenceSection[]
}
async function getReferenceContent(library: string) {
const filePath = join(REFERENCE_DIRECTORY, library, 'index.mdx')
const { data: meta, content } = matter(await fs.readFile(filePath, 'utf-8'))
return { meta, content } as { content: string; meta: GuideFrontmatter }
}
async function getReferenceData(library: string) {
const filePath = join(REFERENCE_DIRECTORY, library, 'data.json')
return JSON.parse(await fs.readFile(filePath, 'utf-8')) as ReferenceData
}
export default async function ReferencePage() {
const library = 'dart'
const { sections } = await getReferenceData(library)
const { meta, content } = await getReferenceContent(library)
return (
<ReferencePageLayout
name="Dart"
icon="reference-dart"
library={library}
version="v2"
isLatestVersion={true}
sections={sections}
meta={meta}
content={content}
/>
)
}
The data used here (name, icon, version, isLatestVersion) is typically what you put in config.json. Consider reading it from data.json to keep a single source of truth.
Having to copy this page is a temporary solution until all definitions can be enclosed under the same rendering layout pipeline.
6. Exclude from the catch-all route
Open app/reference/[...slug]/page.tsx and add your new slug to DEDICATED_ROUTES. This prevents a static build conflict between the dedicated page and the catch-all:
// app/reference/[...slug]/page.tsx
// Paths with dedicated pages are excluded so they don't conflict at build time.
const DEDICATED_ROUTES = new Set(['javascript', 'dart']) // ← add your slug
export async function generateStaticParams() {
const all = await generateReferenceStaticParams()
return all.filter((p) => !DEDICATED_ROUTES.has(p.slug[0]))
}
Without this, Next.js generates two static files for the same path at build time, and in preview/production environments the catch-all's output can win over the dedicated page.
Adding a new language processor
Processors live in scripts/reference/languages/. The file name must match the "language" field in config.json.
scripts/reference/languages/
typescript.ts ← used when config says "language": "typescript"
dart.ts ← used when config says "language": "dart"
The Language Processor contract
A processor must export a single named function processSpec:
import type { SpecCategory, SpecConfig } from '../types.js'
export function processSpec(specDir: string): {
categories: SpecCategory[]
config: SpecConfig
}
specDir— absolute path to the spec source folder (e.g./…/spec/reference/dart)- Returns
categories(the structured content) andconfig(the parsedconfig.json)
The types are defined in scripts/reference/types.ts.
Full output shape
The categories array is everything the MDX generator consumes. Every field is optional except category, definitions, and definition.name.
type ProcessedType =
| string // intrinsic: "string", "number", "void" …
| { kind: 'literal'; value: unknown }
| { kind: 'union'; types: ProcessedType[] }
| { kind: 'intersection'; types: ProcessedType[] }
| { kind: 'array'; elementType: ProcessedType }
| { kind: 'tuple'; elements: ProcessedType[] }
| { kind: 'reference'; name: string; typeArguments?: ProcessedType[] }
| { kind: 'typeParam'; name: string } // unresolved generic, e.g. T
| { kind: 'object'; name?: string; properties: ProcessedProperty[] }
| { kind: 'templateLiteral' }
| { kind: 'indexedAccess'; objectType: ProcessedType; indexType: ProcessedType }
| { kind: string } // catch-all for unknown kinds
interface ProcessedProperty {
name: string
optional?: boolean
description?: string
type: ProcessedType
}
interface ProcessedParam {
name: string
optional?: boolean
description?: string
type: ProcessedType
}
interface ProcessedExample {
title: string
code?: string // TypeScript/JS snippet shown in the main tab
sql?: string // shown in a "Data source" collapsible
response?: string // shown in a "Response" collapsible (typically JSON)
notes?: string // shown in a "Notes" collapsible (markdown)
}
interface ProcessedDefinition {
name: string
description?: string // markdown prose
remarks?: string[] // additional markdown paragraphs
parameters?: ProcessedParam[]
returnType?: ProcessedType
examples?: ProcessedExample[]
}
interface SpecCategory {
category: string // display name, e.g. "Auth"
definitions: ProcessedDefinition[]
}
The MDX generator renders each definition in this order:
<Heading>with a stable anchor iddescription— markdown proseremarks— additional paragraphs, each on its own<RefDefinitionParams>— parameter table (ifparametersis non-empty)<RefDefinitionReturnType>— return type tree (ifreturnTypeis set)- Tabbed examples (if
examplesis non-empty), each tab renderingcode, then optionalsql,response, andnotescollapsibles
Running the script
# Run once to check output
pnpm --filter=docs generate:mdx-reference
# Runs automatically before dev and build
pnpm dev:docs
The script is registered in apps/docs/package.json:
"generate:mdx-reference": "tsx scripts/reference/generate-mdx-reference.ts"
It runs as part of predev and prebuild in the docs app, after codegen:references and before build:markdown (which copies a Markdown-only version into public/docs/).
File map
apps/docs/
├── scripts/reference/
│ ├── README.md ← you are here
│ ├── generate-mdx-reference.ts ← main script: discovery + MDX generation
│ ├── types.ts ← shared TypeScript interfaces
│ └── languages/
│ └── typescript.ts ← TypeDoc JSON → SpecCategory[]
│
├── spec/reference/
│ └── javascript/ ← one folder per reference page
│ ├── config.json
│ ├── *.json ← TypeDoc source files
│ ├── introduction.partial.mdx
│ └── auth.partial.mdx
│
├── content/reference/ ← GENERATED — do not edit by hand
│ └── javascript/
│ ├── index.mdx
│ └── data.json
│
└── app/reference/
├── javascript/
│ └── page.tsx ← dedicated page route
└── [...slug]/
└── page.tsx ← catch-all (excludes dedicated routes)