Files
Sam Julien 62facacee1 feat(quickstart): add platform signup as Step 1 across integration quickstarts
Every integration quickstart in docs/ and showcase/shell-docs/ now opens with
a "Create a free account" step that points the reader at the Enterprise
Intelligence Platform before the framework path. Existing top-of-page
<OpsPlatformCTA> blocks on the six integrations that already had one are
left in place.

- New <SignupLink surface="docs_<int>_quickstart_step1">…</SignupLink> MDX
  component in both apps. It mirrors OpsPlatformCTA's URL+UTM contract
  (https://dashboard.operations.copilotkit.ai/ with the canonical docs
  UTMs, picked up from NEXT_PUBLIC_INTELLIGENCE_SIGNUP_URL when set) and
  fires the same PostHog event the other CTAs use:
  posthog.capture("try_for_free_clicked", { location: surface }).
- Registered as an MDX global in:
    docs/app/integrations/[[...slug]]/page.tsx
    docs/app/(home)/[[...slug]]/page.tsx
    showcase/shell-docs/src/lib/mdx-registry.tsx
- All 28 integration quickstart .mdx files now lead with a Step that uses
  this component as an inline link inside a single sentence of prose —
  no CTA card inside <Steps>.

The <TailoredContent> "Choose your starting point" / "How do you want to
get started?" selector is now wrapped in its own <Step> so it advances
the counter, and the inner CLI/manual paths render as steps 3, 4, 5, …
instead of 2, 3, 4, …. Applies to all 20 quickstarts that use the
picker.

- Indigo→purple gradient text on the Step 1 heading on both surfaces
  (`.fd-steps > .fd-step:first-child h3` on docs/,
  `.docs-steps > div:first-child h3` on shell-docs). Direct-child
  combinator scopes it to the outer first Step so inner first-children
  inside TailoredContentOption don't pick it up. Bump weight to 700
  and font-size to 1.375rem on docs/ to compensate for the
  background-clip:text rendering path (grayscale AA, no solid fill)
  which makes glyphs look lighter/smaller than the adjacent solid
  600/20px headings.
- Tone down the selected TailoredContent option card on both surfaces
  to a near-grayscale wash (from-slate-50 → to-indigo-50/30) and
  shorten the card itself (smaller padding, smaller icon, smaller
  title; extra left padding for breathing room) so the picker takes
  less vertical space and doesn't compete with the Step 1 gradient
  heading. Indigo ring still does the "selected" signal.
- Bump the tablist's bottom margin in shell-docs (my-2 → mt-2 mb-6)
  so the gap between the picker and the first inner Step matches the
  1.5rem gap that every other consecutive-Step transition uses.
- Black SignupLink color in Step 1 on shell-docs so the link doesn't
  clash with the gradient heading above it.
- Shell-docs: reset margin-top on the first heading inside any Step so
  the badge and heading align, and nudge the badge top from -0.125rem
  to 0.1875rem so its vertical center matches the heading line center.
  Moved the badge's appearance (background/border/color/font-weight)
  out of inline style and into globals.css so :first-child overrides
  can win without fighting inline-style specificity.
2026-05-13 11:38:29 -07:00

256 lines
8.6 KiB
TypeScript

