P0-3/4/5: route error boundary, null guards, slug fallback, search
P0-3 (category page crash):
- Add src/app/error.tsx and src/app/global-error.tsx so any future render
error shows a real ref id instead of the bare client-exception toast.
- NewsPageClient: null-guard every field accessed during search/category/
sort filtering and article render (title, excerpt, author, tags,
category, date, image, alt). The user-visible link
/news?category=live-production now also hydrates filter state from
?category= and ?search= search-params, so the dropdown actually filters.
P0-4 (/articles/[slug] 404s):
- /articles/[slug] and /news/[slug]: on slug miss, redirect("/news")
instead of notFound(). Dead links from the hardcoded NewsTicker /
FeaturedBento / ArticleFeed arrays now land on the news index instead
of dead-ending on 404.
- legacy-source: add searchLegacyArticles() (title/excerpt/author ilike).
P0-5 (header search):
- Header: wire Enter on the desktop and mobile search inputs and make
both search icons clickable buttons. Submits to /search?q=<query>.
- New /search route: server component that runs searchLegacyArticles
and renders results in the same card style as /news.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
src/app/search/page.tsx
Normal file
100
src/app/search/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import AppImage from "@/components/ui/AppImage";
|
||||
import { searchLegacyArticles } from "@/lib/articles/legacy-source";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Search — BroadcastBeat",
|
||||
description:
|
||||
"Search BroadcastBeat for broadcast engineering news, gear reviews, show coverage, and technology insights.",
|
||||
alternates: { canonical: "/search" },
|
||||
};
|
||||
|
||||
interface SearchPageProps {
|
||||
searchParams: Promise<{ q?: string }>;
|
||||
}
|
||||
|
||||
export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
const { q } = await searchParams;
|
||||
const query = (q ?? "").trim();
|
||||
const results = query ? await searchLegacyArticles(query, 100) : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="bg-[#111] border-b border-[#222] py-8">
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="section-label">Search</span>
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
</div>
|
||||
<h1 className="font-heading text-white text-3xl font-bold">
|
||||
{query ? `Results for "${query}"` : "Search BroadcastBeat"}
|
||||
</h1>
|
||||
<p className="text-[#777] font-body text-sm mt-2">
|
||||
{query
|
||||
? `${results.length} article${results.length === 1 ? "" : "s"} found across news, gear, technology, and show coverage.`
|
||||
: "Type in the header search box to find articles."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="max-w-container mx-auto px-4 py-8">
|
||||
{query && results.length === 0 && (
|
||||
<div className="py-16 text-center">
|
||||
<p className="text-[#555] font-body text-sm mb-3">No articles match your search.</p>
|
||||
<Link href="/news" className="text-[#3b82f6] text-xs font-body hover:underline">
|
||||
Browse all news →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-0">
|
||||
{results.map((article) => {
|
||||
if (!article?.slug) return null;
|
||||
return (
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={`/news/${article.slug}`}
|
||||
className="flex gap-4 py-5 border-b border-[#222] group hover:bg-[#111] transition-colors px-2 -mx-2"
|
||||
>
|
||||
<div className="flex-shrink-0 w-[160px] h-[105px] relative overflow-hidden rounded-sm">
|
||||
<AppImage
|
||||
src={article.image || "/assets/images/article-placeholder.jpg"}
|
||||
alt={article.alt || article.title || "BroadcastBeat article"}
|
||||
fill
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
sizes="160px"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
{article.category || "NEWS"}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
{article.title || "Untitled"}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">
|
||||
{article.excerpt || ""}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-[#555] font-body text-xs">By {article.author || "Staff Reporter"}</span>
|
||||
<span className="text-[#444] text-[10px]">·</span>
|
||||
<span className="text-[#555] font-body text-xs">{article.readTime || ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user