feat: docs v2 (#2460)

Co-authored-by: Catalin Pit <catalinpit@gmail.com>
This commit is contained in:
Lucas Smith
2026-02-27 22:05:27 +11:00
committed by GitHub
parent f8ac782f2e
commit b92c53dbb2
290 changed files with 32521 additions and 266 deletions
+6
View File
@@ -0,0 +1,6 @@
import { baseOptions } from '@/lib/layout.shared';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
+271
View File
@@ -0,0 +1,271 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import {
BookOpenIcon,
CodeIcon,
FileTextIcon,
GithubIcon,
ServerIcon,
ShieldCheckIcon,
UserIcon,
} from 'lucide-react';
export const metadata: Metadata = {
title: 'Documenso Docs',
description:
'The official documentation for Documenso, the open-source document signing platform. Send documents for signatures, integrate with the API, or self-host with full control.',
};
export default function HomePage() {
return (
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Hero */}
<div className="mb-16 pt-6 text-center">
<h1 className="mb-4 text-4xl font-bold tracking-tight">Documenso Documentation</h1>
<p className="text-fd-muted-foreground mx-auto mb-8 max-w-2xl text-lg">
The open-source document signing platform. Send documents for signatures, integrate with
your apps, or self-host with full control.
</p>
<div className="flex flex-wrap justify-center gap-3">
<Link
href="/docs/users"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso-dark/90 inline-flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
>
Get Started
</Link>
<a
href="https://github.com/documenso/documenso"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
>
<GithubIcon className="size-4" />
View on GitHub
</a>
</div>
</div>
{/* Main Guide Cards */}
<div className="mb-16 grid gap-4 md:grid-cols-3">
<Link
href="/docs/users"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<UserIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">User Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
Send documents, create templates, and manage your team using the web application.
</p>
<span className="text-fd-primary text-sm font-medium">Get started </span>
</Link>
<Link
href="/docs/developers"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400">
<CodeIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">Developer Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
Integrate document signing into your applications with the REST API, webhooks, and
embedding.
</p>
<span className="text-fd-primary text-sm font-medium">View API docs </span>
</Link>
<Link
href="/docs/self-hosting"
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
>
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-purple-500/10 text-purple-600 dark:text-purple-400">
<ServerIcon className="size-6" />
</div>
<h2 className="mb-2 text-lg font-semibold">Self-Hosting Guide</h2>
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
Deploy your own Documenso instance with Docker, Kubernetes, or Railway.
</p>
<span className="text-fd-primary text-sm font-medium">Deploy now </span>
</Link>
</div>
{/* Quick Start & Core Concepts */}
<div className="mb-16 grid gap-8 md:grid-cols-2">
<div className="bg-fd-card/50 rounded-xl border p-6">
<h3 className="mb-4 flex items-center gap-2 font-semibold">
<BookOpenIcon className="text-fd-muted-foreground size-5" />
Quick Start
</h3>
<div className="space-y-4">
<div>
<h4 className="mb-2 text-sm font-medium">Send your first document</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<li>
<Link
href="/docs/users/getting-started/create-account"
className="text-fd-primary hover:underline"
>
Create an account
</Link>
</li>
<li>
<Link
href="/docs/users/getting-started/send-first-document"
className="text-fd-primary hover:underline"
>
Upload and send a document
</Link>
</li>
</ol>
</div>
<div>
<h4 className="mb-2 text-sm font-medium">Integrate with the API</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<li>
<Link
href="/docs/developers/getting-started/authentication"
className="text-fd-primary hover:underline"
>
Get your API key
</Link>
</li>
<li>
<Link
href="/docs/developers/getting-started/first-api-call"
className="text-fd-primary hover:underline"
>
Make your first API call
</Link>
</li>
</ol>
</div>
<div>
<h4 className="mb-2 text-sm font-medium">Deploy self-hosted</h4>
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
<li>
<Link
href="/docs/self-hosting/getting-started/requirements"
className="text-fd-primary hover:underline"
>
Check requirements
</Link>
</li>
<li>
<Link
href="/docs/self-hosting/getting-started/quick-start"
className="text-fd-primary hover:underline"
>
Run with Docker
</Link>
</li>
</ol>
</div>
</div>
</div>
<div className="bg-fd-card/50 rounded-xl border p-6">
<h3 className="mb-4 flex items-center gap-2 font-semibold">
<BookOpenIcon className="text-fd-muted-foreground size-5" />
Core Concepts
</h3>
<div className="grid grid-cols-2 gap-3">
<Link
href="/docs/concepts/document-lifecycle"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
>
<div className="mb-1 font-medium">Document Lifecycle</div>
<div className="text-fd-muted-foreground text-xs">Draft to completed</div>
</Link>
<Link
href="/docs/concepts/recipient-roles"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
>
<div className="mb-1 font-medium">Recipient Roles</div>
<div className="text-fd-muted-foreground text-xs">Signers and approvers</div>
</Link>
<Link
href="/docs/concepts/field-types"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
>
<div className="mb-1 font-medium">Field Types</div>
<div className="text-fd-muted-foreground text-xs">Signatures and inputs</div>
</Link>
<Link
href="/docs/concepts/signing-certificates"
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
>
<div className="mb-1 font-medium">Signing Certificates</div>
<div className="text-fd-muted-foreground text-xs">Digital verification</div>
</Link>
</div>
</div>
</div>
{/* Compliance & Policies */}
<div className="mb-16 grid gap-4 md:grid-cols-2">
<Link
href="/docs/compliance"
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400">
<ShieldCheckIcon className="size-5" />
</div>
<div>
<h3 className="mb-1 font-semibold">Compliance & Legal</h3>
<p className="text-fd-muted-foreground text-sm">
ESIGN, UETA, eIDAS compliance, GDPR, and signature levels explained.
</p>
</div>
</Link>
<Link
href="/docs/policies"
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-slate-500/10 text-slate-600 dark:text-slate-400">
<FileTextIcon className="size-5" />
</div>
<div>
<h3 className="mb-1 font-semibold">Policies & Licensing</h3>
<p className="text-fd-muted-foreground text-sm">
AGPL and Enterprise licenses, fair use, privacy policy, and support.
</p>
</div>
</Link>
</div>
{/* Community CTA */}
<div className="from-fd-primary/5 to-fd-primary/10 rounded-xl border bg-gradient-to-r p-8 text-center">
<h3 className="mb-2 text-lg font-semibold">Join the Community</h3>
<p className="text-fd-muted-foreground mb-6 text-sm">
Documenso is open source. Contribute, ask questions, or share feedback.
</p>
<div className="flex flex-wrap justify-center gap-3">
<a
href="https://github.com/documenso/documenso"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
>
<GithubIcon className="size-4" />
GitHub
</a>
<a
href="https://documen.so/discord"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
>
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
Discord
</a>
<a
href="https://app.documenso.com/signup"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
>
Try Documenso
</a>
</div>
</div>
</main>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,72 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
import { getPageImage, source } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
const gitConfig = {
user: 'documenso',
repo: 'documenso',
branch: 'main',
contentPath: 'apps/docs/content/docs',
};
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) {
notFound();
}
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
{!page.data.full && (
<>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row items-center gap-2 border-b pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${gitConfig.contentPath}/${page.path}`}
/>
</div>
</>
)}
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}
export function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) {
notFound();
}
const ogImage = getPageImage(page).url;
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: ogImage,
},
twitter: {
images: ogImage,
},
};
}
+119
View File
@@ -0,0 +1,119 @@
'use client';
import { useMemo } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/cn';
import { baseOptions } from '@/lib/layout.shared';
import { getFilteredPageTree, source } from '@/lib/source';
import type * as PageTree from 'fumadocs-core/page-tree';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { CodeIcon, ServerIcon, UserIcon } from 'lucide-react';
const ROOT_SECTIONS = [
{
id: 'users',
label: 'Users',
subtitle: 'Send and sign documents',
icon: UserIcon,
href: '/docs/users',
},
{
id: 'developers',
label: 'Developers',
subtitle: 'API and integrations',
icon: CodeIcon,
href: '/docs/developers',
},
{
id: 'self-hosting',
label: 'Self-Hosting',
subtitle: 'Deploy your own instance',
icon: ServerIcon,
href: '/docs/self-hosting',
},
];
// Find first page item in folder children
function getFirstPageUrl(children: PageTree.Node[]): string | undefined {
for (const child of children) {
if (child.type === 'page') {
return child.url;
}
if (child.type === 'folder' && child.children.length > 0) {
const url = getFirstPageUrl(child.children);
if (url) return url;
}
}
return undefined;
}
function SectionSwitcher({ activeSection }: { activeSection: string | null }) {
return (
<div className="flex flex-col gap-1">
{ROOT_SECTIONS.map((section) => {
const isActive = activeSection === section.id;
const Icon = section.icon;
return (
<Link
key={section.id}
href={section.href}
className={cn(
'flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors',
isActive
? 'bg-fd-primary/10 text-fd-primary'
: 'text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
)}
>
<Icon className={cn('mt-0.5 size-4 shrink-0', isActive ? 'text-fd-primary' : '')} />
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{section.label}</span>
<span
className={cn(
'text-xs',
isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70',
)}
>
{section.subtitle}
</span>
</div>
</Link>
);
})}
</div>
);
}
export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { tree, activeSection } = useMemo(() => {
// Check if we're in a root section
const pathParts = pathname.split('/').filter(Boolean);
// pathParts = ['docs', 'users', 'getting-started', ...]
if (pathParts.length >= 2) {
const section = pathParts[1]; // 'users', 'developers', or 'self-hosting'
if (ROOT_SECTIONS.some((s) => s.id === section)) {
return { tree: getFilteredPageTree(section), activeSection: section };
}
}
// Default: show full tree
return { tree: source.getPageTree(), activeSection: null };
}, [pathname]);
return (
<DocsLayout
tree={tree}
{...baseOptions()}
sidebar={{
banner: <SectionSwitcher activeSection={activeSection} />,
}}
>
{children}
</DocsLayout>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+181
View File
@@ -0,0 +1,181 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/shadcn.css';
@import 'fumadocs-ui/css/preset.css';
@theme {
/* Brand utility colors */
--color-documenso: hsl(95.08, 71.08%, 67.45%);
--color-documenso-dark: hsl(95.08, 71.08%, 54%);
--color-documenso-darker: hsl(95.08, 71.08%, 41%);
}
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2222 47.3684% 11.1765%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2222 47.3684% 11.1765%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2222 47.3684% 11.1765%);
--primary: hsl(95.0847 71.0843% 42.451%);
--primary-foreground: hsl(95.6757 72.549% 10%);
--secondary: hsl(210 40% 96.0784%);
--secondary-foreground: hsl(222.2222 47.3684% 11.1765%);
--muted: hsl(210 40% 96.0784%);
--muted-foreground: hsl(215.3846 16.318% 46.8627%);
--accent: hsl(210 40% 96.0784%);
--accent-foreground: hsl(222.2222 47.3684% 11.1765%);
--destructive: hsl(0 100% 50%);
--destructive-foreground: hsl(210 40% 98.0392%);
--border: hsl(214.2857 31.8182% 91.3725%);
--input: hsl(214.2857 31.8182% 91.3725%);
--ring: hsl(95.0847 71.0843% 67.451%);
--chart-1: hsl(211.788 101.9718% 78.6759%);
--chart-2: hsl(217.4076 91.3672% 59.5787%);
--chart-3: hsl(221.4336 86.3731% 54.0624%);
--chart-4: hsl(223.6587 78.718% 47.8635%);
--chart-5: hsl(226.5426 70.0108% 39.9224%);
--sidebar: hsl(223.8136 0.0004% 98.0256%);
--sidebar-foreground: hsl(223.8136 0% 3.9388%);
--sidebar-primary: hsl(223.8136 0% 9.0527%);
--sidebar-primary-foreground: hsl(223.8136 0.0004% 98.0256%);
--sidebar-accent: hsl(223.8136 0.0002% 96.0587%);
--sidebar-accent-foreground: hsl(223.8136 0% 9.0527%);
--sidebar-border: hsl(223.8136 0.0001% 89.8161%);
--sidebar-ring: hsl(223.8136 0% 63.0163%);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--radius: 0.5rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: hsl(0 0% 14.902%);
--foreground: hsl(0 0% 96.8627%);
--card: hsl(0 0% 14.902%);
--card-foreground: hsl(0 0% 94.902%);
--popover: hsl(0 0% 14.902%);
--popover-foreground: hsl(0 0% 90.1961%);
--primary: hsl(95.0847 71.0843% 67.451%);
--primary-foreground: hsl(95.6757 72.549% 10%);
--secondary: hsl(0 0% 23.5294%);
--secondary-foreground: hsl(95.0847 71.0843% 67.451%);
--muted: hsl(0 0% 23.5294%);
--muted-foreground: hsl(0 0% 85.098%);
--accent: hsl(0 0% 27.8431%);
--accent-foreground: hsl(95.0847 71.0843% 67.451%);
--destructive: hsl(0 86.5979% 61.9608%);
--destructive-foreground: hsl(0 87.6289% 19.0196%);
--border: hsl(0 0% 27.8431%);
--input: hsl(0 0% 27.8431%);
--ring: hsl(95.0847 71.0843% 67.451%);
--chart-1: hsl(211.788 101.9718% 78.6759%);
--chart-2: hsl(217.4076 91.3672% 59.5787%);
--chart-3: hsl(221.4336 86.3731% 54.0624%);
--chart-4: hsl(223.6587 78.718% 47.8635%);
--chart-5: hsl(226.5426 70.0108% 39.9224%);
--sidebar: hsl(223.8136 0% 9.0527%);
--sidebar-foreground: hsl(223.8136 0.0004% 98.0256%);
--sidebar-primary: hsl(225.3451 84.0953% 48.9841%);
--sidebar-primary-foreground: hsl(223.8136 0.0004% 98.0256%);
--sidebar-accent: hsl(223.8136 0% 14.9382%);
--sidebar-accent-foreground: hsl(223.8136 0.0004% 98.0256%);
--sidebar-border: hsl(223.8136 0% 15.5096%);
--sidebar-ring: hsl(223.8136 0% 32.1993%);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--radius: 0.5rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { RootProvider } from 'fumadocs-ui/provider/next';
import PlausibleProvider from 'next-plausible';
import './global.css';
const inter = Inter({
subsets: ['latin'],
});
export const metadata: Metadata = {
metadataBase: new URL('https://docs.documenso.com'),
title: {
template: '%s | Documenso Docs',
default: 'Documenso Docs',
},
description:
'The official documentation for Documenso, the open-source document signing platform.',
openGraph: {
siteName: 'Documenso Docs',
type: 'website',
},
twitter: {
card: 'summary_large_image',
site: '@documenso',
},
};
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<PlausibleProvider domain="documenso.com">
<RootProvider>{children}</RootProvider>
</PlausibleProvider>
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}
@@ -0,0 +1,24 @@
import { notFound } from 'next/navigation';
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) {
notFound();
}
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return source.generateParams();
}
+16
View File
@@ -0,0 +1,16 @@
import { source } from '@/lib/source';
export const revalidate = false;
export function GET() {
const lines: string[] = [];
lines.push('# Documentation');
lines.push('');
for (const page of source.getPages()) {
lines.push(`- [${page.data.title}](${page.url}): ${page.data.description}`);
}
return new Response(lines.join('\n'));
}
+26
View File
@@ -0,0 +1,26 @@
import Link from 'next/link';
export default function NotFound() {
return (
<main className="mx-auto flex max-w-xl flex-col items-center justify-center px-4 py-32 text-center">
<h1 className="text-4xl font-bold tracking-tight">Page not found</h1>
<p className="text-fd-muted-foreground mt-4 text-lg">
The page you are looking for may have moved. Our documentation was recently restructured.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<Link
href="/docs/users"
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
>
Browse documentation
</Link>
<Link
href="/"
className="bg-fd-background hover:bg-fd-accent inline-flex items-center rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
>
Go to homepage
</Link>
</div>
</main>
);
}
@@ -0,0 +1,154 @@
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { getPageImage, source } from '@/lib/source';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
export const revalidate = false;
const loadAssets = async () => {
const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([
readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))),
readFile(
fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url)),
),
readFile(
fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url)),
),
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-bold.ttf', import.meta.url))),
]);
return {
logoSrc: `data:image/png;base64,${logoBuffer.toString('base64')}`,
fonts: [
{ name: 'Inter', data: interRegularData, weight: 400 as const, style: 'normal' as const },
{ name: 'Inter', data: interSemiBoldData, weight: 600 as const, style: 'normal' as const },
{ name: 'Inter', data: interBoldData, weight: 700 as const, style: 'normal' as const },
],
};
};
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) {
notFound();
}
const { logoSrc, fonts } = await loadAssets();
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
backgroundColor: 'white',
padding: '60px 80px',
fontFamily: 'Inter',
position: 'relative',
}}
>
{/* Green accent bar */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '6px',
backgroundColor: '#6DC947',
}}
/>
{/* Top: Logo */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logoSrc} alt="Documenso" height="28" />
<span
style={{
color: '#D4D4D8',
fontSize: '28px',
fontWeight: 400,
}}
>
|
</span>
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
</div>
{/* Middle: Title + description */}
<div
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
justifyContent: 'center',
gap: '16px',
}}
>
<h1
style={{
color: '#18181B',
fontSize: page.data.title.length > 40 ? '48px' : '56px',
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: '-0.025em',
margin: 0,
}}
>
{page.data.title}
</h1>
{page.data.description && (
<p
style={{
color: '#71717A',
fontSize: '22px',
fontWeight: 400,
lineHeight: 1.4,
margin: 0,
maxWidth: '900px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{page.data.description}
</p>
)}
</div>
{/* Bottom: URL */}
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>
docs.documenso.com{page.url}
</span>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts,
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}
+12
View File
@@ -0,0 +1,12 @@
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
host: 'https://docs.documenso.com',
sitemap: 'https://docs.documenso.com/sitemap.xml',
};
}
+20
View File
@@ -0,0 +1,20 @@
import type { MetadataRoute } from 'next';
import { source } from '@/lib/source';
const BASE_URL = 'https://docs.documenso.com';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
changeFrequency: 'weekly',
priority: 1.0,
},
...source.getPages().map((page) => ({
url: `${BASE_URL}${page.url}`,
changeFrequency: 'weekly' as const,
priority: 0.7,
})),
];
}
@@ -0,0 +1,168 @@
'use client';
import { useMemo, useState } from 'react';
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
const cache = new Map<string, string>();
export function LLMCopyButton({
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl,
}: {
markdownUrl: string;
}) {
const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached);
setLoading(true);
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text();
cache.set(markdownUrl, content);
return content;
}),
}),
]);
} finally {
setLoading(false);
}
});
return (
<button
disabled={isLoading}
className={cn(
buttonVariants({
color: 'secondary',
size: 'sm',
className: '[&_svg]:text-fd-muted-foreground gap-2 [&_svg]:size-3.5',
}),
)}
onClick={onClick}
>
{checked ? <Check /> : <Copy />}
Copy Markdown
</button>
);
}
export function ViewOptions({
markdownUrl,
githubUrl,
}: {
/**
* A URL to the raw Markdown/MDX content of page
*/
markdownUrl: string;
/**
* Source file URL on GitHub
*/
githubUrl: string;
}) {
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [
{
title: 'Open in GitHub',
href: githubUrl,
icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
},
{
title: 'Open in ChatGPT',
href: `https://chatgpt.com/?${new URLSearchParams({
hints: 'search',
q,
})}`,
icon: (
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
},
{
title: 'Open in Claude',
href: `https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic</title>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
),
},
{
title: 'Open in T3 Chat',
href: `https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: <MessageCircleIcon />,
},
];
}, [githubUrl, markdownUrl]);
return (
<Popover>
<PopoverTrigger
className={cn(
buttonVariants({
color: 'secondary',
size: 'sm',
className: 'gap-2',
}),
)}
>
Open
<ChevronDown className="text-fd-muted-foreground size-3.5" />
</PopoverTrigger>
<PopoverContent className="flex flex-col">
{items.map((item) => (
<a
key={item.href}
href={item.href}
rel="noreferrer noopener"
target="_blank"
className="hover:text-fd-accent-foreground hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg p-2 text-sm [&_svg]:size-4"
>
{item.icon}
{item.title}
<ExternalLinkIcon className="text-fd-muted-foreground ms-auto size-3.5" />
</a>
))}
</PopoverContent>
</Popover>
);
}
+65
View File
@@ -0,0 +1,65 @@
'use client';
import { use, useEffect, useId, useState } from 'react';
import { useTheme } from 'next-themes';
export const Mermaid = ({ chart }: { chart: string }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return <MermaidContent chart={chart} />;
};
const cache = new Map<string, Promise<unknown>>();
const cachePromise = async <T,>(key: string, setPromise: () => Promise<T>): Promise<T> => {
const cached = cache.get(key);
if (cached) {
return cached as Promise<T>;
}
const promise = setPromise();
cache.set(key, promise);
return promise;
};
const MermaidContent = ({ chart }: { chart: string }) => {
const id = useId();
const { resolvedTheme } = useTheme();
const { default: mermaid } = use(cachePromise('mermaid', async () => import('mermaid')));
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
fontFamily: 'inherit',
themeCSS: 'margin: 1.5rem auto 0;',
theme: resolvedTheme === 'dark' ? 'dark' : 'default',
});
const { svg, bindFunctions } = use(
cachePromise(`${chart}-${resolvedTheme}`, async () => {
return mermaid.render(id, chart.replaceAll('\\n', '\n'));
}),
);
return (
<div
ref={(container) => {
if (container) {
bindFunctions?.(container);
}
}}
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
};
+1
View File
@@ -0,0 +1 @@
export { twMerge as cn } from 'tailwind-merge';
+23
View File
@@ -0,0 +1,23 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: (
<span className="flex items-center gap-2 font-semibold">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84 84" className="size-6">
<g fill="currentColor">
<path d="M35.53 12.152c-.968.879-2.038 1.91-3.261 3.118a4.55 4.55 0 0 1-2.722.97l-4.098.079 1.194-1.194C33.883 7.885 37.502 4.265 42 4.265s8.118 3.62 15.357 10.86l1.192 1.192-3.957-.075a4.55 4.55 0 0 1-3.004-1.209l-2.373-2.194a69 69 0 0 0-.66-.61l-.128-.119h-.002a35 35 0 0 0-2.244-1.892C44.17 8.684 43 8.338 42 8.338s-2.17.346-4.18 1.88a35 35 0 0 0-2.275 1.92zM71.77 35.444a69 69 0 0 0-.608-.658l-2.196-2.374a4.55 4.55 0 0 1-1.208-3.002l-.077-3.961 1.194 1.194c7.24 7.24 10.86 10.859 10.86 15.357s-3.62 8.118-10.86 15.357l-1.194 1.194.077-3.961a4.55 4.55 0 0 1 1.209-3.002l2.195-2.373q.315-.338.609-.66l.119-.128v-.002a35 35 0 0 0 1.892-2.244c1.534-2.01 1.88-3.18 1.88-4.181s-.346-2.17-1.88-4.18a35 35 0 0 0-1.892-2.245v-.002zM48.51 71.813q.362-.33.747-.69l2.331-2.157a4.55 4.55 0 0 1 3.003-1.208l3.959-.076-1.193 1.193c-7.24 7.24-10.859 10.86-15.357 10.86s-8.118-3.62-15.357-10.86l-1.194-1.194 3.97.076a4.55 4.55 0 0 1 2.991 1.2l1.601 1.47c1.461 1.4 2.69 2.502 3.808 3.355 2.01 1.534 3.18 1.88 4.181 1.88s2.17-.346 4.18-1.88a35 35 0 0 0 2.275-1.92zM12.156 48.476q.364.4.763.825l2.115 2.287a4.55 4.55 0 0 1 1.209 3.002l.076 3.961-1.194-1.194C7.885 50.117 4.265 46.498 4.265 42s3.62-8.118 10.86-15.357l1.193-1.193-.075 3.959a4.55 4.55 0 0 1-1.21 3.004l-2.18 2.357q-.325.346-.626.676l-.117.127v.002a35 35 0 0 0-1.892 2.244C8.684 39.83 8.338 41 8.338 42s.346 2.17 1.88 4.18a35 35 0 0 0 1.92 2.275z" />
<path d="m12.138 35.543 2.896-3.13a4.55 4.55 0 0 0 1.186-2.626c.012-1.61.038-3.013.096-4.254l.003-.17.006-.005c.053-1.072.131-2.021.246-2.875.337-2.506.92-3.578 1.627-4.286s1.78-1.29 4.285-1.626c.87-.117 1.838-.196 2.935-.25l.002-.002h.06c1.285-.062 2.746-.089 4.43-.1a4.55 4.55 0 0 0 2.711-1.257l2.923-2.825h-1.688c-10.238 0-15.357 0-18.538 3.18-3.18 3.181-3.18 8.3-3.18 18.539zM12.138 48.456v1.688c0 10.239 0 15.358 3.18 18.538s8.3 3.18 18.538 3.18h16.289c10.238 0 15.357 0 18.538-3.18 3.18-3.18 3.18-8.3 3.18-18.537v-1.69l-2.897 3.133a4.55 4.55 0 0 0-1.185 2.618c-.012 1.645-.039 3.075-.1 4.335v.04h-.001a35 35 0 0 1-.25 2.936c-.337 2.506-.92 3.578-1.627 4.286s-1.78 1.29-4.285 1.626c-.855.115-1.804.194-2.876.247l-.005.005-.149.003c-1.246.058-2.658.085-4.277.097-.976.1-1.897.515-2.623 1.185l-3.132 2.897H35.573l-3.163-2.906a4.55 4.55 0 0 0-2.61-1.176 110 110 0 0 1-4.324-.1h-.056l-.002-.002a35 35 0 0 1-2.935-.25c-2.505-.336-3.578-.919-4.285-1.626-.708-.708-1.29-1.78-1.627-4.286a35 35 0 0 1-.25-2.935l-.002-.002-.001-.075c-.06-1.251-.086-2.668-.098-4.296a4.55 4.55 0 0 0-1.186-2.621zM67.781 29.794a4.55 4.55 0 0 0 1.185 2.618l2.897 3.132v-1.688c0-10.239 0-15.358-3.18-18.538s-8.3-3.18-18.538-3.18h-1.689l3.132 2.895a4.55 4.55 0 0 0 2.627 1.186c1.6.012 2.997.038 4.232.096l.247.004.008.008a34 34 0 0 1 2.816.244c2.505.337 3.578.919 4.285 1.626.708.708 1.29 1.78 1.627 4.286.117.87.196 1.839.25 2.936l.001.04c.061 1.26.088 2.69.1 4.335M38.91 23.96l-2.747 2.33a2.9 2.9 0 0 1-1.747.689l-4.597.214 2.397-2.397c4.627-4.627 6.94-6.94 9.815-6.94s5.188 2.313 9.815 6.94l2.383 2.382-4.662-.202a2.9 2.9 0 0 1-1.773-.703l-2.074-1.789c-.728-.685-1.345-1.226-1.908-1.656-1.154-.88-1.592-.9-1.78-.9-.19 0-.627.02-1.781.9l-.055.042h-.003l-.027.023c-.387.3-.8.652-1.257 1.067" />
<path d="M61.023 39.995c-.785-.992-1.911-2.163-3.542-3.803a2.9 2.9 0 0 1-.44-1.426l-.202-4.977 2.369 2.368c4.627 4.627 6.94 6.94 6.94 9.815s-2.313 5.188-6.94 9.815l-2.382 2.381.23-4.757a2.9 2.9 0 0 1 .727-1.787l1.742-1.968a28 28 0 0 0 1.387-1.569l.215-.242v-.03l.049-.062c.88-1.154.9-1.592.9-1.781 0-.19-.02-.627-.9-1.78l-.049-.064v-.024zM22.946 40.124l3.175-3.454c.45-.489.719-1.117.762-1.78l.175-2.71c.027-.86.071-1.584.144-2.216l.012-.192.013-.013.009-.065c.193-1.438.488-1.762.622-1.896s.457-.429 1.896-.622c.461-.062.974-.106 1.555-.138l3.9-.385a2.9 2.9 0 0 0 1.678-.75l3.296-3.017h-3.357c-6.543 0-9.815 0-11.847 2.033-1.732 1.732-1.988 4.363-2.026 9.15q-.009 1.246-.007 2.698v3.356" />
<path d="M22.946 43.82v3.357c0 .97 0 1.866.006 2.698.038 4.787.295 7.418 2.027 9.15 1.731 1.732 4.362 1.988 9.15 2.026q1.246.009 2.697.007h10.411q1.45.002 2.697-.007c4.788-.038 7.419-.294 9.15-2.026 2.033-2.033 2.033-5.304 2.033-11.848V43.81l-3.384 3.67a2.9 2.9 0 0 0-.69 1.29c-.006 2.38-.038 4.033-.193 5.306l-.002.068-.008.008-.012.098c-.194 1.438-.489 1.762-.623 1.896-.133.133-.457.429-1.895.622l-.099.013-.008.008-.114.007c-.724.086-1.57.133-2.602.159l-2.32.141c-.661.04-1.288.305-1.778.75l-3.538 3.212h-3.697l-3.536-3.306a2.9 2.9 0 0 0-1.69-.769q-.41 0-.79-.004c-1.906-.016-3.288-.063-4.384-.21-1.439-.194-1.762-.49-1.896-.623-.134-.134-.429-.458-.622-1.896l-.009-.065-.012-.013-.002-.027-.004-.108c-.13-1.084-.171-2.442-.185-4.283l-.02-.472a2.9 2.9 0 0 0-.755-1.833zM57.01 32.35l.19 2.586c.049.652.315 1.27.757 1.751l3.16 3.447v-3.367c0-6.544 0-9.815-2.032-11.848s-5.305-2.033-11.848-2.033H43.85l3.391 3.09c.475.432 1.08.696 1.721.748l3.933.322q.562.033 1.045.085l.29.024.013.012.066.01c1.438.192 1.762.488 1.895.621.134.134.43.458.623 1.896.098.733.152 1.595.182 2.655" />
<path d="m27.226 54.158-.013-.013.002.027.012.013zM29.849 56.78l4.289.199c-1.852-.015-3.208-.06-4.29-.198M27.044 49.476a3 3 0 0 0-.08-.57 3 3 0 0 1 .04.376l.02.472c.014 1.84.056 3.2.185 4.283l.004.108.013.013zM17.915 41.972c0 2.45 1.679 4.491 5.038 7.903q-.009-1.246-.007-2.698v-3.344l-.007-.008v-.005l-.052-.068c-.88-1.153-.9-1.59-.9-1.78s.02-.627.9-1.78l.059-.077v-3.348q-.001-1.452.006-2.698c-3.358 3.412-5.037 5.454-5.037 7.903M40.25 61.116l-.048-.037h-.01l-.022-.021h-3.344q-1.45.002-2.697-.007c3.412 3.358 5.453 5.038 7.902 5.038 2.45 0 4.491-1.68 7.903-5.038q-1.246.009-2.697.007h-3.35l-.075.058c-1.154.88-1.592.9-1.78.9-.19 0-.627-.02-1.781-.9" />
</g>
</svg>
Documenso
</span>
),
},
githubUrl: 'https://github.com/documenso/documenso',
};
}
+13
View File
@@ -0,0 +1,13 @@
import type * as React from 'react';
export function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
ref.current = value;
}
});
};
}
+84
View File
@@ -0,0 +1,84 @@
import type * as PageTree from 'fumadocs-core/page-tree';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docs } from 'fumadocs-mdx:collections/server';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: '/docs',
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});
// Shared sections that should appear in all main section sidebars
const SHARED_SECTIONS = ['concepts', 'compliance', 'policies'];
// Section titles for the sidebar separators
const SECTION_TITLES: Record<string, string> = {
users: 'User Guide',
developers: 'Developer Guide',
'self-hosting': 'Self-Hosting Guide',
};
/**
* Get a filtered page tree showing a specific root folder's content
* plus the shared sections (Concepts, Compliance, Policies)
*/
export function getFilteredPageTree(rootName: string): PageTree.Root {
const fullTree = source.getPageTree();
// Find the main section folder
const rootFolder = fullTree.children.find(
(child): child is PageTree.Folder =>
child.type === 'folder' &&
typeof child.name === 'string' &&
child.name.toLowerCase() === rootName.toLowerCase(),
);
if (!rootFolder) {
return fullTree;
}
// Find shared section folders
const sharedFolders = fullTree.children.filter(
(child): child is PageTree.Folder =>
child.type === 'folder' &&
typeof child.name === 'string' &&
SHARED_SECTIONS.includes(child.name.toLowerCase()),
);
// Create separator for main section
const mainSeparator: PageTree.Separator = {
type: 'separator',
name: SECTION_TITLES[rootName.toLowerCase()] || rootName,
};
// Create separator for shared sections
const sharedSeparator: PageTree.Separator = {
type: 'separator',
name: 'Resources',
};
// Return a new tree with separators and content
return {
name: fullTree.name,
children: [mainSeparator, ...rootFolder.children, sharedSeparator, ...sharedFolders],
};
}
export function getPageImage(page: InferPageType<typeof source>) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
}
export async function getLLMText(page: InferPageType<typeof source>) {
const processed = await page.data.getText('processed');
return `# ${page.data.title}
${processed}`;
}
+14
View File
@@ -0,0 +1,14 @@
import { Mermaid } from '@/components/mdx/mermaid';
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMDXComponents(components?: MDXComponents): any {
return {
...defaultMdxComponents,
...TabsComponents,
Mermaid,
...components,
};
}