Files
Ben Sabic 79227ae991 docs: refresh adapter pages with hand-authored MDX (#474)
## Summary

Refreshes the adapter docs end-to-end so every adapter — official,
vendor-official, and community — now ships hand-authored MDX, lives
under a clean URL structure, and renders on a polished
sidebar/right-rail layout dedicated to `/adapters` (the shared `/docs`
chrome is untouched).

```mermaid
flowchart LR
  subgraph Before
    direction TB
    OB[official] --> CB[community<br/>incl. 5 vendor pages]
  end
  subgraph After
    direction TB
    OA[official] --> VA[vendor-official<br/>5 pages] --> CA[community]
  end
  Before -.-> After
```

### Content & routing

- **New `/adapters/vendor-official/<slug>` route** for vendor-maintained
adapters (Beeper Matrix, Photon iMessage, Liveblocks, Resend, Zernio).
Sidebar gets a third labelled group ("Vendor-Official Adapters") between
Official and Community, with a top divider matching the existing
Community treatment.
- **All 13 vendor-official + community adapters migrated** from runtime
README fetching to hand-authored MDX with rich `features:` matrices and
full body content (install, quick start, configuration, auth,
gateway/streaming, troubleshooting). README fetch stays as a fallback
for any future community adapter that hasn't been migrated yet, gated by
a new `mdxBody: true` frontmatter flag.
- **Messenger filter pages removed** (`/adapters/for/<messenger>` + the
"Browse by messenger" chip row on `/adapters`). Existing URLs
308-redirect to `/adapters`.
- **Permanent redirects** from
`/adapters/community/{matrix,imessage,resend,zernio,liveblocks}` to
their new `/adapters/vendor-official/...` paths.
- **Fixed** `/docs/adapters` and `/docs/state` so the bare pages are
accessible again — the previous catch-all redirect (`:slug*`) was
swallowing them. Switched to `:slug+` so subpath URLs still 308 while
the bare pages render.

### Visual polish

- **Adapter-only sidebar variant** (`AdaptersDocsLayout` +
`AdaptersSidebar`) with uppercase eyebrow separators, tighter rows, and
a thin themed scrollbar utility class. The shared `/docs` sidebar is
untouched.
- **Restyled `AdapterHero`**: drops the badges row + packageName, sits
the title inline with the logo, larger 17 px tagline, horizontal divider
beneath the block.
- **Restyled `PackageInstall`** as a tabbed dark single-line snippet
with a `$` prompt prefix and a copy button — replaces the previous
multi-line `CodeBlock` layout.
- **New "Deploy your chat app on Vercel" upsell card** (`<Upsell />`)
replaces the old `EditSource / ScrollTop / Feedback / CopyPage` footer
cluster on every adapter detail page.
- **Listing & messenger pages**: align the H1 to a tighter `text-4xl
sm:text-[44px]`, and the section headers to `text-base font-medium
tracking-tight` with a one-line muted lede.

### Tooling & tests

- Added `mdxBody: true` opt-in to the adapter frontmatter schema
(`source.config.ts`), and updated both detail-page handlers
(`community/[slug]` and the new `vendor-official/[slug]`) to render the
MDX body when present, falling back to README fetch otherwise.
- Refactored both detail-page handlers to flatten the body-render
branches into a `renderBody()` helper, removing the nested ternaries
that were tripping `lint/style/noNestedTernary`.
- New test file
[`packages/integration-tests/src/docs-adapters.test.ts`](https://github.com/vercel/chat/blob/docs/refresh-adapters/packages/integration-tests/src/docs-adapters.test.ts)
— **220 new assertions** covering:
- Adapter MDX frontmatter completeness, slug ↔ filename consistency, and
`type ∈ {platform, state}`.
- Vendor-official invariants: exactly the expected slugs,
`vendorOfficial: true`, `community: true`, `author`, `mdxBody: true`,
`<FeatureSupport />` rendered.
- Community invariants: `community: true` (never vendor-official),
`mdxBody: true`, `<FeatureSupport />`.
- Official invariants: never flagged, `packageName` always under
`@chat-adapter/*`.
- `adapters.json` ↔ MDX sync on `packageName` / `type` / `community` /
`vendorOfficial`.
- Extended `VALID_DOC_PACKAGES` so `docs-content.test.ts` accepts the
new vendor-official + community packages, plus `@chat-adapter/web`,
`@chat-adapter/web/react`, and `@chat-adapter/messenger`.

### Per-package AGENTS.md

- Added `AGENTS.md` to every official adapter and state adapter (14
packages), each tailored to that adapter's surface — overview, directory
layout, build/test commands, public exports, thread ID format, webhook
flow, authentication, format conversion, cards/streaming, platform
quirks, testing approach, coding conventions, and release rules.
- Added a one-line `CLAUDE.md` (`@AGENTS.md`) beside each so Claude Code
picks up the same instructions through its built-in resolver — same
convention as the root.

### Web adapter copy

- Cleaned up the Web adapter tagline (removed inline backticks) and
dropped the now-redundant "v1 scope" section from the body.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-05-12 16:01:19 +10:00

150 lines
4.7 KiB
TypeScript

"use client";
import type { Node } from "fumadocs-core/page-tree";
import {
SidebarFolder,
SidebarFolderContent,
SidebarFolderLink,
SidebarFolderTrigger,
SidebarItem,
SidebarSeparator,
} from "fumadocs-ui/components/sidebar/base";
import type { SidebarPageTreeComponents } from "fumadocs-ui/components/sidebar/page-tree";
import { useTreeContext, useTreePath } from "fumadocs-ui/contexts/tree";
import { usePathname } from "next/navigation";
import { Fragment, useEffect, useRef } from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { useSidebarContext } from "@/hooks/geistdocs/use-sidebar";
import { cn } from "@/lib/utils";
import { SearchButton } from "./search";
const SUB_SEPARATOR_NAMES = new Set(["Platforms", "State"]);
export const Sidebar = () => {
const { root } = useTreeContext();
const { isOpen, setIsOpen } = useSidebarContext();
const pathname = usePathname();
const previousPathname = useRef(pathname);
useEffect(() => {
if (pathname !== previousPathname.current) {
setIsOpen(false);
previousPathname.current = pathname;
}
}, [pathname, setIsOpen]);
const renderSidebarList = (items: Node[]) =>
items.map((item) => {
if (item.type === "separator") {
return <Separator item={item} key={item.$id} />;
}
if (item.type === "folder") {
const children = renderSidebarList(item.children);
return (
<Folder item={item} key={item.$id}>
{children}
</Folder>
);
}
return <Item item={item} key={item.$id} />;
});
return (
<div
className="pointer-events-none sticky top-(--fd-docs-row-1) z-20 h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] [grid-area:sidebar] *:pointer-events-auto max-md:hidden md:layout:[--fd-sidebar-width:268px]"
data-sidebar-placeholder
>
<div className="h-full overflow-y-auto px-4 pt-12 pb-4">
<Fragment key={root.$id}>{renderSidebarList(root.children)}</Fragment>
</div>
<Sheet onOpenChange={setIsOpen} open={isOpen}>
<SheetContent className="gap-0" side="left">
<SheetHeader className="mt-8">
<SheetTitle className="sr-only">Mobile Menu</SheetTitle>
<SheetDescription className="sr-only">
Navigation for the documentation.
</SheetDescription>
<SearchButton onClick={() => setIsOpen(false)} />
</SheetHeader>
<div className="flex-1 overflow-y-auto px-4 pb-4">
{renderSidebarList(root.children)}
</div>
</SheetContent>
</Sheet>
</div>
);
};
const TOP_DIVIDER_FOLDER_NAMES = new Set(["Community Adapters"]);
export const Folder: SidebarPageTreeComponents["Folder"] = ({
children,
item,
}) => {
const path = useTreePath();
const defaultOpen = item.defaultOpen ?? path.includes(item);
const hasTopDivider =
typeof item.name === "string" && TOP_DIVIDER_FOLDER_NAMES.has(item.name);
return (
<SidebarFolder
className={cn(hasTopDivider && "mt-4 border-t pt-4")}
defaultOpen={defaultOpen}
>
{item.index ? (
<SidebarFolderLink
className="flex items-center gap-2 text-pretty py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground data-[active=true]:text-foreground [&_svg]:size-3.5"
external={item.index.external}
href={item.index.url}
>
{item.icon}
{item.name}
</SidebarFolderLink>
) : (
<SidebarFolderTrigger className="flex items-center gap-2 text-pretty py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground [&_svg]:size-3.5">
{item.icon}
{item.name}
</SidebarFolderTrigger>
)}
<SidebarFolderContent className="ml-2">{children}</SidebarFolderContent>
</SidebarFolder>
);
};
export const Item: SidebarPageTreeComponents["Item"] = ({ item }) => (
<SidebarItem
className="block w-full truncate text-pretty py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground data-[active=true]:text-foreground"
external={item.external}
href={item.url}
icon={item.icon}
>
{item.name}
</SidebarItem>
);
export const Separator: SidebarPageTreeComponents["Separator"] = ({ item }) => {
const isSub =
typeof item.name === "string" && SUB_SEPARATOR_NAMES.has(item.name);
return (
<SidebarSeparator
className={cn(
"mt-4 mb-2 flex items-center gap-2 px-0 font-medium text-sm first-child:mt-0",
isSub &&
"mt-3 mb-1 text-muted-foreground/60 text-xs uppercase tracking-wide"
)}
>
{item.icon}
{item.name}
</SidebarSeparator>
);
};