import React from 'react'; import { Link, useParams } from 'react-router-dom'; import { ArrowLeftIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; import { renderToHTMLString } from '@tiptap/static-renderer'; import { Reveal } from '../components/Reveal'; import { ButtonLink } from '../components/Button'; import { ArticleCard } from '../components/ArticleCard'; import { ArticleMeta } from '../components/ArticleMeta'; import { TableOfContents, extractToc, type TocItem } from '../components/TableOfContents'; import { Newsletter } from '../components/home/Newsletter'; import { useSite } from '../api'; 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 { detectDirection } from '../utils/direction'; import { wrapScriptRuns } from '../utils/bidiText'; import type { Article } from '../data/site'; /** بدنه مقاله را به HTML و فهرست مطالب تبدیل می‌کند — سند TipTap جدید یا پاراگراف‌های قدیمی */ function renderBody( article: Article, dir: 'rtl' | 'ltr', ): { html: string | null; toc: TocItem[]; legacy: string[] } { const body = article.body as unknown; if (Array.isArray(body)) return { html: null, toc: [], legacy: body }; if (body && typeof body === 'object' && (body as { type?: string }).type === 'doc') { try { const html = renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never }); // تیترها شناسه لنگر می‌گیرند و متن لاتین/فارسی برای قاعده «۲ پیکسل کوچک‌تر» بسته‌بندی می‌شود const { items, html: htmlWithIds } = extractToc(html, (root) => wrapScriptRuns(root, dir)); return { html: htmlWithIds, toc: items, legacy: [] }; } catch { return { html: null, toc: [], legacy: [] }; } } return { html: null, toc: [], legacy: [] }; } export function ArticleDetail() { const { slug } = useParams<{slug: string;}>(); const { articles, brand } = useSite(); const { t, href, lang } = useLang(); const article = articles.find((a) => a.slug === slug); // عنوان تب، توضیح متا، OG و JSON-LD از خود مقاله useEffect(() => { if (!article) return; applySeo({ title: `${article.title} | ${brand.name}`, // توضیحات سئوی اختصاصی در اولویت است؛ در نبود آن، چکیده description: article.meta_description || article.excerpt, 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, }, }); }, [article?.id, lang, href]); if (!article) { return (

{t('articleNotFound')}

{t('articleNotFoundText')}

{t('backToBlog')}
); } const dir = detectDirection(article.title, article.excerpt, docToText(article.body)); const { html, toc, legacy } = renderBody(article, dir); const related = articles.filter((a) => a.slug !== article.slug && a.status !== 'draft').slice(0, 3); /** * جهت غالب متن مقاله — بدنه (با data-dir، طبق قاعده CSS مقاله) و سمت فهرست مطالب * از همین پیروی می‌کنند: فارسی راست‌به‌چپ (فهرست سمت راست)، انگلیسی چپ‌به‌راست (فهرست سمت چپ). */ const hasToc = toc.length > 0; // در چیدمان راست‌به‌چپ ستون شماره ۱ سمت راست است؛ پس ستون‌ها صریح شماره می‌خورند const gridColumns = hasToc ? 'lg:grid-cols-[21rem_minmax(0,1fr)]' : ''; const bodyColumns = 'lg:col-start-2'; const tocColumns = 'lg:col-start-1'; return ( <>
{article.image_alt

{article.excerpt}

{html ? (
) : ( legacy.map((paragraph, i) =>

{paragraph}

) )}
{/* فهرست مطالب — در دسکتاپ کنار متن می‌چسبد و همراه مقاله پایین می‌آید */} {hasToc && ( )}

{t('relatedArticles')}

{related.map((item, i) => )}
); }