diff --git a/README.md b/README.md index 8d745f32..8dee818a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ No additional configuration is needed, while preserving the accessibility and cl ## Advantages - Works on React.js Server Components (RSC). More details in the section "[App Organization](./docs/01-getting-started/04-app-organization.md)"; +- Full support for next.js v14 and next.js v15. More details in the section "[App Organization](./docs/01-getting-started/04-app-organization.md)"; - Zero configuration of the project, bundler, or markdown documents. More details in the section "[Customization](./docs/03-customization/README.md)"; - Supports loading content from various sources, including GitHub. More details in the section "[Data Source](./docs/02-structure/03-data-source.md)"; - Supports fully automatic documentation generation, as well as custom generation. More details in the section "[Structure](./docs/02-structure/README.md)"; diff --git a/docs/01-getting-started/04-app-organization.md b/docs/01-getting-started/04-app-organization.md index ae74d04f..f4bc895a 100644 --- a/docs/01-getting-started/04-app-organization.md +++ b/docs/01-getting-started/04-app-organization.md @@ -28,10 +28,10 @@ Currently, Robindoc works only with the App Router. Once RSC is available for th Next.js supports dynamic routes, so it is recommended to set up one [dynamic segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) for all documentation pages. -```tsx filename="app/docs/[[...path]]/page.tsx" +```tsx filename="app/docs/[[...path]]/page.tsx" switcher tab="v14 TSX" import { Page, Sidebar, getMeta, getPages } from "../robindoc"; -const Page: React.FC<{ params: { path?: string[] } }> = ({ params }) => { +const Page: React.FC<{ params }: { params: { path?: string[] } }> = async ({ params }) => { const pathname = "/docs/" + (params.path?.join("/") || ""); return ; @@ -40,11 +40,49 @@ const Page: React.FC<{ params: { path?: string[] } }> = ({ params }) => { export default Page; ``` +```jsx filename="app/docs/[[...path]]/page.js" switcher tab="v14 JSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +const Page = async ({ params }) => { + const pathname = "/docs/" + (params.path?.join("/") || ""); + + return ; +}; + +export default Page; +``` + +```tsx filename="app/docs/[[...path]]/page.tsx" switcher tab="v15 TSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +const Page: React.FC<{ params }: { params: Promise<{ path?: string[] }> }> = async ({ params }) => { + const { path } = await params; + const pathname = "/docs/" + (path?.join("/") || ""); + + return ; +}; + +export default Page; +``` + +```jsx filename="app/docs/[[...path]]/page.js" switcher tab="v15 JSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +const Page = async ({ params }) => { + const { path } = await params; + const pathname = "/docs/" + (path?.join("/") || ""); + + return ; +}; + +export default Page; +``` + For more details about the props, refer to the [`Page`](../03-customization/01-elements/page.md) element page. You should also set up metadata generation and static parameters generation (if you want to use SSG, which is highly recommended): -```tsx filename="app/docs/[[...path]]/page.tsx" +```tsx filename="app/docs/[[...path]]/page.tsx" switcher tab="v14 TSX" import { Page, Sidebar, getMeta, getPages } from "../robindoc"; // ... @@ -66,6 +104,66 @@ export const generateStaticParams = async () => { }; ``` +```jsx filename="app/docs/[[...path]]/page.js" switcher tab="v14 JSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +// ... + +export const generateMetadata = async ({ params }) => { + const pathname = "/docs/" + (params.path?.join("/") || ""); + const meta = await getMeta(pathname); + + return meta; +}; + +export const generateStaticParams = async () => { + const pages = await getPages("/docs/"); + return pages.map((page) => ({ path: page.split("/").slice(2) })); +}; +``` + +```tsx filename="app/docs/[[...path]]/page.tsx" switcher tab="v15 TSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +// ... + +export const generateMetadata = async ({ + params, +}: { + params: Promise<{ path?: string[] }>; +}) => { + const { path } = await params; + const pathname = "/docs/" + (path?.join("/") || ""); + const meta = await getMeta(pathname); + + return meta; +}; + +export const generateStaticParams = async () => { + const pages = await getPages("/docs/"); + return pages.map((page) => ({ path: page.split("/").slice(2) })); +}; +``` + +```jsx filename="app/docs/[[...path]]/page.js" switcher tab="v15 JSX" +import { Page, Sidebar, getMeta, getPages } from "../robindoc"; + +// ... + +export const generateMetadata = async ({ params }) => { + const { path } = await params; + const pathname = "/docs/" + (path?.join("/") || ""); + const meta = await getMeta(pathname); + + return meta; +}; + +export const generateStaticParams = async () => { + const pages = await getPages("/docs/"); + return pages.map((page) => ({ path: page.split("/").slice(2) })); +}; +``` + ## Robindoc Setup It is recommended to place the Robindoc initialization near this route. @@ -163,8 +261,8 @@ export default Layout; Since the image in Vercel does not include indirect files - for working with documentation on the server - local documentation files need to be passed explicitly via `outputFileTracingIncludes` config. -```js filename="next.config.js" -/** @type {import('next').NextConfig} */ +```js filename="next.config.js" switcher tab="v14" +/** @type {import("next").NextConfig} */ const nextConfig = { experimental: { outputFileTracingIncludes: { @@ -174,6 +272,15 @@ const nextConfig = { }; ``` +```js filename="next.config.js" switcher tab="v15" +/** @type {import("next").NextConfig} */ +const nextConfig = { + outputFileTracingIncludes: { + "/api/search": ["./docs/**/*", "./blog/**/*", "./README.md"], + }, +}; +``` + For more details on search configuration, refer to the [Search](../03-customization/03-search.md) page. ## Sitemap Setup diff --git a/packages/robindoc/package.json b/packages/robindoc/package.json index bc4bc607..a027271f 100644 --- a/packages/robindoc/package.json +++ b/packages/robindoc/package.json @@ -1,6 +1,6 @@ { "name": "robindoc", - "version": "2.2.1", + "version": "2.3.0", "description": "", "main": "./lib/index.js", "scripts": { diff --git a/packages/robindoc/src/components/elements/article/document.tsx b/packages/robindoc/src/components/elements/article/document.tsx index 9899271a..fad1f8da 100644 --- a/packages/robindoc/src/components/elements/article/document.tsx +++ b/packages/robindoc/src/components/elements/article/document.tsx @@ -23,6 +23,7 @@ import { TaskListItem, TaskOrderedList, TaskUnorderedList } from "@src/component import { type PagesType } from "./types"; import { + formatId, formatLinkHref, isNewCodeToken, parseCodeLang, @@ -110,7 +111,7 @@ export const Document: React.FC = ({ | null | { props: RobinProps; childTokens: Token[]; componentName: string; type: "base" } | { type: "dummy" } = null; - let codeQueue: { [lang: string]: JSX.Element } = {}; + let codeQueue: { [lang: string]: { element: JSX.Element; tabName: string } } = {}; const insertedCodeKeys: string[] = []; const DocumentToken: React.FC<{ token: Token | Token[] }> = ({ token }) => { if (!token) return null; @@ -157,8 +158,9 @@ export const Document: React.FC = ({ } } - if (Array.isArray(token)) - return token.map((t, index) => ); + if (Array.isArray(token)) { + return token.map((t, index) => ); + } switch (token.type) { case "heading": @@ -249,9 +251,12 @@ export const Document: React.FC = ({ case "code": const { lang, configuration } = parseCodeLang(token.lang); if (configuration.switcher) { - const tabKey = typeof configuration.tab === "string" ? configuration.tab : lang; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - codeQueue[tabKey] = ; + const tabKey = typeof configuration.tab === "string" ? formatId(configuration.tab) : lang; + codeQueue[tabKey] = { + tabName: configuration.tab.toString(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: , + }; return null; } diff --git a/packages/robindoc/src/components/elements/article/utils.ts b/packages/robindoc/src/components/elements/article/utils.ts index b60dc88c..91092166 100644 --- a/packages/robindoc/src/components/elements/article/utils.ts +++ b/packages/robindoc/src/components/elements/article/utils.ts @@ -46,6 +46,10 @@ export const parseMarkdown = (content: string) => { return { tokens, headings }; }; +export const formatId = (raw: string) => { + return raw.toLowerCase().replace(/\W/g, "_").replace(/_+/g, "_"); +}; + export const validateComponentName = (name: string) => { return /^[A-Z][a-zA-Z0-9]+$/.test(name); }; @@ -71,12 +75,15 @@ export const parseCodeLang = (raw: string) => { return { lang, configuration }; }; -export const isNewCodeToken = (token: Token | Token[], codeQueue: { [lang: string]: JSX.Element }) => { +export const isNewCodeToken = ( + token: Token | Token[], + codeQueue: { [lang: string]: { element: JSX.Element; tabName: string } }, +) => { if (Array.isArray(token) || !Object.keys(codeQueue).length) return false; if (token.type === "code") { const { lang, configuration } = parseCodeLang(token.lang); - const tabKey = typeof configuration.tab === "string" ? configuration.tab : lang; + const tabKey = typeof configuration.tab === "string" ? formatId(configuration.tab) : lang; if (codeQueue[tabKey] || !configuration.switcher) return true; } diff --git a/packages/robindoc/src/components/ui/tabs/index.tsx b/packages/robindoc/src/components/ui/tabs/index.tsx index 77658b57..1fc04bbc 100644 --- a/packages/robindoc/src/components/ui/tabs/index.tsx +++ b/packages/robindoc/src/components/ui/tabs/index.tsx @@ -6,28 +6,29 @@ import "./tabs.scss"; export interface TabsProps { insertStyles?: boolean; - tabsData: { [tabKey: string]: JSX.Element }; + tabsData: { [tabKey: string]: { element: JSX.Element; tabName: string } }; blockKey?: string; type?: TabsHeaderProps["type"]; } export const Tabs: React.FC = ({ insertStyles, tabsData, blockKey, type }) => { - const tabs = Object.keys(tabsData); + const tabsKeys = Object.keys(tabsData); - if (tabs.length === 1) { - return tabsData[tabs[0]]; + if (tabsKeys.length === 1) { + return tabsData[tabsKeys[0]].element; } - const tabsKey = blockKey || tabs.sort().join("-"); + const tabs = tabsKeys.map((tab) => ({ name: tabsData[tab].tabName, id: tab })); + const tabsTypeId = blockKey || tabsKeys.sort().join("-"); return ( -
- {insertStyles && } - -
- {tabs.map((tabKey) => ( +
+ {insertStyles && } + +
+ {tabsKeys.map((tabKey) => (
- {tabsData[tabKey]} + {tabsData[tabKey].element}
))}
diff --git a/packages/robindoc/src/components/ui/tabs/tabs-header/index.tsx b/packages/robindoc/src/components/ui/tabs/tabs-header/index.tsx index 46378328..21837995 100644 --- a/packages/robindoc/src/components/ui/tabs/tabs-header/index.tsx +++ b/packages/robindoc/src/components/ui/tabs/tabs-header/index.tsx @@ -5,8 +5,8 @@ import clsx from "clsx"; import { saveTab } from "@src/core/utils/tabs-store"; export interface TabsHeaderProps { - tabs: string[]; - tabsKey: string; + tabs: { name: string; id: string }[]; + tabsTypeId: string; type?: "code"; } @@ -14,27 +14,27 @@ const typeClassNames = { code: "r-tab-header-code", }; -export const TabsHeader: React.FC = ({ tabs, tabsKey, type }) => { +export const TabsHeader: React.FC = ({ tabs, tabsTypeId, type }) => { const changeTabHandler = (tab: string) => { const classNames = Array.from(document.documentElement.classList); classNames.forEach((className) => { - if (className.startsWith(`r-tabs-global__${tabsKey}`)) { + if (className.startsWith(`r-tabs-global__${tabsTypeId}`)) { document.documentElement.classList.remove(className); } }); - document.documentElement.classList.add(`r-tabs-global__${tabsKey}`, `r-tabs-global__${tabsKey}_${tab}`); - saveTab(tabsKey, tab); + document.documentElement.classList.add(`r-tabs-global__${tabsTypeId}`, `r-tabs-global__${tabsTypeId}_${tab}`); + saveTab(tabsTypeId, tab); }; return (
{tabs.map((tab) => (
changeTabHandler(tab)} + key={tab.id} + className={clsx(`r-tab-header r-tab-header_${tab.id}`, type && typeClassNames[type])} + onClick={() => changeTabHandler(tab.id)} > - {tab} + {tab.name}
))}
diff --git a/packages/robindoc/src/components/ui/tabs/tabs-styles/index.tsx b/packages/robindoc/src/components/ui/tabs/tabs-styles/index.tsx index 19e67811..b5d454c6 100644 --- a/packages/robindoc/src/components/ui/tabs/tabs-styles/index.tsx +++ b/packages/robindoc/src/components/ui/tabs/tabs-styles/index.tsx @@ -1,18 +1,18 @@ import React from "react"; export interface TabsStylesProps { - tabsKey: string; - tabs: string[]; + tabsTypeId: string; + tabsKeys: string[]; } -export const TabsStyles: React.FC = ({ tabsKey, tabs }) => ( +export const TabsStyles: React.FC = ({ tabsTypeId, tabsKeys }) => (