Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/codebase-wiki-pack-description-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@inkeep/open-knowledge-server": patch
"@inkeep/open-knowledge": patch
---

Shorten the "Codebase wiki" starter-pack description to a single short clause ("Architecture, modules, and flows.") so its card matches the length of the other packs on the starter-pack picker instead of overflowing with a full paragraph.
46 changes: 31 additions & 15 deletions docs/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,37 @@ const nextConfig: NextConfig = {
// through the rewrites below.
skipTrailingSlashRedirect: true,
async rewrites() {
return [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/array/:path*',
destination: 'https://us-assets.i.posthog.com/array/:path*',
},
// Catch-all must come last — the static/array asset rules above must win.
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
];
return {
// Per-page raw Markdown for agents: `/docs/<slug>.md` (and `.mdx`) maps
// to the markdown route handler at `/llms.mdx/<slug>`. Must run in
// `beforeFiles` so it wins before the `/docs/[...slug]` page catch-all,
// which would otherwise match `…/overview.md` as a slug segment and 404.
beforeFiles: [
{
source: '/docs/:path*.md',
destination: '/llms.mdx/:path*',
},
{
source: '/docs/:path*.mdx',
destination: '/llms.mdx/:path*',
},
],
afterFiles: [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/array/:path*',
destination: 'https://us-assets.i.posthog.com/array/:path*',
},
// Catch-all must come last — the static/array asset rules above must win.
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
],
};
},
// HSTS with `includeSubDomains; preload` (Vercel's injected default is
// max-age only). Chrome blocks a download when ANY hop in its redirect
Expand Down
12 changes: 10 additions & 2 deletions docs/src/app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { metaDescription, SITE_NAME, TWITTER_HANDLE } from '@/lib/site';
import { PageMarkdownActions } from '@/components/page-markdown-actions';
import { metaDescription, SITE_NAME, SITE_URL, TWITTER_HANDLE } from '@/lib/site';
import { source } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';

Expand All @@ -20,7 +21,14 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
footer={hideFooter ? { enabled: false } : undefined}
article={hideFooter ? { className: 'pb-12' } : undefined}
>
<DocsTitle>{page.data.title}</DocsTitle>
<div className="flex items-start justify-between gap-4">
<DocsTitle>{page.data.title}</DocsTitle>
<PageMarkdownActions
className="mt-1.5 shrink-0"
markdownPath={`${page.url}.md`}
markdownUrl={`${SITE_URL}${page.url}.md`}
/>
</div>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX components={getMDXComponents()} />
Expand Down
13 changes: 2 additions & 11 deletions docs/src/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { getLLMText } from '@/lib/get-llm-text';
import { source } from '@/lib/source';

export const revalidate = false;

export async function GET() {
const pages = source.getPages();

const scan = pages.map(async (page) => {
const processed = await page.data.getText('processed');

return `# ${page.data.title} (${page.url})

${page.data.description || ''}

${processed}`;
});
const scanned = await Promise.all(scan);
const scanned = await Promise.all(pages.map((page) => getLLMText(page)));

return new Response(scanned.join('\n\n'));
}
58 changes: 58 additions & 0 deletions docs/src/app/llms.mdx/[...slug]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, mock, test } from 'bun:test';

const overviewPage = {
url: '/docs/get-started/overview',
data: {
title: 'Overview',
description: 'What OpenKnowledge is.',
getText: async () => 'PROCESSED BODY',
},
};

mock.module('@/lib/source', () => ({
source: {
getPage: (slug: string[]) =>
slug.join('/') === 'get-started/overview' ? overviewPage : undefined,
generateParams: () => [{ slug: ['get-started', 'overview'] }],
},
}));

mock.module('next/navigation', () => ({
notFound: () => {
throw new Error('NEXT_HTTP_ERROR_FALLBACK;404');
},
}));

const { GET, generateStaticParams } = await import('./route.ts');

function props(slug: string[]) {
return { params: Promise.resolve({ slug }) };
}

describe('GET /docs/<slug>.md (markdown route handler)', () => {
test('serves text/markdown with the page rendered by getLLMText', async () => {
const res = await GET(new Request('https://openknowledge.ai/docs/get-started/overview.md'), {
...props(['get-started', 'overview']),
});
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8');

const body = await res.text();
expect(body).toContain('# Overview (/docs/get-started/overview)');
expect(body).toContain('PROCESSED BODY');
});

test('calls notFound() for an unknown page', async () => {
await expect(
GET(new Request('https://openknowledge.ai/docs/nope.md'), { ...props(['nope']) }),
).rejects.toThrow('404');
});

test('generateStaticParams delegates to the loader and yields slug-shaped params', () => {
const params = generateStaticParams();
expect(params.length).toBeGreaterThan(0);
for (const entry of params) {
expect(Array.isArray(entry.slug)).toBe(true);
}
});
});
23 changes: 23 additions & 0 deletions docs/src/app/llms.mdx/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { notFound } from 'next/navigation';
import { getLLMText } from '@/lib/get-llm-text';
import { source } from '@/lib/source';

export const dynamic = 'force-static';

interface RouteProps {
params: Promise<{ slug: string[] }>;
}

