Make blog cards square + clickable, add fallback article detail pages
- Blog card thumbnails (homepage magazine carousel, /blog listing, and
the "more posts" carousel) are now square instead of 16:9.
- Fallback blog cards (src/data/fallbackBlogs.ts) were previously
plain, non-clickable divs since /blogs is still empty on this
tenant and no real detail page existed for them. They now link to a
real local /blog/{id}/{slug} page via getFallbackBlogById() — image
and title are both inside the link. That page shows the real
title/image/category we have plus a "coming soon" note instead of
fabricating article body content.
- Homepage magazine section's fallback posts now link to their own
detail page instead of generically to /blog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4ffdf03727
commit
e44fc90d96
@@ -40,7 +40,7 @@ fallbacks should update it.
|
||||
| Gap | Fallback | Remove when |
|
||||
|---|---|---|
|
||||
| No prices | `src/lib/demoPricing.ts`, gated by `NEXT_PUBLIC_DEMO_PRICING=1` — deterministic price per product id | store-items exist |
|
||||
| No blog posts | `src/data/fallbackBlogs.ts` — the real posts from moalem.shop/articles, shown on the homepage and `/blog` as non-clickable cards (no real detail page exists for them) | `/blogs` returns items |
|
||||
| No blog posts | `src/data/fallbackBlogs.ts` — the real posts from moalem.shop/articles, shown on the homepage and `/blog` as square-thumbnail cards that link to a real local `/blog/{id}/{slug}` page (`getFallbackBlogById()`); that page shows title/category/image plus a "coming soon" note since only those three fields are real, no scraped body content | `/blogs` returns items |
|
||||
| No category art past slot position 8 | placeholder SVGs in `public/images/categories/{id}.svg`, generated by `scripts/gen-category-placeholders.mjs` for every category at every level, not just level-2 | the `categories` slot has an image for that position |
|
||||
| Wrong logo/favicon in API | `getFavicon()` is wired live (see "Brand") but the uploaded asset itself is wrong; no code fallback needed once the dashboard upload is fixed | the correct asset is uploaded |
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import BlogCarousel, { type BlogCardData } from "@/components/BlogCarousel";
|
||||
import { getBlogById, getBlogs, getComments, safe } from "@/lib/api";
|
||||
import { getSession } from "@/lib/auth";
|
||||
import { blogHref } from "@/lib/slug";
|
||||
import { FALLBACK_BLOGS, getFallbackBlogById } from "@/data/fallbackBlogs";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -14,6 +15,12 @@ export async function generateMetadata({
|
||||
params: Promise<{ id: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
|
||||
const fallback = getFallbackBlogById(id);
|
||||
if (fallback) {
|
||||
return { title: fallback.title, openGraph: { images: [{ url: fallback.image }] } };
|
||||
}
|
||||
|
||||
try {
|
||||
const { blog } = await getBlogById(id);
|
||||
const title = blog.title ?? blog.nameFa ?? "";
|
||||
@@ -27,9 +34,53 @@ export async function generateMetadata({
|
||||
}
|
||||
}
|
||||
|
||||
// A fallback post (see src/data/fallbackBlogs.ts) has a real id/title/image
|
||||
// but no scraped body content yet — /blogs is still empty on this tenant.
|
||||
// Its detail page still exists so the magazine cards can link somewhere
|
||||
// real, it just shows what data we actually have instead of the full
|
||||
// article body.
|
||||
function FallbackBlogDetail({
|
||||
post,
|
||||
others,
|
||||
}: {
|
||||
post: { id: string; title: string; image: string; category: string };
|
||||
others: typeof FALLBACK_BLOGS;
|
||||
}) {
|
||||
const morePosts: BlogCardData[] = others
|
||||
.filter((p) => p.id !== post.id)
|
||||
.slice(0, 4)
|
||||
.map((p) => ({ id: p.id, href: blogHref(p.id, p.title), title: p.title, image: p.image, category: p.category }));
|
||||
|
||||
return (
|
||||
<main className="boxed py-8 md:py-12">
|
||||
<Breadcrumbs items={[{ label: "مجله معلم", href: "/blog" }, { label: post.category }]} />
|
||||
|
||||
<article className="mx-auto max-w-3xl">
|
||||
<span className="text-[12px] font-bold text-brand">{post.category}</span>
|
||||
<h1 className="mt-2 text-xl font-bold text-ink md:text-2xl">{post.title}</h1>
|
||||
|
||||
<span className="relative mt-5 block aspect-[16/9] w-full overflow-hidden rounded-xl bg-surface">
|
||||
<Image src={post.image} alt={post.title} fill sizes="768px" className="object-cover" priority />
|
||||
</span>
|
||||
|
||||
<p className="mt-6 rounded-lg bg-surface px-4 py-3 text-sm leading-7 text-body">
|
||||
متن کامل این مقاله بهزودی از طریق پنل مدیریت اضافه میشود.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
{morePosts.length > 0 && <BlogCarousel posts={morePosts} />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function BlogDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
|
||||
const fallback = getFallbackBlogById(id);
|
||||
if (fallback) {
|
||||
return <FallbackBlogDetail post={fallback} others={FALLBACK_BLOGS} />;
|
||||
}
|
||||
|
||||
let blog;
|
||||
try {
|
||||
({ blog } = await getBlogById(id));
|
||||
|
||||
@@ -40,17 +40,19 @@ export default async function BlogListPage({
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{usingFallback
|
||||
? FALLBACK_BLOGS.map((post) => (
|
||||
// No real detail page exists yet for these — see AGENTS.md fallback
|
||||
// table — so the card is informational, not a link.
|
||||
<div key={post.id} className="overflow-hidden rounded-xl border border-line bg-white">
|
||||
<span className="relative block aspect-[16/9] w-full bg-surface">
|
||||
<Link
|
||||
key={post.id}
|
||||
href={blogHref(post.id, post.title)}
|
||||
className="overflow-hidden rounded-xl border border-line bg-white transition hover:border-brand/40 hover:shadow-[0_6px_20px_rgba(0,0,0,0.07)]"
|
||||
>
|
||||
<span className="relative block aspect-square w-full bg-surface">
|
||||
<Image src={post.image} alt={post.title} fill sizes="360px" className="object-cover" />
|
||||
<span className="absolute right-2 top-2 rounded-full bg-white/90 px-2.5 py-1 text-[11px] font-bold text-brand">
|
||||
{post.category}
|
||||
</span>
|
||||
</span>
|
||||
<p className="clamp-2 p-3 text-[13px] leading-6 text-ink-soft">{post.title}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
: result.items.map((post) => {
|
||||
const title = post.title ?? post.nameFa ?? "";
|
||||
@@ -62,7 +64,7 @@ export default async function BlogListPage({
|
||||
href={blogHref(post.id, title)}
|
||||
className="overflow-hidden rounded-xl border border-line bg-white transition hover:border-brand/40 hover:shadow-[0_6px_20px_rgba(0,0,0,0.07)]"
|
||||
>
|
||||
<span className="relative block aspect-[16/9] w-full bg-surface">
|
||||
<span className="relative block aspect-square w-full bg-surface">
|
||||
{image && <Image src={image} alt={title} fill sizes="360px" className="object-cover" />}
|
||||
{category && (
|
||||
<span className="absolute right-2 top-2 rounded-full bg-white/90 px-2.5 py-1 text-[11px] font-bold text-brand">
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ export default async function HomePage() {
|
||||
}))
|
||||
: FALLBACK_BLOGS.map((b) => ({
|
||||
id: b.id,
|
||||
href: "/blog",
|
||||
href: blogHref(b.id, b.title),
|
||||
title: b.title,
|
||||
image: b.image,
|
||||
category: b.category,
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function BlogCarousel({ posts }: { posts: BlogCardData[] }) {
|
||||
href={post.href}
|
||||
className="flex h-full flex-col overflow-hidden rounded-xl border border-line bg-white transition hover:border-brand/40 hover:shadow-[0_6px_20px_rgba(0,0,0,0.07)]"
|
||||
>
|
||||
<span className="relative block aspect-[16/9] w-full bg-surface">
|
||||
<span className="relative block aspect-square w-full bg-surface">
|
||||
{post.image && (
|
||||
<Image
|
||||
src={post.image}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// The blog module is enabled on this tenant but GET /blogs is still empty.
|
||||
// These posts are the real ones published on moalem.shop/articles, kept as a
|
||||
// fallback so the magazine section has content until the posts are imported.
|
||||
// Each one still gets a real local detail page at /blog/{id}/{slug} (see
|
||||
// getFallbackBlogById() below) — only title/image/category are real, there
|
||||
// is no scraped body content yet, so that page shows just those plus a
|
||||
// "more articles" list rather than fabricating a body.
|
||||
// Delete this file once /tenants/moalem.shop/blogs returns items.
|
||||
export interface FallbackBlog {
|
||||
id: string;
|
||||
@@ -73,3 +77,7 @@ export const FALLBACK_BLOGS: FallbackBlog[] = [
|
||||
category: "راهنمای خرید",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFallbackBlogById(id: string): FallbackBlog | undefined {
|
||||
return FALLBACK_BLOGS.find((post) => post.id === id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user