port mintlify to fumadocs

This commit is contained in:
miloschwartz
2026-09-25 15:31:51 -04:00
parent dc54fb1017
commit 63199a588c
373 changed files with 13533 additions and 4577 deletions
+90
View File
@@ -0,0 +1,90 @@
import 'server-only';
import type { LanguageModel } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
/**
* Bring-your-own-key model configuration for the docs assistant. Everything comes from
* environment variables on the server; keys are never sent to the browser.
*
* AI_PROVIDER anthropic | openai | google | openai-compatible
* (optional: inferred from whichever key is set)
* AI_MODEL model id for that provider (optional: sensible default per provider)
*
* ANTHROPIC_API_KEY for `anthropic`
* OPENAI_API_KEY for `openai`
* GOOGLE_GENERATIVE_AI_API_KEY for `google`
* AI_BASE_URL + AI_API_KEY for `openai-compatible` (OpenRouter, Ollama, vLLM,
* LiteLLM, a Pangolin AI Gateway resource, ...)
*/
export type Provider = 'anthropic' | 'openai' | 'google' | 'openai-compatible';
const defaultModels: Record<Provider, string | undefined> = {
anthropic: 'claude-opus-5',
openai: 'gpt-5.6-luna',
google: 'gemini-2.5-pro',
'openai-compatible': undefined,
};
function detectProvider(): Provider | undefined {
const explicit = process.env.AI_PROVIDER?.trim().toLowerCase();
if (explicit) {
if (explicit in defaultModels) return explicit as Provider;
throw new Error(`Unknown AI_PROVIDER "${explicit}"`);
}
if (process.env.AI_BASE_URL) return 'openai-compatible';
if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
if (process.env.OPENAI_API_KEY) return 'openai';
if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) return 'google';
return undefined;
}
export interface ModelConfig {
provider: Provider;
modelId: string;
model: LanguageModel;
}
export class AIConfigError extends Error {}
export function getModel(): ModelConfig {
const provider = detectProvider();
if (!provider) {
throw new AIConfigError(
'The docs assistant is not configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, or AI_BASE_URL + AI_API_KEY on the server.',
);
}
const modelId = process.env.AI_MODEL?.trim() || defaultModels[provider];
if (!modelId) throw new AIConfigError(`AI_MODEL is required for provider "${provider}".`);
switch (provider) {
case 'anthropic':
return { provider, modelId, model: createAnthropic()(modelId) };
case 'openai':
return { provider, modelId, model: createOpenAI()(modelId) };
case 'google':
return { provider, modelId, model: createGoogleGenerativeAI()(modelId) };
case 'openai-compatible': {
const baseURL = process.env.AI_BASE_URL;
if (!baseURL) throw new AIConfigError('AI_BASE_URL is required for openai-compatible.');
const compatible = createOpenAICompatible({
name: 'custom',
baseURL,
apiKey: process.env.AI_API_KEY,
});
return { provider, modelId, model: compatible.chatModel(modelId) };
}
}
}
export function isAIConfigured() {
try {
getModel();
return true;
} catch {
return false;
}
}
+32
View File
@@ -0,0 +1,32 @@
import 'server-only';
import { source } from '@/lib/source';
import { siteUrl } from '@/lib/shared';
let pageIndex: string | undefined;
/** compact list of every page, so the model knows what exists before searching */
function getPageIndex() {
pageIndex ??= source
.getPages()
.map((page) => `- ${page.url} | ${page.data.title}${page.data.description ? ` — ${page.data.description}` : ''}`)
.sort()
.join('\n');
return pageIndex;
}
export function getSystemPrompt() {
return `You are the documentation assistant for Pangolin, an open-source networking and security platform (identity-aware access to apps, infrastructure, and AI workloads). You answer questions from people reading the docs at ${siteUrl}.
How to work:
- Ground every answer in the documentation. Before answering anything specific (config keys, commands, versions, UI steps, feature availability), use search_docs and read_page to look it up. Read the full page when the answer depends on details.
- The page index below lists every page. If a page's title clearly matches the question, you can read it directly.
- Cite the pages you used as Markdown links with site-relative URLs, e.g. [Install a Site](/manage/sites/install-site). Link to sections with #anchors when you have them.
- If the docs don't cover something, say so plainly and point to the closest relevant page or to the community (Discord: https://pangolin.net/discord, GitHub discussions: https://github.com/fosrl/pangolin/discussions). Do not invent configuration options, flags, or behavior.
- Page sources contain MDX components such as <Note>, <Steps>, <Card> or <ResponseField>. Never output those tags; rewrite their content as plain Markdown (blockquotes, lists, headings, tables).
- Be concise and practical. Prefer short steps and code blocks that the user can copy. Use the same terminology as the docs (sites, resources, clients, Newt, Olm, Gerbil, Badger, remote nodes).
- Pangolin has a hosted Cloud edition and a self-hosted edition (Community and Enterprise). When the answer differs between them, say which one you are describing.
- Each user message may include "[Client Context: ...]" with the page the user is currently viewing. Use it to resolve questions like "this page" or "how do I do this".
Page index (URL | title — description):
${getPageIndex()}`;
}
+38
View File
@@ -0,0 +1,38 @@
import 'server-only';
/**
* Minimal in-memory sliding-window limiter so a public docs site can't be used to burn
* through your API credits. It is per server instance; put a shared limiter (or your
* reverse proxy's) in front if you run several replicas.
*/
const windowMs = 60_000;
const limit = Number(process.env.AI_RATE_LIMIT_PER_MINUTE ?? 10);
const hits = new Map<string, number[]>();
export function checkRateLimit(key: string): { ok: boolean; retryAfter: number } {
if (!Number.isFinite(limit) || limit <= 0) return { ok: true, retryAfter: 0 };
const now = Date.now();
const recent = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
if (recent.length >= limit) {
hits.set(key, recent);
return { ok: false, retryAfter: Math.ceil((windowMs - (now - recent[0])) / 1000) };
}
recent.push(now);
hits.set(key, recent);
// keep the map from growing forever
if (hits.size > 10_000) {
for (const [k, v] of hits) if (v.every((t) => now - t >= windowMs)) hits.delete(k);
}
return { ok: true, retryAfter: 0 };
}
export function clientKey(req: Request) {
return (
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
req.headers.get('x-real-ip') ||
'anonymous'
);
}
+74
View File
@@ -0,0 +1,74 @@
import 'server-only';
import { tool } from 'ai';
import { z } from 'zod';
import { getPageText, source } from '@/lib/source';
import { searchServer } from '@/lib/search';
const MAX_PAGE_CHARS = 60_000;
function findPage(pathOrUrl: string) {
let path = pathOrUrl.trim();
try {
path = new URL(path, 'http://x').pathname;
} catch {
// not a URL, use as is
}
path = path.replace(/\.mdx?$/, '').replace(/^\/+|\/+$/g, '');
const slugs = path === '' || path === 'index' ? [] : path.split('/');
return source.getPage(slugs);
}
export const searchDocs = tool({
description:
'Full-text search over the Pangolin documentation. Returns matching pages and sections with their URLs. Use several short keyword queries rather than one long question.',
inputSchema: z.object({
query: z.string().describe('keywords, e.g. "newt docker compose" or "OIDC auto provisioning"'),
limit: z.number().int().min(1).max(25).default(10),
}),
async execute({ query, limit }) {
const results = await searchServer.search(query, { limit: limit * 3 });
const pages = new Map<string, { url: string; title: string; matches: string[] }>();
for (const result of results) {
const url = result.url.split('#')[0];
const page = pages.get(url) ?? {
url,
title: findPage(url)?.data.title ?? url,
matches: [],
};
if (result.type !== 'page' && page.matches.length < 3) {
const text = result.content.replace(/<\/?mark>/g, '');
page.matches.push(result.type === 'heading' ? `${text} (${result.url})` : text);
}
pages.set(url, page);
if (pages.size >= limit) break;
}
return [...pages.values()];
},
});
export const readPage = tool({
description:
'Read the full Markdown content of one documentation page. Pass the page URL path from search results or the page index, e.g. "/manage/sites/install-site".',
inputSchema: z.object({
path: z.string(),
}),
async execute({ path }) {
const page = findPage(path);
if (!page) return { error: `No page found at "${path}". Use search_docs to find the right URL.` };
const text = await getPageText(page);
if (text.length <= MAX_PAGE_CHARS) return { url: page.url, content: text };
return {
url: page.url,
truncated: true,
note: `Page is ${text.length} characters; only the first ${MAX_PAGE_CHARS} are included. Search for specific sections if the answer is not here.`,
content: text.slice(0, MAX_PAGE_CHARS),
};
},
});
export const tools = {
search_docs: searchDocs,
read_page: readPage,
};
+1
View File
@@ -0,0 +1 @@
export { cn } from 'cn';
+64
View File
@@ -0,0 +1,64 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { appName, enableDarkMode, links } from './shared';
export function Logo() {
return (
<>
<img
src="/logo/light.png"
alt={appName}
width={665}
height={164}
className="pg-logo dark:hidden"
/>
<img
src="/logo/dark.png"
alt={appName}
width={933}
height={164}
className="pg-logo hidden dark:block"
/>
</>
);
}
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: <Logo />,
url: '/',
},
themeSwitch: { enabled: enableDarkMode },
links: [
// pill buttons styled like the pangolin.net navbar (secondary + primary)
{
type: 'custom',
on: 'nav',
children: (
<div className="flex items-center gap-2">
<a href={links.login} className="pg-btn pg-btn-secondary">
Log in
</a>
<a href={links.signup} className="pg-btn">
Start for free
</a>
</div>
),
},
{
type: 'main',
text: 'Log in',
url: links.login,
external: true,
on: 'menu',
},
{
type: 'main',
text: 'Start for free',
url: links.signup,
external: true,
on: 'menu',
},
],
};
}
+374
View File
@@ -0,0 +1,374 @@
{
"groups": [
{
"group": "About",
"pages": [
"index",
"about/how-pangolin-works",
"about/pangolin-vs-reverse-proxy-vs-vpn",
"about/pangolin-vs-bifrost-vs-litellm",
"about/pangolin-cloud-vs-self-hosted"
]
},
{
"group": "Manage Pangolin",
"pages": [
{
"group": "Sites",
"icon": "plug",
"pages": [
"manage/sites/understanding-sites",
"manage/sites/install-site",
"manage/sites/configure-site",
"manage/sites/update-site",
"manage/sites/auto-update",
"manage/sites/credentials",
"manage/sites/site-provisioning",
{
"group": "Kubernetes",
"pages": [
"manage/sites/kubernetes/helm",
"manage/sites/kubernetes/kustomize",
"manage/sites/kubernetes/configuration",
"manage/sites/kubernetes/troubleshooting"
]
}
]
},
{
"group": "Resources",
"icon": "link",
"pages": [
"manage/resources/understanding-resources",
"manage/resource-launcher",
{
"group": "Public Resources",
"pages": [
"manage/resources/public/http-https",
"manage/resources/public/ai-gateway",
"manage/resources/public/ssh",
"manage/resources/public/rdp",
"manage/resources/public/vnc",
"manage/resources/public/raw-resources",
"manage/resources/public/authentication",
"manage/resources/public/resource-policies",
"manage/resources/public/targets",
"manage/resources/public/wildcard-resources",
"manage/resources/public/healthchecks-failover",
"manage/resources/public/maintenance"
]
},
{
"group": "Private Resources",
"pages": [
"manage/resources/private/host",
"manage/resources/private/cidr",
"manage/resources/private/private-http",
"manage/resources/private/ai-gateway",
"manage/resources/private/ssh",
"manage/resources/private/authentication",
"manage/resources/private/destinations",
"manage/resources/private/port-restrictions",
"manage/resources/private/alias",
"manage/resources/private/multi-site-routing"
]
}
]
},
{
"group": "Clients",
"icon": "desktop",
"pages": [
"manage/clients/understanding-clients",
"manage/clients/nat-traversal",
"manage/clients/install-client",
"manage/clients/configure-client",
"manage/clients/client-logs",
"manage/clients/update-client",
"manage/clients/credentials",
"manage/clients/fingerprinting",
"manage/clients/archiving-blocking",
"manage/clients/firewalls",
{
"group": "Kubernetes",
"pages": [
"manage/clients/kubernetes/deployment"
]
}
]
},
{
"group": "AI Gateway",
"icon": "sparkles",
"pages": [
"manage/ai/overview",
"manage/ai/multiple-gateway-resources",
"manage/ai/virtual-api-keys",
{
"group": "Providers",
"pages": [
"manage/ai/providers/overview",
"manage/ai/providers/configuration",
"manage/ai/providers/model-routing",
"manage/ai/providers/openai",
"manage/ai/providers/anthropic",
"manage/ai/providers/google-gemini",
"manage/ai/providers/vertex-ai",
"manage/ai/providers/bedrock",
"manage/ai/providers/microsoft-foundry",
"manage/ai/providers/open-router",
"manage/ai/providers/vercel-ai-gateway",
{
"group": "Custom",
"pages": [
"manage/ai/providers/custom",
"manage/ai/providers/custom/ollama",
"manage/ai/providers/custom/vllm",
"manage/ai/providers/custom/bifrost",
"manage/ai/providers/custom/cloud-apis-through-a-site"
]
}
]
},
{
"group": "Configure AI Clients and Agents",
"pages": [
"manage/ai/configure-ai-clients/claude",
"manage/ai/configure-ai-clients/codex",
"manage/ai/configure-ai-clients/opencode",
"manage/ai/configure-ai-clients/gemini",
"manage/ai/configure-ai-clients/open-webui",
"manage/ai/configure-ai-clients/claude-desktop",
"manage/ai/configure-ai-clients/openclaw"
]
},
"manage/ai/budgets",
"manage/ai/model-catalog",
"manage/ai/session-logs",
"manage/ai/usage-analytics"
]
},
"manage/domains",
{
"group": "Organizations",
"icon": "building",
"pages": [
"manage/organizations/manage-organizations",
"manage/organizations/org-id",
"manage/labels"
]
},
{
"group": "Access Control",
"icon": "user-group",
"pages": [
"manage/access-control/create-user",
"manage/access-control/mfa",
"manage/access-control/security-keys",
"manage/access-control/session-length",
"manage/access-control/password-rotation",
"manage/access-control/change-password",
"manage/ssh",
"manage/access-control/rules",
"manage/geoblocking",
"manage/asnblocking",
"manage/access-control/links",
"manage/access-control/approvals",
"manage/access-control/forwarded-headers",
"manage/access-control/login-page"
]
},
{
"group": "Identity Providers",
"icon": "id-card",
"pages": [
"manage/identity-providers/add-an-idp",
"manage/identity-providers/auto-provisioning",
"manage/identity-providers/openid-connect",
"manage/identity-providers/google",
"manage/identity-providers/azure"
]
},
{
"group": "Logs & Analytics",
"icon": "chart-bar",
"pages": [
"manage/analytics/request",
"manage/analytics/access",
"manage/analytics/connection",
"manage/analytics/action",
"manage/ai/session-logs",
"manage/ai/usage-analytics",
{
"group": "Event Streaming",
"pages": [
"manage/analytics/streaming",
"manage/analytics/streaming/http",
"manage/analytics/streaming/s3"
]
}
]
},
{
"group": "Alerting",
"icon": "bell",
"pages": [
"manage/alerting/alert-rules",
"manage/alerting/health-checks"
]
},
{
"group": "Blueprints",
"icon": "file-code",
"pages": [
"manage/blueprints",
"manage/community-blueprints-repo"
]
},
{
"group": "Remote Nodes",
"icon": "circle-nodes",
"pages": [
"manage/remote-node/understanding-nodes",
"manage/remote-node/quick-install-remote",
"manage/remote-node/config-file",
"manage/remote-node/backhaul"
]
},
"manage/endpoints-and-pops",
"manage/integration-api",
"manage/branding"
]
},
{
"group": "Self-host Pangolin",
"pages": [
"self-host/quick-install",
"self-host/choosing-a-vps",
{
"group": "Manual Installation",
"pages": [
"self-host/manual/docker-compose",
"self-host/manual/unraid",
{
"group": "Kubernetes",
"pages": [
"self-host/manual/kubernetes/overview",
"self-host/manual/kubernetes/choose-method",
"self-host/manual/kubernetes/prerequisites",
"self-host/manual/kubernetes/helm",
"self-host/manual/kubernetes/kustomize",
"self-host/manual/kubernetes/helmfile",
{
"group": "GitOps",
"pages": [
"self-host/manual/kubernetes/gitops/overview",
"self-host/manual/kubernetes/gitops/argocd",
"self-host/manual/kubernetes/gitops/flux"
]
},
{
"group": "Pangolin",
"pages": [
"self-host/manual/kubernetes/pangolin/helm",
"self-host/manual/kubernetes/pangolin/kustomize",
"self-host/manual/kubernetes/pangolin/configuration",
"self-host/manual/kubernetes/pangolin/troubleshooting"
]
}
]
},
"self-host/manual/podman-quadlets"
]
},
{
"group": "Clustering",
"pages": [
"self-host/clustering/understanding-clustering",
"self-host/clustering/requirements",
"self-host/clustering/deploy-a-cluster"
]
},
"self-host/dns-and-networking",
{
"group": "Advanced Configuration",
"pages": [
"self-host/advanced/config-file",
"self-host/advanced/increasing-subnet-capacity",
"self-host/advanced/private-config-file",
"self-host/advanced/wild-card-domains",
"self-host/advanced/cloudflare-proxy",
"self-host/advanced/without-tunneling",
"self-host/advanced/container-cli-tool",
"self-host/advanced/database-options",
"self-host/advanced/integration-api",
"self-host/advanced/observability",
"self-host/advanced/enable-geolocation",
"self-host/advanced/enable-asn-lookup",
"self-host/advanced/traefik-log-rotation",
"self-host/telemetry"
]
},
"self-host/how-to-update",
{
"group": "Community Guides",
"pages": [
"self-host/community-guides/overview",
"self-host/community-guides/rules",
"self-host/community-guides/remove-geoblock-plugin",
"self-host/community-guides/crowdsec",
"self-host/community-guides/metrics",
"self-host/community-guides/homeassistant",
"self-host/community-guides/middlewaremanager",
"self-host/community-guides/traefiklogsdashboard",
"self-host/community-guides/geolite2automation"
]
},
"self-host/enterprise-edition"
]
},
{
"group": "Development",
"pages": [
"development/system-architecture",
"development/contributing",
"development/feature-requests-and-bug-reports",
"manage/common-api-routes"
]
}
],
"tabs": [
{
"label": "Downloads",
"href": "https://pangolin.net/downloads"
},
{
"label": "Website",
"href": "https://pangolin.net/"
},
{
"label": "GitHub",
"href": "https://github.com/fosrl/pangolin"
},
{
"label": "Releases",
"href": "https://pangolin.net/news/tags/release"
},
{
"label": "Trust Center",
"href": "https://trust.pangolin.net/"
}
],
"anchors": [
{
"label": "Discord",
"href": "https://pangolin.net/discord",
"icon": "discord"
},
{
"label": "Slack",
"href": "https://pangolin.net/slack",
"icon": "slack"
}
]
}
+92
View File
@@ -0,0 +1,92 @@
import type * as PageTree from 'fumadocs-core/page-tree';
import { Icon } from '@/components/icons';
import { source } from './source';
import navigation from './navigation.json';
/**
* The sidebar is defined in `lib/navigation.json` (same shape as Mintlify's `docs.json`
* groups), because the groups do not map 1:1 to folders on disk. Pages that exist in
* `content/docs` but are not listed here are still published, just hidden from the sidebar.
*/
type NavItem = string | NavGroup;
interface NavGroup {
group: string;
icon?: string;
pages: NavItem[];
}
export interface NavLink {
label: string;
href: string;
icon?: string | null;
}
export const navTabs: NavLink[] = navigation.tabs;
export const navAnchors: NavLink[] = navigation.anchors;
function pageNode(path: string): PageTree.Item | null {
const slugs = path === 'index' ? [] : path.split('/');
const page = source.getPage(slugs);
if (!page) {
console.warn(`[navigation] page not found: ${path}`);
return null;
}
return {
$id: `page:${path}`,
type: 'page',
name: page.data.title,
url: page.url,
icon: page.data.icon ? <Icon name={page.data.icon} /> : undefined,
};
}
function buildItems(items: NavItem[], idPrefix: string): PageTree.Node[] {
const out: PageTree.Node[] = [];
for (const item of items) {
if (typeof item === 'string') {
const node = pageNode(item);
if (node) out.push(node);
continue;
}
const id = `${idPrefix}/${item.group}`;
out.push({
$id: id,
type: 'folder',
name: item.group,
icon: item.icon ? <Icon name={item.icon} /> : undefined,
children: buildItems(item.pages, id),
});
}
return out;
}
let cached: PageTree.Root | undefined;
export function getNavigationTree(): PageTree.Root {
if (cached) return cached;
const children: PageTree.Node[] = [];
for (const group of navigation.groups as NavGroup[]) {
// top-level groups render as sidebar section headings
children.push({ $id: `sep:${group.group}`, type: 'separator', name: group.group });
children.push(...buildItems(group.pages, group.group));
}
cached = { $id: 'root', name: 'Docs', children };
return cached;
}
/** Page paths in sidebar order (first occurrence wins), used for llms-full.txt */
export function getOrderedPagePaths(): string[] {
const seen = new Set<string>();
function walk(items: NavItem[]) {
for (const item of items) {
if (typeof item === 'string') seen.add(item);
else walk(item.pages);
}
}
walk(navigation.groups as NavGroup[]);
return [...seen];
}
+82
View File
@@ -0,0 +1,82 @@
[
{
"source": "/self-host/clustering",
"destination": "/self-host/clustering/understanding-clustering",
"permanent": false
},
{
"source": "/self-host/advanced/enable-geoblocking",
"destination": "/self-host/advanced/enable-geolocation",
"permanent": false
},
{
"source": "/self-host/advanced/enable-asnblocking",
"destination": "/self-host/advanced/enable-asn-lookup",
"permanent": false
},
{
"source": "/telemetry",
"destination": "/self-host/telemetry",
"permanent": false
},
{
"source": "/community/:slug*",
"destination": "/self-host/:slug*",
"permanent": false
},
{
"source": "/manage/resources/tcp-udp-resources",
"destination": "/manage/resources/public/raw-resources",
"permanent": false
},
{
"source": "/manage/healthchecks-failover",
"destination": "/manage/resources/public/healthchecks-failover",
"permanent": false
},
{
"source": "/manage/resources/private/icmp-access",
"destination": "/manage/resources/private/port-restrictions",
"permanent": false
},
{
"source": "/manage/sites/install-kubernetes",
"destination": "/manage/sites/kubernetes/helm",
"permanent": true
},
{
"source": "/manage/ai/claude",
"destination": "/manage/ai/configure-ai-clients/claude",
"permanent": false
},
{
"source": "/manage/ai/codex",
"destination": "/manage/ai/configure-ai-clients/codex",
"permanent": false
},
{
"source": "/manage/ai/opencode",
"destination": "/manage/ai/configure-ai-clients/opencode",
"permanent": false
},
{
"source": "/manage/ai/gemini",
"destination": "/manage/ai/configure-ai-clients/gemini",
"permanent": false
},
{
"source": "/manage/ai/open-webui",
"destination": "/manage/ai/configure-ai-clients/open-webui",
"permanent": false
},
{
"source": "/manage/ai/claude-desktop",
"destination": "/manage/ai/configure-ai-clients/claude-desktop",
"permanent": false
},
{
"source": "/manage/ai/openclaw",
"destination": "/manage/ai/configure-ai-clients/openclaw",
"permanent": false
}
]
+10
View File
@@ -0,0 +1,10 @@
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from './source';
/**
* One full-text index (Orama) shared by the search dialog (`/api/search`) and the
* AI assistant's `search_docs` tool.
*/
export const searchServer = createFromSource(source, {
language: 'english',
});
+53
View File
@@ -0,0 +1,53 @@
import { createGetUrl } from 'fumadocs-core/source';
export const appName = 'Pangolin Docs';
export const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.pangolin.net';
export const siteDescription =
'Modern networking and security platform providing secure access and connectivity to apps, infrastructure, and AI workloads. Connect and protect your users.';
/** docs are served from the site root, same URLs as the old Mintlify site */
export const docsRoute = '/';
export const docsImageRoute = '/og';
export const docsContentRoute = '/llms.mdx';
export const gitConfig = {
user: 'fosrl',
repo: 'docs-v2',
branch: 'main',
};
const getContentUrl = createGetUrl(docsContentRoute);
export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {
const segments = [...page.slugs, 'content.md'];
return { segments, url: getContentUrl(segments, page.locale) };
}
const getImageUrl = createGetUrl(docsImageRoute);
export function getPageImageUrl(page: { slugs: string[]; locale?: string }) {
const segments = [...page.slugs, 'image.png'];
return { segments, url: getImageUrl(segments, page.locale) };
}
/**
* The Mintlify site was light-only (`appearance.strict`). Flip this to enable the
* theme switch; the dark palette in `app/global.css` is ready for it.
*/
export const enableDarkMode = false;
export const links = {
login: 'https://app.pangolin.net/auth/login',
signup: 'https://app.pangolin.net/auth/signup',
github: 'https://github.com/fosrl/pangolin',
featureRequest: 'https://github.com/fosrl/pangolin/discussions',
};
/** top banner (text lives in `app/(docs)/layout.tsx`) */
export const banner = {
/** change the id whenever the text changes so dismissed banners show again */
id: 'ai-gateway-launch',
link: { label: 'Get started', href: '/manage/ai/overview' },
};
+57
View File
@@ -0,0 +1,57 @@
import { llms, loader } from 'fumadocs-core/source';
import { docsContentRoute, docsRoute } from './shared';
import { defineDocs } from 'fumadocs-mdx/macro';
import { applyMdxPreset } from 'fumadocs-mdx/config';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
import { transformerMetaHighlight } from '@shikijs/transformers';
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: pageSchema,
mdxOptions: applyMdxPreset({
rehypeCodeOptions: {
// languages used in the content that Shiki doesn't ship
langAlias: { env: 'dotenv', dns: 'txt', promql: 'txt' },
langs: ['dotenv'],
fallbackLanguage: 'txt',
themes: {
light: 'gruvbox-light-hard',
dark: 'gruvbox-dark-hard',
},
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
// Mintlify-style `{1,3-5}` line highlights
transformerMetaHighlight(),
],
},
}),
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
});
export type Page = ReturnType<typeof source.getPages>[number];
export async function getPageText(page: Page) {
return `# ${page.data.title} (${page.url})
${page.data.description ? `> ${page.data.description}\n\n` : ''}${await page.data.getText('processed')}`;
}
export const docsLlms = llms(source, {
renderPage: getPageText,
});
export { docsContentRoute };