feat: add script to generate default blog cover image
- Introduced a new script `make-blog-cover.mjs` to create a default cover image for blog posts. - The image is generated in PNG format with dimensions 1200x630, suitable for Open Graph. - Utilizes the site's branding colors and logo from `public/nobat724.svg`. - Includes custom font styling using the Vazirmatn font. - The generated image is saved to `public/assets/images/blog-default-cover.png`.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { revalidateTag, revalidatePath } from "next/cache";
|
||||
|
||||
/**
|
||||
* باطلسازی on-demand کش ISR بلاگ.
|
||||
*
|
||||
* `clinicpro` (App\Blog\Service\BlogCacheInvalidator) بعد از هر
|
||||
* create/update/delete/review این مسیر را صدا میزند تا تغییر پنل ادمین بدون
|
||||
* انتظار یکساعتهٔ `revalidate: 3600` روی سایت بیاید.
|
||||
*
|
||||
* قرارداد: POST { tags: string[] } با هدر `X-Revalidate-Secret`.
|
||||
*/
|
||||
export async function POST(request) {
|
||||
const secret = process.env.REVALIDATE_SECRET;
|
||||
if (!secret || request.headers.get("x-revalidate-secret") !== secret) {
|
||||
return NextResponse.json({ revalidated: false }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const tags = body?.tags;
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ revalidated: false, error: "tags required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
tags.forEach((tag) => revalidateTag(String(tag)));
|
||||
// فهرست مقالات صفحهٔ خودش را با tag نمیگیرد، پس مسیرش جدا باطل میشود.
|
||||
if (tags.includes("blog-list")) revalidatePath("/blogs");
|
||||
|
||||
return NextResponse.json({ revalidated: true, tags });
|
||||
}
|
||||
@@ -11,9 +11,8 @@ import {
|
||||
findCityById,
|
||||
} from "@/lib/domainHelpers";
|
||||
import { safeJsonLd } from "@/lib/sanitize";
|
||||
import { normalizeBlog, imageUrl } from "@/helper";
|
||||
import { normalizeBlog, blogCover } from "@/helper";
|
||||
|
||||
const FALLBACK_IMG = "/assets/images/og-image.png";
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// scope دامنه در کلید cache مینشیند: یک پست روی دامنهٔ شهریِ دیگر ۴۰۴ است، پس
|
||||
@@ -21,7 +20,9 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const getBlog = cache(async (slug, cityId) => {
|
||||
const query = cityId != null ? `?city_id=${cityId}` : "";
|
||||
const res = await fetch(`${API_URL}/api/v1/blog/${slug}${query}`, {
|
||||
next: { revalidate: 3600, tags: [`blog-${slug}`] },
|
||||
// هر دو tag ثبت میشود چون بکاند با slug و uuid هر دو باطل میکند و URL
|
||||
// سایت uuid است؛ `blog-list` هم برای باطلسازی گروهی همهٔ مقالهها.
|
||||
next: { revalidate: 3600, tags: [`blog-${slug}`, "blog-list"] },
|
||||
});
|
||||
// ۴۰۴ یعنی «روی این دامنه وجود ندارد» — پستِ شهرِ دیگر (یا پستِ دامنهٔ اصلی)
|
||||
// نباید روی دامنهٔ شهری باز شود؛ بکاند همان فیلتر لیست را اینجا هم اعمال میکند.
|
||||
@@ -72,11 +73,9 @@ export async function generateMetadata({ params }) {
|
||||
? blog.tags.map((t) => (typeof t === "string" ? t : t?.name))
|
||||
: []),
|
||||
].filter(Boolean);
|
||||
const image = blog.og_image
|
||||
? imageUrl(blog.og_image)
|
||||
: blog.image_url
|
||||
? imageUrl(blog.image_url)
|
||||
: FALLBACK_IMG;
|
||||
// ترتیب تقدم og_image → image_url → کاور پیشفرض برند، همان چیزی که صفحه و
|
||||
// کارتها نشان میدهند؛ یک منبع واحد تا OG با تصویر داخل صفحه فرق نکند.
|
||||
const image = blogCover(blog);
|
||||
|
||||
// C1-b — پست شهریافته به دامنهٔ همان شهر canonical میشود؛ پست سراسری (یا پستِ
|
||||
// رکورد ریشهٔ nobat724) به دامنهٔ اصلی، نه دامنهٔ جاری — وگرنه یک محتوا روی چند
|
||||
@@ -149,7 +148,7 @@ async function Blog({ params }) {
|
||||
"@type": "Article",
|
||||
headline: blog.title,
|
||||
...(articleDescription && { description: articleDescription }),
|
||||
...(blog.images?.[0]?.url && { image: imageUrl(blog.images[0].url) }),
|
||||
image: blogCover(blog),
|
||||
mainEntityOfPage: { "@type": "WebPage", "@id": `${origin}/blog/${slug}` },
|
||||
...(blog.created && {
|
||||
datePublished: new Date(blog.created * 1000).toISOString(),
|
||||
|
||||
+72
-1
@@ -904,4 +904,75 @@ html {
|
||||
|
||||
.hover-item-clinic:hover h3 {
|
||||
color: #f17732;
|
||||
}
|
||||
}
|
||||
/* ── محتوای مقاله (خروجی CKEditor پنل ادمین) ──────────────────────────────
|
||||
sanitizeHtml عمداً class و style را حذف میکند، پس استایل جدول/لیست/تیتر
|
||||
باید از اینجا بیاید وگرنه محتوا لخت و بدون ساختار دیده میشود. */
|
||||
.blog-content h2,
|
||||
.blog-content h3,
|
||||
.blog-content h4,
|
||||
.blog-content h5 {
|
||||
color: #3b3b3b;
|
||||
font-weight: 700;
|
||||
margin: 24px 0 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.blog-content h2 { font-size: 20px; }
|
||||
.blog-content h3 { font-size: 18px; }
|
||||
.blog-content h4,
|
||||
.blog-content h5 { font-size: 16px; }
|
||||
.blog-content p { margin-bottom: 12px; }
|
||||
.blog-content ul,
|
||||
.blog-content ol {
|
||||
margin: 12px 0;
|
||||
padding-right: 22px;
|
||||
}
|
||||
.blog-content ul { list-style: disc; }
|
||||
.blog-content ol { list-style: decimal; }
|
||||
.blog-content li { margin-bottom: 6px; }
|
||||
.blog-content a {
|
||||
color: #5559ce;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.blog-content blockquote {
|
||||
margin: 16px 0;
|
||||
padding: 12px 16px;
|
||||
border-right: 3px solid #f17732;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
color: #3b3b3b;
|
||||
}
|
||||
.blog-content figure { margin: 16px 0; }
|
||||
.blog-content figcaption {
|
||||
margin-top: 8px;
|
||||
color: #9b9b9b;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.blog-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
/* جدول روی موبایل نباید صفحه را افقی اسکرول کند. */
|
||||
.blog-content figure.table,
|
||||
.blog-content .table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.blog-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.blog-content :is(th, td) {
|
||||
border: 1px solid #e5e5e5;
|
||||
padding: 8px 12px;
|
||||
text-align: right;
|
||||
}
|
||||
.blog-content th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 700;
|
||||
color: #3b3b3b;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user