وبلاگ و سئو: پیش‌نویس/انتشار مقاله + کلمه کلیدی سئو در دیتابیس + OG/canonical/JSON-LD و متای سمت سرور + تغییر رمز و محدودسازی ورود + تست‌ها

- مقاله‌ها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمی‌گردانند؛ autosave دیگر پیش‌نویس نیمه‌کاره را زنده نمی‌کند
- کلمه کلیدی هدف چک‌لیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود)
- robots.txt و sitemap.xml از بک‌اند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزنده‌های بدون JS (app/seo_pages.py)
- فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیش‌نویس برای ادمین، برچسب پیش‌نویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن
- تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت
- محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429
- ساخت دکمه + برای ردیف سئوی صفحه‌های بدون ردیف (SingletonPill)
- تست‌های pytest (۱۵ مورد) + پین‌کردن requirements + حذف package.json سرگردان فرانت
This commit is contained in:
MMD
2026-09-07 04:26:18 +03:30
parent a29deb8bb1
commit b31124e1e4
31 changed files with 1202 additions and 152 deletions
+51 -4
View File
@@ -9,9 +9,46 @@ import { useSite } from '../api'
import { EditableItem } from '../admin/Editable'
import { PlusIcon } from 'lucide-react'
function DraftBadge({ className = '' }: { className?: string }) {
return (
<span
className={`border border-amber-400/60 bg-ink/90 px-2 py-1 text-xs font-bold text-amber-300 ${className}`}
>
پیش‌نویس
</span>
)
}
export function Blog() {
const { articles } = useSite()
const [featured, ...rest] = articles
// حالت خالی — وبلاگ بدون مقاله نباید کرش کند
if (articles.length === 0) {
return (
<>
<PageHero pageKey="blog" />
<section className="bg-ink py-24 lg:py-32" aria-label="مقالات">
<div className="mx-auto max-w-site px-6 text-center lg:px-10">
<h2 className="text-3xl font-bold text-ivory">هنوز مقاله‌ای منتشر نشده است</h2>
<p className="mt-4 text-sm font-light text-ivory/60">
به‌زودی نخستین نوشته‌های ما اینجا منتشر می‌شود.
</p>
<Link
to="/admin/write"
data-admin-link
className="mt-8 inline-flex items-center gap-2 bg-copper px-6 py-3 text-sm font-bold text-char transition-transform duration-200 hover:scale-105"
>
<PlusIcon className="h-4 w-4" />
نوشتن مقاله جدید
</Link>
</div>
</section>
<Newsletter />
</>
)
}
const [featured, ...rest] = articles
return (
<>
@@ -44,7 +81,12 @@ export function Blog() {
/>
</div>
<div className="flex flex-col justify-center p-8 lg:p-12">
<p className="text-xs font-light text-copper">مقاله ویژه — {featured.category}</p>
<p className="text-xs font-light text-copper">
مقاله ویژه — {featured.category}
{featured.status === 'draft' && (
<DraftBadge className="mr-3 inline-block align-middle" />
)}
</p>
<h2 className="mt-6 text-2xl font-bold leading-[2.2] md:leading-[2.2] text-ivory transition-colors duration-200 ease-lux group-hover:text-copper md:text-3xl">
{featured.title}
</h2>
@@ -71,7 +113,12 @@ export function Blog() {
{rest.map((article, i) => (
<Reveal key={article.id} delay={0.05 * i} className="h-full">
<EditableItem resource="articles" item={article} className="h-full" editHref={`/admin/write/${article.id}`}>
<ArticleCard article={article} />
<div className="relative h-full">
{article.status === 'draft' && (
<DraftBadge className="absolute left-3 top-3 z-10" />
)}
<ArticleCard article={article} />
</div>
</EditableItem>
</Reveal>
))}
@@ -82,4 +129,4 @@ export function Blog() {
<Newsletter />
</>
)
}
}