feat: added blog posts

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-15 11:55:30 -07:00
parent eea2d68098
commit 52a64fb96c
19 changed files with 749 additions and 1347 deletions

View file

@ -35,9 +35,7 @@ function SearchIcon({ className }: { className?: string }) {
}
export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
const featured = blogs[0];
if (!featured) {
if (blogs.length === 0) {
return (
<div className="relative overflow-hidden bg-neutral-50 px-4 md:px-8 dark:bg-neutral-950">
<Container className="relative pt-12 pb-24 md:pt-20">
@ -47,6 +45,17 @@ export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
);
}
// `blogs` arrives pre-sorted from the server: explicitly featured posts
// first (ordered by `featured_order` asc, then date desc), then the rest
// by date desc. If nothing is explicitly featured, fall back to treating
// the newest post as the cover so the layout never feels empty up top.
// `MagazineSearchGrid` re-filters using `heroSlugs` so the hero/featured
// posts never duplicate into the archive grid.
const explicitlyFeatured = blogs.filter((b) => b.featured);
const heroBlogs = explicitlyFeatured.length > 0 ? explicitlyFeatured : blogs.slice(0, 1);
const heroSlugs = new Set(heroBlogs.map((b) => b.slug));
const [coverStory, ...secondaryFeatured] = heroBlogs;
return (
<div className="relative overflow-hidden bg-neutral-50 px-4 pt-20 md:px-8 dark:bg-neutral-950">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.15),transparent)] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.12),transparent)]" />
@ -57,14 +66,38 @@ export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
</h1>
</header>
<MagazineFeatured blog={featured} />
<MagazineFeatured blog={coverStory} />
<MagazineSearchGrid blogs={blogs} featuredSlug={featured.slug} />
{secondaryFeatured.length > 0 ? (
<MoreFeatured blogs={secondaryFeatured} />
) : null}
<MagazineSearchGrid blogs={blogs} excludedSlugs={heroSlugs} />
</Container>
</div>
);
}
function MoreFeatured({ blogs }: { blogs: BlogEntry[] }) {
return (
<section aria-labelledby="more-featured-heading" className="mb-14">
<h2
id="more-featured-heading"
className="mb-6 font-serif text-2xl font-medium text-neutral-900 dark:text-neutral-100"
>
More featured
</h2>
<ul className="grid gap-6 sm:grid-cols-2">
{blogs.map((blog) => (
<li key={blog.slug}>
<MagazineCard blog={blog} />
</li>
))}
</ul>
</section>
);
}
function MagazineFeatured({ blog }: { blog: BlogEntry }) {
return (
<Link
@ -112,10 +145,11 @@ function MagazineFeatured({ blog }: { blog: BlogEntry }) {
function MagazineSearchGrid({
blogs: allBlogs,
featuredSlug,
excludedSlugs,
}: {
blogs: BlogEntry[];
featuredSlug: string;
/** Slugs already shown above the archive (cover story + "More featured"). */
excludedSlugs: Set<string>;
}) {
const [search, setSearch] = useState("");
@ -128,12 +162,15 @@ function MagazineSearchGrid({
);
const gridItems = useMemo(() => {
// When the reader is searching, surface every match (including
// featured posts they may be looking for); otherwise hide the posts
// that are already rendered as featured above the archive.
const results = search.trim() ? searcher.search(search) : allBlogs;
if (search.trim()) {
return results;
}
return results.filter((b) => b.slug !== featuredSlug);
}, [search, searcher, allBlogs, featuredSlug]);
return results.filter((b) => !excludedSlugs.has(b.slug));
}, [search, searcher, allBlogs, excludedSlugs]);
return (
<section aria-labelledby="archive-heading">

View file

@ -25,6 +25,8 @@ export interface BlogEntry {
image: string;
author: string;
authorAvatar: string;
featured: boolean;
featuredOrder?: number;
}
export default async function BlogPage() {
@ -38,6 +40,8 @@ export default async function BlogPage() {
image?: string;
author?: string;
authorAvatar?: string;
featured?: boolean;
featured_order?: number;
};
}>;
@ -51,8 +55,20 @@ export default async function BlogPage() {
image: page.data.image ?? "/og-image.png",
author: page.data.author ?? "SurfSense Team",
authorAvatar: page.data.authorAvatar ?? "/logo.png",
featured: page.data.featured ?? false,
featuredOrder: page.data.featured_order,
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
.sort((a, b) => {
// Featured first; then by `featured_order` asc within featured;
// then by `date` desc as the universal tie-breaker.
if (a.featured !== b.featured) return a.featured ? -1 : 1;
if (a.featured && b.featured) {
const aOrder = a.featuredOrder ?? Number.POSITIVE_INFINITY;
const bOrder = b.featuredOrder ?? Number.POSITIVE_INFINITY;
if (aOrder !== bOrder) return aOrder - bOrder;
}
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
return <BlogWithSearchMagazine blogs={blogs} />;
}