export async function GET(_request: Request, props: RouteProps) {
const { slug } = await props.params;
const page = source.getPage(slug);
if (!page) notFound();

return new Response(await getLLMText(page), {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
}

export function generateStaticParams() {
return source.generateParams();
}
2 changes: 1 addition & 1 deletion docs/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function GET() {
[
'# OpenKnowledge',
'## Docs',
...pages.map((page) => `- [${page.data.title}](${SITE_URL}${page.url})`),
...pages.map((page) => `- [${page.data.title}](${SITE_URL}${page.url}.md)`),
].join('\n\n'),
);
}
105 changes: 105 additions & 0 deletions docs/src/components/page-markdown-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client';

import { Check, ChevronDown, Copy, FileText } from 'lucide-react';
import { useState } from 'react';
import { ClaudeIcon } from '@/components/icons/claude';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';

interface PageMarkdownActionsProps {
markdownPath: string;
markdownUrl: string;
className?: string;
}

const menuContentClass = 'border-fd-border bg-fd-popover text-fd-popover-foreground';
const menuItemClass = 'gap-2 focus:bg-fd-accent focus:text-fd-accent-foreground';

export function PageMarkdownActions({
markdownPath,
markdownUrl,
className,
}: PageMarkdownActionsProps) {
const [copied, setCopied] = useState(false);

const copyMarkdown = async () => {
try {
const res = await fetch(markdownPath);
if (!res.ok) return;
await navigator.clipboard.writeText(await res.text());
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
} catch {}
};

const prompt = `Read ${markdownUrl} so I can ask questions about it.`;
const chatGptUrl = `https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`;
const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;

return (
<div
className={cn(
'not-prose inline-flex w-fit items-stretch overflow-hidden rounded-md border border-fd-border text-fd-muted-foreground text-xs',
className,
)}
>
<button
type="button"
onClick={copyMarkdown}
aria-label={copied ? 'Copied' : 'Copy this page as Markdown'}
data-copied={copied}
className="inline-flex cursor-pointer items-center gap-1.5 px-2.5 py-1 font-medium transition-colors hover:bg-fd-accent hover:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring focus-visible:ring-inset"
>
{copied ? (
<Check className="size-3 text-fd-primary" aria-hidden="true" />
) : (
<Copy className="size-3" aria-hidden="true" />
)}
<span aria-live="polite">{copied ? 'Copied' : 'Copy page'}</span>
</button>

<DropdownMenu>
<DropdownMenuTrigger
aria-label="More Markdown options"
className="inline-flex cursor-pointer items-center border-fd-border border-l px-1 transition-colors hover:bg-fd-accent hover:text-fd-foreground focus-visible:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring focus-visible:ring-inset"
>
<ChevronDown className="size-3.5" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className={menuContentClass}>
<DropdownMenuItem asChild className={menuItemClass}>
<a href={markdownPath} target="_blank" rel="noreferrer">
<FileText className="size-4" aria-hidden="true" />
View as Markdown
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild className={menuItemClass}>
<a href={chatGptUrl} target="_blank" rel="noreferrer">
<span
aria-hidden="true"
className={cn(
'flex size-4 items-center justify-center rounded-full',
'bg-fd-foreground font-bold text-[10px] text-fd-background',
)}
>
AI
</span>
Open in ChatGPT
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild className={menuItemClass}>
<a href={claudeUrl} target="_blank" rel="noreferrer">
{/* Decorative — the "Open in Claude" label is the accessible name. */}
<ClaudeIcon aria-hidden="true" className="size-4 text-[#d97757]" />
Open in Claude
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
48 changes: 48 additions & 0 deletions docs/src/lib/get-llm-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from 'bun:test';
import { getLLMText } from './get-llm-text.ts';

type Page = Parameters<typeof getLLMText>[0];

function fakePage(data: Partial<Page['data']> = {}, url = '/docs/get-started/overview'): Page {
return {
url,
data: {
title: 'Overview',
description: 'What OpenKnowledge is.',
getText: async (type: 'raw' | 'processed') => `BODY(${type})`,
...data,
},
} as unknown as Page;
}

describe('getLLMText', () => {
test('renders title + URL header, description, then processed body', async () => {
const md = await getLLMText(fakePage());
expect(md).toBe(
`# Overview (/docs/get-started/overview)

What OpenKnowledge is.

BODY(processed)`,
);
});

test('requests the processed Markdown variant (snippets resolved), not raw', async () => {
let requested: string | undefined;
await getLLMText(
fakePage({
getText: async (type) => {
requested = type;
return '';
},
}),
);
expect(requested).toBe('processed');
});

test('tolerates a missing description without emitting "undefined"', async () => {
const md = await getLLMText(fakePage({ description: undefined }));
expect(md).not.toContain('undefined');
expect(md).toContain('# Overview (/docs/get-started/overview)');
});
});
12 changes: 12 additions & 0 deletions docs/src/lib/get-llm-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { InferPageType } from 'fumadocs-core/source';
import type { source } from '@/lib/source';

export async function getLLMText(page: InferPageType<typeof source>): Promise<string> {
const processed = await page.data.getText('processed');

return `# ${page.data.title} (${page.url})

${page.data.description || ''}

${processed}`;
}
3 changes: 1 addition & 2 deletions packages/server/src/seed/starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1390,8 +1390,7 @@ export const STARTER_PACKS: Readonly<Record<PackId, StarterPack>> = {
'codebase-wiki': {
id: 'codebase-wiki',
name: 'Codebase wiki',
description:
'An agent-authored, navigable wiki of your codebase — architecture, modules, flows, concepts, and guides, with diagrams, cross-links, and source references. Generated and refreshed by the `wiki` workflow; version-controlled and private by default.',
description: 'Architecture, modules, and flows.',
defaultSubfolder: undefined,
folders: CODEBASE_WIKI_FOLDERS,
templates: CODEBASE_WIKI_TEMPLATES,
Expand Down