diff --git a/apps/code/src/components/profile/ProfileView.tsx b/apps/code/src/components/profile/ProfileView.tsx
index 4c240e6ed5..57018fe84e 100644
--- a/apps/code/src/components/profile/ProfileView.tsx
+++ b/apps/code/src/components/profile/ProfileView.tsx
@@ -1,6 +1,7 @@
"use client";
import { Calendar, Coins, Flame, Github, Hash, Zap } from "lucide-react";
+import dynamic from "next/dynamic";
import Link from "next/link";
import {
@@ -14,7 +15,6 @@ import {
ProfileShareActions,
XIcon,
} from "@/components/profile/ProfileShareActions";
-import { ProfileTokensChart } from "@/components/profile/ProfileTokensChart";
import { ProfileViewerCta } from "@/components/profile/ProfileViewerCta";
import { ProfileWrapped } from "@/components/profile/ProfileWrapped";
import { useAppConfig } from "@/lib/config";
@@ -27,6 +27,16 @@ import type { paths } from "@/lib/api/v1";
export type ProfileData =
paths["/user/profile"]["get"]["responses"][200]["content"]["application/json"]["profile"];
+// The tokens chart pulls in recharts and renders below the fold, so keep it
+// out of the profile page's initial bundle.
+const ProfileTokensChart = dynamic(
+ () =>
+ import("@/components/profile/ProfileTokensChart").then(
+ (mod) => mod.ProfileTokensChart,
+ ),
+ { ssr: false, loading: () =>
},
+);
+
const AGENT_BY_SOURCE = new Map();
for (const agent of AGENTS) {
for (const source of agent.sources) {
diff --git a/apps/code/src/components/providers.tsx b/apps/code/src/components/providers.tsx
index 5cf3cb1397..664e0dc931 100644
--- a/apps/code/src/components/providers.tsx
+++ b/apps/code/src/components/providers.tsx
@@ -5,13 +5,12 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ThemeProvider } from "next-themes";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
-import { useEffect, useMemo } from "react";
+import { useEffect, useState } from "react";
import { Toaster } from "@/components/ui/sonner";
import { AppConfigProvider } from "@/lib/config";
import type { AppConfig } from "@/lib/config-server";
-import type { PostHogConfig } from "posthog-js";
import type { ReactNode } from "react";
interface ProvidersProps {
@@ -20,7 +19,9 @@ interface ProvidersProps {
}
export function Providers({ children, config }: ProvidersProps) {
- const queryClient = useMemo(
+ // useState, not useMemo: React may discard a useMemo cache, which would
+ // silently swap in a fresh QueryClient and drop the whole query cache.
+ const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
@@ -31,20 +32,6 @@ export function Providers({ children, config }: ProvidersProps) {
},
},
}),
- [],
- );
-
- const posthogOptions = useMemo>(
- () => ({
- // Ingest through our own origin (see the /ingest rewrites in
- // next.config.ts) so ad blockers that block *.posthog.com don't
- // silently drop client events.
- api_host: "/ingest",
- ui_host: config.posthogHost,
- capture_pageview: "history_change",
- autocapture: true,
- }),
- [config.posthogHost],
);
useEffect(() => {
@@ -52,8 +39,20 @@ export function Providers({ children, config }: ProvidersProps) {
return;
}
const key = config.posthogKey;
+ const host = config.posthogHost;
+ // Built inline and keyed on primitives: a memoized options object that
+ // React discards would re-run this effect, and its cleanup would cancel
+ // the already-queued init.
const init = () => {
- posthog.init(key, posthogOptions);
+ posthog.init(key, {
+ // Ingest through our own origin (see the /ingest rewrites in
+ // next.config.ts) so ad blockers that block *.posthog.com don't
+ // silently drop client events.
+ api_host: "/ingest",
+ ui_host: host,
+ capture_pageview: "history_change",
+ autocapture: true,
+ });
};
// Captures fired before init() are dropped by posthog-js, so the idle
// deferral must be bounded — a busy main thread can starve
@@ -64,7 +63,7 @@ export function Providers({ children, config }: ProvidersProps) {
}
const timer = setTimeout(init, 300);
return () => clearTimeout(timer);
- }, [config.posthogKey, posthogOptions]);
+ }, [config.posthogKey, config.posthogHost]);
return (
diff --git a/apps/code/src/lib/utils/markdown.tsx b/apps/code/src/lib/utils/markdown.tsx
index 9f32ec1c2d..1ddcd680bb 100644
--- a/apps/code/src/lib/utils/markdown.tsx
+++ b/apps/code/src/lib/utils/markdown.tsx
@@ -1,4 +1,10 @@
-import { SyntaxHighlightedPre } from "./markdown-code-block";
+import dynamic from "next/dynamic";
+
+// Defer the prism-based highlighter chunk until a markdown document actually
+// renders a fenced code block.
+const SyntaxHighlightedPre = dynamic(() =>
+ import("./markdown-code-block").then((mod) => mod.SyntaxHighlightedPre),
+);
// Get markdown component options with custom styling
export function getMarkdownOptions() {
diff --git a/apps/docs/app/(home)/[[...slug]]/page.tsx b/apps/docs/app/(home)/[[...slug]]/page.tsx
index cf0bb14804..a77959d9d6 100644
--- a/apps/docs/app/(home)/[[...slug]]/page.tsx
+++ b/apps/docs/app/(home)/[[...slug]]/page.tsx
@@ -7,7 +7,6 @@ import {
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
-import posthog from "posthog-js";
import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
import { EnterpriseCTA } from "@/components/enterprise-cta";
@@ -90,6 +89,11 @@ export default async function Page(props: {
// Ignore errors (rate limits, network issues, missing auth in Docker builds)
}
+ // Left undefined when the GitHub lookup fails: fumadocs then omits the line
+ // entirely, which is better than stamping the build date on a page that was
+ // not edited — "Last updated" must only ever reflect a real content edit.
+ const lastUpdate = time ? new Date(time) : undefined;
+
const MDXContent = page.data.body;
const path = page.url === "/" ? "" : page.url;
@@ -120,7 +124,7 @@ export default async function Page(props: {
style: "clerk",
footer: ,
}}
- lastUpdate={time ? new Date(time) : new Date()}
+ lastUpdate={lastUpdate}
>
{
+ onRateAction={async (url) => {
"use server";
- posthog.capture("on_rate_docs", feedback);
return await Promise.resolve({
githubUrl: `https://github.com/theopenco/llmgateway/blob/main/apps/docs/content${url}.mdx`,
});
diff --git a/apps/docs/app/(home)/layout.tsx b/apps/docs/app/(home)/layout.tsx
index f1521b7e31..b884e3510f 100644
--- a/apps/docs/app/(home)/layout.tsx
+++ b/apps/docs/app/(home)/layout.tsx
@@ -1,14 +1,7 @@
-import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
-import { MessageCircleIcon } from "lucide-react";
import { baseOptions } from "@/app/layout.config";
-import {
- AISearch,
- AISearchPanel,
- AISearchTrigger,
-} from "@/components/ai/search";
-import { cn } from "@/lib/cn";
+import { AskAI } from "@/components/ai/ask-ai";
import { source } from "@/lib/source";
import type { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
@@ -42,21 +35,7 @@ const docsOptions: DocsLayoutProps = {
export default function Layout({ children }: { children: ReactNode }) {
return (
-
-
-
-
- Ask AI
-
-
+
{children}
);
diff --git a/apps/docs/components/ai/ask-ai.tsx b/apps/docs/components/ai/ask-ai.tsx
new file mode 100644
index 0000000000..1e5c899ee6
--- /dev/null
+++ b/apps/docs/components/ai/ask-ai.tsx
@@ -0,0 +1,68 @@
+"use client";
+import { buttonVariants } from "fumadocs-ui/components/ui/button";
+import { MessageCircleIcon } from "lucide-react";
+import dynamic from "next/dynamic";
+import { useCallback, useEffect, useEffectEvent, useState } from "react";
+
+import { cn } from "@/lib/cn";
+
+// The panel module pulls in @ai-sdk/react, its transport and the remark/shiki
+// markdown pipeline. Loading it on demand keeps all of that out of the initial
+// bundle of every docs page; the trigger below stays server-rendered so the
+// button never pops in after hydration.
+const AISearchPanel = dynamic(() => import("./search"), {
+ loading: () => null,
+});
+
+export function AskAI() {
+ const [open, setOpen] = useState(false);
+ // Once opened, the panel stays mounted so the conversation survives closing
+ // and reopening — the chat state lives inside the lazily loaded module.
+ const [hasOpened, setHasOpened] = useState(false);
+
+ const toggle = useCallback((next: boolean) => {
+ if (next) {
+ setHasOpened(true);
+ }
+ setOpen(next);
+ }, []);
+
+ const onKeyPress = useEffectEvent((e: KeyboardEvent) => {
+ if (e.key === "Escape" && open) {
+ toggle(false);
+ e.preventDefault();
+ }
+
+ if (e.key === "/" && (e.metaKey || e.ctrlKey) && !open) {
+ toggle(true);
+ e.preventDefault();
+ }
+ });
+
+ useEffect(() => {
+ window.addEventListener("keydown", onKeyPress);
+ return () => window.removeEventListener("keydown", onKeyPress);
+ }, []);
+
+ return (
+ <>
+ {hasOpened && }
+
+ >
+ );
+}
diff --git a/apps/docs/components/ai/search.tsx b/apps/docs/components/ai/search.tsx
index b76334cb79..dd4a036e91 100644
--- a/apps/docs/components/ai/search.tsx
+++ b/apps/docs/components/ai/search.tsx
@@ -14,11 +14,9 @@ import {
import {
type ComponentProps,
createContext,
- type ReactNode,
type SyntheticEvent,
use,
useEffect,
- useEffectEvent,
useMemo,
useRef,
useState,
@@ -349,8 +347,16 @@ function Message({
);
}
-export function AISearch({ children }: { children: ReactNode }) {
- const [open, setOpen] = useState(false);
+// Default export so ask-ai.tsx can lazily import this module: it drags in the
+// AI SDK, its transport and the remark/shiki markdown pipeline, none of which a
+// reader who never opens Ask AI should have to download.
+export default function AISearchPanel({
+ open,
+ setOpen,
+}: {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}) {
const chat = useChat({
id: "search",
transport: new DefaultChatTransport({
@@ -359,43 +365,9 @@ export function AISearch({ children }: { children: ReactNode }) {
});
return (
- ({ chat, open, setOpen }), [chat, open])}>
- {children}
-
- );
-}
-
-export function AISearchTrigger({
- position = "default",
- className,
- ...props
-}: ComponentProps<"button"> & { position?: "default" | "float" }) {
- const { open, setOpen } = useAISearchContext();
-
- return (
-
- );
-}
-
-export function AISearchPanel() {
- const { open, setOpen } = useAISearchContext();
- useHotKey();
-
- return (
- <>