search: return all matches (not just 100) + add typeahead autocomplete
- searchLegacyArticles() default limit: 50 → 5000. /search page
explicitly passes 5000 too. ilike on title/excerpt/author is fast
enough on the 29k-row archive that returning everything is fine.
- New API route /api/search/suggest?q=...&limit=8 returns the top
matching article titles + category + slug, with a 30-second CDN
cache + 60-second stale-while-revalidate.
- The search input in the Browse bar now debounces (150ms) and
fires that API per keystroke once 2+ chars are typed. Matches
appear in a styled dropdown below the input — click to navigate
directly to /news/<slug>, or press Enter to fall through to the
full /search page. "See all results for …" link in the dropdown
footer.
- Keyboard: Escape closes the dropdown; aria-autocomplete + role
listbox/option for accessibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
55
src/app/api/search/suggest/route.ts
Normal file
55
src/app/api/search/suggest/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||||
|
||||
function client() {
|
||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||
db: { schema: SCHEMA as "public" },
|
||||
auth: { persistSession: false },
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = (searchParams.get("q") || "").trim();
|
||||
if (q.length < 2) {
|
||||
return NextResponse.json({ items: [] });
|
||||
}
|
||||
const limit = Math.min(parseInt(searchParams.get("limit") || "8", 10) || 8, 15);
|
||||
const like = `%${q.replace(/[%_]/g, " ")}%`;
|
||||
|
||||
const sb = client();
|
||||
const { data, error } = await sb
|
||||
.from("wp_imported_posts")
|
||||
.select("wp_slug, title, category")
|
||||
.eq("status", "published")
|
||||
.or(`title.ilike.${like},author_name.ilike.${like}`)
|
||||
.order("wp_published_at", { ascending: false, nullsFirst: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ items: [], error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
const items = (data || []).map((r: any) => ({
|
||||
slug: r.wp_slug,
|
||||
title: r.title,
|
||||
category: r.category,
|
||||
href: `/news/${r.wp_slug}`,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{ items, query: q },
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, s-maxage=30, stale-while-revalidate=60",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ interface SearchPageProps {
|
||||
export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
const { q } = await searchParams;
|
||||
const query = (q ?? "").trim();
|
||||
const results = query ? await searchLegacyArticles(query, 100) : [];
|
||||
const results = query ? await searchLegacyArticles(query, 5000) : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
|
||||
Reference in New Issue
Block a user