StoryHub
مكان هادئ لكتابة القصص وقراءتها.
منصة للكتابة الطويلة مع طبقة اجتماعية — متابعة، إعجابات، تعليقات متفرّعة، حفظ، إشعارات — ثنائية اللغة بالكامل (عربي/إنجليزي) مع تخطيط من اليمين إلى اليسار، وقابلة للتثبيت كتطبيق (PWA). واجهة Django REST Framework وتطبيق React + TypeScript، يعملان على خادم Linux جهّزته وأشغّله.
نموذج البيانات، التطبيقات السبعة كلها، كل نقطة نهاية، المصادقة، Celery، الكاش — والاختبارات الـ 85.
صُمّمت بأداة تصميم بالذكاء الاصطناعي وكُتبت بأدوات برمجة بالذكاء الاصطناعي — دون أي مطوّرين آخرين. أما ربطها بالـ API — العقد، تدفق المصادقة، أشكال البيانات — فمن عملي.
الخادم، Nginx، systemd، PostgreSQL، النسخ الاحتياطي باسترجاع مُجرَّب، وطبقة SEO.
ستة أشياء في StoryHub تُظهر طريقة عملي — افتح أيّاً منها؛ وحيث يوجد كود، فهو أمامك مباشرة.
01 الصلاحيات قاعدة واحدة تقرر من يقرأ القصة
التغذيات وصفحات الكتّاب والروابط المباشرة والإعجابات والتعليقات والحفظ وخريطة الموقع، كلها تسأل دالة queryset واحدة هي visible_to(). إضافة القصص «للمتابعين فقط» عنت تعديل موضع واحد لا سبعة.
class StoryQuerySet(models.QuerySet): """The one place that knows who may read a story.""" def visible_to(self, user, *, with_own_unpublished=False): published = Q(status=Story.StatusChoices.PUBLISHED) public = Q(visibility=Story.VisibilityChoices.PUBLIC) if user is None or not user.is_authenticated: return self.filter(published & public) follows_author = Exists( Follow.objects.filter(follower=user, following=OuterRef("author_id")) ) allowed = published & (public | Q(author=user) | Q(follows_author)) if with_own_unpublished: allowed |= Q(author=user) return self.filter(allowed)
02 ORM والأداء حالة التفاعل تُحسب في قاعدة البيانات
الإعجاب والحفظ والمتابعة تُلحَق بالاستعلام عبر Exists/OuterRef — استعلام واحد للصفحة بدل استعلام لكل عنصر. وعدّ المتابعين يستخدم Subquery بعد أن ضخّم Count عبر join الأرقام.
03 البحث والمشاركة تطبيق صفحة واحدة يقرؤه Google وواتساب
Django يقدّم الهيكل المبني مع <head> مُعاد كتابته لكل مسار — العنوان، Open Graph، الرابط القانوني، JSON-LD — مع خريطة موقع تعيد استخدام قاعدة الرؤية، وmiddleware يضع X-Robots-Tag بدل حجب الـ API في robots.txt.
# Crawlers must be allowed to *fetch* the API; what they # must not do is list its JSON as pages. NOINDEX_PREFIXES = ("/api/", "/auth/", "/admin/") class NoIndexApiMiddleware: def __call__(self, request): response = self.get_response(request) if request.path.startswith(NOINDEX_PREFIXES): response["X-Robots-Tag"] = "noindex, nofollow" return response
04 الأمان تعقيم HTML مرة واحدة عند الكتابة
نصوص القصص تُنظَّف وفق قائمة سماح (nh3) قبل تخزينها، فلا يحتاج أي مسار قراءة — الـ API، لوحة الإدارة، أي تصدير لاحق — أن يتذكّر الهروب من شيء.
05 المصادقة تسجيل الدخول بـ Google يُتحقق منه على الخادم
رموز ID تُفحص بـ google-auth، والبريد الموثّق شرط، وأسماء المستخدمين تُولَّد عند التعارض، وحسابات Google فقط تحصل على تدفق تعيين كلمة مرور بدل تغييرها.
06 التشغيل سكربت نشر لا يستطيع كسر نفسه
deploy.sh يعمل كدالة bash واحدة تُستدعى في سطره الأخير — لأن git pull استبدل مرةً السكربت الجاري في منتصفه فتخطّى خطوة بصمت.
# A function is parsed whole before it runs, so the copy # in memory is the one that finishes — even if `git pull` # replaces this file midway. set -euo pipefail deploy() { $RUN_AS git -C "$APP" pull --ff-only $RUN_AS bash -c "cd '$APP/backend' && .venv/bin/python manage.py migrate" $RUN_AS bash -c "cd '$APP/frontend' && npm ci && npm run build" systemctl restart storyhub-web storyhub-worker } deploy "$@"
خادم واحد، نطاق واحد، وكل خدمة في حسابها.
Nginx يقدّم الواجهة المبنية والملفات الثابتة ويمرّر الـ API إلى Gunicorn؛ وبجانبه PostgreSQL وRedis وعامل Celery كوحدات systemd معزولة. نسخة احتياطية ليلية تُنسخ إلى تخزين خارجي، وجُرّب الاسترجاع قبل الإطلاق. CI يشغّل الاختبارات والبناء عند كل push؛ والنشر نفسه سكربت بأمر واحد عبر SSH — يدوي عمداً عند هذا الحجم.
- Nginxيقدّم التطبيق ويمرّر الـ API
- Gunicornيشغّل Django
- PostgreSQLقاعدة البيانات
- Redisالكاش ووسيط Celery
- Celeryالبريد خارج مسار الطلب
- systemdوحدات معزولة بترتيب اعتماد
- Let’s EncryptTLS وHSTS ورؤوس الأمان
- Backblaze B2نسخ ليلية خارجية
- Resendبريد بـ SPF وDKIM وDMARC
- UptimeRobotفحص كل خمس دقائق
- GitHub Actions85 اختباراً + بناء الواجهة، عند كل push
صُمّمت بـ Google Stitch وأُعيد بناؤها بـ Claude، ثم دمجتها مع الـ API بنفسي: وضع فاتح وداكن مبني على رموز، تخطيط من اليمين إلى اليسار يُعامل كبيانات، تحديثات متفائلة تبقى متزامنة في كل القوائم، محرّر Tiptap برفع صور مضمّن، العربية والإنجليزية عبر i18next، وservice worker للتثبيت.
العدّة


