Skip to content

Commit

Permalink
Merge pull request #363 from vordgi/rd-361
Browse files Browse the repository at this point in the history
rd-361 updates for nextjs v15
  • Loading branch information
vordgi authored Oct 22, 2024
2 parents deea136 + a3351cb commit 33a5a7c
Show file tree
Hide file tree
Showing 13 changed files with 514 additions and 127 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down
117 changes: 112 additions & 5 deletions docs/01-getting-started/04-app-organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Page pathname={pathname} />;
Expand All @@ -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 <Page pathname={pathname} />;
};

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 <Page pathname={pathname} />;
};

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 <Page pathname={pathname} />;
};

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";

// ...
Expand All @@ -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.
Expand Down Expand Up @@ -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: {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/robindoc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "robindoc",
"version": "2.2.1",
"version": "2.3.0",
"description": "",
"main": "./lib/index.js",
"scripts": {
Expand Down
17 changes: 11 additions & 6 deletions packages/robindoc/src/components/elements/article/document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { TaskListItem, TaskOrderedList, TaskUnorderedList } from "@src/component

import { type PagesType } from "./types";
import {
formatId,
formatLinkHref,
isNewCodeToken,
parseCodeLang,
Expand Down Expand Up @@ -110,7 +111,7 @@ export const Document: React.FC<ContentProps> = ({
| 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;
Expand Down Expand Up @@ -157,8 +158,9 @@ export const Document: React.FC<ContentProps> = ({
}
}

if (Array.isArray(token))
return token.map((t, index) => <DocumentToken token={t} key={(t as Tokens.Text).raw || index} />);
if (Array.isArray(token)) {
return token.map((t, index) => <DocumentToken token={t} key={(t as Tokens.Text).raw + index} />);
}

switch (token.type) {
case "heading":
Expand Down Expand Up @@ -249,9 +251,12 @@ export const Document: React.FC<ContentProps> = ({
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] = <CodeSection lang={lang as any} code={token.text} {...configuration} />;
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: <CodeSection lang={lang as any} code={token.text} {...configuration} />,
};
return null;
}

Expand Down
11 changes: 9 additions & 2 deletions packages/robindoc/src/components/elements/article/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand All @@ -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;
}

Expand Down
23 changes: 12 additions & 11 deletions packages/robindoc/src/components/ui/tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TabsProps> = ({ 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 (
<div className={`r-tabs r-tabs__${tabsKey}`}>
{insertStyles && <TabsStyles tabs={tabs} tabsKey={tabsKey} />}
<TabsHeader tabs={tabs} tabsKey={tabsKey} type={type} />
<div className={`r-tab-list r-tab-list__${tabsKey}`}>
{tabs.map((tabKey) => (
<div className={`r-tabs r-tabs__${tabsTypeId}`}>
{insertStyles && <TabsStyles tabsKeys={tabsKeys} tabsTypeId={tabsTypeId} />}
<TabsHeader tabs={tabs} tabsTypeId={tabsTypeId} type={type} />
<div className={`r-tab-list r-tab-list__${tabsTypeId}`}>
{tabsKeys.map((tabKey) => (
<div key={tabKey} className={`r-tab r-tab_${tabKey}`}>
{tabsData[tabKey]}
{tabsData[tabKey].element}
</div>
))}
</div>
Expand Down
20 changes: 10 additions & 10 deletions packages/robindoc/src/components/ui/tabs/tabs-header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,36 @@ 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";
}

const typeClassNames = {
code: "r-tab-header-code",
};

export const TabsHeader: React.FC<TabsHeaderProps> = ({ tabs, tabsKey, type }) => {
export const TabsHeader: React.FC<TabsHeaderProps> = ({ 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 (
<div className="r-tabs-header">
{tabs.map((tab) => (
<div
key={tab}
className={clsx(`r-tab-header r-tab-header_${tab}`, type && typeClassNames[type])}
onClick={() => 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}
</div>
))}
</div>
Expand Down
14 changes: 7 additions & 7 deletions packages/robindoc/src/components/ui/tabs/tabs-styles/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from "react";

export interface TabsStylesProps {
tabsKey: string;
tabs: string[];
tabsTypeId: string;
tabsKeys: string[];
}

export const TabsStyles: React.FC<TabsStylesProps> = ({ tabsKey, tabs }) => (
export const TabsStyles: React.FC<TabsStylesProps> = ({ tabsTypeId, tabsKeys }) => (
<style
dangerouslySetInnerHTML={{
__html: `
html:not(.r-tabs-global__${tabsKey}) .r-tabs__${tabsKey} .r-tab:not(.r-tab_${tabs[0]}) {display: none}
html:not(.r-tabs-global__${tabsKey}) .r-tabs__${tabsKey} .r-tab-header_${tabs[0]} {background: var(--neutral50);z-index: 2;pointer-events: none;color:var(--neutral950)}
${tabs.map((lang) => `.r-tabs-global__${tabsKey}_${lang} .r-tabs__${tabsKey} .r-tab:not(.r-tab_${lang}) {display: none}`).join("")}
${tabs.map((lang) => `.r-tabs-global__${tabsKey}_${lang} .r-tabs__${tabsKey} .r-tab-header_${lang} {background: var(--neutral50);z-index: 2;pointer-events: none;color:var(--neutral950)}`).join("")}
html:not(.r-tabs-global__${tabsTypeId}) .r-tabs__${tabsTypeId} .r-tab:not(.r-tab_${tabsKeys[0]}) {display: none}
html:not(.r-tabs-global__${tabsTypeId}) .r-tabs__${tabsTypeId} .r-tab-header_${tabsKeys[0]} {background: var(--neutral50);z-index: 2;pointer-events: none;color:var(--neutral950)}
${tabsKeys.map((tabKey) => `.r-tabs-global__${tabsTypeId}_${tabKey} .r-tabs__${tabsTypeId} .r-tab:not(.r-tab_${tabKey}) {display: none}`).join("")}
${tabsKeys.map((tabKey) => `.r-tabs-global__${tabsTypeId}_${tabKey} .r-tabs__${tabsTypeId} .r-tab-header_${tabKey} {background: var(--neutral50);z-index: 2;pointer-events: none;color:var(--neutral950)}`).join("")}
`,
}}
/>
Expand Down
8 changes: 3 additions & 5 deletions site/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
outputFileTracingIncludes: {
'/api/search': ['../docs/**/*', '../README.md'],
},
}
outputFileTracingIncludes: {
'/api/search': ['../docs/**/*', '../README.md'],
},
};

export default nextConfig;
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"@vercel/analytics": "1.3.1",
"match-sorter": "6.3.4",
"next": "14.2.14",
"next": "15.0.0",
"react": "^18",
"react-dom": "^18",
"robindoc": "link:..\\packages\\robindoc",
Expand Down
Loading

0 comments on commit 33a5a7c

Please sign in to comment.