feat(blog): جدول — ستونها قابل تغییر اندازه/افزودن و حذف؛ لینک کپشن تصویر؛ هدینگهای نسخه انگلیسی بزرگتر
This commit is contained in:
@@ -13,9 +13,10 @@ import { useLang } from '../i18n/lang';
|
||||
import { applySeo } from '../seo';
|
||||
import { useEffect } from 'react';
|
||||
import { ARTICLE_EXTENSIONS } from '../components/admin/ArticleEditor';
|
||||
import { docToText } from '../utils/tiptapDoc';
|
||||
import { docToText, docHasText } from '../utils/tiptapDoc';
|
||||
import { detectDirection } from '../utils/direction';
|
||||
import { wrapScriptRuns } from '../utils/bidiText';
|
||||
import { toPublishedISO } from '../utils/date';
|
||||
import type { Article } from '../data/site';
|
||||
|
||||
/** بدنه مقاله را به HTML و فهرست مطالب تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||
@@ -44,28 +45,60 @@ export function ArticleDetail() {
|
||||
const { t, href, lang } = useLang();
|
||||
const article = articles.find((a) => a.slug === slug);
|
||||
|
||||
// عنوان تب، توضیح متا، OG و JSON-LD از خود مقاله
|
||||
// عنوان تب، توضیح متا، OG و JSON-LD از خود مقاله — همارز نسخهٔ سمت سرور (seo_pages.py)
|
||||
useEffect(() => {
|
||||
if (!article) return;
|
||||
const pageUrl = new URL(href(`/blog/${article.slug}`), window.location.origin).href;
|
||||
const absolute = (path: string) => new URL(path, window.location.origin).href;
|
||||
const authorName = (article.author ?? '').trim();
|
||||
const description = article.meta_description || article.excerpt;
|
||||
const articleSchema: Record<string, unknown> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.title,
|
||||
description,
|
||||
image: article.image ? absolute(article.image) : undefined,
|
||||
inLanguage: lang,
|
||||
// نویسندهٔ انسانی ثبتشده → Person (سیگنال E-E-A-T)؛ وگرنه برند
|
||||
author: authorName
|
||||
? { '@type': 'Person', name: authorName }
|
||||
: { '@type': 'Organization', name: brand.name },
|
||||
publisher: { '@type': 'Organization', name: brand.name },
|
||||
mainEntityOfPage: pageUrl,
|
||||
};
|
||||
const published = toPublishedISO(article.date);
|
||||
if (published) articleSchema.datePublished = published;
|
||||
const breadcrumbSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: t('home'), item: absolute(href('/')) },
|
||||
{ '@type': 'ListItem', position: 2, name: t('blog'), item: absolute(href('/blog')) },
|
||||
{ '@type': 'ListItem', position: 3, name: article.title, item: pageUrl },
|
||||
],
|
||||
};
|
||||
// پرسشوپاسخهای پایان مقاله → FAQPage (برای موتورهای هوش مصنوعی)
|
||||
const faqEntities = (article.faqs ?? [])
|
||||
.filter((f) => (f.question ?? '').trim() && (f.answer ?? '').trim())
|
||||
.map((f) => ({
|
||||
'@type': 'Question',
|
||||
name: f.question.trim(),
|
||||
acceptedAnswer: { '@type': 'Answer', text: f.answer.trim() },
|
||||
}));
|
||||
const faqSchema = faqEntities.length
|
||||
? { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqEntities }
|
||||
: null;
|
||||
applySeo({
|
||||
title: `${article.title} | ${brand.name}`,
|
||||
// توضیحات سئوی اختصاصی در اولویت است؛ در نبود آن، چکیده
|
||||
description: article.meta_description || article.excerpt,
|
||||
description,
|
||||
path: href(`/blog/${article.slug}`),
|
||||
image: article.image,
|
||||
type: 'article',
|
||||
lang,
|
||||
jsonLd: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.title,
|
||||
description: article.meta_description || article.excerpt,
|
||||
image: article.image ? new URL(article.image, window.location.origin).href : undefined,
|
||||
inLanguage: lang,
|
||||
author: { '@type': 'Organization', name: article.author?.trim() || brand.name },
|
||||
publisher: { '@type': 'Organization', name: brand },
|
||||
mainEntityOfPage: new URL(href(`/blog/${article.slug}`), window.location.origin).href,
|
||||
},
|
||||
// alternate انگلیسی فقط با وجود ترجمهٔ واقعی
|
||||
hasEn: !!(article.title_en?.trim() || article.excerpt_en?.trim() || docHasText(article.body_en)),
|
||||
jsonLd: [articleSchema, breadcrumbSchema, ...(faqSchema ? [faqSchema] : [])],
|
||||
});
|
||||
}, [article?.id, lang, href]);
|
||||
|
||||
@@ -104,6 +137,8 @@ export function ArticleDetail() {
|
||||
alt={article.image_alt || article.title}
|
||||
title={article.image_title || undefined}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
fetchPriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-ink/75" aria-hidden="true" />
|
||||
<div
|
||||
|
||||
@@ -80,7 +80,7 @@ export function Blog() {
|
||||
<img
|
||||
src={featured.image}
|
||||
alt={`${t('articleImagePrefix')} ${featured.title}`}
|
||||
className="h-full min-h-[20rem] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||
className="h-full min-h-[20rem] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105" fetchPriority="high" decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center p-8 lg:p-12">
|
||||
|
||||
@@ -21,15 +21,37 @@ export function ProjectDetail() {
|
||||
const statusLabel = useStatusLabel(project?.status ?? '')
|
||||
const [lightbox, setLightbox] = useState<number | null>(null)
|
||||
|
||||
// عنوان تب، توضیح متا و OG از خود پروژه
|
||||
// عنوان تب، توضیح متا، OG و JSON-LD از خود پروژه — همارز نسخهٔ سمت سرور (seo_pages.py)
|
||||
useEffect(() => {
|
||||
if (!project) return;
|
||||
const absolute = (path: string) => new URL(path, window.location.origin).href;
|
||||
applySeo({
|
||||
title: `${project.title} | ${brand.name}`,
|
||||
description: project.summary,
|
||||
path: href(`/projects/${project.slug}`),
|
||||
image: project.image,
|
||||
lang,
|
||||
// alternate انگلیسی فقط با وجود ترجمهٔ واقعی
|
||||
hasEn: !!(project.title_en?.trim() || project.summary_en?.trim()),
|
||||
jsonLd: [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: project.title,
|
||||
description: project.summary,
|
||||
image: project.image ? absolute(project.image) : undefined,
|
||||
inLanguage: lang,
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: t('home'), item: absolute(href('/')) },
|
||||
{ '@type': 'ListItem', position: 2, name: t('projects'), item: absolute(href('/projects')) },
|
||||
{ '@type': 'ListItem', position: 3, name: project.title, item: absolute(href(`/projects/${project.slug}`)) },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [project?.id, lang, href]);
|
||||
|
||||
@@ -69,7 +91,7 @@ export function ProjectDetail() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex h-[62vh] min-h-[440px] items-end overflow-hidden">
|
||||
<img src={project.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||
<img src={project.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" fetchPriority="high" decoding="async" />
|
||||
<div className="absolute inset-0 bg-ink/72" aria-hidden="true" />
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -147,6 +169,8 @@ export function ProjectDetail() {
|
||||
src={project.gallery[0] || project.image}
|
||||
alt=""
|
||||
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</button>
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
@@ -161,6 +185,8 @@ export function ProjectDetail() {
|
||||
src={img}
|
||||
alt=""
|
||||
className="aspect-square w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user