تا وقتی کسی کلون/بیلد تازه میگیرد، همان تصاویر را داشته باشد: - seed-media شامل هر ۱۹ فایل تصویری است که در دیتابیس با پیشوند /media ثبتاند - minio-setup بعد از ساخت باکت، محتوای seed-media را با mc mirror در bucket میگذارد — روی حجم موجود idempotent است و داده تازه را نمیخورد
گروه ساختمانی آرامش — پروژه کامل (فرانتاند + بکاند)
مونوریپوی سایت گروه ساختمانی آرامش شامل:
frontend/— سایت React + Vite + Tailwind (تمام متنها و تصاویر از API خوانده میشود)backend/— بکاند FastAPI + SQLAlchemy 2 + PostgreSQL + JWT + MinIO
معماری
Traefik (پورت ۵۱۷۳ — تنها نقطه ورود)
├── / → Caddy (فایلهای استاتیک SPA)
├── /api → FastAPI (بکاند)
└── /media → MinIO (باکت aramesh-media — تصاویر آپلودی)
- همه محتوای متنی سایت در PostgreSQL است — هیچ متنی در کد فرانتاند هاردکد نشده؛
با یک درخواست
GET /api/siteبارگذاری و از پنل مدیریت ویرایش میشود - تصاویر آپلودی در MinIO (Object Storage سازگار با S3) ذخیره میشوند و با مسیر نسبی
/media/...برگردانده میشوند — بکاند ذخیرهسازی بدون تغییر محتوای دیتابیس قابل تعویض است - مسیریابی همهچیز روی Traefik است (با Docker labels) — nginx در پروژه وجود ندارد
ساختار
├── frontend/ # سایت React
│ ├── src/ # کد منبع (api.tsx = لایه اتصال به بکاند)
│ ├── Dockerfile # بیلد Node + سرو استاتیک با Caddy
│ └── Caddyfile # فقط سرو فایلهای استاتیک (مسیریابی روی Traefik است)
├── backend/ # بکاند FastAPI
│ ├── app/ # کد منبع (آپلود تصویر: app/api/admin.py — سئوی سرور: app/seo_pages.py)
│ ├── tests/ # تستهای API (pytest)
│ ├── requirements.txt
│ ├── Dockerfile
│ └── .env.example
├── scripts/backup.sh # پشتیبانگیری دیتابیس و تصاویر
├── docker-compose.yml # اجرای کامل با یک دستور
└── docker-compose.prod.yml # لایه تولید: TLS با Let's Encrypt
اجرا با Docker (سادهترین راه)
cp .env.example .env # مقادیر change-me را عوض کنید — بدون .env استک بالا نمیآید
docker compose up --build
- سایت: http://localhost:5173 (از طریق Traefik)
- مستندات API: http://localhost:8001/docs (با
docker-compose.override.ymlمحلی) - کنسول MinIO: http://localhost:9001 — کاربر/رمز از متغیرهای
MINIO_ROOT_*در compose - دیتابیس PostgreSQL و آبجکتاستور MinIO هر دو در کانتینر جدا با volume دائمی
- بکاند هنگام شروع، خودش جداول را میسازد و فقط جدولهای خالی را seed میکند — ارتقای نسخه دادههای فعلی را از بین نمیبرد
- در اولین اجرا، سرویس
minio-setupباکتaramesh-mediaرا میسازد و خواندن عمومی را فعال میکند
انتشار واقعی (TLS)
برای بالا آوردن با HTTPS روی دامنه واقعی، لایه docker-compose.prod.yml را هم سوار کنید:
# در .env: DOMAIN=example.com و ACME_EMAIL=you@example.com
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
- Traefik گواهی Let's Encrypt میگیرد و تمدید میکند؛ همه درخواستهای HTTP به HTTPS هدایت میشوند
- در این حالت
SITE_URL=https://example.comرا هم در.envبگذارید تا sitemap و آدرسهای OG مطلق شوند
اجرای محلی (توسعه)
بکاند
cd backend
python -m venv .venv
.venv\Scripts\activate # ویندوز — در لینوکس: source .venv/bin/activate
pip install -r requirements.txt
# فایل .env از روی .env.example بسازید و DATABASE_URL را تنظیم کنید
python -m app.seed # ساخت جداول + محتوای اولیه
uvicorn app.main:app --reload
فرانتاند
cd frontend
npm install
npm run dev
در حالت توسعه، Vite درخواستهای /api و /media را به Traefik (پورت ۵۱۷۳) پروکسی میکند؛
پس استک Docker باید در حال اجرا باشد. آدرس بکاند در frontend/src/config.ts از
VITE_API_URL خوانده میشود — پیشفرض خالی یعنی درخواستهای هممبدأ.
امکانات بکاند
- API عمومی: برند، منو، هیرو، خدمات، پروژهها، مقالات، نظرات، سوالات متداول، تیم، تایملاین، آمار، دفاتر، مدلهای سرمایهگذاری و... — همه در دیتابیس
- فرم تماس
POST /api/contactو خبرنامهPOST /api/newsletter - پنل مدیریت با JWT: ورود
POST /api/auth/login(پیشفرض:admin/aramesh123) و سپس CRUD کامل همه محتواها در/api/admin/... - تغییر رمز
POST /api/auth/change-password— از نوار مدیریت پایین سایت (دکمه «رمز») هم در دسترس است - محدودسازی ورود: بعد از ۵ ورود ناموفق در ۱۵ دقیقه از یک IP، درخواست با 429 رد میشود
- آپلود تصویر
POST /api/admin/upload(فقط توکن ادمین، حداکثر ۱۰MB) — فایل در MinIO ذخیره و URL نسبی/media/...برمیگردد - ویرایش جزئی
PATCH /api/admin/{resource}/{id}— فقط فیلدهای ارسالشده عوض میشوند - ترتیببندی
POST /api/admin/{resource}/reorder— با{"ids": [...]} - همه محتوا در یک درخواست:
GET /api/site(مقالههای پیشنویس در آن نمیآیند) - تستها:
cd backend && pip install -r requirements-dev.txt && pytest tests
در محیط واقعی حتماً
SECRET_KEY،ADMIN_PASSWORDوMINIO_ROOT_PASSWORDرا تغییر دهید (از.env) و بعد از اولین ورود، رمز را از نوار مدیریت عوض کنید.
ویرایش زنده سایت (دوقلوی دیجیتال)
ادمینها نیازی به داشبورد جدا ندارند — خود سایت ویرایشگر است:
- به
/admin/loginبروید و وارد شوید - نوار شناور پایین صفحه ظاهر میشود؛ حالت ویرایش را روشن کنید
- روی هر متن کلیک کنید و ویرایش کنید — خروج از کادر، بلافاصله ذخیره میشود (Enter ذخیره، Esc لغو)؛ وضعیت ذخیره در نوار پایین دیده میشود
- آیتمهای مجموعهها (خدمات، پروژهها، مقالات، نظرات، تیم و...) با هاور، دکمههای ویرایش / حذف / جابهجایی ترتیب نشان میدهند؛ دکمه «افزودن» هم انتهای هر لیست هست
- تصویرها از همان کشوها به MinIO آپلود میشوند
- در حالت ویرایش، لینکها کلیک نمیشوند تا ویرایش متن ممکن باشد
- بازدیدکنندههای عادی هیچ اثری از این لایه نمیبینند (بدون توکن، همه درخواستهای
مدیریتی
401برمیگردند) - تمام تغییرها بلافاصله در PostgreSQL ثبت میشوند — با Ctrl+F5 هم میمانند
وبلاگ — نوشتن با ویرایشگر WYSIWYG
مقالات با ویرایشگر غنی TipTap نوشته میشوند (همان تجربه Medium):
- در حالت ویرایش، روی هر مقاله در صفحه وبلاگ دکمه ویرایش را بزنید — کشوی نوشتن باز میشود
- تیتر (H2/H3)، بولد/ایتالیک/زیرخط، لیست نقطهای و شمارهای، نقلقول، لینک و واگرد
- درج تصویر داخل متن: دکمه تصویر، یا چسباندن/کشیدن تصویر داخل نوشته — فایل مستقیم در MinIO ذخیره و در متن قرار میگیرد
- راستبهچپ کامل با فونت و تم سایت
- ذخیره خودکار: هر مکث کوتاه در تایپ، تغییرها را ذخیره میکند؛ بستن کشو هم ذخیره میکند
پیشنویس و انتشار: مقاله تازه همیشه پیشنویس شروع میشود — در سایت عمومی دیده
نمیشود و تا انتشار، تغییرهای خودکار آن چیزی را خراب نمیکند. با دکمه «انتشار در سایت»
عمومی میشود؛ دوباره میتوان آن را به پیشنویس برگرداند. پیشنویسها فقط برای ادمین
(در صفحه وبلاگ، با برچسب «پیشنویس») دیده میشوند و در GET /api/site هرگز نمیآیند.
فرمت ذخیره: بدنه مقاله یک سند ساختاریافته TipTap (JSON) در PostgreSQL است — نه HTML خام. صفحه مقاله آن را با رندر استاتیک TipTap به HTML تبدیل میکند، پس فقط تگهای شناختهشده (پاراگراف، تیتر، لیست، نقلقول، تصویر، لینک) تولید میشوند. مقالات قدیمی (لیست پاراگراف) در اولین اجرا بهصورت خودکار به این فرمت تبدیل میشوند.
سئو
- بررسی سئو در صفحه نوشتن: فهرست سبز/نارنجی/قرمز به سبک یوست ولی سازگار با فارسی —
طول عنوان، کلمه کلیدی هدف (در عنوان/پاراگراف اول/تیترها)، طول چکیده، تعداد واژه،
تیتربندی H2، متن جایگزین تصاویر، پیوندها و نشانی (slug) — کلمه کلیدی هدف در دیتابیس
ذخیره میشود (فیلد
seo_keyphraseمقاله) - سئوی هر صفحه: در حالت ویرایش، دکمه سئو روی هیرو هر صفحه (و کنار هیرو صفحه اصلی) — عنوان تب و توضیح متای هر صفحه از دیتابیس خوانده و اعمال میشود؛ اگر ردیف سئوی صفحهای هنوز ساخته نشده باشد، دکمه + همانجا آن را میسازد
- صفحه مقاله و پروژه بهطور خودکار عنوان و توضیح متا را از محتوای خودشان میگیرند
- تگهای OG و توییتر و canonical برای همه صفحات از
src/seo.tsست میشود و مقالهها اسکیمای JSON-LD نوع Article میگیرند - robots.txt و sitemap.xml از بکاند سرو میشوند و مقاله/پروژههای منتشرشده را
شامل میشوند (پیشنویسها نمیآیند) — برای آدرس مطلق،
SITE_URLرا در.envبگذارید - متا برای خزندههای بدون جاوااسکریپت (تلگرام، واتساپ و...): بکاند صفحات
/blog،/blog/{slug}،/projectsو/projects/{slug}را با همانindex.htmlفرانتاند سرو میکند و پیش از ارسال، title / description / OG / JSON-LD را از دیتابیس داخل آن تزریق میکند (app/seo_pages.py) — بازدیدکنندههای عادی همان SPA همیشگی را میبینند
پشتیبانگیری
همه محتوای سایت در PostgreSQL و تصاویر در MinIO است — هر دو را با یک دستور پشتیبان بگیرید:
bash scripts/backup.sh # در backups/ — نگهداری پیشفرض ۱۴ روز
- خروجی:
backups/db-<زمان>.sql.gz(فشرده) وbackups/images-<زمان>/(آینه باکت) - زمانبندی با cron یا Task Scheduler: نمونه دستور داخل خود اسکریپت هست
- بازیابی دیتابیس:
gunzip -c backups/db-<زمان>.sql.gz | docker compose exec -T db psql -U aramesh arameshplaza
نکات امنیتی
- مقادیر محرمانه در
.envریشه هستند (در.gitignore) و درdocker-compose.ymlبا سینتکس${VAR:?}اجباری شدهاند — بدون.envاستک بالا نمیآید - رمز عبور ادمین با bcrypt هش میشود و از نوار مدیریت پایین سایت (دکمه «رمز») قابل تعویض است
- ورود با محدودیت تلاش محافظت میشود: ۵ ورود ناموفق در ۱۵ دقیقه از هر IP → خطای 429
- توکنهای JWT امضا دارند و مدت اعتبارشان قابل تنظیم است
- تصاویر باکت MinIO عمومی (فقط خواندن) هستند؛ آپلود فقط با توکن ادمین ممکن است
- مقالههای پیشنویس در هیچ انتهای عمومی نمیآیند و در HTML تزریقشده سئو هم افشا نمیشوند