CI / typecheck + build (turbo) (push) Canceled after 0s
The home, doc, and search pages queried Postgres during SSG/static data collection, so 'next build' failed in CI (no DB). Mark them force-dynamic (and drop generateStaticParams from the doc page) so they render at request time — instant at this corpus scale and build-safe without a live database.
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import Link from "next/link";
|
|
import { keywordSearch, hybridSearch } from "@mcpedia/core";
|
|
|
|
// Render at request time — queries Postgres on each request (not at build,
|
|
// where CI has no DB).
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Mode = "keyword" | "hybrid";
|
|
|
|
export default async function SearchPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ q?: string; mode?: string }>;
|
|
}) {
|
|
const { q, mode } = await searchParams;
|
|
const query = q?.trim() ?? "";
|
|
const activeMode: Mode = mode === "hybrid" ? "hybrid" : "keyword";
|
|
|
|
const hits = query
|
|
? activeMode === "hybrid"
|
|
? await hybridSearch(query, 30)
|
|
: await keywordSearch(query, 30)
|
|
: [];
|
|
|
|
const toggle = (m: Mode) => `/search?q=${encodeURIComponent(query)}&mode=${m}`;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-semibold tracking-tight">Search</h1>
|
|
<div className="flex rounded overflow-hidden border border-zinc-300 dark:border-zinc-700 text-sm">
|
|
<Link
|
|
href={toggle("keyword")}
|
|
className={`px-3 py-1.5 ${activeMode === "keyword" ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
|
|
>
|
|
Keyword
|
|
</Link>
|
|
<Link
|
|
href={toggle("hybrid")}
|
|
className={`px-3 py-1.5 ${activeMode === "hybrid" ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
|
|
>
|
|
Hybrid
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="get" className="flex gap-2">
|
|
<input
|
|
name="q"
|
|
defaultValue={query}
|
|
placeholder="e.g. websocket contract typescript"
|
|
className="flex-1 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm"
|
|
/>
|
|
<input type="hidden" name="mode" value={activeMode} />
|
|
<button
|
|
type="submit"
|
|
className="rounded bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900 px-4 py-2 text-sm font-medium"
|
|
>
|
|
Search
|
|
</button>
|
|
</form>
|
|
|
|
{query && hits.length === 0 && (
|
|
<p className="text-sm text-zinc-500">No results for “{query}”.</p>
|
|
)}
|
|
|
|
<ul className="space-y-3">
|
|
{hits.map((h) => (
|
|
<li
|
|
key={h.doc.slug}
|
|
className="rounded border border-zinc-200 dark:border-zinc-800 p-3"
|
|
>
|
|
<Link
|
|
href={`/${h.doc.section}/${h.doc.slug}`}
|
|
className="font-medium hover:underline"
|
|
>
|
|
{h.doc.title}
|
|
</Link>
|
|
{/* Snippet comes from Postgres ts_headline (keyword mode) or our own
|
|
chunk content (hybrid mode) — both trusted, first-party data, not
|
|
user input. The only markup is <mark> from ts_headline. */}
|
|
<p
|
|
className="text-sm text-zinc-600 dark:text-zinc-400 mt-1"
|
|
dangerouslySetInnerHTML={{ __html: h.snippet }}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|