feat(blog): جداسازی دوزبانهٔ اسلاگ/کلیدواژه/نویسنده/تگ‌ها/متا + میگریشن ۰۰۰۲، رفع «دقیقه مطالعه دقیقه»، فونت مقاله ۱۵/۱۷px، جدول تیپ‌تپ در ادیتور، مسیر /en/blog با slug_en

This commit is contained in:
aramesh-site deploy
2026-10-06 10:11:55 +00:00
parent 426dc783df
commit 61a2e95278
11 changed files with 862 additions and 197 deletions
+21 -16
View File
@@ -14,7 +14,7 @@ import { CategorySelect, LuxeTagsSelect } from '../components/admin/LuxeSelect'
import type { CategoryOption } from '../components/admin/LuxeSelect'
import { ARTICLE_CATEGORIES } from './entities'
import { FEATURED_IMAGE_ALT_SUFFIX } from '../data/constants'
import { formatReadingTime } from '../utils/readingTime'
import { formatReadingTime, readingTimeMinutes } from '../utils/readingTime'
import { toArticleBodyDoc } from '../utils/tiptapDoc'
import type { Article, ArticleBodyDoc } from '../data/site'
@@ -341,6 +341,9 @@ export function ArticleWritePage() {
const metaLength = (draft.meta_description ?? '').trim().length
// در زبان انگلیسی، فیلدهای متنی به ستون‌های _en می‌روند؛ اسلاگ/تصویر/وضعیت مشترک‌اند
const suf = <N extends string>(name: N) => `${name}${editLang === 'en' ? '_en' : ''}` as keyof Draft
// مقدار فعال فیلد دوزبانه — برای value ورودی‌ها و چک‌لیست سئو
const activeVal = (name: 'title' | 'slug' | 'author' | 'seo_keyphrase' | 'meta_description' | 'excerpt'): string =>
String((editLang === 'en' ? draft[`${name}_en` as keyof Draft] : draft[name]) ?? '')
// متن جایگزین تصویر شاخص — با watch('title') به‌صورت زنده ساخته می‌شود
const imageAltFallback = [watchedTitle.trim(), FEATURED_IMAGE_ALT_SUFFIX]
@@ -410,7 +413,7 @@ export function ArticleWritePage() {
</button>
)}
<Link
to={`${editLang === 'en' ? '/en' : ''}/blog/${draft.slug}`}
to={`${editLang === 'en' ? '/en' : ''}/blog/${editLang === 'en' ? draft.slug_en || draft.slug : draft.slug}`}
data-admin-link
className="flex items-center gap-1.5 border border-ivory/20 px-4 py-2 text-xs font-light text-ivory/70 transition-colors hover:border-copper hover:text-copper"
>
@@ -496,11 +499,12 @@ export function ArticleWritePage() {
doc={(editLang === 'en' ? draft.body_en : draft.body) as unknown as Record<string, unknown>}
onChange={(json) => {
// زمان مطالعه به‌صورت پویا از متن سند محاسبه می‌شود — به زبان جاری
const minutes = formatReadingTime(json)
const minutes = readingTimeMinutes(json)
if (editLang === 'en') {
queueSave({ [suf('body')]: json, [suf('read')]: `${minutes} min read` })
} else {
queueSave({ body: json, read: `${minutes} دقیقه` })
// formatReadingTime خودش برچسب «مطالعه» را دارد — پسوند اضافه ممنوع
queueSave({ body: json, read: formatReadingTime(json) })
}
}}
/>
@@ -524,8 +528,8 @@ export function ArticleWritePage() {
<textarea
id="meta-description"
rows={3}
value={draft.meta_description ?? ''}
onChange={(e) => queueSave({ meta_description: e.target.value })}
value={activeVal('meta_description')}
onChange={(e) => queueSave({ [suf('meta_description')]: e.target.value })}
placeholder="توضیح جذابی برای گوگل بنویسید که شامل کلمه کلیدی باشد…"
className="mt-3 w-full resize-y border border-ivory/15 bg-ink px-4 py-3 text-sm font-light leading-8 text-ivory focus:border-copper focus:outline-none"
/>
@@ -548,11 +552,12 @@ export function ArticleWritePage() {
{/* چکیده و متادیتا */}
<SeoChecklist
input={{
title: (editLang === 'en' ? draft.title_en : draft.title) ?? '',
slug: draft.slug,
excerpt: draft.meta_description || (editLang === 'en' ? draft.excerpt_en : draft.excerpt) || '',
title: activeVal('title'),
slug: activeVal('slug'),
excerpt: activeVal('meta_description') || activeVal('excerpt'),
body: (editLang === 'en' ? draft.body_en : draft.body) ?? draft.body,
keyphrase: draft.seo_keyphrase,
keyphrase: activeVal('seo_keyphrase'),
author: draft.author,
}}
/>
@@ -564,8 +569,8 @@ export function ArticleWritePage() {
<input
id="seo-keyphrase"
type="text"
value={draft.seo_keyphrase}
onChange={(e) => queueSave({ seo_keyphrase: e.target.value })}
value={activeVal('seo_keyphrase')}
onChange={(e) => queueSave({ [suf('seo_keyphrase')]: e.target.value })}
className="mt-2 w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory focus:border-copper focus:outline-none"
/>
</div>
@@ -585,7 +590,7 @@ export function ArticleWritePage() {
<input
id="article-author"
type="text"
{...textField('author')}
{...textField(suf('author'))}
placeholder="نام و نام خانوادگی نویسنده"
className="mt-2 w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory placeholder:text-ivory/30 focus:border-copper focus:outline-none"
/>
@@ -628,8 +633,8 @@ export function ArticleWritePage() {
<label className="text-xs font-light text-ivory/50">هشتگ‌ها / برچسب‌ها</label>
<div className="mt-2">
<LuxeTagsSelect
value={normalizeTags(draft.tags)}
onChange={(tags) => queueSave({ tags })}
value={normalizeTags(editLang === 'en' ? draft.tags_en : draft.tags)}
onChange={(tags) => queueSave({ [suf('tags')]: tags })}
/>
</div>
</div>
@@ -664,7 +669,7 @@ export function ArticleWritePage() {
<input
type="text"
dir="ltr"
{...textField('slug')}
{...textField(suf('slug'))}
className="mt-2 w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory focus:border-copper focus:outline-none"
/>
</div>