mirror of
https://github.com/zenlm/zen-blog.git
synced 2026-10-30 15:05:49 +00:00
The home now says who makes Zen LM, Zoo Labs Foundation, a 501(c)(3) non-profit, the two jobs it is chosen for, that Zen 6 and Zen 6 Flash are available now, and that Zen 7 is a research preview with a link to request access. The footer names the foundation, and "Qwen" leaves the site keywords. Posts marked "upstream: true" carry data-upstream on their page, card and read-more link, and scripts/check-zen.mjs fails the build when a name from zen.upstream shows anywhere else. The build stops measuring remote images: fumadocs fetched every one to read its size, and a single host answering 403 failed the build.
122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
import { docs, meta } from "@/.source";
|
|
import { loader } from "fumadocs-core/source";
|
|
import { createMDXSource } from "fumadocs-mdx";
|
|
import Link from "next/link";
|
|
|
|
const blogSource = loader({
|
|
baseUrl: "/blog",
|
|
source: createMDXSource(docs, meta),
|
|
});
|
|
|
|
const formatDate = (date: Date): string => {
|
|
return date.toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
};
|
|
|
|
interface BlogData {
|
|
title: string;
|
|
description: string;
|
|
date: string;
|
|
tags?: string[];
|
|
featured?: boolean;
|
|
readTime?: string;
|
|
author?: string;
|
|
authorImage?: string;
|
|
thumbnail?: string;
|
|
upstream?: boolean;
|
|
}
|
|
|
|
interface BlogPage {
|
|
url: string;
|
|
data: BlogData;
|
|
}
|
|
|
|
interface ReadMoreSectionProps {
|
|
currentSlug: string[];
|
|
currentTags?: string[];
|
|
}
|
|
|
|
export function ReadMoreSection({
|
|
currentSlug,
|
|
currentTags = [],
|
|
}: ReadMoreSectionProps) {
|
|
const allPages = blogSource.getPages() as BlogPage[];
|
|
|
|
const currentUrl = `/blog/${currentSlug.join("/")}`;
|
|
|
|
const otherPosts = allPages
|
|
.filter((page) => page.url !== currentUrl)
|
|
.map((page) => {
|
|
const tagOverlap = currentTags.filter((tag) =>
|
|
page.data.tags?.includes(tag)
|
|
).length;
|
|
|
|
return {
|
|
...page,
|
|
relevanceScore: tagOverlap,
|
|
date: new Date(page.data.date),
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.relevanceScore !== b.relevanceScore) {
|
|
return b.relevanceScore - a.relevanceScore;
|
|
}
|
|
return b.date.getTime() - a.date.getTime();
|
|
})
|
|
.slice(0, 3);
|
|
|
|
if (otherPosts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<section className="border-t border-border p-0">
|
|
<div className="p-6 lg:p-10">
|
|
<h2 className="text-2xl font-medium mb-8">Read more</h2>
|
|
|
|
<div className="flex flex-col gap-8">
|
|
{otherPosts.map((post) => {
|
|
const formattedDate = formatDate(post.date);
|
|
|
|
return (
|
|
<Link
|
|
key={post.url}
|
|
href={post.url}
|
|
data-upstream={post.data.upstream ? "" : undefined}
|
|
className="group grid grid-cols-1 lg:grid-cols-12 items-center gap-4 cursor-pointer"
|
|
>
|
|
{post.data.thumbnail && (
|
|
<div className="flex-shrink-0 col-span-1 lg:col-span-4">
|
|
<div className="relative w-full h-full">
|
|
<img
|
|
src={post.data.thumbnail}
|
|
alt={post.data.title}
|
|
className="w-full h-full object-cover rounded-lg group-hover:opacity-80 transition-opacity"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="space-y-2 flex-1 col-span-1 lg:col-span-8">
|
|
<h3 className="text-lg group-hover:underline underline-offset-4 font-semibold text-card-foreground group-hover:text-primary transition-colors line-clamp-2">
|
|
{post.data.title}
|
|
</h3>
|
|
<p className="text-muted-foreground text-sm line-clamp-3 group-hover:underline underline-offset-4">
|
|
{post.data.description}
|
|
</p>
|
|
<time className="block text-xs font-medium text-muted-foreground">
|
|
{formattedDate}
|
|
</time>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|