Update blog editor features
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { ArrowLeftIcon, ChevronLeftIcon, ClockIcon } from 'lucide-react';
|
||||
import { ArrowLeftIcon, ChevronLeftIcon } 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 { applySeo } from '../seo';
|
||||
@@ -12,21 +14,21 @@ import { useEffect } from 'react';
|
||||
import { ARTICLE_EXTENSIONS } from '../components/admin/ArticleEditor';
|
||||
import type { Article } from '../data/site';
|
||||
|
||||
/** بدنه مقاله را به HTML تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||
function renderBody(article: Article): { html: string | null; legacy: string[] } {
|
||||
/** بدنه مقاله را به HTML و فهرست مطالب تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||
function renderBody(article: Article): { html: string | null; toc: TocItem[]; legacy: string[] } {
|
||||
const body = article.body as unknown;
|
||||
if (Array.isArray(body)) return { html: null, legacy: body };
|
||||
if (Array.isArray(body)) return { html: null, toc: [], legacy: body };
|
||||
if (body && typeof body === 'object' && (body as { type?: string }).type === 'doc') {
|
||||
try {
|
||||
return {
|
||||
html: renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never }),
|
||||
legacy: [],
|
||||
};
|
||||
const html = renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never });
|
||||
// تیترهای H2/H3 شناسه لنگر میگیرند تا فهرست مطالب به آنها بپرد
|
||||
const { items, html: htmlWithIds } = extractToc(html);
|
||||
return { html: htmlWithIds, toc: items, legacy: [] };
|
||||
} catch {
|
||||
return { html: null, legacy: [] };
|
||||
return { html: null, toc: [], legacy: [] };
|
||||
}
|
||||
}
|
||||
return { html: null, legacy: [] };
|
||||
return { html: null, toc: [], legacy: [] };
|
||||
}
|
||||
|
||||
export function ArticleDetail() {
|
||||
@@ -39,7 +41,8 @@ export function ArticleDetail() {
|
||||
if (!article) return;
|
||||
applySeo({
|
||||
title: `${article.title} | ${brand}`,
|
||||
description: article.excerpt,
|
||||
// توضیحات سئوی اختصاصی در اولویت است؛ در نبود آن، چکیده
|
||||
description: article.meta_description || article.excerpt,
|
||||
path: `/blog/${article.slug}`,
|
||||
image: article.image,
|
||||
type: 'article',
|
||||
@@ -47,10 +50,10 @@ export function ArticleDetail() {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.title,
|
||||
description: article.excerpt,
|
||||
description: article.meta_description || article.excerpt,
|
||||
image: article.image ? new URL(article.image, window.location.origin).href : undefined,
|
||||
inLanguage: 'fa',
|
||||
author: { '@type': 'Organization', name: brand },
|
||||
author: { '@type': 'Organization', name: article.author?.trim() || brand },
|
||||
publisher: { '@type': 'Organization', name: brand },
|
||||
mainEntityOfPage: new URL(`/blog/${article.slug}`, window.location.origin).href,
|
||||
},
|
||||
@@ -70,13 +73,17 @@ export function ArticleDetail() {
|
||||
|
||||
}
|
||||
|
||||
const { html, legacy } = renderBody(article);
|
||||
const { html, toc, legacy } = renderBody(article);
|
||||
const related = articles.filter((a) => a.slug !== article.slug && a.status !== 'draft').slice(0, 3);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex h-[56vh] min-h-[400px] items-end overflow-hidden">
|
||||
<img src={article.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||
<img
|
||||
src={article.image}
|
||||
alt={article.image_alt || article.title}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-ink/75" aria-hidden="true" />
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -96,36 +103,43 @@ export function ArticleDetail() {
|
||||
<span className="text-copper">{article.category}</span>
|
||||
</nav>
|
||||
<h1 className="mt-5 max-w-[700px] text-hero font-bold text-ivory">{article.title}</h1>
|
||||
<div className="mt-6 flex items-center gap-5 text-xs font-light text-ivory/60">
|
||||
<span>{article.date}</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<ClockIcon className="h-3.5 w-3.5" />
|
||||
{article.read}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* نویسنده، تاریخ جلالی و زمان مطالعه */}
|
||||
<ArticleMeta article={article} className="mt-7" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<article className="bg-ink py-20 lg:py-28">
|
||||
<div className="mx-auto max-w-3xl px-6">
|
||||
<Reveal>
|
||||
<p className="border-r-2 border-copper pr-6 text-lg font-light leading-10 text-ivory/80">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
</Reveal>
|
||||
{html ? (
|
||||
<Reveal delay={0.05}>
|
||||
<div
|
||||
className="article-body mt-4 text-base font-light text-ivory/70"
|
||||
dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Reveal>
|
||||
) : (
|
||||
legacy.map((paragraph, i) =>
|
||||
<Reveal key={i} delay={0.03 * i}>
|
||||
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<div className="mx-auto grid max-w-5xl gap-12 lg:grid-cols-[minmax(0,1fr)_17rem] lg:gap-14">
|
||||
<div className="min-w-0">
|
||||
<Reveal>
|
||||
<p className="border-r-2 border-copper pr-6 text-lg font-light leading-10 text-ivory/80">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
</Reveal>
|
||||
)
|
||||
)}
|
||||
{html ? (
|
||||
<Reveal delay={0.05}>
|
||||
<div
|
||||
className="article-body mt-4 text-base font-light text-ivory/70"
|
||||
dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Reveal>
|
||||
) : (
|
||||
legacy.map((paragraph, i) =>
|
||||
<Reveal key={i} delay={0.03 * i}>
|
||||
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||
</Reveal>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* فهرست مطالب — در دسکتاپ کنار متن چسبان میماند */}
|
||||
{toc.length > 0 && (
|
||||
<aside className="order-first lg:order-none">
|
||||
<TableOfContents items={toc} className="lg:sticky lg:top-28" />
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user