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,
})),
];
}