diff --git a/.eslintrc b/.eslintrc index f8b03f98a19..2af52fb5361 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,9 @@ "@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}], "react-hooks/exhaustive-deps": "error", "react/no-unknown-property": ["error", {"ignore": ["meta"]}], - "react-compiler/react-compiler": "error" + "react-compiler/react-compiler": "error", + "@next/next/no-img-element": "off", + "@next/next/no-html-link-for-pages": "off" }, "env": { "node": true, diff --git a/next-env.d.ts b/next-env.d.ts index 52e831b4342..1b3be0840f3 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js index 861792c8e53..bf45dd5b382 100644 --- a/next.config.js +++ b/next.config.js @@ -11,8 +11,11 @@ const nextConfig = { experimental: { scrollRestoration: true, reactCompiler: true, + newDevOverlay: true, }, + env: {}, + serverExternalPackages: [], webpack: (config, {dev, isServer, ...options}) => { if (process.env.ANALYZE) { const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); diff --git a/package.json b/package.json index 6d6b53f92de..3fd49e67c80 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "CC", "scripts": { "analyze": "ANALYZE=true next build", - "dev": "next-remote-watch ./src/content", + "dev": "next dev", "build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs", "lint": "next lint", "lint:fix": "next lint --fix", @@ -15,12 +15,11 @@ "prettier:diff": "yarn nit:source", "lint-heading-ids": "node scripts/headingIdLinter.js", "fix-headings": "node scripts/headingIdLinter.js --fix", - "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss", + "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids", "tsc": "tsc --noEmit", "start": "next start", "postinstall": "is-ci || husky install .husky", - "check-all": "npm-run-all prettier lint:fix tsc rss", - "rss": "node scripts/generateRss.js" + "check-all": "npm-run-all prettier lint:fix tsc" }, "dependencies": { "@codesandbox/sandpack-react": "2.13.5", @@ -28,19 +27,21 @@ "@docsearch/react": "^3.8.3", "@headlessui/react": "^1.7.0", "@radix-ui/react-context-menu": "^2.1.5", + "@types/mdast": "^4.0.4", "body-scroll-lock": "^3.1.3", "classnames": "^2.2.6", "date-fns": "^2.16.1", "debounce": "^1.2.1", "github-slugger": "^1.3.0", - "next": "15.1.0", + "next": "^15.2.0-canary.33", "next-remote-watch": "^1.0.0", "parse-numeric-range": "^1.2.0", "react": "^19.0.0", "react-collapsed": "4.0.4", "react-dom": "^19.0.0", "remark-frontmatter": "^4.0.1", - "remark-gfm": "^3.0.1" + "remark-gfm": "^3.0.1", + "unist-builder": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.12.9", @@ -62,6 +63,7 @@ "autoprefixer": "^10.4.2", "babel-eslint": "10.x", "babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112", + "chokidar": "^4.0.3", "eslint": "7.x", "eslint-config-next": "12.0.3", "eslint-config-react-app": "^5.2.1", diff --git a/scripts/generateRss.js b/scripts/generateRss.js deleted file mode 100644 index e0f3d5561dd..00000000000 --- a/scripts/generateRss.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. and its affiliates. - */ -const {generateRssFeed} = require('../src/utils/rss'); - -generateRssFeed(); diff --git a/src/app/[[...markdownPath]]/page.tsx b/src/app/[[...markdownPath]]/page.tsx new file mode 100644 index 00000000000..85dc607008b --- /dev/null +++ b/src/app/[[...markdownPath]]/page.tsx @@ -0,0 +1,172 @@ +import fs from 'fs/promises'; +import path from 'path'; +import {Page} from 'components/Layout/Page'; +import sidebarHome from '../../sidebarHome.json'; +import sidebarLearn from '../../sidebarLearn.json'; +import sidebarReference from '../../sidebarReference.json'; +import sidebarCommunity from '../../sidebarCommunity.json'; +import sidebarBlog from '../../sidebarBlog.json'; +import {generateMDX} from '../../utils/generateMDX'; + +import {generateMetadata as generateSeoMetadata} from '../../utils/generateMetadata'; + +import {getRouteMeta, RouteItem} from 'components/Layout/getRouteMeta'; +import {LanguageItem} from 'components/MDX/LanguagesContext'; +import {cache} from 'react'; + +function getActiveSection(pathname: string) { + if (pathname === '/') { + return 'home'; + } else if (pathname.startsWith('/reference')) { + return 'reference'; + } else if (pathname.startsWith('/learn')) { + return 'learn'; + } else if (pathname.startsWith('/community')) { + return 'community'; + } else if (pathname.startsWith('/blog')) { + return 'blog'; + } else { + return 'unknown'; + } +} + +async function getRouteTree(section: string) { + switch (section) { + case 'home': + case 'unknown': + return sidebarHome; + case 'learn': + return sidebarLearn; + case 'reference': + return sidebarReference; + case 'community': + return sidebarCommunity; + case 'blog': + return sidebarBlog; + default: + throw new Error(`Unknown section: ${section}`); + } +} + +const getPageContent = cache(async function getPageContent( + markdownPath: any[] +) { + const rootDir = path.join(process.cwd(), 'src/content'); + let mdxPath = markdownPath?.join('/') || 'index'; + let mdx; + + try { + mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8'); + } catch { + mdx = await fs.readFile(path.join(rootDir, mdxPath, 'index.md'), 'utf8'); + } + + return await generateMDX(mdx, mdxPath, {}); +}); + +// This replaces getStaticPaths +export async function generateStaticParams() { + const rootDir = path.join(process.cwd(), 'src/content'); + + async function getFiles(dir: string): Promise { + const entries = await fs.readdir(dir, {withFileTypes: true}); + const files = await Promise.all( + entries.map(async (entry) => { + const res = path.resolve(dir, entry.name); + return entry.isDirectory() + ? getFiles(res) + : res.slice(rootDir.length + 1); + }) + ); + + return files + .flat() + .filter( + (file: string) => file.endsWith('.md') && !file.startsWith('errors/') + ); + } + + function getSegments(file: string) { + let segments = file.slice(0, -3).replace(/\\/g, '/').split('/'); + if (segments[segments.length - 1] === 'index') { + segments.pop(); + } + return segments; + } + + const files = await getFiles(rootDir); + + return files.map((file: any) => ({ + markdownPath: getSegments(file), + })); +} + +export default async function WrapperPage({ + params, +}: { + params: Promise<{markdownPath: string[]}>; +}) { + const {markdownPath} = await params; + + // Get the MDX content and associated data + const {content, toc, meta} = await getPageContent(markdownPath); + + const pathname = '/' + (markdownPath?.join('/') || ''); + const section = getActiveSection(pathname); + const routeTree = await getRouteTree(section); + + // Load the list of translated languages conditionally. + let languages: Array | null = null; + if (pathname.endsWith('/translations')) { + languages = await ( + await fetch( + 'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json' + ) + ).json(); // { code: string; name: string; enName: string}[] + } + + // Pass the content and TOC directly, as `getPageContent` should already return them in the correct format + return ( + + {content} + + ); +} +// Configure dynamic segments to be statically generated +export const dynamicParams = false; + +export async function generateMetadata({ + params, +}: { + params: Promise<{markdownPath: string[]}>; +}) { + const {markdownPath} = await params; + const pathname = '/' + (markdownPath?.join('/') || ''); + const section = getActiveSection(pathname); + const routeTree = await getRouteTree(section); + const {route, order} = getRouteMeta(pathname, routeTree as RouteItem); + const { + title = route?.title || '', + description = route?.description || '', + titleForTitleTag, + } = await getPageContent(markdownPath).then(({meta}) => meta); + + return generateSeoMetadata({ + title, + isHomePage: pathname === '/', + path: pathname, + description, + titleForTitleTag, + image: `/images/og-${section}.png`, + searchOrder: + section === 'learn' || (section === 'blog' && pathname !== '/blog') + ? order + : undefined, + }); +} diff --git a/src/pages/500.js b/src/app/error.tsx similarity index 61% rename from src/pages/500.js rename to src/app/error.tsx index b043e35b262..31725c447c3 100644 --- a/src/pages/500.js +++ b/src/app/error.tsx @@ -1,3 +1,5 @@ +'use client'; + /* * Copyright (c) Facebook, Inc. and its affiliates. */ @@ -5,14 +7,18 @@ import {Page} from 'components/Layout/Page'; import {MDXComponents} from 'components/MDX/MDXComponents'; import sidebarLearn from '../sidebarLearn.json'; +import {RouteItem} from 'components/Layout/getRouteMeta'; +import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata'; const {Intro, MaxWidth, p: P, a: A} = MDXComponents; -export default function NotFound() { +export default function Error() { return ( @@ -29,3 +35,11 @@ export default function NotFound() { ); } + +export async function generateMetadata({}: {}) { + return generateSeoMetadata({ + title: 'Something Went Wrong', + isHomePage: false, + path: '/500', + }); +} diff --git a/src/app/errors/[[...errorCode]]/page.tsx b/src/app/errors/[[...errorCode]]/page.tsx new file mode 100644 index 00000000000..b9158d0a54a --- /dev/null +++ b/src/app/errors/[[...errorCode]]/page.tsx @@ -0,0 +1,118 @@ +import {Page} from 'components/Layout/Page'; +import sidebarLearn from '../../../sidebarLearn.json'; +import type {RouteItem} from 'components/Layout/getRouteMeta'; +import {generateMDX} from 'utils/generateMDX'; +import fs from 'fs/promises'; +import path from 'path'; +import {ErrorDecoderProvider} from 'components/ErrorDecoderProvider'; +import {notFound} from 'next/navigation'; +import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata'; + +let errorCodesCache: Record | null = null; + +async function getErrorCodes() { + if (!errorCodesCache) { + const response = await fetch( + 'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json' + ); + errorCodesCache = await response.json(); + } + return errorCodesCache; +} + +export async function generateStaticParams() { + const errorCodes = await getErrorCodes(); + + const staticParams = Object.keys(errorCodes!).map((code) => ({ + errorCode: [code], + })) as Array<{errorCode: string[] | undefined}>; + + staticParams.push({errorCode: undefined}); + + return staticParams; +} + +async function getErrorPageContent(params: {errorCode: string[]}) { + if (params.errorCode?.length > 1) { + notFound(); + } + + const code = params.errorCode?.[0]; + + const errorCodes = await getErrorCodes(); + + if (code && !errorCodes?.[code]) { + notFound(); + } + + const rootDir = path.join(process.cwd(), 'src/content/errors'); + let mdxPath = params?.errorCode || 'index'; + let mdx; + + try { + mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8'); + } catch { + mdx = await fs.readFile(path.join(rootDir, 'generic.md'), 'utf8'); + } + + const {content, toc, meta} = await generateMDX(mdx, mdxPath, { + code, + errorCodes, + }); + + return { + content, + toc, + meta, + errorCode: code, + errorMessage: code ? errorCodes![code] : null, + }; +} + +export default async function ErrorDecoderPage({ + params, +}: { + params: Promise<{errorCode: string[]}>; +}) { + const {content, errorMessage, errorCode} = await getErrorPageContent( + await params + ); + + return ( + + +
{content}
+
+
+ ); +} + +// Disable dynamic params to ensure all pages are statically generated +export const dynamicParams = false; + +export async function generateMetadata({ + params, +}: { + params: Promise<{errorCode: string[]}>; +}) { + const {errorCode} = await params; + + const title = errorCode + ? `Minified React error #${errorCode}` + : 'Minified Error Decoder'; + + return generateSeoMetadata({ + title, + path: `errors/${errorCode}`, + isHomePage: false, + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 00000000000..079a0a8740c --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,157 @@ +import {siteConfig} from '../siteConfig'; +import {Analytics} from 'components/Analytics'; +import {ScrollHandler} from 'components/SafariScrollHandler'; + +import '@docsearch/css'; +import '../styles/algolia.css'; +import '../styles/index.css'; +import '../styles/sandpack.css'; + +import {Suspense} from 'react'; +import {DevContentRefresher} from 'components/DevContentRefresher'; +import {ThemeScript} from 'components/ThemeScript'; +import {UwuScript} from 'components/UwuScript'; +import {Metadata} from 'next'; + +export const viewport = { + themeColor: [ + {media: '(prefers-color-scheme: light)', color: '#23272f'}, + {media: '(prefers-color-scheme: dark)', color: '#23272f'}, + ], +}; + +export const metadata: Metadata = { + description: + 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript.', + openGraph: { + siteName: 'React', + type: 'website', + images: [{url: '/images/og-default.png'}], + }, + twitter: { + card: 'summary_large_image', + site: '@reactjs', + creator: '@reactjs', + images: ['/images/og-default.png'], + }, + verification: { + google: 'sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0', + }, + other: { + 'msapplication-TileColor': '#2b5797', + 'fb:app_id': '623268441017527', + }, + icons: { + icon: [ + {url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png'}, + {url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png'}, + ], + apple: [ + {url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png'}, + ], + other: [ + {rel: 'mask-icon', url: '/safari-pinned-tab.svg', color: '#404756'}, + ], + }, + manifest: '/site.webmanifest', +}; + +function FontPreload() { + return ( + <> + + + + + + + + + + + + ); +} + +export default function RootLayout({children}: {children: React.ReactNode}) { + return ( + + + + + + + + + {process.env.NODE_ENV !== 'production' && } + {children} + + + + + + ); +} diff --git a/src/pages/404.js b/src/app/not-found.tsx similarity index 60% rename from src/pages/404.js rename to src/app/not-found.tsx index 2a88fc29a4e..1ea86a10623 100644 --- a/src/pages/404.js +++ b/src/app/not-found.tsx @@ -5,12 +5,19 @@ import {Page} from 'components/Layout/Page'; import {MDXComponents} from 'components/MDX/MDXComponents'; import sidebarLearn from '../sidebarLearn.json'; +import {RouteItem} from 'components/Layout/getRouteMeta'; +import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata'; const {Intro, MaxWidth, p: P, a: A} = MDXComponents; export default function NotFound() { return ( - +

This page doesn’t exist.

@@ -27,3 +34,11 @@ export default function NotFound() {
); } + +export async function generateMetadata({}: {}) { + return generateSeoMetadata({ + title: 'Not Found', + isHomePage: false, + path: '/404', + }); +} diff --git a/src/app/rss.xml/route.js b/src/app/rss.xml/route.js new file mode 100644 index 00000000000..9ade331e77d --- /dev/null +++ b/src/app/rss.xml/route.js @@ -0,0 +1,69 @@ +import Feed from 'rss'; +import fs from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; + +const getAllFiles = (dirPath, arrayOfFiles = []) => { + const files = fs.readdirSync(dirPath); + + files.forEach((file) => { + const filePath = path.join(dirPath, file); + if (fs.statSync(filePath).isDirectory()) { + arrayOfFiles = getAllFiles(filePath, arrayOfFiles); + } else { + arrayOfFiles.push(filePath); + } + }); + + return arrayOfFiles; +}; + +export async function GET() { + const feed = new Feed({ + title: 'React Blog', + description: + 'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.', + feed_url: 'https://react.dev/rss.xml', + site_url: 'https://react.dev/', + language: 'en', + favicon: 'https://react.dev/favicon.ico', + pubDate: new Date(), + generator: 'react.dev rss module', + }); + + const dirPath = path.join(process.cwd(), 'src/content/blog'); + const filesByOldest = getAllFiles(dirPath); + const files = filesByOldest.reverse(); + + for (const filePath of files) { + const id = path.basename(filePath); + if (id !== 'index.md') { + const content = fs.readFileSync(filePath, 'utf-8'); + const {data} = matter(content); + const slug = filePath.split('/').slice(-4).join('/').replace('.md', ''); + + if (!data.title || !data.author || !data.date || !data.description) { + throw new Error( + `${id}: Blog posts must include title, author, date, and description in metadata.` + ); + } + + feed.item({ + id, + title: data.title, + author: data.author, + date: new Date(data.date), + url: `https://react.dev/blog/${slug}`, + description: data.description, + }); + } + } + + return new Response(feed.xml({indent: true}), { + headers: { + 'Content-Type': 'application/rss+xml', + }, + }); +} + +export const dynamic = 'force-static'; diff --git a/src/components/Analytics.tsx b/src/components/Analytics.tsx new file mode 100644 index 00000000000..07d637e86c0 --- /dev/null +++ b/src/components/Analytics.tsx @@ -0,0 +1,63 @@ +'use client'; + +import {useEffect} from 'react'; +import Script from 'next/script'; + +declare global { + interface Window { + gtag: (...args: any[]) => void; + } +} + +export function Analytics() { + useEffect(() => { + if (typeof window !== 'undefined') { + const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload'; + const handleTermination = () => { + window.gtag?.('event', 'timing', { + event_label: 'JS Dependencies', + event: 'unload', + }); + }; + window.addEventListener(terminationEvent, handleTermination); + return () => + window.removeEventListener(terminationEvent, handleTermination); + } + }, []); + + useEffect(() => { + // If only we had router events. But patching pushState is what Vercel Analytics does. + // https://va.vercel-scripts.com/v1/script.debug.js + const originalPushState = history.pushState; + + history.pushState = function (...args) { + const oldCleanedUrl = window.location.href.split(/[\?\#]/)[0]; + originalPushState.apply(history, args); + const newCleanedUrl = window.location.href.split(/[\?\#]/)[0]; + if (oldCleanedUrl !== newCleanedUrl) { + window?.gtag('set', 'page', newCleanedUrl); + window?.gtag('send', 'pageview'); + } + }; + return () => { + history.pushState = originalPushState; + }; + }, []); + + return ( + <> + + + ); +} diff --git a/src/components/DevContentRefresher.tsx b/src/components/DevContentRefresher.tsx new file mode 100644 index 00000000000..31a0961ed8f --- /dev/null +++ b/src/components/DevContentRefresher.tsx @@ -0,0 +1,29 @@ +'use client'; + +import {useRouter} from 'next/navigation'; +import {useRef, useEffect} from 'react'; + +export function DevContentRefresher() { + const router = useRouter(); + const wsRef = useRef(null); + + useEffect(() => { + wsRef.current = new WebSocket('ws://localhost:3001'); + + wsRef.current.onmessage = (event) => { + const message = JSON.parse(event.data); + + if (message.event === 'refresh') { + console.log('Refreshing content...'); + // @ts-ignore + router.hmrRefresh(); // Triggers client-side refresh + } + }; + + return () => { + wsRef.current?.close(); + }; + }, [router]); + + return null; +} diff --git a/src/components/ErrorDecoderProvider.tsx b/src/components/ErrorDecoderProvider.tsx new file mode 100644 index 00000000000..bad1ed2d081 --- /dev/null +++ b/src/components/ErrorDecoderProvider.tsx @@ -0,0 +1,19 @@ +'use client'; + +import {ErrorDecoderContext} from './ErrorDecoderContext'; + +export function ErrorDecoderProvider({ + children, + errorMessage, + errorCode, +}: { + children: React.ReactNode; + errorMessage: string | null; + errorCode: string | null; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx index 34db728ced2..16b974c10dd 100644 --- a/src/components/Layout/Feedback.tsx +++ b/src/components/Layout/Feedback.tsx @@ -3,12 +3,12 @@ */ import {useState} from 'react'; -import {useRouter} from 'next/router'; import cn from 'classnames'; +import {usePathname} from 'next/navigation'; export function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) { - const {asPath} = useRouter(); - const cleanedPath = asPath.split(/[\?\#]/)[0]; + const pathname = usePathname(); + const cleanedPath = pathname.split(/[\?\#]/)[0]; // Reset on route changes. return ; } diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index 24d379589de..ea0c53e1cfd 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -1,16 +1,16 @@ +'use client'; + /* * Copyright (c) Facebook, Inc. and its affiliates. */ -import {Suspense} from 'react'; import * as React from 'react'; -import {useRouter} from 'next/router'; +import {Suspense} from 'react'; import {SidebarNav} from './SidebarNav'; import {Footer} from './Footer'; import {Toc} from './Toc'; -// import SocialBanner from '../SocialBanner'; import {DocsPageFooter} from 'components/DocsFooter'; -import {Seo} from 'components/Seo'; + import PageHeading from 'components/PageHeading'; import {getRouteMeta} from './getRouteMeta'; import {TocContext} from '../MDX/TocContext'; @@ -20,8 +20,8 @@ import type {RouteItem} from 'components/Layout/getRouteMeta'; import {HomeContent} from './HomeContent'; import {TopNav} from './TopNav'; import cn from 'classnames'; -import Head from 'next/head'; +// Prefetch the code block component import(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock'); interface PageProps { @@ -36,6 +36,7 @@ interface PageProps { }; section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown'; languages?: Languages | null; + pathname: string; } export function Page({ @@ -44,11 +45,11 @@ export function Page({ routeTree, meta, section, + pathname, languages = null, }: PageProps) { - const {asPath} = useRouter(); - const cleanedPath = asPath.split(/[\?\#]/)[0]; - const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta( + const cleanedPath = pathname.split(/[\?\#]/)[0]; + const {route, nextRoute, prevRoute, breadcrumbs} = getRouteMeta( cleanedPath, routeTree ); @@ -113,31 +114,17 @@ export function Page({ showSidebar = false; } - let searchOrder; - if (section === 'learn' || (section === 'blog' && !isBlogIndex)) { - searchOrder = order; - } - return ( <> - {(isHomePage || isBlogIndex) && ( - - - + // RSS Feed link is now handled by metadata in layout.tsx + )} - {/**/}
-
+
{content}
- {showToc && toc.length > 0 && } + {showToc && toc.length > 0 && }
diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx index 72003df74f2..f67b0ed2b41 100644 --- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx @@ -5,12 +5,12 @@ import {useRef, useLayoutEffect, Fragment} from 'react'; import cn from 'classnames'; -import {useRouter} from 'next/router'; import {SidebarLink} from './SidebarLink'; import {useCollapse} from 'react-collapsed'; import usePendingRoute from 'hooks/usePendingRoute'; import type {RouteItem} from 'components/Layout/getRouteMeta'; import {siteConfig} from 'siteConfig'; +import {usePathname} from 'next/navigation'; interface SidebarRouteTreeProps { isForceExpanded: boolean; @@ -77,7 +77,7 @@ export function SidebarRouteTree({ routeTree, level = 0, }: SidebarRouteTreeProps) { - const slug = useRouter().asPath.split(/[\?\#]/)[0]; + const slug = usePathname().split(/[\?\#]/)[0]; const pendingRoute = usePendingRoute(); const currentRoutes = routeTree.routes as RouteItem[]; return ( diff --git a/src/components/Layout/Toc.tsx b/src/components/Layout/Toc.tsx index 5308c602ce5..a8d26989874 100644 --- a/src/components/Layout/Toc.tsx +++ b/src/components/Layout/Toc.tsx @@ -11,7 +11,11 @@ export function Toc({headings}: {headings: Toc}) { // TODO: We currently have a mismatch between the headings in the document // and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges). // Select the max TOC item we have here for now, but remove this after the fix. - const selectedIndex = Math.min(currentIndex, headings.length - 1); + const selectedIndex = + currentIndex !== undefined + ? Math.min(currentIndex, headings.length - 1) + : -1; + return (