Heimerle + Meule Logo
Heimerle + Meule

Docs Setup

Set up the shared ECOS documentation experience in a new monorepo application.

This guide explains how to add the shared @ecos/docs package to a Next.js application in the ECOS monorepo. The package provides the documentation layout, page components, and shared MDX components. Each app provides its own routes and connects them to the shared content in packages/docs/content.

Add the dependencies

Add @ecos/docs as a workspace dependency. The app also needs Fumadocs to configure MDX and load the documentation source, and lucide-react if you want to resolve icons declared in MDX frontmatter:

1{
2  "dependencies": {
3    "@ecos/docs": "workspace:*",
4    "fumadocs-core": "^16.15.16",
5    "fumadocs-mdx": "^15.4.5",
6    "lucide-react": "^0.460.0"
7  }
8}

Keep Fumadocs versions aligned with the other apps in the monorepo. The app should already have the Next.js, React, and @ecos/ui dependencies used by the shared package.

Configure Next.js for MDX and workspace packages

Wrap the app's existing Next.js configuration with the Fumadocs MDX plugin:

1import { createMDX } from "fumadocs-mdx/next";
2import type { NextConfig } from "next";
3
4const nextConfig: NextConfig = {
5transpilePackages: [
6  "@ecos/docs",
7  "@ecos/ui",
8  "@ecos/org-config",
9  "@ecos/tenant",
10],
11};
12
13const withMDX = createMDX();
14export default withMDX(nextConfig);
15

Merge these settings into the app's current next.config.ts; do not replace existing configuration or plugin wrappers. If the app already uses plugins such as next-intl, keep them in the existing wrapper chain and include the Fumadocs wrapper as in the other apps.

Create the documentation source

Add src/lib/fumadocs.ts to the app. Point defineDocs at the shared content directory and set the docs URL base path:

1import { defineDocs } from "fumadocs-mdx/macro";
2import { loader } from "fumadocs-core/source";
3import { createElement } from "react";
4import { icons } from "lucide-react";
5
6const docs = defineDocs({
7  dir: "../../packages/docs/content",
8});
9
10export const source = loader({
11  baseUrl: "/docs",
12  source: docs.toFumadocsSource(),
13  icon(icon) {
14    if (!icon) return;
15    if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
16  },
17});
18

The dir is relative to the app's root. Keep it pointed at the monorepo's shared content directory unless the app has a deliberate reason to use a separate content source.

Add the docs routes

Create a docs layout at src/app/docs/layout.tsx (or under the app's existing locale or route-group segment). Pass the source page tree and the app's organization ID to DocsLayout:

1import { DocsLayout } from "@ecos/docs";
2import type { ReactNode } from "react";
3import { source } from "@/lib/fumadocs";
4import { ORG_ID_HEIMERLE_MEULE_WEBSITE } from "@ecos/org-config";
5
6export default function Layout({
7  children,
8}: Readonly<{ children: ReactNode }>) {
9  return (
10    <DocsLayout
11      orgId={ORG_ID_HEIMERLE_MEULE_WEBSITE}
12      pageTree={source.pageTree}
13    >
14      {children}
15    </DocsLayout>
16  );
17}
18

Replace the example organization constant with the ID for the new app. orgId must be a supported OrgId from @ecos/org-config; register the app's organization there first if it does not exist yet.

Add the optional catch-all page at src/app/docs/[[...slug]]/page.tsx to render the selected MDX document:

1import { notFound } from "next/navigation";
2import { DocPage, mdxComponents } from "@ecos/docs";
3import { source } from "@/lib/fumadocs";
4
5export default async function Page(
6  props: Readonly<{ params: Promise<{ slug?: string[] }> }>,
7) {
8  const { slug } = await props.params;
9  const page = source.getPage(slug);
10
11  if (!page) notFound();
12
13  const Mdx = page.data.body;
14
15  return (
16    <DocPage
17      heading={page.data.title}
18      description={page.data.description}
19      toc={page.data.toc}
20    >
21      <Mdx components={mdxComponents} />
22    </DocPage>
23  );
24}
25

If the app localizes routes, put both route files below its locale segment, such as src/app/[locale]/docs/. Adjust imports or aliases to match the app's TypeScript configuration. A separate not-found.tsx can render a not-found state inside the docs layout if desired.

Include shared package styles

The docs components use Tailwind classes from workspace packages. In the app's global stylesheet, make sure Tailwind scans the shared package sources. For example, when the stylesheet is at src/app/globals.css:

1@import "tailwindcss";
2
3@source "../../../../packages/**/*.{ts,tsx,js,jsx,mdx}";
4

Keep the app's existing theme variables and other @source entries. The shared docs layout uses the ECOS UI theme tokens and components, so the app should load its normal global theme and UI styles as well.

Adding a Documentation Page

Create an .mdx file under packages/docs/content. Organize pages into folders that reflect their section and URL. For example:

packages/docs/content/
└── design-system/
    ├── index.mdx
    └── colours.mdx

Add frontmatter with at least a title and description to each page. Its folder path determines the docs URL, so design-system/colours.mdx is available at /docs/design-system/colours.

For a folder that groups multiple pages, create an index.mdx in that folder. Use <DocumentationGrid /> on the index page to display links to its child pages, as in the developers and design system section pages.

When an MDX page needs a component that is not already available, add it to the mapping in packages/docs/src/components/docs-mdx-components.tsx. Import the component, then add its name and JSX mapping to mdxComponents. Components from @ecos/ui should be imported from that package; shared documentation components should be imported from their source in packages/docs/src.