mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
8a872529fe
* docs: make /worlds the canonical home for World docs The world pages (Local/Postgres/Vercel) and Building a World were duplicated inside the v4 and v5 docs trees while /worlds/[id] rendered the v4 copy — hiding v5-only content like multi-region and leaving two diverging sources of truth. - Move world docs to an unversioned docs/content/worlds/ collection (based on the v5 copies, with inline 4.x callouts for factory naming and 5.x-only env vars), rendered at /worlds/* - Add /worlds/building-a-world; flatten the docs Deploying section to a single intro page and drop its Rocket icon - Point every link, frontmatter ref, and worlds-manifest docs field at /worlds/*; add redirects for the removed v5 and building-a-world URLs - Keep world docs on agent-facing surfaces: search, llms.txt, sitemap.md/.xml, and .md exports now serve the worlds collection - Extend the docs link linter to validate worlds pages (with heading anchors) and their outgoing links Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: version the world docs like the docs trees (v4/v5 switcher) Instead of a single unversioned copy, world docs now follow the same versioning strategy as the docs pages: content/worlds/v4 is served at /worlds/* (current) and content/worlds/v5 at /v5/worlds/*, restoring the original per-version content. Each world detail page (and Building a World) renders the docs version switcher — the worlds listing page has no natural home for it, so it lives on the world pages themselves. - Render-time href rewriting on v5 pages now covers /worlds/... links (shared rewriteHrefForVersion helper, also used by the v5 docs and cookbook routes), and the markdown-export rewrite does the same - v5 world pages are noindexed with a canonical to /worlds/<id>; community worlds stay unversioned (/v5/worlds/<id> redirects) - /v5/docs/deploying/world/* redirects now land on /v5/worlds/*; /v5/worlds and /v5/worlds/compare redirect to the unversioned pages - Link linter models the versioned worlds URL spaces (v5 pages resolve /worlds hrefs against the v5 collection); sitemap.md and the .md export routes cover /v5/worlds/* Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: fix v4 multi-region anchor and tighten version-prefix matching Address PR review: - The v4 Deploying page linked /worlds/vercel#multi-region, but the Multi-region section only exists on the v5 world page; use the explicit cross-version /v5/worlds/vercel#multi-region link (this was the Docs Links CI failure) - rewriteHrefForVersion now uses the boundary-checked hasPathPrefix (shared leaf module lib/geistdocs/path-prefix.ts, also used by source.ts) instead of bare startsWith - buildVersionUrl's shared-route fast path is segment-based rather than substring includes() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
218 lines
6.7 KiB
TypeScript
218 lines
6.7 KiB
TypeScript
import type { TableOfContents } from 'fumadocs-core/toc';
|
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
|
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
|
import type { Metadata } from 'next';
|
|
import { notFound, redirect } from 'next/navigation';
|
|
import type { ComponentProps, ComponentType, ReactNode } from 'react';
|
|
import { FluidComputeCallout } from '@/components/custom/fluid-compute-callout';
|
|
import { getMDXComponents } from '@/components/geistdocs/mdx-components';
|
|
import { v5WorldsSource, worldsSource } from '@/lib/geistdocs/source';
|
|
import { rewriteHrefForVersion } from '@/lib/geistdocs/version-href';
|
|
import type { DocsVersionId } from '@/lib/geistdocs/versions';
|
|
import { getWorldData } from '@/lib/worlds-data';
|
|
import { WorldDataProvider } from './WorldDataProvider';
|
|
import { WorldDetailHero } from './WorldDetailHero';
|
|
import { WorldDetailToc } from './WorldDetailToc';
|
|
import { WorldInstructions } from './WorldInstructions';
|
|
import { WorldTestingPerformance } from './WorldTestingPerformance';
|
|
import { WorldTestingPerformanceMDX } from './WorldTestingPerformanceMDX';
|
|
import { WorldVersionSelect } from './WorldVersionSelect';
|
|
|
|
const isPreview = process.env.VERCEL_ENV === 'preview';
|
|
|
|
/** MDX wrapper — passes preview gate to benchmark section */
|
|
const WorldTestingPerformanceForMDX = (props: Record<string, unknown>) => (
|
|
<WorldTestingPerformanceMDX {...props} showBenchmarks={isPreview} />
|
|
);
|
|
|
|
// Official worlds with a page in the content/worlds/<version> collections
|
|
const officialWorldMdxSlugs: Record<string, string[]> = {
|
|
local: ['local'],
|
|
postgres: ['postgres'],
|
|
vercel: ['vercel'],
|
|
};
|
|
|
|
const VERSION_SOURCES = {
|
|
v4: worldsSource,
|
|
v5: v5WorldsSource,
|
|
} as const;
|
|
|
|
const VERSION_PREFIXES = {
|
|
v4: '',
|
|
v5: '/v5',
|
|
} as const;
|
|
|
|
export const officialWorldIds = Object.keys(officialWorldMdxSlugs);
|
|
|
|
export async function generateWorldMetadata(
|
|
id: string,
|
|
version: DocsVersionId
|
|
): Promise<Metadata> {
|
|
const data = await getWorldData(id);
|
|
|
|
if (!data) {
|
|
return {
|
|
title: 'World Not Found',
|
|
};
|
|
}
|
|
|
|
const versionPrefix = VERSION_PREFIXES[version];
|
|
const isPreRelease = version === 'v5';
|
|
|
|
return {
|
|
title: `${data.world.name} World${isPreRelease ? ' · Pre-release' : ''} | Workflow SDK`,
|
|
description: data.world.description,
|
|
openGraph: {
|
|
images: [`/og/worlds/${id}`],
|
|
},
|
|
alternates: {
|
|
canonical: `/worlds/${id}`,
|
|
types: {
|
|
'text/markdown': `${versionPrefix}/worlds/${id}.md`,
|
|
},
|
|
},
|
|
...(isPreRelease
|
|
? {
|
|
robots: {
|
|
index: false,
|
|
follow: true,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export async function WorldDetailPage({
|
|
id,
|
|
version,
|
|
}: {
|
|
id: string;
|
|
version: DocsVersionId;
|
|
}) {
|
|
const data = await getWorldData(id);
|
|
|
|
if (!data) {
|
|
notFound();
|
|
}
|
|
|
|
const { world, meta } = data;
|
|
|
|
// For official worlds, load MDX content and extract TOC
|
|
const isOfficial = world.type === 'official' && officialWorldMdxSlugs[id];
|
|
|
|
// Community worlds have no versioned content — their canonical page lives
|
|
// at /worlds/<id> only.
|
|
if (version !== 'v4' && !isOfficial) {
|
|
redirect(`/worlds/${id}`);
|
|
}
|
|
|
|
const source = VERSION_SOURCES[version];
|
|
const versionPrefix = VERSION_PREFIXES[version];
|
|
|
|
let mdxContent: ReactNode = null;
|
|
let tocItems: { id: string; title: ReactNode }[] = [];
|
|
|
|
if (isOfficial) {
|
|
const slugs = officialWorldMdxSlugs[id];
|
|
const page = source.getPage(slugs);
|
|
|
|
if (page) {
|
|
const pageData = page.data as typeof page.data & {
|
|
body: ComponentType<{ components?: Record<string, unknown> }>;
|
|
toc: TableOfContents;
|
|
};
|
|
const MDX = pageData.body;
|
|
|
|
// Extract TOC from MDX headings (only h2s, not h3s)
|
|
tocItems = pageData.toc
|
|
.filter((item) => item.depth === 2)
|
|
.map((item) => ({
|
|
id: item.url.slice(1), // Remove leading #
|
|
title: item.title,
|
|
}));
|
|
|
|
// Content links are authored against the raw /docs/... and /worlds/...
|
|
// URL spaces; on the pre-release route they are rewritten into the /v5
|
|
// view so navigation doesn't escape to the current-version pages.
|
|
const RelativeLink = createRelativeLink(source, page);
|
|
const VersionedLink = (props: ComponentProps<'a'>) => (
|
|
<RelativeLink
|
|
{...props}
|
|
href={rewriteHrefForVersion(props.href, versionPrefix)}
|
|
/>
|
|
);
|
|
|
|
mdxContent = (
|
|
<MDX
|
|
components={getMDXComponents({
|
|
a: VersionedLink,
|
|
Step,
|
|
Steps,
|
|
Tabs,
|
|
Tab,
|
|
FluidComputeCallout,
|
|
WorldTestingPerformance: WorldTestingPerformanceForMDX,
|
|
})}
|
|
/>
|
|
);
|
|
}
|
|
} else {
|
|
// Community worlds use hardcoded TOC
|
|
tocItems = [
|
|
{ id: 'installation', title: 'Installation & Usage' },
|
|
{ id: 'testing', title: 'Testing & Compatibility' },
|
|
];
|
|
}
|
|
|
|
return (
|
|
<WorldDataProvider worldId={id} world={world} meta={meta}>
|
|
<div className="[&_h1]:tracking-tighter [&_h2]:tracking-tighter [&_h3]:tracking-tighter">
|
|
<div className="mx-auto w-full max-w-[1080px] px-4">
|
|
{/* Hero Section */}
|
|
<div className="mt-[var(--fd-nav-height)]">
|
|
<WorldDetailHero
|
|
id={id}
|
|
world={world}
|
|
versionSelect={
|
|
isOfficial ? <WorldVersionSelect current={version} /> : null
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content + TOC Grid */}
|
|
<div className="relative grid grid-cols-1 lg:grid-cols-[1fr_200px] gap-8 lg:gap-12">
|
|
{/* Main Content */}
|
|
<main className="min-w-0">
|
|
{isOfficial && mdxContent ? (
|
|
// Official worlds: MDX controls the entire content structure
|
|
<div className="py-8 sm:py-12 prose prose-neutral dark:prose-invert max-w-none">
|
|
{mdxContent}
|
|
</div>
|
|
) : (
|
|
// Community worlds: use template components
|
|
<>
|
|
<WorldInstructions id={id} world={world} />
|
|
<WorldTestingPerformance
|
|
worldId={id}
|
|
world={world}
|
|
meta={meta}
|
|
showBenchmarks={isPreview}
|
|
/>
|
|
</>
|
|
)}
|
|
</main>
|
|
|
|
{/* TOC Sidebar - sticky on desktop, hidden on mobile */}
|
|
<aside className="hidden lg:block pt-8 sm:pt-12">
|
|
<div className="sticky top-24">
|
|
<WorldDetailToc items={tocItems} />
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</WorldDataProvider>
|
|
);
|
|
}
|