Skip to content

Commit 9f441fd

Browse files
committed
docs: add llms
1 parent 40a3178 commit 9f441fd

11 files changed

Lines changed: 330 additions & 10 deletions

File tree

apps/public/src/app/(home)/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
OPENPANEL_BASE_URL,
3+
OPENPANEL_DESCRIPTION,
4+
OPENPANEL_NAME,
5+
} from '@/lib/openpanel-brand';
16
import { AnalyticsInsights } from './_sections/analytics-insights';
27
import { Collaboration } from './_sections/collaboration';
38
import { CtaBanner } from './_sections/cta-banner';
@@ -9,9 +14,34 @@ import { Sdks } from './_sections/sdks';
914
import { Testimonials } from './_sections/testimonials';
1015
import { WhyOpenPanel } from './_sections/why-openpanel';
1116

17+
const jsonLd = {
18+
'@context': 'https://schema.org',
19+
'@graph': [
20+
{
21+
'@type': 'Organization',
22+
name: OPENPANEL_NAME,
23+
url: OPENPANEL_BASE_URL,
24+
sameAs: ['https://github.com/Openpanel-dev/openpanel'],
25+
description: OPENPANEL_DESCRIPTION,
26+
},
27+
{
28+
'@type': 'SoftwareApplication',
29+
name: OPENPANEL_NAME,
30+
applicationCategory: 'AnalyticsApplication',
31+
operatingSystem: 'Web',
32+
url: OPENPANEL_BASE_URL,
33+
description: OPENPANEL_DESCRIPTION,
34+
},
35+
],
36+
};
37+
1238
export default function HomePage() {
1339
return (
1440
<>
41+
<script
42+
type="application/ld+json"
43+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
44+
/>
1545
<Hero />
1646
<WhyOpenPanel />
1747
<AnalyticsInsights />
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1+
import {
2+
OPENPANEL_BASE_URL,
3+
OPENPANEL_DESCRIPTION,
4+
OPENPANEL_NAME,
5+
} from '@/lib/openpanel-brand';
16
import { getLLMText, source } from '@/lib/source';
27

3-
export const revalidate = false;
8+
export const dynamic = 'force-static';
9+
10+
const header = `# ${OPENPANEL_NAME} – Full documentation for LLMs
11+
12+
${OPENPANEL_DESCRIPTION}
13+
14+
This file contains the full text of all documentation pages. Each section is separated by --- and includes a canonical URL.
15+
16+
`;
417

518
export async function GET() {
6-
const scan = source.getPages().map(getLLMText);
7-
const scanned = await Promise.all(scan);
19+
const pages = source.getPages().slice().sort((a, b) => a.url.localeCompare(b.url));
20+
const scanned = await Promise.all(pages.map(getLLMText));
821

9-
return new Response(scanned.join('\n\n'));
22+
return new Response(header + scanned.join('\n\n'), {
23+
headers: {
24+
'Content-Type': 'text/plain; charset=utf-8',
25+
},
26+
});
1027
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
OPENPANEL_BASE_URL,
3+
OPENPANEL_DESCRIPTION,
4+
OPENPANEL_NAME,
5+
} from '@/lib/openpanel-brand';
6+
7+
export const dynamic = 'force-static';
8+
9+
const body = `# ${OPENPANEL_NAME}
10+
11+
> ${OPENPANEL_DESCRIPTION}
12+
13+
## Main pages
14+
- [Home](${OPENPANEL_BASE_URL}/)
15+
- [Features](${OPENPANEL_BASE_URL}/features) (event tracking, funnels, retention, web analytics, and more)
16+
- [Guides](${OPENPANEL_BASE_URL}/guides)
17+
- [Articles](${OPENPANEL_BASE_URL}/articles)
18+
- [Open source](${OPENPANEL_BASE_URL}/open-source)
19+
- [Supporter](${OPENPANEL_BASE_URL}/supporter)
20+
- [About](${OPENPANEL_BASE_URL}/about)
21+
- [Contact](${OPENPANEL_BASE_URL}/contact)
22+
23+
## Core docs
24+
- [What is OpenPanel?](${OPENPANEL_BASE_URL}/docs)
25+
- [Install OpenPanel](${OPENPANEL_BASE_URL}/docs/get-started/install-openpanel)
26+
- [Track Events](${OPENPANEL_BASE_URL}/docs/get-started/track-events)
27+
- [Identify Users](${OPENPANEL_BASE_URL}/docs/get-started/identify-users)
28+
29+
## SDKs
30+
- [SDKs Overview](${OPENPANEL_BASE_URL}/docs/sdks)
31+
- [JavaScript](${OPENPANEL_BASE_URL}/docs/sdks/javascript)
32+
- [React](${OPENPANEL_BASE_URL}/docs/sdks/react)
33+
- [Next.js](${OPENPANEL_BASE_URL}/docs/sdks/nextjs)
34+
- [Vue](${OPENPANEL_BASE_URL}/docs/sdks/vue)
35+
- [React Native](${OPENPANEL_BASE_URL}/docs/sdks/react-native)
36+
- [Swift](${OPENPANEL_BASE_URL}/docs/sdks/swift)
37+
- [Kotlin](${OPENPANEL_BASE_URL}/docs/sdks/kotlin)
38+
- [Python](${OPENPANEL_BASE_URL}/docs/sdks/python)
39+
40+
## API
41+
- [Authentication](${OPENPANEL_BASE_URL}/docs/api/authentication)
42+
- [Track API](${OPENPANEL_BASE_URL}/docs/api/track)
43+
- [Export API](${OPENPANEL_BASE_URL}/docs/api/export)
44+
- [Insights API](${OPENPANEL_BASE_URL}/docs/api/insights)
45+
46+
## Self-hosting
47+
- [Self-hosting Guide](${OPENPANEL_BASE_URL}/docs/self-hosting/self-hosting)
48+
- [Docker Compose](${OPENPANEL_BASE_URL}/docs/self-hosting/deploy-docker-compose)
49+
- [Environment Variables](${OPENPANEL_BASE_URL}/docs/self-hosting/environment-variables)
50+
51+
## Pricing
52+
- [Pricing](${OPENPANEL_BASE_URL}/pricing)
53+
54+
## Compare (alternatives)
55+
- [Mixpanel alternative](${OPENPANEL_BASE_URL}/compare/mixpanel-alternative)
56+
- [PostHog alternative](${OPENPANEL_BASE_URL}/compare/posthog-alternative)
57+
- [Google Analytics alternative](${OPENPANEL_BASE_URL}/compare/google-analytics-alternative)
58+
- [Amplitude alternative](${OPENPANEL_BASE_URL}/compare/amplitude-alternative)
59+
- [Plausible alternative](${OPENPANEL_BASE_URL}/compare/plausible-alternative)
60+
- [Umami alternative](${OPENPANEL_BASE_URL}/compare/umami-alternative)
61+
- [Compare all](${OPENPANEL_BASE_URL}/compare)
62+
63+
## Trust & legal
64+
- [Privacy Policy](${OPENPANEL_BASE_URL}/privacy)
65+
- [Terms of Service](${OPENPANEL_BASE_URL}/terms)
66+
67+
## Source
68+
- [GitHub](https://github.com/Openpanel-dev/openpanel)
69+
70+
## Optional
71+
- [Full docs for LLMs](${OPENPANEL_BASE_URL}/llms-full.txt)
72+
`;
73+
74+
export async function GET() {
75+
return new Response(body, {
76+
headers: {
77+
'Content-Type': 'text/plain; charset=utf-8',
78+
},
79+
});
80+
}

apps/public/src/app/md/route.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { OPENPANEL_BASE_URL } from '@/lib/openpanel-brand';
2+
import { articleSource, guideSource, pageSource, source } from '@/lib/source';
3+
import { NextResponse } from 'next/server';
4+
5+
const ALLOWED_PAGE_PATHS = new Set([
6+
'privacy',
7+
'terms',
8+
'about',
9+
'contact',
10+
'cookies',
11+
]);
12+
13+
export const runtime = 'nodejs';
14+
15+
function stubMarkdown(canonicalUrl: string, path: string): string {
16+
return `# ${path}\n\nThis page is available at: [${canonicalUrl}](${canonicalUrl})\n`;
17+
}
18+
19+
async function getProcessedText(page: {
20+
data: { getText?: (type: 'processed' | 'raw') => Promise<string> };
21+
}): Promise<string | null> {
22+
try {
23+
const getText = page.data.getText;
24+
if (typeof getText === 'function') {
25+
return await getText('processed');
26+
}
27+
} catch {
28+
// ignore
29+
}
30+
return null;
31+
}
32+
33+
export async function GET(request: Request) {
34+
const url = new URL(request.url);
35+
const pathname = url.pathname;
36+
37+
// Rewrites preserve the original request URL, so pathname is e.g. /docs/foo.md
38+
// Derive path from pathname when present; otherwise use query (e.g. /md?path=...)
39+
const pathParam = pathname.endsWith('.md')
40+
? pathname.slice(0, -3)
41+
: url.searchParams.get('path');
42+
43+
if (!pathParam || pathParam.includes('..')) {
44+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
45+
}
46+
47+
const path = pathParam.startsWith('/') ? pathParam : `/${pathParam}`;
48+
49+
if (path.startsWith('/docs')) {
50+
const slug = path
51+
.replace(/^\/docs\/?/, '')
52+
.split('/')
53+
.filter(Boolean);
54+
const page = source.getPage(slug);
55+
if (!page) {
56+
return new NextResponse('Not found', { status: 404 });
57+
}
58+
const processed = await page.data.getText('processed');
59+
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
60+
const body = `# ${page.data.title}\n\nURL: ${canonical}\n\n${processed}`;
61+
return new Response(body, {
62+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
63+
});
64+
}
65+
66+
if (path.startsWith('/articles')) {
67+
const slug = path
68+
.replace(/^\/articles\/?/, '')
69+
.split('/')
70+
.filter(Boolean);
71+
if (slug.length === 0)
72+
return new NextResponse('Not found', { status: 404 });
73+
const page = articleSource.getPage(slug);
74+
if (!page) {
75+
return new NextResponse('Not found', { status: 404 });
76+
}
77+
const text = await getProcessedText(page);
78+
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
79+
const body = text
80+
? `# ${page.data.title}\n\nURL: ${canonical}\n\n${text}`
81+
: stubMarkdown(canonical, path);
82+
return new Response(body, {
83+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
84+
});
85+
}
86+
87+
if (path.startsWith('/guides')) {
88+
const slug = path
89+
.replace(/^\/guides\/?/, '')
90+
.split('/')
91+
.filter(Boolean);
92+
if (slug.length === 0)
93+
return new NextResponse('Not found', { status: 404 });
94+
const page = guideSource.getPage(slug);
95+
if (!page) {
96+
return new NextResponse('Not found', { status: 404 });
97+
}
98+
const text = await getProcessedText(page);
99+
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
100+
const body = text
101+
? `# ${page.data.title}\n\nURL: ${canonical}\n\n${text}`
102+
: stubMarkdown(canonical, path);
103+
return new Response(body, {
104+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
105+
});
106+
}
107+
108+
if (
109+
path === '/' ||
110+
(path.startsWith('/') && path.split('/').filter(Boolean).length === 1)
111+
) {
112+
const segment = path.replace(/^\//, '');
113+
const slug = segment ? [segment] : [];
114+
const page = slug.length ? pageSource.getPage(slug) : null;
115+
if (page) {
116+
try {
117+
const getText = (
118+
page.data as { getText?: (mode: string) => Promise<string> }
119+
).getText;
120+
if (typeof getText === 'function') {
121+
const processed = await getText('processed');
122+
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
123+
const body = `# ${page.data.title}\n\nURL: ${canonical}\n\n${processed}`;
124+
return new Response(body, {
125+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
126+
});
127+
}
128+
} catch {
129+
// fall through to stub if getText not available
130+
}
131+
if (ALLOWED_PAGE_PATHS.has(segment)) {
132+
return new Response(
133+
stubMarkdown(`${OPENPANEL_BASE_URL}/${segment}`, path),
134+
{
135+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
136+
},
137+
);
138+
}
139+
}
140+
if (ALLOWED_PAGE_PATHS.has(segment)) {
141+
return new Response(
142+
stubMarkdown(`${OPENPANEL_BASE_URL}/${segment}`, path),
143+
{
144+
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
145+
},
146+
);
147+
}
148+
}
149+
150+
return new NextResponse('Not found', { status: 404 });
151+
}

apps/public/src/app/robots.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
User-Agent: *
22
Allow: /
33
Allow: /og*
4+
Allow: /llms.txt
5+
Allow: /llms-full.txt
6+
Allow: /*.md
7+
48
Sitemap: https://openpanel.dev/sitemap.xml

apps/public/src/app/sitemap.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5757
changeFrequency: 'monthly',
5858
priority: 0.7,
5959
},
60+
{
61+
url: url('/llms.txt'),
62+
lastModified: new Date(),
63+
changeFrequency: 'monthly',
64+
priority: 0.3,
65+
},
66+
{
67+
url: url('/llms-full.txt'),
68+
lastModified: new Date(),
69+
changeFrequency: 'monthly',
70+
priority: 0.3,
71+
},
6072
...articles.map((item) => ({
6173
url: url(item.url),
6274
lastModified: item.data.date,

apps/public/src/lib/layout.shared.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2+
import { OPENPANEL_BASE_URL, OPENPANEL_NAME } from './openpanel-brand';
23

3-
export const siteName = 'OpenPanel';
4+
export const siteName = OPENPANEL_NAME;
45
export const baseUrl =
56
process.env.NODE_ENV === 'production'
6-
? 'https://openpanel.dev'
7+
? OPENPANEL_BASE_URL
78
: 'http://localhost:3000';
89
export const url = (path: string) => {
910
if (path.startsWith('http')) {

apps/public/src/lib/metadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Metadata } from 'next';
2+
import { OPENPANEL_DESCRIPTION, OPENPANEL_NAME } from './openpanel-brand';
23
import { url as baseUrl } from './layout.shared';
34

4-
const siteName = 'OpenPanel';
5-
const defaultDescription =
6-
'OpenPanel is a simple, affordable open-source alternative to Mixpanel for web and product analytics. Get powerful insights without the complexity.';
5+
const siteName = OPENPANEL_NAME;
6+
const defaultDescription = OPENPANEL_DESCRIPTION;
77
const defaultImage = baseUrl('/ogimage.png');
88

99
export function getOgImageUrl(url: string): string {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const OPENPANEL_NAME = 'OpenPanel';
2+
export const OPENPANEL_BASE_URL = 'https://openpanel.dev';
3+
export const OPENPANEL_DESCRIPTION =
4+
'OpenPanel is an open-source web and product analytics platform, an open-source alternative to Mixpanel with optional self-hosting.';

apps/public/src/lib/source.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { type InferPageType, loader } from 'fumadocs-core/source';
1111
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
1212
import { toFumadocsSource } from 'fumadocs-mdx/runtime/server';
13+
import { OPENPANEL_BASE_URL } from './openpanel-brand';
1314
import type { CompareData } from './compare';
1415
import type { FeatureData } from './features';
1516
import { loadFeatureSourceSync } from './features';
@@ -49,8 +50,11 @@ export function getPageImage(page: InferPageType<typeof source>) {
4950

5051
export async function getLLMText(page: InferPageType<typeof source>) {
5152
const processed = await page.data.getText('processed');
53+
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
5254

53-
return `# ${page.data.title}
55+
return `---
56+
## ${page.data.title}
57+
URL: ${canonical}
5458
5559
${processed}`;
5660
}

0 commit comments

Comments
 (0)