Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
import { programsSource, source } from "@/lib/source";
import { createSearchAPI } from "fumadocs-core/search/server";

export const { GET } = createFromSource(source);
export const { GET } = createSearchAPI("advanced", {
language: "english",
indexes: [
...source.getPages().map((page) => ({
title: page.data.title,
description: page.data.description,
url: page.url,
id: page.url,
structuredData: page.data.structuredData,
tag: "docs",
})),
...programsSource.getPages().map((page) => ({
title: page.data.title,
description: page.data.description,
url: page.url,
id: page.url,
structuredData: page.data.structuredData,
tag: "programs",
})),
],
});
6 changes: 5 additions & 1 deletion src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@theme inline {
--font-sans: var(--font-inter);
--font-mono: var(--font-inter-mono);
--font-mono: var(--font-jetbrains-mono);
--font-funnel-display: var(--font-funnel-display);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
Expand Down Expand Up @@ -123,3 +123,7 @@
@apply bg-background text-foreground;
}
}

kbd {
@apply font-mono;
}
28 changes: 24 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import SearchDialog from "@/components/search";
import { GoogleAnalytics } from "@next/third-parties/google";
import { RootProvider } from "fumadocs-ui/provider";
import type { Metadata } from "next";
import { Funnel_Display, Inter } from "next/font/google";
import { Funnel_Display, Inter, JetBrains_Mono } from "next/font/google";
import type { ReactNode } from "react";
import "./global.css";

const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
weight: "variable",
display: "swap",
preload: true,
});

const funnelDisplay = Funnel_Display({
subsets: ["latin", "latin-ext"],
weight: "variable",
variable: "--font-funnel-display",
display: "swap",
preload: true,
});

const jetbrainsMono = JetBrains_Mono({
subsets: ["latin", "latin-ext"],
weight: "variable",
variable: "--font-jetbrains-mono",
display: "swap",
preload: true,
});

export const metadata: Metadata = {
Expand All @@ -26,13 +40,19 @@ export const metadata: Metadata = {
},
};

export default function Layout({ children }: { children: ReactNode }) {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${inter.variable} ${funnelDisplay.variable} font-sans antialiased`}
className={`${inter.variable} ${funnelDisplay.variable} ${jetbrainsMono.variable} font-sans antialiased`}
>
<RootProvider>{children}</RootProvider>
<RootProvider
search={{
SearchDialog,
}}
>
{children}
</RootProvider>
</body>
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS!} />
</html>
Expand Down
72 changes: 72 additions & 0 deletions src/app/static.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { programsSource, source } from "@/lib/source";
import { StructuredData } from "fumadocs-core/mdx-plugins";
import { type DocumentRecord } from "fumadocs-core/search/algolia";
import { NextResponse } from "next/server";

export const revalidate = false;

// Type for page data with structured data
interface PageWithStructuredData {
url: string;
data: {
title: string;
description?: string;
structuredData: StructuredData;
};
}

// Helper function to map pages to DocumentRecord format
function mapPageToDocumentRecord(
page: PageWithStructuredData,
tag: string,
): DocumentRecord {
return {
_id: page.url,
structured: page.data.structuredData,
url: page.url,
title: page.data.title,
description: page.data.description || "",
tag,
};
}

// Helper function to safely get pages and map them
function mapPagesToDocuments(
sourceInstance: typeof source,
tag: string,
): DocumentRecord[] {
try {
return sourceInstance
.getPages()
.map((page) => mapPageToDocumentRecord(page, tag));
} catch (error) {
console.error(`Error mapping ${tag} pages:`, error);
return [];
}
}

export function GET() {
try {
// Process both sources efficiently with better error handling
const docsPages = mapPagesToDocuments(source, "docs");
const programPages = mapPagesToDocuments(programsSource, "programs");

// Combine arrays efficiently
const results: DocumentRecord[] = [...docsPages, ...programPages];

// Add cache headers for better performance
return new NextResponse(JSON.stringify(results), {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
} catch (error) {
console.error("Error generating static.json:", error);
return NextResponse.json(
{ error: "Failed to generate search data" },
{ status: 500 },
);
}
}
100 changes: 100 additions & 0 deletions src/components/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogFooter,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
TagsList,
TagsListItem,
} from "fumadocs-ui/components/dialog/search";
import { useI18n } from "fumadocs-ui/contexts/i18n";
import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react";
import { useState } from "react";

const items = [
{
name: "All",
value: "",
},
{
name: "Docs",
description: "Only results about documentation",
value: "docs",
},
{
name: "Programming",
description: "Only results about programming languages",
value: "programs",
},
];

export default function CustomSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const [tag, setTag] = useState<string | undefined>();
const { search, setSearch, query } = useDocsSearch({
type: "fetch",
locale,
tag,
delayMs: 500,
});

return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay className="backdrop-blur-md" />
<SearchDialogContent className="sm:-mt-10">
<SearchDialogHeader className="flex flex-col items-start">
<div className="flex w-full items-center justify-between gap-x-3">
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</div>
<TagsList tag={tag} onTagChange={setTag}>
{items.map((item) => (
<TagsListItem key={item.value} value={item.value ?? ""}>
{item.name}
</TagsListItem>
))}
</TagsList>
</SearchDialogHeader>
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
<SearchDialogFooter className="text-muted-foreground flex items-center justify-between p-2 py-1.5 text-xs">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<kbd className="bg-fd-background inline-flex size-6 items-center justify-center rounded-md border">
<ArrowUpIcon className="size-3" />
</kbd>
<kbd className="bg-fd-background inline-flex size-6 items-center justify-center rounded-md border">
<ArrowDownIcon className="size-3" />
</kbd>
<span className="ml-1">Navigate</span>
</div>
<div className="flex items-center gap-1">
<kbd className="bg-fd-background inline-flex items-center justify-center rounded-md border px-2 py-1">
<CornerDownLeftIcon className="size-3" />
</kbd>
<span>Select</span>
</div>
<div className="flex items-center gap-1">
<kbd className="bg-fd-background rounded-md border px-2 py-1 text-[10px]">
ESC
</kbd>
<span>Close</span>
</div>
</div>
</SearchDialogFooter>
</SearchDialogContent>
</SearchDialog>
);
}
Loading