import { source } from "@/app/source";
import type { Metadata } from "next";
import {
DocsPage,
DocsBody,
DocsDescription,
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { Badge } from "@/components/ui/badge";
import { CloudIcon } from "lucide-react";
import { Tabs, Tab } from "@/components/react/tabs";
import { Steps, Step } from "fumadocs-ui/components/steps";
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Pre, CodeBlock } from "fumadocs-ui/components/codeblock";
import { Callout } from "fumadocs-ui/components/callout";
import { Frame } from "@/components/react/frame";
import { Mermaid } from "@theguild/remark-mermaid/mermaid";
import { Cards, Card } from "fumadocs-ui/components/card";
import { PropertyReference } from "@/components/react/property-reference";
import { InsecurePasswordProtected } from "@/components/react/insecure-password-protected";
import { LinkToCopilotCloud } from "@/components/react/link-to-copilot-cloud";
import { OpsPlatformCTA } from "@/components/react/ops-platform-cta";
import { SignupLink } from "@/components/react/signup-link";
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
import { NavigationLink } from "@/components/react/subdocs-menu";
import { getSnippetTOCForPage } from "@/lib/snippet-toc";
import { CustomPager } from "@/components/ui/custom-pager";
import { PageBreadcrumb } from "fumadocs-ui/layouts/docs/page";
import Link from "next/link";
import { cn } from "@/lib/utils";
/**
* TODO: This should be dynamic, but it's not working.
*/
const cloudOnlyFeatures = ["Authenticated Actions", "Guardrails"];
const premiumFeatureTitles = [
"Observability Hooks",
"Error Observability Connectors",
]; // heuristic for pages that import premium snippets
const mdxComponents = {
...defaultMdxComponents,
InsecurePasswordProtected: InsecurePasswordProtected,
LinkToCopilotCloud: LinkToCopilotCloud,
OpsPlatformCTA: OpsPlatformCTA,
SignupLink: SignupLink,
Accordions: Accordions,
Accordion: Accordion,
Tabs: Tabs,
Tab: Tab,
Steps: Steps,
Step: Step,
TypeTable: TypeTable,
Callout: Callout,
Frame: Frame,
Mermaid: Mermaid,
Cards: Cards,
Card: Card,
PropertyReference: PropertyReference,
a: ({ href, children, className, ...props }: any) => {
// Check if link is external (for suppressHydrationWarning)
const isExternal =
href &&
typeof href === "string" &&
(href.startsWith("http://") || href.startsWith("https://"));
// Don't wrap anchor links (hash links) in NavigationLink to avoid nested <a> tags
if (href && typeof href === "string" && href.startsWith("#")) {
return (
<a
href={href}
className={className}
{...props}
suppressHydrationWarning={isExternal}
>
{children}
</a>
);
}
// Don't wrap links that have data-card attribute or peer className (fumadocs heading anchors)
if (
props &&
(props["data-card"] !== undefined || props["data-heading"] !== undefined)
) {
return (
<a
href={href}
className={className}
{...props}
suppressHydrationWarning={isExternal}
>
{children}
</a>
);
}
// Don't wrap links with 'peer' className (fumadocs uses this for heading anchor links)
if (
className &&
typeof className === "string" &&
className.includes("peer")
) {
return (
<a
href={href}
className={className}
{...props}
suppressHydrationWarning={isExternal}
>
{children}
</a>
);
}
return (
<NavigationLink href={href as string} className={className} {...props}>
{children}
</NavigationLink>
);
},
// HTML `ref` attribute conflicts with `forwardRef`
pre: ({ ref: _ref, ...props }: any) => (
<CodeBlock {...props}>
<Pre>{props.children}</Pre>
</CodeBlock>
),
};
export default async function Page({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const resolvedParams = await params;
const page = source.getPage(["integrations", ...(resolvedParams.slug || [])]);
if (!page) notFound();
const MDX = page.data.body;
const cloudOnly = cloudOnlyFeatures.includes(page.data.title);
// Consider a page "Premium" if its slug path contains a "premium" segment OR title matches known premium features OR frontmatter premium flag
const bySlugPremium = Array.isArray(page.slugs)
? page.slugs.includes("premium")
: false;
const byTitlePremium = premiumFeatureTitles.includes(page.data.title || "");
const byFrontmatterPremium = Boolean((page as any).data?.premium);
const isPremium = bySlugPremium || byTitlePremium || byFrontmatterPremium;
// Compute premium overview href based on current section (first slug segment)
const baseSegment =
Array.isArray(page.slugs) && page.slugs.length ? `/${page.slugs[0]}` : "/";
const premiumOverviewHref =
baseSegment === "/"
? "/premium/overview"
: `${baseSegment}/premium/overview`;
// Check if the page should hide the header or TOC
const hideHeader = (page.data as any).hideHeader || false;
const hideTOC = (page.data as any).hideTOC || false;
// Get TOC from imported snippets and merge with page TOC (only if TOC is not hidden)
// Use try-catch to handle build-time issues gracefully
let snippetTOC: any[] = [];
if (!hideTOC) {
try {
snippetTOC = await getSnippetTOCForPage([
"integrations",
...(resolvedParams.slug || []),
]);
} catch (error) {
console.warn("Failed to load snippet TOC:", error);
snippetTOC = [];
}
}
const combinedTOC = hideTOC ? [] : [...(page.data.toc || []), ...snippetTOC];
return (
<DocsPage
toc={combinedTOC}
full={page.data.full}
footer={{ component: <CustomPager tree={source.pageTree} page={page} /> }}
tableOfContent={{
style: "clerk",
}}
breadcrumb={{ enabled: false }}
>
<div className="px-8 py-6 xl:py-12 xl:px-16 max-sm:px-4 max-sm:py-6">
<PageBreadcrumb className="mb-4" />
<div>
{!hideHeader && (
<div className="gap-5 flex flex-col">
<div className="flex gap-3 items-center">
<DocsTitle className="flex items-center text-[32px] font-medium text-[#010507] dark:text-white md:text-[40px] leading-12">
{page.data.title}
{cloudOnly && (
<Badge
variant="secondary"
className="ml-3 mt-1 inline-flex items-center gap-1.5 py-1.5 px-3 bg-indigo-600/90 text-white hover:bg-indigo-600 border-0 rounded-md transition-colors"
>
<CloudIcon className="w-3 h-3" />
<span className="text-xs">Cloud Only</span>
</Badge>
)}
{isPremium && (
<a href={premiumOverviewHref} className="ml-3">
<Badge
variant="secondary"
className="inline-flex items-center gap-2 py-2 px-3.5 bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-200 hover:bg-indigo-200 border-0 rounded-md transition-colors"
>
<img
src="https://cdn.copilotkit.ai/docs/copilotkit/icons/copilotkit-color.svg"
alt="CopilotKit"
className="w-4 h-4"
/>
<span className="text-sm font-semibold tracking-tight">
Premium
</span>
</Badge>
</a>
)}
</DocsTitle>
</div>
<DocsDescription>{page.data.description}</DocsDescription>
</div>
)}
<DocsBody>
<MDX components={mdxComponents} />
</DocsBody>
</div>
</div>
</DocsPage>
);
}
export async function generateStaticParams() {
return source
.generateParams()
.filter((params) => params.slug && params.slug[0] === "integrations")
.map((params) => ({
slug: params.slug?.slice(1) || [],
}));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const resolvedParams = await params;
const page = source.getPage(["integrations", ...(resolvedParams.slug || [])]);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
} satisfies Metadata;
}