11import type { Metadata } from "next" ;
2+ import type React from "react" ;
23import Link from "next/link" ;
34import { notFound } from "next/navigation" ;
45import { MDXRemote } from "next-mdx-remote/rsc" ;
56import matter from "gray-matter" ;
7+ import { LinkIcon } from "lucide-react" ;
68import { PropertyReference } from "@/components/property-reference" ;
79import {
810 Callout ,
@@ -19,14 +21,16 @@ import {
1921 DocsDescription ,
2022} from "fumadocs-ui/page" ;
2123import { ShellDocsLayout } from "@/components/shell-docs-layout" ;
22- import type * as PageTree from "fumadocs-core/page-tree " ;
24+ import { ReferenceVersionSelector } from "@/components/reference-version-selector " ;
2325import {
24- REFERENCE_CONTENT_DIR ,
25- loadAllReferenceItems ,
26+ REFERENCE_VERSIONS ,
27+ buildReferencePageTree ,
28+ referenceHref ,
2629 referenceStaticParams ,
30+ referenceVersionHref ,
31+ resolveReferencePage ,
2732} from "@/lib/reference-items" ;
2833import { stripLeadingImports } from "@/lib/docs-render" ;
29- import { safeReadFileSync } from "@/lib/safe-fs" ;
3034import { buildDocMetadata } from "@/lib/seo-metadata" ;
3135
3236// Self-canonical for /reference/<slug>. Reference pages are not
@@ -41,11 +45,8 @@ export async function generateMetadata({
4145 params : Promise < { slug : string [ ] } > ;
4246} ) : Promise < Metadata > {
4347 const { slug } = await params ;
44- const slugPath = slug . join ( "/" ) ;
45- const canonicalPath = `/reference/${ slugPath } ` ;
46- // Read the reference MDX directly to extract frontmatter. Reuse
47- // safeReadFileSync so a crafted slug can't escape REFERENCE_CONTENT_DIR.
48- const raw = safeReadFileSync ( REFERENCE_CONTENT_DIR , `${ slugPath } .mdx` ) ;
48+ const resolved = resolveReferencePage ( slug ) ;
49+ const raw = resolved ?. raw ?? null ;
4950 let title : string | undefined ;
5051 let description : string | undefined ;
5152 if ( raw !== null ) {
@@ -64,7 +65,9 @@ export async function generateMetadata({
6465 return buildDocMetadata ( {
6566 title : title ?? slug [ slug . length - 1 ] ,
6667 description,
67- canonicalPath,
68+ canonicalPath : resolved
69+ ? referenceHref ( resolved . version , resolved . pageSlug )
70+ : `/reference/${ slug . join ( "/" ) } ` ,
6871 } ) ;
6972}
7073
@@ -77,6 +80,12 @@ const mdxComponents = {
7780 Accordions,
7881 Accordion,
7982 OpsPlatformCTA,
83+ LinkIcon,
84+ Frame : ( { children } : { children : React . ReactNode } ) => (
85+ < div className = "my-6 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-4" >
86+ { children }
87+ </ div >
88+ ) ,
8089 // Strip unknown imports — MDX import statements become no-ops in next-mdx-remote
8190} ;
8291
@@ -90,15 +99,12 @@ export default async function ReferenceSlugPage({
9099 params : Promise < { slug : string [ ] } > ;
91100} ) {
92101 const { slug } = await params ;
93- const slugPath = slug . join ( "/" ) ;
94- // slugPath is user-supplied (URL segments). Route the filesystem read
95- // through safeReadFileSync so crafted paths like `..%2F..%2Fsecrets`
96- // can't escape REFERENCE_CONTENT_DIR.
97- const raw = safeReadFileSync ( REFERENCE_CONTENT_DIR , `${ slugPath } .mdx` ) ;
98- if ( raw === null ) {
102+ const resolved = resolveReferencePage ( slug ) ;
103+ if ( resolved === null ) {
99104 notFound ( ) ;
100105 }
101106
107+ const { version, pageSlug, contentSlug, raw } = resolved ;
102108 let content = "" ;
103109 let data : Record < string , unknown > = { } ;
104110 try {
@@ -107,43 +113,36 @@ export default async function ReferenceSlugPage({
107113 data = parsed . data ;
108114 } catch ( err ) {
109115 console . error (
110- `[reference] Failed to parse frontmatter in ${ slugPath } .mdx:` ,
116+ `[reference] Failed to parse frontmatter in ${ contentSlug } .mdx:` ,
111117 err ,
112118 ) ;
113119 notFound ( ) ;
114120 }
115121
116122 const cleanedContent = stripLeadingImports ( content ) ;
117123
118- const allItems = loadAllReferenceItems ( ) ;
119124 const title =
120125 typeof data . title === "string" && data . title . length > 0
121126 ? data . title
122127 : slug [ slug . length - 1 ] ;
123128 const description =
124129 typeof data . description === "string" ? data . description : undefined ;
125-
126- // Build a Fumadocs PageTree from the reference items, grouped by
127- // category. Reference's IA is its own (Components / Hooks) — we don't
128- // share the docs nav tree here.
129- const pageTree : PageTree . Root = {
130- name : "Reference" ,
131- children : [ "Components" , "Hooks" ] . flatMap ( ( cat ) => [
132- { type : "separator" as const , name : cat } ,
133- ...allItems
134- . filter ( ( i ) => i . category === cat )
135- . map (
136- ( item ) : PageTree . Item => ( {
137- type : "page" ,
138- name : item . title ,
139- url : `/reference/${ item . slug } ` ,
140- } ) ,
141- ) ,
142- ] ) ,
143- } ;
130+ const pageTree = buildReferencePageTree ( version ) ;
131+ const versionOptions = REFERENCE_VERSIONS . map ( ( referenceVersion ) => ( {
132+ version : referenceVersion ,
133+ href : referenceVersionHref ( referenceVersion , pageSlug ) ,
134+ } ) ) ;
144135
145136 return (
146- < ShellDocsLayout tree = { pageTree } >
137+ < ShellDocsLayout
138+ tree = { pageTree }
139+ banner = {
140+ < ReferenceVersionSelector
141+ activeVersion = { version }
142+ options = { versionOptions }
143+ />
144+ }
145+ >
147146 < DocsPage
148147 toc = { [ ] }
149148 tableOfContent = { { enabled : false } }
@@ -161,7 +160,15 @@ export default async function ReferenceSlugPage({
161160 Reference
162161 </ Link >
163162 { " / " }
164- < span className = "capitalize" > { slug [ 0 ] } </ span >
163+ < span > { version } </ span >
164+ { pageSlug && (
165+ < >
166+ { " / " }
167+ < span className = "capitalize" >
168+ { pageSlug . split ( "/" ) [ 0 ] }
169+ </ span >
170+ </ >
171+ ) }
165172 </ div >
166173 < DocsTitle className = "text-2xl font-bold" > { title } </ DocsTitle >
167174 { description && (
0 commit comments