diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts index c98b2d61c..a43f28363 100644 --- a/apps/website/next.config.ts +++ b/apps/website/next.config.ts @@ -22,7 +22,6 @@ export default { experimental: { ppr: true, reactCompiler: true, - useCache: true, dynamicOnHover: true, }, eslint: { diff --git a/apps/website/package.json b/apps/website/package.json index 5023a68e3..2bea28c90 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -48,6 +48,7 @@ "dependencies": { "@radix-ui/react-collapsible": "^1.1.3", "@react-icons/all-files": "^4.1.0", + "@tanstack/react-query": "^5.76.1", "@vercel/analytics": "^1.5.0", "@vercel/edge-config": "^1.4.0", "@vercel/postgres": "^0.10.0", @@ -61,7 +62,7 @@ "lucide-react": "^0.503.0", "meilisearch": "^0.49.0", "motion": "^12.9.2", - "next": "15.4.0-canary.31", + "next": "15.4.0-canary.35", "next-mdx-remote-client": "^2.1.1", "next-themes": "^0.4.6", "nuqs": "^2.4.3", diff --git a/apps/website/src/app/api/docs/entrypoints/route.ts b/apps/website/src/app/api/docs/entrypoints/route.ts new file mode 100644 index 000000000..48d712660 --- /dev/null +++ b/apps/website/src/app/api/docs/entrypoints/route.ts @@ -0,0 +1,16 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { fetchEntryPoints } from '@/util/fetchEntryPoints'; + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const packageName = searchParams.get('packageName'); + const version = searchParams.get('version'); + + if (!packageName || !version) { + return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }); + } + + const response = await fetchEntryPoints(packageName, version); + + return NextResponse.json(response); +} diff --git a/apps/website/src/app/api/docs/sitemap/route.ts b/apps/website/src/app/api/docs/sitemap/route.ts new file mode 100644 index 000000000..de0fbd29d --- /dev/null +++ b/apps/website/src/app/api/docs/sitemap/route.ts @@ -0,0 +1,21 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { fetchSitemap } from '@/util/fetchSitemap'; + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const packageName = searchParams.get('packageName'); + const version = searchParams.get('version'); + const entryPoint = searchParams.get('entryPoint'); + + if (!packageName || !version) { + return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }); + } + + const response = await fetchSitemap({ + entryPoint, + packageName, + version, + }); + + return NextResponse.json(response); +} diff --git a/apps/website/src/app/api/docs/versions/route.ts b/apps/website/src/app/api/docs/versions/route.ts new file mode 100644 index 000000000..02ec14a43 --- /dev/null +++ b/apps/website/src/app/api/docs/versions/route.ts @@ -0,0 +1,15 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { fetchVersions } from '@/util/fetchVersions'; + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const packageName = searchParams.get('packageName'); + + if (!packageName) { + return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }); + } + + const response = await fetchVersions(packageName); + + return NextResponse.json(response); +} diff --git a/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/layout.tsx b/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/layout.tsx index b0d6e9685..db174f12f 100644 --- a/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/layout.tsx +++ b/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/layout.tsx @@ -1,24 +1,5 @@ -'use cache'; - -import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted'; import type { Metadata } from 'next'; -import Link from 'next/link'; import { Suspense, type PropsWithChildren } from 'react'; -import { EntryPointSelect } from '@/components/EntrypointSelect'; -import { Footer } from '@/components/Footer'; -import { Navigation } from '@/components/Navigation'; -import { Scrollbars } from '@/components/OverlayScrollbars'; -import { PackageSelect } from '@/components/PackageSelect'; -import { SearchButton } from '@/components/SearchButton'; -import { ThemeSwitchNoSRR } from '@/components/ThemeSwitch'; -import { VersionSelect } from '@/components/VersionSelect'; -import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarTrigger } from '@/components/ui/Sidebar'; -import { buttonStyles } from '@/styles/ui/button'; -import { PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants'; -import { ENV } from '@/util/env'; -import { fetchEntryPoints } from '@/util/fetchEntryPoints'; -import { fetchVersions } from '@/util/fetchVersions'; -import { parseDocsPathParams } from '@/util/parseDocsPathParams'; import { CmdK } from './CmdK'; export async function generateMetadata({ @@ -44,82 +25,11 @@ export default async function Layout({ params, children, }: PropsWithChildren<{ - readonly params: Promise<{ - readonly item?: string[] | undefined; - readonly packageName: string; - readonly version: string; - }>; + readonly params: Promise<{ readonly packageName: string; readonly version: string }>; }>) { - const { packageName, version, item } = await params; - - const versions = fetchVersions(packageName); - - const hasEntryPoints = PACKAGES_WITH_ENTRY_POINTS.includes(packageName); - - const entryPoints = hasEntryPoints ? fetchEntryPoints(packageName, version) : Promise.resolve([]); - const { entryPoints: parsedEntrypoints } = parseDocsPathParams(item); - return ( <> - - -
-
- - {packageName} - -
- - - - -
-
- - {/*

{version}

*/} - - {hasEntryPoints ? : null} - -
-
- - - - - -
- - {ENV.IS_LOCAL_DEV ? ( -
- Local test environment -
- ) : null} - {ENV.IS_PREVIEW ? ( -
- Preview environment -
- ) : null} -
-
-
- -
-
- - {packageName} - -
-
- {children} -
-
+ {children} diff --git a/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/page.tsx b/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/page.tsx index e17047f05..25f8af8d7 100644 --- a/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/page.tsx +++ b/apps/website/src/app/docs/packages/[packageName]/[version]/[[...item]]/page.tsx @@ -1,5 +1,3 @@ -'use cache'; - import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import rehypeShikiFromHighlighter from '@shikijs/rehype/core'; diff --git a/apps/website/src/app/docs/packages/layout.tsx b/apps/website/src/app/docs/packages/layout.tsx new file mode 100644 index 000000000..f71e760b6 --- /dev/null +++ b/apps/website/src/app/docs/packages/layout.tsx @@ -0,0 +1,49 @@ +// import Link from 'next/link'; +import type { PropsWithChildren } from 'react'; +import { Footer } from '@/components/Footer'; +import { Navigation } from '@/components/Navigation'; +import { Scrollbars } from '@/components/OverlayScrollbars'; +import { SidebarHeader } from '@/components/Sidebar'; +import { Sidebar, SidebarContent, SidebarInset, SidebarTrigger } from '@/components/ui/Sidebar'; +import { ENV } from '@/util/env'; + +export default async function Layout({ children }: PropsWithChildren) { + return ( + <> + + + + + + + + + + {ENV.IS_LOCAL_DEV ? ( +
+ Local test environment +
+ ) : null} + {ENV.IS_PREVIEW ? ( +
+ Preview environment +
+ ) : null} +
+
+
+ +
+ {/*
+ + {packageName} + +
*/} +
+ {children} +
+
+ + ); +} diff --git a/apps/website/src/app/providers.tsx b/apps/website/src/app/providers.tsx index 404ea64f3..a80ee7f3f 100644 --- a/apps/website/src/app/providers.tsx +++ b/apps/website/src/app/providers.tsx @@ -1,5 +1,6 @@ 'use client'; +import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Provider as JotaiProvider } from 'jotai'; import { useRouter } from 'next/navigation'; import { ThemeProvider } from 'next-themes'; @@ -10,7 +11,30 @@ import { SidebarProvider } from '@/components/ui/Sidebar'; import { useSystemThemeFallback } from '@/hooks/useSystemThemeFallback'; import { useUnregisterServiceWorker } from '@/hooks/useUnregisterServiceWorker'; +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1_000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined; + +function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + browserQueryClient ??= makeQueryClient(); + return browserQueryClient; + } +} + export function Providers({ children }: PropsWithChildren) { + const queryClient = getQueryClient(); const router = useRouter(); useUnregisterServiceWorker(); useSystemThemeFallback(); @@ -20,7 +44,9 @@ export function Providers({ children }: PropsWithChildren) { - {children} + + {children} + diff --git a/apps/website/src/components/DocItem.tsx b/apps/website/src/components/DocItem.tsx index 7c029bf18..6defe885b 100644 --- a/apps/website/src/components/DocItem.tsx +++ b/apps/website/src/components/DocItem.tsx @@ -1,5 +1,3 @@ -'use cache'; - import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter'; import { ConstructorNode } from './ConstructorNode'; import { DeprecatedNode } from './DeprecatedNode'; diff --git a/apps/website/src/components/EntrypointSelect.tsx b/apps/website/src/components/EntrypointSelect.tsx index c2bf78c43..b99364352 100644 --- a/apps/website/src/components/EntrypointSelect.tsx +++ b/apps/website/src/components/EntrypointSelect.tsx @@ -1,23 +1,25 @@ 'use client'; import { useParams, useRouter } from 'next/navigation'; -import { use } from 'react'; import { parseDocsPathParams } from '@/util/parseDocsPathParams'; import { Select, SelectList, SelectOption, SelectTrigger } from './ui/Select'; -export function EntryPointSelect({ - entryPointsPromise, -}: { - readonly entryPointsPromise: Promise<{ readonly entryPoint: string }[]>; -}) { +export function EntryPointSelect({ entryPoints }: { readonly entryPoints: { readonly entryPoint: string }[] }) { const router = useRouter(); - const params = useParams(); - const entryPoints = use(entryPointsPromise); + const params = useParams<{ + item?: string[] | undefined; + packageName: string; + version: string; + }>(); - const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item as string[] | undefined); + const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item); return ( - {(item) => ( diff --git a/apps/website/src/components/Navigation.tsx b/apps/website/src/components/Navigation.tsx index 446472daa..6b6ec4d9b 100644 --- a/apps/website/src/components/Navigation.tsx +++ b/apps/website/src/components/Navigation.tsx @@ -1,26 +1,38 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; import { ChevronDown, ChevronUp } from 'lucide-react'; -import { notFound } from 'next/navigation'; -import { fetchSitemap } from '@/util/fetchSitemap'; +import { notFound, useParams } from 'next/navigation'; +import { parseDocsPathParams } from '@/util/parseDocsPathParams'; import { resolveNodeKind } from './DocKind'; import { NavigationItem } from './NavigationItem'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible'; -export async function Navigation({ - entryPoint, - packageName, - version, -}: { - readonly entryPoint?: string | undefined; - readonly packageName: string; - readonly version: string; -}) { - const node = await fetchSitemap({ entryPoint, packageName, version }); +export function Navigation() { + const params = useParams<{ + item?: string[] | undefined; + packageName: string; + version: string; + }>(); - if (!node) { + const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item); + + const { data: node, status } = useQuery({ + queryKey: ['sitemap', params.packageName, params.version, parsedEntrypoints.join('.')], + queryFn: async () => { + const response = await fetch( + `/api/docs/sitemap?packageName=${params.packageName}&version=${params.version}&entryPoint=${parsedEntrypoints.join('.')}`, + ); + + return response.json(); + }, + }); + + if ((status === 'success' && !node) || status === 'error') { notFound(); } - const groupedNodes = node.reduce((acc: any, node: any) => { + const groupedNodes = node?.reduce((acc: any, node: any) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing (acc[node.kind.toLowerCase()] ||= []).push(node); return acc; @@ -28,7 +40,7 @@ export async function Navigation({ return (