App Organization
Robindoc can be considered to consist of several parts:
- Documentation Structure (more about documentation structure);
- Robindoc Initialization (more about initialization);
- Page Markup and Configuration (more about customization);
- Documentation Markdown Files (more about writing MD).
Organizing Documentation Files
One of the main advantages of Robindoc is that documentation files can reside in any source — whether they are files outside the current directory or a remote git repository (more about sources on the "Data Source" page).
Robindoc’s main approach is that you don’t adjust the location of markdown files for Robindoc; instead, Robindoc builds the site from your markdown files. In other words, you place files so that they can be read in GitHub, and Robindoc serves as a convenient interface.
However, when using the automatic mode for generating the structure, the documentation file structure should match the desired structure on the site. In manual mode, you can organize the documentation files as you wish, but it is still recommended to reflect the site’s structure.
Application Setup and Configuration
You can initialize Robindoc on any subpath of your site, as long as you specify the basePath
in the project initialization and pass the correct path in the Robindoc components.
After initialization, you will receive Sidebar, Page, getStaticParams, getMetadata, and getPageData. Read more on the Initialization page.
Global elements - RobinProvider
, Header
, Footer
, Containers
and Sidebar
- should ideally be placed above all pages and reused across all.
Currently, Robindoc works only with the App Router. Once RSC is available for the Pages Router, Robindoc will automatically support it as well.
Page Setup
Next.js supports dynamic routes, so it is recommended to set up one dynamic segment for all documentation pages.
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
const Page: React.FC<{ params }: { params: { segments?: string[] } }> = async ({ params }) => {
const pathname = "/docs/" + (params.segments?.join("/") || "");
return <Page pathname={pathname} />;
};
export default Page;
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
const Page = async ({ params }) => {
const pathname = "/docs/" + (params.segments?.join("/") || "");
return <Page pathname={pathname} />;
};
export default Page;
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
const Page: React.FC<{ params }: { params: Promise<{ segments?: string[] }> }> = async ({ params }) => {
const { segments } = await params;
const pathname = "/docs/" + (segments?.join("/") || "");
return <Page pathname={pathname} />;
};
export default Page;
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
const Page = async ({ params }) => {
const { segments } = await params;
const pathname = "/docs/" + (segments?.join("/") || "");
return <Page pathname={pathname} />;
};
export default Page;
For more details about the props, refer to the Page
element page.
You should also set up metadata generation and static parameters generation (if you want to use SSG, which is highly recommended):
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
// ...
export const generateMetadata = async ({
params,
}: {
params: { segments?: string[] };
}) => {
const pathname = "/docs/" + (params.segments?.join("/") || "");
const metadata = await getMetadata(pathname);
return metadata;
};
export const generateStaticParams = async () => {
const staticParams = await getStaticParams("/docs/");
return staticParams;
};
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
// ...
export const generateMetadata = async ({ params }) => {
const pathname = "/docs/" + (params.segments?.join("/") || "");
const metadata = await getMetadata(pathname);
return metadata;
};
export const generateStaticParams = async () => {
const staticParams = await getStaticParams("/docs/");
return staticParams;
};
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
// ...
export const generateMetadata = async ({
params,
}: {
params: Promise<{ segments?: string[] }>;
}) => {
const { segments } = await params;
const pathname = "/docs/" + (segments?.join("/") || "");
const metadata = await getMetadata(pathname);
return metadata;
};
export const generateStaticParams = async () => {
const staticParams = await getStaticParams("/docs/");
return staticParams;
};
import { Page, Sidebar, getMetadata, getStaticParams } from "../robindoc";
// ...
export const generateMetadata = async ({ params }) => {
const { segments } = await params;
const pathname = "/docs/" + (segments?.join("/") || "");
const metadata = await getMetadata(pathname);
return metadata;
};
export const generateStaticParams = async () => {
const staticParams = await getStaticParams("/docs/");
return staticParams;
};
Robindoc Setup
It is recommended to place the Robindoc initialization near this route.
import { initializeRobindoc } from "robindoc";
export const { Page, Sidebar, getStaticParams, getMetadata, getPageData } =
initializeRobindoc({
configuration: {
sourceRoot: "../docs",
basePath: "/docs",
gitToken: "YOUR_TOKEN",
fetcher: (url, init) =>
fetch(url, { ...init, cache: "force-cache", next: { tags: ["docs"] } }),
},
items: "auto",
});
import { initializeRobindoc } from "robindoc";
export const { Page, Sidebar, getStaticParams, getMetadata, getPageData } =
initializeRobindoc({
configuration: {
sourceRoot: "../docs",
basePath: "/docs",
gitToken: "YOUR_TOKEN",
fetcher: (url, init) =>
fetch(url, { ...init, cache: "force-cache", next: { tags: ["docs"] } }),
},
items: "auto",
});
Layout Setup
The Next.js Layout should be placed one level up so that it remains static for all pages.
import { RobinProvider, Header, Footer, DocsContainer } from "robindoc";
import { Sidebar } from "./robindoc";
import Logo from "./logo";
import "robindoc/lib/styles.css";
const Layout: React.FC<React.PropsWithChildren> = ({ children }) => (
<RobinProvider>
<Header logo={<Logo />} />
<DocsContainer>
<Sidebar />
{children}
</DocsContainer>
<Footer copyright="© 2024 All rights reserved" />
</RobinProvider>
);
export default Layout;
import { RobinProvider, Header, Footer, DocsContainer } from "robindoc";
import { Sidebar } from "./robindoc";
import Logo from "./logo";
import "robindoc/lib/styles.css";
const Layout = ({ children }) => (
<RobinProvider>
<Header logo={<Logo />} />
<DocsContainer>
<Sidebar />
{children}
</DocsContainer>
<Footer copyright="© 2024 All rights reserved" />
</RobinProvider>
);
export default Layout;
For more details on configuring elements, refer to the RobinProvider
, Header
, Sidebar
, Footer
, and Containers
block pages.
Search Setup
If you want to enable search, you can create your own API route and pass the path to it in your Header.
export const GET = async (request: Request) => {
const url = new URL(request.url);
const search = url.searchParams.get("s");
const headers = new Headers();
headers.set("Content-Type", "application/json; charset=UTF-8");
if (!search) return new Response(JSON.stringify([]), { headers });
const searchResults = await advancedSearcher(search);
return new Response(JSON.stringify(searchResults), { headers });
};
export const GET = async (request) => {
const url = new URL(request.url);
const search = url.searchParams.get("s");
const headers = new Headers();
headers.set("Content-Type", "application/json; charset=UTF-8");
if (!search) return new Response(JSON.stringify([]), { headers });
const searchResults = await advancedSearcher(search);
return new Response(JSON.stringify(searchResults), { headers });
};
const Layout: React.FC<React.PropsWithChildren> = ({ children }) => (
<RobinProvider>
<Header logo={<Logo />} searcher="/api/search" />
{/* ... */}
</RobinProvider>
);
export default Layout;
const Layout = ({ children }) => (
<RobinProvider>
<Header logo={<Logo />} searcher="/api/search" />
{/* ... */}
</RobinProvider>
);
export default Layout;
Vercel
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.
/** @type {import("next").NextConfig} */
const nextConfig = {
experimental: {
outputFileTracingIncludes: {
"/api/search": ["./docs/**/*", "./blog/**/*", "./README.md"],
},
},
};
/** @type {import("next").NextConfig} */
const nextConfig = {
experimental: {
outputFileTracingIncludes: {
"/api/search": ["./docs/**/*", "./blog/**/*", "./README.md"],
},
},
};
import type { NextConfig } from "next";
const nextConfig = {
outputFileTracingIncludes: {
"/api/search": ["./docs/**/*", "./blog/**/*", "./README.md"],
},
};
/** @type {import("next").NextConfig} */
const nextConfig: NextConfig = {
outputFileTracingIncludes: {
"/api/search": ["./docs/**/*", "./blog/**/*", "./README.md"],
},
};
For more details on search configuration, refer to the Search page.
Sitemap Setup
To generate a sitemap in next.js, you can use a special sitemap file in combination with getStaticParams tool:
import { type MetadataRoute } from "next";
import { getStaticParams } from "./docs/robindoc";
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
const staticParams = await getStaticParams();
return staticParams.map(({ segments }) => ({
url: `https://robindoc.com${segments.join("/")}`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.7,
}));
};
export default sitemap;
import { type MetadataRoute } from "next";
import { getStaticParams } from "./docs/robindoc";
const sitemap = async () => {
const staticParams = await getStaticParams();
return staticParams.map(({ segments }) => ({
url: `https://robindoc.com/${segments.join('/')}`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.7,
}));
};
export default sitemap;