docs: add llms
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
OPENPANEL_BASE_URL,
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_NAME,
|
||||
} from '@/lib/openpanel-brand';
|
||||
import { AnalyticsInsights } from './_sections/analytics-insights';
|
||||
import { Collaboration } from './_sections/collaboration';
|
||||
import { CtaBanner } from './_sections/cta-banner';
|
||||
@@ -9,9 +14,34 @@ import { Sdks } from './_sections/sdks';
|
||||
import { Testimonials } from './_sections/testimonials';
|
||||
import { WhyOpenPanel } from './_sections/why-openpanel';
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Organization',
|
||||
name: OPENPANEL_NAME,
|
||||
url: OPENPANEL_BASE_URL,
|
||||
sameAs: ['https://github.com/Openpanel-dev/openpanel'],
|
||||
description: OPENPANEL_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: OPENPANEL_NAME,
|
||||
applicationCategory: 'AnalyticsApplication',
|
||||
operatingSystem: 'Web',
|
||||
url: OPENPANEL_BASE_URL,
|
||||
description: OPENPANEL_DESCRIPTION,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<Hero />
|
||||
<WhyOpenPanel />
|
||||
<AnalyticsInsights />
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import {
|
||||
OPENPANEL_BASE_URL,
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_NAME,
|
||||
} from '@/lib/openpanel-brand';
|
||||
import { getLLMText, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
const header = `# ${OPENPANEL_NAME} – Full documentation for LLMs
|
||||
|
||||
${OPENPANEL_DESCRIPTION}
|
||||
|
||||
This file contains the full text of all documentation pages. Each section is separated by --- and includes a canonical URL.
|
||||
|
||||
`;
|
||||
|
||||
export async function GET() {
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
const pages = source.getPages().slice().sort((a, b) => a.url.localeCompare(b.url));
|
||||
const scanned = await Promise.all(pages.map(getLLMText));
|
||||
|
||||
return new Response(scanned.join('\n\n'));
|
||||
return new Response(header + scanned.join('\n\n'), {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
80
apps/public/src/app/llms.txt/route.ts
Normal file
80
apps/public/src/app/llms.txt/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
OPENPANEL_BASE_URL,
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_NAME,
|
||||
} from '@/lib/openpanel-brand';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
const body = `# ${OPENPANEL_NAME}
|
||||
|
||||
> ${OPENPANEL_DESCRIPTION}
|
||||
|
||||
## Main pages
|
||||
- [Home](${OPENPANEL_BASE_URL}/)
|
||||
- [Features](${OPENPANEL_BASE_URL}/features) (event tracking, funnels, retention, web analytics, and more)
|
||||
- [Guides](${OPENPANEL_BASE_URL}/guides)
|
||||
- [Articles](${OPENPANEL_BASE_URL}/articles)
|
||||
- [Open source](${OPENPANEL_BASE_URL}/open-source)
|
||||
- [Supporter](${OPENPANEL_BASE_URL}/supporter)
|
||||
- [About](${OPENPANEL_BASE_URL}/about)
|
||||
- [Contact](${OPENPANEL_BASE_URL}/contact)
|
||||
|
||||
## Core docs
|
||||
- [What is OpenPanel?](${OPENPANEL_BASE_URL}/docs)
|
||||
- [Install OpenPanel](${OPENPANEL_BASE_URL}/docs/get-started/install-openpanel)
|
||||
- [Track Events](${OPENPANEL_BASE_URL}/docs/get-started/track-events)
|
||||
- [Identify Users](${OPENPANEL_BASE_URL}/docs/get-started/identify-users)
|
||||
|
||||
## SDKs
|
||||
- [SDKs Overview](${OPENPANEL_BASE_URL}/docs/sdks)
|
||||
- [JavaScript](${OPENPANEL_BASE_URL}/docs/sdks/javascript)
|
||||
- [React](${OPENPANEL_BASE_URL}/docs/sdks/react)
|
||||
- [Next.js](${OPENPANEL_BASE_URL}/docs/sdks/nextjs)
|
||||
- [Vue](${OPENPANEL_BASE_URL}/docs/sdks/vue)
|
||||
- [React Native](${OPENPANEL_BASE_URL}/docs/sdks/react-native)
|
||||
- [Swift](${OPENPANEL_BASE_URL}/docs/sdks/swift)
|
||||
- [Kotlin](${OPENPANEL_BASE_URL}/docs/sdks/kotlin)
|
||||
- [Python](${OPENPANEL_BASE_URL}/docs/sdks/python)
|
||||
|
||||
## API
|
||||
- [Authentication](${OPENPANEL_BASE_URL}/docs/api/authentication)
|
||||
- [Track API](${OPENPANEL_BASE_URL}/docs/api/track)
|
||||
- [Export API](${OPENPANEL_BASE_URL}/docs/api/export)
|
||||
- [Insights API](${OPENPANEL_BASE_URL}/docs/api/insights)
|
||||
|
||||
## Self-hosting
|
||||
- [Self-hosting Guide](${OPENPANEL_BASE_URL}/docs/self-hosting/self-hosting)
|
||||
- [Docker Compose](${OPENPANEL_BASE_URL}/docs/self-hosting/deploy-docker-compose)
|
||||
- [Environment Variables](${OPENPANEL_BASE_URL}/docs/self-hosting/environment-variables)
|
||||
|
||||
## Pricing
|
||||
- [Pricing](${OPENPANEL_BASE_URL}/pricing)
|
||||
|
||||
## Compare (alternatives)
|
||||
- [Mixpanel alternative](${OPENPANEL_BASE_URL}/compare/mixpanel-alternative)
|
||||
- [PostHog alternative](${OPENPANEL_BASE_URL}/compare/posthog-alternative)
|
||||
- [Google Analytics alternative](${OPENPANEL_BASE_URL}/compare/google-analytics-alternative)
|
||||
- [Amplitude alternative](${OPENPANEL_BASE_URL}/compare/amplitude-alternative)
|
||||
- [Plausible alternative](${OPENPANEL_BASE_URL}/compare/plausible-alternative)
|
||||
- [Umami alternative](${OPENPANEL_BASE_URL}/compare/umami-alternative)
|
||||
- [Compare all](${OPENPANEL_BASE_URL}/compare)
|
||||
|
||||
## Trust & legal
|
||||
- [Privacy Policy](${OPENPANEL_BASE_URL}/privacy)
|
||||
- [Terms of Service](${OPENPANEL_BASE_URL}/terms)
|
||||
|
||||
## Source
|
||||
- [GitHub](https://github.com/Openpanel-dev/openpanel)
|
||||
|
||||
## Optional
|
||||
- [Full docs for LLMs](${OPENPANEL_BASE_URL}/llms-full.txt)
|
||||
`;
|
||||
|
||||
export async function GET() {
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
},
|
||||
});
|
||||
}
|
||||
151
apps/public/src/app/md/route.ts
Normal file
151
apps/public/src/app/md/route.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { OPENPANEL_BASE_URL } from '@/lib/openpanel-brand';
|
||||
import { articleSource, guideSource, pageSource, source } from '@/lib/source';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const ALLOWED_PAGE_PATHS = new Set([
|
||||
'privacy',
|
||||
'terms',
|
||||
'about',
|
||||
'contact',
|
||||
'cookies',
|
||||
]);
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
function stubMarkdown(canonicalUrl: string, path: string): string {
|
||||
return `# ${path}\n\nThis page is available at: [${canonicalUrl}](${canonicalUrl})\n`;
|
||||
}
|
||||
|
||||
async function getProcessedText(page: {
|
||||
data: { getText?: (type: 'processed' | 'raw') => Promise<string> };
|
||||
}): Promise<string | null> {
|
||||
try {
|
||||
const getText = page.data.getText;
|
||||
if (typeof getText === 'function') {
|
||||
return await getText('processed');
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Rewrites preserve the original request URL, so pathname is e.g. /docs/foo.md
|
||||
// Derive path from pathname when present; otherwise use query (e.g. /md?path=...)
|
||||
const pathParam = pathname.endsWith('.md')
|
||||
? pathname.slice(0, -3)
|
||||
: url.searchParams.get('path');
|
||||
|
||||
if (!pathParam || pathParam.includes('..')) {
|
||||
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const path = pathParam.startsWith('/') ? pathParam : `/${pathParam}`;
|
||||
|
||||
if (path.startsWith('/docs')) {
|
||||
const slug = path
|
||||
.replace(/^\/docs\/?/, '')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
const page = source.getPage(slug);
|
||||
if (!page) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
const processed = await page.data.getText('processed');
|
||||
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
|
||||
const body = `# ${page.data.title}\n\nURL: ${canonical}\n\n${processed}`;
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
|
||||
if (path.startsWith('/articles')) {
|
||||
const slug = path
|
||||
.replace(/^\/articles\/?/, '')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
if (slug.length === 0)
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
const page = articleSource.getPage(slug);
|
||||
if (!page) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
const text = await getProcessedText(page);
|
||||
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
|
||||
const body = text
|
||||
? `# ${page.data.title}\n\nURL: ${canonical}\n\n${text}`
|
||||
: stubMarkdown(canonical, path);
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
|
||||
if (path.startsWith('/guides')) {
|
||||
const slug = path
|
||||
.replace(/^\/guides\/?/, '')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
if (slug.length === 0)
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
const page = guideSource.getPage(slug);
|
||||
if (!page) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
const text = await getProcessedText(page);
|
||||
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
|
||||
const body = text
|
||||
? `# ${page.data.title}\n\nURL: ${canonical}\n\n${text}`
|
||||
: stubMarkdown(canonical, path);
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
path === '/' ||
|
||||
(path.startsWith('/') && path.split('/').filter(Boolean).length === 1)
|
||||
) {
|
||||
const segment = path.replace(/^\//, '');
|
||||
const slug = segment ? [segment] : [];
|
||||
const page = slug.length ? pageSource.getPage(slug) : null;
|
||||
if (page) {
|
||||
try {
|
||||
const getText = (
|
||||
page.data as { getText?: (mode: string) => Promise<string> }
|
||||
).getText;
|
||||
if (typeof getText === 'function') {
|
||||
const processed = await getText('processed');
|
||||
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
|
||||
const body = `# ${page.data.title}\n\nURL: ${canonical}\n\n${processed}`;
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// fall through to stub if getText not available
|
||||
}
|
||||
if (ALLOWED_PAGE_PATHS.has(segment)) {
|
||||
return new Response(
|
||||
stubMarkdown(`${OPENPANEL_BASE_URL}/${segment}`, path),
|
||||
{
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ALLOWED_PAGE_PATHS.has(segment)) {
|
||||
return new Response(
|
||||
stubMarkdown(`${OPENPANEL_BASE_URL}/${segment}`, path),
|
||||
{
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
User-Agent: *
|
||||
Allow: /
|
||||
Allow: /og*
|
||||
Allow: /llms.txt
|
||||
Allow: /llms-full.txt
|
||||
Allow: /*.md
|
||||
|
||||
Sitemap: https://openpanel.dev/sitemap.xml
|
||||
|
||||
@@ -57,6 +57,18 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: url('/llms.txt'),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: url('/llms-full.txt'),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
...articles.map((item) => ({
|
||||
url: url(item.url),
|
||||
lastModified: item.data.date,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { OPENPANEL_BASE_URL, OPENPANEL_NAME } from './openpanel-brand';
|
||||
|
||||
export const siteName = 'OpenPanel';
|
||||
export const siteName = OPENPANEL_NAME;
|
||||
export const baseUrl =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'https://openpanel.dev'
|
||||
? OPENPANEL_BASE_URL
|
||||
: 'http://localhost:3000';
|
||||
export const url = (path: string) => {
|
||||
if (path.startsWith('http')) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { OPENPANEL_DESCRIPTION, OPENPANEL_NAME } from './openpanel-brand';
|
||||
import { url as baseUrl } from './layout.shared';
|
||||
|
||||
const siteName = 'OpenPanel';
|
||||
const defaultDescription =
|
||||
'OpenPanel is a simple, affordable open-source alternative to Mixpanel for web and product analytics. Get powerful insights without the complexity.';
|
||||
const siteName = OPENPANEL_NAME;
|
||||
const defaultDescription = OPENPANEL_DESCRIPTION;
|
||||
const defaultImage = baseUrl('/ogimage.png');
|
||||
|
||||
export function getOgImageUrl(url: string): string {
|
||||
|
||||
4
apps/public/src/lib/openpanel-brand.ts
Normal file
4
apps/public/src/lib/openpanel-brand.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const OPENPANEL_NAME = 'OpenPanel';
|
||||
export const OPENPANEL_BASE_URL = 'https://openpanel.dev';
|
||||
export const OPENPANEL_DESCRIPTION =
|
||||
'OpenPanel is an open-source web and product analytics platform, an open-source alternative to Mixpanel with optional self-hosting.';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||
import { toFumadocsSource } from 'fumadocs-mdx/runtime/server';
|
||||
import { OPENPANEL_BASE_URL } from './openpanel-brand';
|
||||
import type { CompareData } from './compare';
|
||||
import type { FeatureData } from './features';
|
||||
import { loadFeatureSourceSync } from './features';
|
||||
@@ -49,8 +50,11 @@ export function getPageImage(page: InferPageType<typeof source>) {
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
const canonical = `${OPENPANEL_BASE_URL}${page.url}`;
|
||||
|
||||
return `# ${page.data.title}
|
||||
return `---
|
||||
## ${page.data.title}
|
||||
URL: ${canonical}
|
||||
|
||||
${processed}`;
|
||||
}
|
||||
|
||||
17
apps/public/src/middleware.ts
Normal file
17
apps/public/src/middleware.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
if (pathname.endsWith('.md')) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = '/md';
|
||||
url.searchParams.set('path', pathname.slice(0, -3));
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next|api|favicon|robots|sitemap|og).*\\.md)'],
|
||||
};
|
||||
Reference in New Issue
Block a user