+
+ >
+ );
+}
diff --git a/apps/framework-docs-v2/src/app/[...slug]/layout.tsx b/apps/framework-docs-v2/src/app/(docs)/layout.tsx
similarity index 74%
rename from apps/framework-docs-v2/src/app/[...slug]/layout.tsx
rename to apps/framework-docs-v2/src/app/(docs)/layout.tsx
index 043aa12216..fb0caf00fa 100644
--- a/apps/framework-docs-v2/src/app/[...slug]/layout.tsx
+++ b/apps/framework-docs-v2/src/app/(docs)/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { Suspense } from "react";
+import { headers } from "next/headers";
import { SideNav } from "@/components/navigation/side-nav";
import { AnalyticsProvider } from "@/components/analytics-provider";
import { SidebarInset } from "@/components/ui/sidebar";
@@ -7,20 +8,22 @@ import { showDataSourcesPage } from "@/flags";
interface DocLayoutProps {
children: ReactNode;
- params: Promise<{
- slug?: string[];
- }>;
}
async function FilteredSideNav() {
// Evaluate feature flag
+ // Note: Accessing headers() in the parent component marks this as dynamic,
+ // which allows Date.now() usage in the flags SDK
const showDataSources = await showDataSourcesPage().catch(() => false);
// Pass flag to SideNav, which will filter navigation items after language filtering
return ;
}
-export default async function DocLayout({ children, params }: DocLayoutProps) {
+export default async function DocLayout({ children }: DocLayoutProps) {
+ // Access headers() to mark this layout as dynamic, which allows Date.now() usage
+ // in the flags SDK without triggering Next.js static generation errors
+ await headers();
return (
diff --git a/apps/framework-docs-v2/src/app/api/templates/route.ts b/apps/framework-docs-v2/src/app/api/templates/route.ts
index 4b4b63504e..9a2d2fe393 100644
--- a/apps/framework-docs-v2/src/app/api/templates/route.ts
+++ b/apps/framework-docs-v2/src/app/api/templates/route.ts
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import { getAllItems } from "@/lib/templates";
-export const dynamic = "force-static";
+// export const dynamic = "force-static";
export async function GET() {
try {
diff --git a/apps/framework-docs-v2/src/app/layout.tsx b/apps/framework-docs-v2/src/app/layout.tsx
index 967ccb9aa6..036d2e3fdd 100644
--- a/apps/framework-docs-v2/src/app/layout.tsx
+++ b/apps/framework-docs-v2/src/app/layout.tsx
@@ -1,16 +1,13 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { Suspense } from "react";
-import { cookies } from "next/headers";
import "@/styles/globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { LanguageProviderWrapper } from "@/components/language-provider-wrapper";
-import { TopNav } from "@/components/navigation/top-nav";
+import { TopNavWithFlags } from "@/components/navigation/top-nav-with-flags";
import { SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";
import { ScrollRestoration } from "@/components/scroll-restoration";
-import { getGitHubStars } from "@/lib/github-stars";
-import { showHostingSection, showGuidesSection, showAiSection } from "@/flags";
import { VercelToolbar } from "@vercel/toolbar/next";
export const metadata: Metadata = {
@@ -19,22 +16,13 @@ export const metadata: Metadata = {
};
// Force dynamic to enable cookie-based flag overrides
-export const dynamic = "force-dynamic";
+// export const dynamic = "force-dynamic";
export default async function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
- const stars = await getGitHubStars();
-
- // Evaluate feature flags (reads cookies automatically for overrides)
- const [showHosting, showGuides, showAi] = await Promise.all([
- showHostingSection().catch(() => false),
- showGuidesSection().catch(() => false),
- showAiSection().catch(() => true),
- ]);
-
const shouldInjectToolbar = process.env.NODE_ENV === "development";
return (
@@ -51,14 +39,7 @@ export default async function RootLayout({
- }>
-
-
+
{children}
diff --git a/apps/framework-docs-v2/src/app/page.tsx b/apps/framework-docs-v2/src/app/page.tsx
index 6d280ef3c0..d65e2187e3 100644
--- a/apps/framework-docs-v2/src/app/page.tsx
+++ b/apps/framework-docs-v2/src/app/page.tsx
@@ -11,7 +11,7 @@ import { IconDatabase, IconCloud, IconSparkles } from "@tabler/icons-react";
import { showHostingSection, showAiSection } from "@/flags";
import { cn } from "@/lib/utils";
-export const dynamic = "force-dynamic";
+// export const dynamic = "force-dynamic";
export default async function HomePage() {
// Evaluate feature flags
diff --git a/apps/framework-docs-v2/src/app/templates/layout.tsx b/apps/framework-docs-v2/src/app/templates/layout.tsx
new file mode 100644
index 0000000000..25d5d3afb1
--- /dev/null
+++ b/apps/framework-docs-v2/src/app/templates/layout.tsx
@@ -0,0 +1,31 @@
+import type { ReactNode } from "react";
+import { Suspense } from "react";
+import { TemplatesSideNav } from "./templates-side-nav";
+import { AnalyticsProvider } from "@/components/analytics-provider";
+import { SidebarInset } from "@/components/ui/sidebar";
+
+interface TemplatesLayoutProps {
+ children: ReactNode;
+}
+
+export default async function TemplatesLayout({
+ children,
+}: TemplatesLayoutProps) {
+ return (
+
+
+ }>
+
+
+
+
+ {/* Reserve space for the right TOC on xl+ screens */}
+
+ {children}
+
+
+ );
+ }
+
+ // Explicit animate flag
+ if (shouldAnimate) {
+ return (
+
+
+
+ );
+ }
+
+ // Shell commands: Use animated terminal only when explicitly copy=false with filename
+ // and animate flag is not explicitly false
+ // Otherwise, always use ShellSnippet (the Terminal tab UI with copy button)
+ if (isShell) {
+ // Only use animated terminal when explicitly no copy button wanted
+ if (filename && props["data-copy"] === "false" && !shouldNotAnimate) {
+ return (
+
+
+
+ );
+ }
+
+ // All other shell commands use ShellSnippet (Terminal tab with copy)
+ return (
+
+
+
+ );
+ }
+
+ // Non-shell: animate if filename present and copy not explicitly set
+ // unless animate is explicitly false
+ const legacyAnimate =
+ filename && props["data-copy"] === undefined && !shouldNotAnimate;
+
+ if (legacyAnimate) {
+ return (
+
+ );
+}
+
+/**
+ * Server-side inline code component
+ *
+ * Supports Nextra-style inline highlighting: `code{:lang}`
+ */
+export function ServerInlineCode({
+ children,
+ className,
+ ...props
+}: React.HTMLAttributes): React.ReactElement {
+ const isCodeBlock =
+ className?.includes("language-") ||
+ (props as Record)["data-language"];
+
+ if (isCodeBlock) {
+ // This is a code block that should be handled by ServerCodeBlock
+ // This is a fallback for when code is not wrapped in pre
+ const language = getLanguage(props as ServerCodeBlockProps);
+ const codeText = extractTextContent(children).trim();
+
+ return (
+
+
+
+ );
+ }
+
+ // Check for inline code with language hint: `code{:lang}`
+ const textContent =
+ typeof children === "string" ? children : extractTextContent(children);
+ const inlineLangMatch = textContent.match(/^(.+)\{:(\w+)\}$/);
+
+ if (inlineLangMatch) {
+ const [, code, lang] = inlineLangMatch;
+ if (code && lang) {
+ return ;
+ }
+ }
+
+ // Inline code - simple styled element
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/framework-docs-v2/src/components/mdx/server-figure.tsx b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx
new file mode 100644
index 0000000000..75b1a7621a
--- /dev/null
+++ b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx
@@ -0,0 +1,119 @@
+import React from "react";
+
+interface MDXFigureProps extends React.HTMLAttributes {
+ "data-rehype-pretty-code-figure"?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * Extracts text content from a React node (for figcaption titles)
+ */
+function extractTextFromNode(node: React.ReactNode): string {
+ if (typeof node === "string") {
+ return node;
+ }
+ if (typeof node === "number") {
+ return String(node);
+ }
+ if (Array.isArray(node)) {
+ return node.map(extractTextFromNode).join("");
+ }
+ if (React.isValidElement(node)) {
+ const props = node.props as Record;
+ return extractTextFromNode(props.children as React.ReactNode);
+ }
+ return "";
+}
+
+/**
+ * Server-side component that handles figure wrapper from rehype-pretty-code
+ * Extracts the title from figcaption and passes it to the pre element
+ */
+export function ServerFigure({
+ children,
+ ...props
+}: MDXFigureProps): React.ReactElement {
+ // Only handle code block figures
+ // data-rehype-pretty-code-figure is present (even if empty string) for code blocks
+ if (props["data-rehype-pretty-code-figure"] === undefined) {
+ return {children};
+ }
+
+ // For code blocks, extract figcaption title and pass to pre
+ const childrenArray = React.Children.toArray(children);
+
+ // Find figcaption and pre elements
+ let figcaption: React.ReactElement | null = null;
+ let preElement: React.ReactElement | null = null;
+
+ childrenArray.forEach((child) => {
+ if (React.isValidElement(child)) {
+ const childType = child.type;
+ const childProps = (child.props as Record) || {};
+
+ // Check if it's a native HTML element by checking if type is a string
+ if (typeof childType === "string") {
+ if (childType === "figcaption") {
+ figcaption = child;
+ } else if (childType === "pre") {
+ preElement = child;
+ }
+ } else {
+ // For React components (like ServerCodeBlock)
+ // Check if it has code block attributes
+ const hasCodeBlockAttrs =
+ childProps["data-rehype-pretty-code-fragment"] !== undefined ||
+ childProps["data-language"] !== undefined ||
+ childProps["data-theme"] !== undefined;
+
+ // If it has code block attributes, it's the pre element
+ if (hasCodeBlockAttrs || !preElement) {
+ preElement = child;
+ }
+ }
+ }
+ });
+
+ // Extract filename from figcaption (title from markdown)
+ let figcaptionTitle: string | undefined;
+ if (figcaption !== null) {
+ const figcaptionProps = figcaption.props as Record;
+ figcaptionTitle = extractTextFromNode(
+ figcaptionProps.children as React.ReactNode,
+ ).trim();
+ }
+
+ const preProps =
+ preElement ? (preElement.props as Record) || {} : {};
+
+ // Prioritize figcaption title (from markdown title="...") over any existing attributes
+ const filename =
+ figcaptionTitle ||
+ (preProps["data-rehype-pretty-code-title"] as string | undefined) ||
+ (preProps["data-filename"] as string | undefined);
+
+ // If we have a pre element, ensure the filename is set on both attributes
+ if (preElement) {
+ const hasCodeBlockAttrs =
+ preProps["data-language"] !== undefined ||
+ preProps["data-theme"] !== undefined;
+ const fragmentValue =
+ preProps["data-rehype-pretty-code-fragment"] !== undefined ?
+ preProps["data-rehype-pretty-code-fragment"]
+ : hasCodeBlockAttrs ? ""
+ : undefined;
+
+ const updatedPre = React.cloneElement(preElement, {
+ ...preProps,
+ "data-filename": filename || undefined,
+ "data-rehype-pretty-code-title": filename || undefined,
+ ...(fragmentValue !== undefined ?
+ { "data-rehype-pretty-code-fragment": fragmentValue }
+ : {}),
+ });
+ return <>{updatedPre}>;
+ }
+
+ // Fallback: render children
+ return <>{children}>;
+}
diff --git a/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx b/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx
new file mode 100644
index 0000000000..1ad348c608
--- /dev/null
+++ b/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import React from "react";
+import {
+ Snippet,
+ SnippetCopyButton,
+ SnippetHeader,
+ SnippetTabsContent,
+ SnippetTabsList,
+ SnippetTabsTrigger,
+} from "@/components/ui/snippet";
+
+interface ShellSnippetProps {
+ code: string;
+ language: string;
+}
+
+/**
+ * Client component for shell/terminal code snippets
+ * Displays with "Terminal" label and copy button
+ */
+export function ShellSnippet({ code, language }: ShellSnippetProps) {
+ const [value, setValue] = React.useState("terminal");
+
+ return (
+
+
+
+ Terminal
+
+
+
+ {code}
+
+ );
+}
diff --git a/apps/framework-docs-v2/src/components/mdx/template-card.tsx b/apps/framework-docs-v2/src/components/mdx/template-card.tsx
index 0e3ccba497..c229f41f38 100644
--- a/apps/framework-docs-v2/src/components/mdx/template-card.tsx
+++ b/apps/framework-docs-v2/src/components/mdx/template-card.tsx
@@ -11,7 +11,8 @@ import {
CardFooter,
CardHeader,
} from "@/components/ui/card";
-import { IconBrandGithub } from "@tabler/icons-react";
+import { IconBrandGithub, IconRocket, IconBook } from "@tabler/icons-react";
+import { Button } from "@/components/ui/button";
import {
Snippet,
SnippetCopyButton,
@@ -51,12 +52,7 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
const isTemplate = item.type === "template";
const template = isTemplate ? (item as TemplateMetadata) : null;
const app = !isTemplate ? (item as AppMetadata) : null;
-
- const categoryColors = {
- starter: "border-blue-200 dark:border-blue-800",
- framework: "border-purple-200 dark:border-purple-800",
- example: "border-green-200 dark:border-green-800",
- };
+ const [chipsExpanded, setChipsExpanded] = React.useState(false);
const categoryLabels = {
starter: "Starter",
@@ -78,103 +74,120 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
const description = isTemplate ? template!.description : app!.description;
const name = isTemplate ? template!.name : app!.name;
+ // Combine frameworks and features into a single array with type info
+ const allChips = [
+ ...frameworks.map((f) => ({ value: f, type: "framework" as const })),
+ ...features.map((f) => ({ value: f, type: "feature" as const })),
+ ];
+
+ const MAX_VISIBLE_CHIPS = 3;
+ const visibleChips =
+ chipsExpanded ? allChips : allChips.slice(0, MAX_VISIBLE_CHIPS);
+ const hiddenCount = allChips.length - MAX_VISIBLE_CHIPS;
+
return (
-
-