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:
@@ -1,26 +1,32 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { sanitizeHtml } from "@/lib/sanitize";
|
||||
import { imageUrl } from "@/helper";
|
||||
import { blogCover } from "@/helper";
|
||||
|
||||
function Caption({ data }) {
|
||||
const cover = data?.images?.[0]?.url ? imageUrl(data.images[0].url) : "";
|
||||
// هیچ مقالهای بدون تصویر نمایش داده نمیشود؛ نبودِ تصویر شاخص یعنی کاور برند.
|
||||
const cover = blogCover(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
{cover && cover.trim() !== "" && (
|
||||
<div className="relative w-full aspect-video rounded-[8px] overflow-hidden">
|
||||
<Image
|
||||
src={cover}
|
||||
alt={data?.title || "blog cover"}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full aspect-video rounded-[8px] overflow-hidden">
|
||||
<Image
|
||||
src={cover}
|
||||
alt={data?.title || "blog cover"}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 68vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
{data?.summary && (
|
||||
<p className="mt-[16px] sm:mt-[20px] pr-[12px] border-r-[3px] border-[#E5E5E5] text-[#3B3B3B] text-[14px] md:text-[15px] lg:text-[16px] font-medium leading-[28px] md:leading-[30px]">
|
||||
{data.summary}
|
||||
</p>
|
||||
)}
|
||||
{data?.body?.value && (
|
||||
<div
|
||||
className="my-[12px] sm:my-[16px] md:my-[20px] lg:my-[24px] text-[#525252] text-[14px] md:text-[15px] lg:text-[16px] font-normal leading-[26px] sm:leading-[28px] md:leading-[30px] lg:leading-[32px]"
|
||||
className="blog-content my-[12px] sm:my-[16px] md:my-[20px] lg:my-[24px] text-[#525252] text-[14px] md:text-[15px] lg:text-[16px] font-normal leading-[26px] sm:leading-[28px] md:leading-[30px] lg:leading-[32px]"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeHtml(data.body.value) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* سؤالات متداول مقاله — همان آرایهای که در `app/blog/[slug]/page.js` به
|
||||
* FAQPage JSON-LD میرود. حضور مرئیِ همین پرسش/پاسخها شرط اعتبار آن schema است؛
|
||||
* بدون آن، rich result رد میشود.
|
||||
*
|
||||
* از <details>/<summary> بومی استفاده میشود تا بدون جاوااسکریپت و بدون
|
||||
* وابستگی جدید، آکاردئون دسترسپذیر داشته باشیم.
|
||||
*/
|
||||
function Faq({ data }) {
|
||||
const items = (Array.isArray(data?.faq) ? data.faq : []).filter(
|
||||
(f) => f?.q && f?.a
|
||||
);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mt-[16px] sm:mt-[20px] md:mt-[24px]">
|
||||
<h2 className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold mb-[12px] md:mb-[16px]">
|
||||
سؤالات متداول
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-[8px] md:gap-[12px]">
|
||||
{items.map((item, idx) => (
|
||||
<li
|
||||
key={`${item.q}-${idx}`}
|
||||
className="rounded-[8px] border border-[#EFEFEF] bg-[#FCFCFC] px-[12px] md:px-[16px] py-[10px] md:py-[12px]"
|
||||
>
|
||||
<details>
|
||||
<summary className="cursor-pointer list-none text-[#3B3B3B] text-[13px] md:text-[15px] font-bold">
|
||||
{item.q}
|
||||
</summary>
|
||||
<p className="mt-[8px] text-[#525252] text-[13px] md:text-[15px] font-normal leading-[26px] md:leading-[30px]">
|
||||
{item.a}
|
||||
</p>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Faq;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* منابع مقاله (`sources` در پاسخ API) — سیگنال E-E-A-T که تا این تغییر اصلاً
|
||||
* رندر نمیشد. هر آیتم `{ url, title }` است؛ آیتم بدون url رد میشود.
|
||||
*
|
||||
* لینکها `nofollow` میگیرند: منبع بیرونی تأیید ما را همراه ندارد.
|
||||
*/
|
||||
function Sources({ data }) {
|
||||
const items = (Array.isArray(data?.sources) ? data.sources : []).filter(
|
||||
(s) => s?.url
|
||||
);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mt-[16px] sm:mt-[20px] md:mt-[24px]">
|
||||
<h2 className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold mb-[12px] md:mb-[16px]">
|
||||
منابع
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-[6px] md:gap-[8px] list-disc pr-[18px]">
|
||||
{items.map((item, idx) => (
|
||||
<li key={`${item.url}-${idx}`} className="text-[#525252] text-[12px] md:text-[14px]">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
className="hover:text-[#3B3B3B] break-all"
|
||||
>
|
||||
{item.title || item.url}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sources;
|
||||
@@ -1,4 +1,6 @@
|
||||
import Caption from "./Caption";
|
||||
import Faq from "./Faq";
|
||||
import Sources from "./Sources";
|
||||
import SocialMedia from "./SocialMedia";
|
||||
import CommentUser from "./commentUser";
|
||||
|
||||
@@ -6,6 +8,8 @@ function Detail({ data }) {
|
||||
return (
|
||||
<div className="w-full lg:w-[68%]">
|
||||
<Caption data={data} />
|
||||
<Faq data={data} />
|
||||
<Sources data={data} />
|
||||
<SocialMedia />
|
||||
{/* <CommentUser data={data} loading={false} /> */}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,10 @@ import { convertTimestampToJalali } from "@/helper";
|
||||
function Head({ data, cityName }) {
|
||||
const firstTag = data?.tag?.[0];
|
||||
const createdDate = data?.created ? convertTimestampToJalali(data.created) : "";
|
||||
// normalizeBlog تگها را به [{name}] تبدیل میکند؛ نامِ خالی حذف میشود.
|
||||
const tags = (Array.isArray(data?.tag) ? data.tag : [])
|
||||
.map((t) => (typeof t === "string" ? t : t?.name))
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -66,7 +70,31 @@ function Head({ data, cityName }) {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{data?.reading_time > 0 && (
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
|
||||
زمان مطالعه:
|
||||
</p>
|
||||
<p className="text-[#9B9B9B] text-[11px] md:text-[14px] lg:text-[16px] font-medium">
|
||||
{data.reading_time} دقیقه
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* فهرست کامل تگها — تا این تغییر فقط تگ اول در بردکرامب دیده میشد.
|
||||
صفحهٔ فیلتر تگ وجود ندارد، پس چیپها بدون لینک رندر میشوند. */}
|
||||
{tags.length > 0 && (
|
||||
<ul className="flex flex-wrap items-center justify-start gap-2 mt-[12px] md:mt-[16px]">
|
||||
{tags.map((tag, idx) => (
|
||||
<li
|
||||
key={`${tag}-${idx}`}
|
||||
className="bg-[#F5F5F5] text-[#525252] rounded-full px-[12px] py-[4px] text-[11px] md:text-[13px] font-medium"
|
||||
>
|
||||
{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ import CircularLoading from "@/app/component/loading/Circular";
|
||||
import CustomLoading from "@/app/component/loading/Custom";
|
||||
import TextLoading from "@/app/component/loading/Text";
|
||||
import Link from "next/link";
|
||||
import { convertTimestampToJalali, imageUrl } from "@/helper";
|
||||
import { blogCover, convertTimestampToJalali } from "@/helper";
|
||||
|
||||
function Item({ data, idx, loading }) {
|
||||
const cover = data?.images?.[0]?.url
|
||||
? imageUrl(data.images[0].url)
|
||||
: "/assets/images/cover-blog-1.png";
|
||||
const cover = blogCover(data);
|
||||
const createdDate = data?.created ? convertTimestampToJalali(data.created) : "";
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,12 +2,10 @@ import CustomLoading from "@/app/component/loading/Custom";
|
||||
import TextLoading from "@/app/component/loading/Text";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { convertTimestampToJalali, imageUrl } from "@/helper";
|
||||
import { blogCover, convertTimestampToJalali } from "@/helper";
|
||||
|
||||
function Article({ data }) {
|
||||
const imageSrc = data.images?.[0]?.url
|
||||
? imageUrl(data.images[0].url, "/assets/images/cover-blog-1.png")
|
||||
: "/assets/images/cover-blog-1.png";
|
||||
const imageSrc = blogCover(data);
|
||||
const createdDate = data.created ? convertTimestampToJalali(data.created) : "";
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user