Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch origin "$BASE_REF" --depth=1
if git diff --name-only "origin/$BASE_REF"...HEAD | grep '^packages/' | grep -qEv '\.md$|^packages/chat/resources/'; then
if git diff --name-only "origin/$BASE_REF"...HEAD | grep '^packages/' | grep -qEv '\.md$|^packages/chat/resources/|^packages/integration-tests/'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/app/[lang]/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ const heroDescription =
"A unified TypeScript SDK for building chat bots with type-safe handlers, JSX cards, and multi-platform support—powered by Vercel";

export const metadata: Metadata = {
title: metadataTitle,
title: { absolute: metadataTitle },
description: heroDescription,
openGraph: {
title: { absolute: metadataTitle },
},
twitter: {
card: "summary_large_image",
},
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/app/[lang]/(home)/resources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const metadata: Metadata = {
"ai agent",
"vercel",
],
openGraph: {
title: "Resources",
},
twitter: {
card: "summary_large_image",
},
Expand Down
43 changes: 43 additions & 0 deletions apps/docs/app/[lang]/adapters.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { notFound } from "next/navigation";
import { getAdapter, getReadme } from "@/lib/geistdocs/adapter-readme";
import {
adaptersSource,
getAdapterLLMText,
} from "@/lib/geistdocs/adapters-source";

export const revalidate = false;

export async function GET(
_req: Request,
{ params }: RouteContext<"/[lang]/adapters.mdx/[[...slug]]">
) {
const { slug, lang } = await params;
const page = adaptersSource.getPage(slug, lang);

if (!page) {
notFound();
}

// Official adapters render their MDX body directly. Community and
// vendor-official adapters render an upstream README (unless they opt into an
// MDX body), so serve that same content in the markdown version.
const type = slug?.[0];
let body: string | undefined;
if (type !== "official") {
const data = page.data as unknown as { mdxBody?: boolean; slug: string };
if (data.mdxBody !== true) {
const adapter = getAdapter(data.slug);
if (adapter) {
body = await getReadme(adapter);
}
}
}

return new Response(await getAdapterLLMText(page, body), {
headers: {
"Content-Type": "text/markdown",
},
});
}

export const generateStaticParams = () => adaptersSource.generateParams();
127 changes: 24 additions & 103 deletions apps/docs/app/[lang]/adapters/(detail)/community/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import type { ComponentProps, FC } from "react";
import adaptersJson from "@/adapters.json";
import { AdapterHero } from "@/components/geistdocs/adapter-hero";
import { DocsBody, DocsPage } from "@/components/geistdocs/docs-page";
import { FeatureSupport } from "@/components/geistdocs/feature-support";
import { getMDXComponents } from "@/components/geistdocs/mdx-components";
import { Upsell } from "@/components/geistdocs/upsell";
import type { AdapterFeatureValue } from "@/lib/adapter-features";
import {
type Adapter,
getAdapter,
getIssuesUrl,
getReadme,
} from "@/lib/geistdocs/adapter-readme";
import { adaptersSource } from "@/lib/geistdocs/adapters-source";
import { ReadmeContent } from "../../../components/readme-content";

Expand All @@ -33,105 +36,6 @@ const wrapWithNofollow = (BaseLink: FC<MdxLinkProps>): FC<MdxLinkProps> => {
return NofollowExternalLink;
};

const LOCAL_PACKAGE_PATTERN = /github\.com\/vercel\/chat\/tree\/[^/]+\/(.+)/;
const GITHUB_SUBPATH_PATTERN =
/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/;
const GITHUB_REPO_REF_PATTERN =
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/?$/;
const GITHUB_REPO_PATTERN = /github\.com\/([^/]+)\/([^/]+)/;
const GITHUB_REPO_ROOT_PATTERN = /^(https:\/\/github\.com\/[^/]+\/[^/]+)/;

const UNPINNED_REF_PATTERN = /^(main|master|head|dev|develop|trunk|default)$/i;

const MAX_README_BYTES = 500_000;

type Adapter = (typeof adaptersJson)[number];

const getAdapter = (slug: string): Adapter | undefined =>
adaptersJson.find((a) => a.slug === slug);

const getIssuesUrl = (readmeUrl: string | undefined): string | undefined => {
if (!readmeUrl) {
return;
}
const match = readmeUrl.match(GITHUB_REPO_ROOT_PATTERN);
return match ? `${match[1]}/issues` : undefined;
};

const warnUnpinned = (adapter: Adapter, ref: string | undefined) => {
if (ref && !UNPINNED_REF_PATTERN.test(ref)) {
return;
}
console.warn(
`[adapters] Community adapter "${adapter.name}" uses an unpinned README ref "${
ref ?? "<default branch>"
}". Pin to a commit SHA or tag in adapters.json to freeze content at review time.`
);
};

const truncate = (content: string): string =>
content.length <= MAX_README_BYTES
? content
: `${content.slice(0, MAX_README_BYTES)}\n\n> _README truncated — view the full version on GitHub._`;

const fetchGitHubReadme = async (url: string): Promise<string | undefined> => {
const response = await fetch(url, {
headers: { Accept: "application/vnd.github.raw+json" },
next: { revalidate: 3600 },
});
if (response.ok) {
return response.text();
}
};

const getReadme = async (adapter: Adapter): Promise<string | undefined> => {
if (!adapter.readme) {
return;
}
const repoUrl = adapter.readme;

const localMatch = repoUrl.match(LOCAL_PACKAGE_PATTERN);
if (localMatch) {
const [, pkgPath] = localMatch;
const filePath = join(process.cwd(), "..", "..", pkgPath, "README.md");
try {
return truncate(await readFile(filePath, "utf-8"));
} catch {
return;
}
}

const subpathMatch = repoUrl.match(GITHUB_SUBPATH_PATTERN);
if (subpathMatch) {
const [, owner, repo, ref, path] = subpathMatch;
warnUnpinned(adapter, ref);
const content = await fetchGitHubReadme(
`https://api.github.com/repos/${owner}/${repo}/readme/${path}?ref=${ref}`
);
return content ? truncate(content) : undefined;
}

const repoRefMatch = repoUrl.match(GITHUB_REPO_REF_PATTERN);
if (repoRefMatch) {
const [, owner, repo, ref] = repoRefMatch;
warnUnpinned(adapter, ref);
const content = await fetchGitHubReadme(
`https://api.github.com/repos/${owner}/${repo}/readme?ref=${ref}`
);
return content ? truncate(content) : undefined;
}

const repoMatch = repoUrl.match(GITHUB_REPO_PATTERN);
if (repoMatch) {
const [, owner, repo] = repoMatch;
warnUnpinned(adapter, undefined);
const content = await fetchGitHubReadme(
`https://api.github.com/repos/${owner}/${repo}/readme`
);
return content ? truncate(content) : undefined;
}
};

const CommunityNotice = ({ adapter }: { adapter: Adapter }) => {
const issuesUrl = getIssuesUrl(adapter.readme);

Expand Down Expand Up @@ -194,10 +98,11 @@ const Page = async ({ params }: { params: Promise<PageParams> }) => {

const data = page.data as unknown as AdapterFrontmatter;
const adapter = getAdapter(slug);
const markdownPath = `/adapters/community/${slug}.md`;
const useMdxBody = data.mdxBody === true;
let readme: string | undefined;
if (!useMdxBody && adapter) {
readme = await getReadme(adapter);
readme = await getReadme(adapter, { warnOnUnpinnedRef: true });
}
const MDX = page.data.body;
const BoundFeatureSupport = renderBoundFeatureSupport(
Expand Down Expand Up @@ -253,6 +158,16 @@ const Page = async ({ params }: { params: Promise<PageParams> }) => {
toc={page.data.toc}
>
<DocsBody>
{/* biome-ignore lint/a11y/useAnchorContent: intentionally aria-hidden hint surfacing the markdown URL for AI/LLM crawlers, not for screen readers */}
<a
aria-hidden="true"
className="sr-only"
href={markdownPath}
tabIndex={-1}
>
AI/LLM: This adapter page is available in plain markdown format at
{markdownPath}
</a>
<AdapterHero
community
logo={data.logo}
Expand Down Expand Up @@ -289,11 +204,17 @@ export const generateMetadata = async ({
title: page.data.title,
description: page.data.description,
openGraph: {
title: page.data.title,
images: `/${lang}/adapters/community/${slug}/og`,
},
twitter: {
card: "summary_large_image",
},
alternates: {
types: {
"text/markdown": `/adapters/community/${slug}.md`,
},
},
};
};

Expand Down
17 changes: 17 additions & 0 deletions apps/docs/app/[lang]/adapters/(detail)/official/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Page = async ({ params }: { params: Promise<PageParams> }) => {

const data = page.data as unknown as AdapterFrontmatter;
const MDX = page.data.body;
const markdownPath = `/adapters/official/${slug}.md`;
const BoundFeatureSupport = renderBoundFeatureSupport(
data.features,
data.type
Expand All @@ -64,6 +65,16 @@ const Page = async ({ params }: { params: Promise<PageParams> }) => {
toc={page.data.toc}
>
<DocsBody>
{/* biome-ignore lint/a11y/useAnchorContent: intentionally aria-hidden hint surfacing the markdown URL for AI/LLM crawlers, not for screen readers */}
<a
aria-hidden="true"
className="sr-only"
href={markdownPath}
tabIndex={-1}
>
AI/LLM: This adapter page is available in plain markdown format at
{markdownPath}
</a>
<AdapterHero
beta={data.beta}
logo={data.logo}
Expand Down Expand Up @@ -104,11 +115,17 @@ export const generateMetadata = async ({
title: page.data.title,
description: page.data.description,
openGraph: {
title: page.data.title,
images: `/${lang}/adapters/official/${slug}/og`,
},
twitter: {
card: "summary_large_image",
},
alternates: {
types: {
"text/markdown": `/adapters/official/${slug}.md`,
},
},
};
};

Expand Down
Loading