النشر والوصول
من يقرأ ومن يحرّر، وعلامة publish:، ومعاينة موقعك كما يراه الزائر، ووضعه على الإنترنت، وتعليقات القرّاء.
قراءة عامة، تحرير للمشرف
في أسطرلاب صنفان من الناس: الزوار، وهم يقرؤون، والمشرف، وهو يحرّر. المشرف أنت. والزائر كل من سواك يفتح الموقع.
حين تشغّل التطبيق أول مرة لا تكون له كلمة مرور، فيعمل في الوضع المحلي المفتوح: يُعامَل كل زائر على أنه المشرف، ويقول تحذير ذلك عند التشغيل. لا بأس بهذا على حاسوبك أنت. لكنه لا يصلح على شبكة لا تثق بها كل الثقة، فقبل أن تضع خزانتك هناك اضبط كلمة مرور للمشرف:
cp .env.example .env
npm run hash-password # يطلب كلمة مرور ويطبع تجزئة argon2id
لا يطبع السكربت كلمة مرورك، بل يطبع تجزئتها: بصمة مشتقّة منها يستطيع الخادم أن يتحقق بها من كلمة المرور، ولا يستطيع أحد أن يستخرج منها كلمة المرور نفسها. ضع هذه التجزئة في .env بين علامتي اقتباس مفردتين (لأنها تحوي الرمز $، وسطر الأوامر يحاول تفسيره لو تُرك مكشوفًا)، وأضف معها نصًّا طويلًا عشوائيًّا يوقّع به الخادم ملفات الجلسة (وسيأتي شرح الجلسة بعد قليل):
ADMIN_PASSWORD_HASH='$argon2id$v=19$m=65536,...'
SESSION_SECRET=some-long-random-string # مثلًا openssl rand -hex 32
ما إن تُضبط التجزئة حتى يرى الزوار عرض القراءة: الملاحظات معروضة كاملة، ومعها البحث والمخطط والروابط الراجعة، لكن من غير محرر، ومن غير أي سبيل إلى إنشاء شيء أو إعادة تسميته أو حذفه. وفي شريط الحالة رابط هادئ اسمه تسجيل الدخول يفتح نافذة الدخول؛ فإذا أدخلت كلمة المرور الصحيحة وضع الخادم في متصفحك علامة موقّعة تقول «هذا هو المشرف» وفتح لك التحرير في مكانك، بلا إعادة تحميل للصفحة. ويتحكّم في هذا الأمر مفتاحان آخران: PUBLIC=false يجعل القراءة نفسها تحتاج إلى تسجيل دخول، وHOME_NOTE يختار الملاحظة التي يهبط عليها الزائر أول مرة.
PUBLIC=false يشترط كلمة مرور، ويرفض الخادم أن يبدأ من دونها. فمن غير ADMIN_PASSWORD_HASH لا معنى لعبارة «مسجَّل الدخول» أصلًا، ولكانت «خاصّة» تعني عكسها تمامًا: كل طلب مجهول يُعامَل على أنه المشرف. وبدل أن يبدأ أسطرلاب على هذه الحال يطبع تذكيرًا بالأمر npm run hash-password ويتوقف. (أما التشغيل المفتوح عمدًا على شبكة موثوقة فما زال جائزًا؛ فقط لا تدّعِ الخصوصية معه.) وللسبب نفسه يحتاج النسخ الاحتياطي والمزامنة إلى كلمة مرور في كل وضع: فمن دونها يستطيع كل من يصل إلى المنفذ أن يوجّه مستودع git البعيد إلى خادمه هو، ويدفع خزانتك كلها إليه.
الجلسات. الجلسة هي ذاكرة الخادم بأنك سجّلت دخولك، وتعيش في ملف تعريف ارتباط (كوكي) داخل متصفحك، وهو قصاصة صغيرة يرسلها المتصفح مع كل طلب. لا تستطيع سكربتات الصفحة قراءة هذا الملف (httpOnly)، ولا يُرسل مع الطلبات القادمة من مواقع أخرى (SameSite=Lax)، ويحمل علامة Secure كلما جاء الطلب عبر HTTPS، سواء وصل مباشرة أو عبر وكيل مذكور في TRUSTED_PROXIES يقول ذلك في الترويسة X-Forwarded-Proto. واضبط SECURE_COOKIES=true أو false لتقرّر الأمر بنفسك، مثل false على شبكة منزلية تعمل بـHTTP العادي. تعيش الجلسة سبعة أيام وتجدّد نفسها ما دمت تستخدم التطبيق. وتسجيل الخروج يخرجك من كل مكان، على كل جهاز، فورًا: تحمل كل جلسة رقمًا مخزّنًا في ASTROLABE_DATA، وتسجيل الخروج يرفع هذا الرقم، فتتوقف كل الجلسات الأقدم عن العمل. وتغيير ADMIN_PASSWORD_HASH يفعل الشيء نفسه: كل ملف جلسة صدر تحت كلمة المرور القديمة يموت لحظةَ تثبيت الجديدة، وهذا بالضبط ما تريده بعد ضياع حاسوب محمول. (وترقية أسطرلاب تُبطل الجلسات القائمة مرة واحدة أيضًا؛ فتسجّل دخولك من جديد.)
حدّ محاولات تسجيل الدخول. عشر محاولات فاشلة في الدقيقة لكل عنوان IP، ومعها سقف عام لكل العناوين. وتُحتسب المحاولة قبل فحص كلمة المرور، فألف تخمين تُرسل دفعةً واحدة لا يُفحص منها إلا عشرة. ولا يُفحص في اللحظة الواحدة أكثر من كلمتي مرور، لأن كل فحص argon2id يكلّف 64 ميغابايت من الذاكرة بحكم تصميمه، فلا يستطيع سيلٌ من محاولات الدخول أن يخنق الخادم الذي يقدّم ملاحظاتك في الوقت نفسه.
ما الذي يُعدّ منشورًا
شيء واحد لا غير: publish: true في مقدمة الملاحظة. (المقدمة هي الكتلة المحصورة بين سطرَي --- في أعلى الملاحظة، حيث تعيش خصائصها.)
---
title: في الهوامش
publish: true
tags: [writing]
date: 2026-08-16
---
يقلب الاختصار Ctrl/Cmd Shift P هذه العلامة في الملاحظة المفتوحة، ويفعل الشيء نفسه أمرا نشر الملاحظة / إلغاء نشر الملاحظة في لوحة الأوامر. والتغيير تعديلُ سطر واحد في المقدمة، ولا يُمسّ من الملف شيء سواه. وتحمل ملاحظة .tex العلامة نفسها في كتلة التعليق الخاصة بها وتُنشر بالطريقة عينها.
وعلى الهاتف أو اللوح يسأل النشر قبل أن يقع — من ورقة الملاحظة في تخطيط الهاتف، ومن الضغط المطوّل على ملاحظة — لأن الإبهام يخطئ بعرض زر، والنشر هو الفعل الوحيد الذي يبلغ الغرباء. أما الفأرة وCtrl/Cmd Shift P فتنشران بخطوة واحدة.
وكل ما يستطيع الزائر أن يراه ينبع من هذه العلامة. فالشجرة، والبحث، والمخطط، والروابط الراجعة، وقائمة التدوينات، وخلاصة RSS، وبثّ التغييرات الحيّ الذي يُبلغ الصفحة المفتوحة بما تغيّر، كلها يُعاد حسابها عند كل طلب على مجموعة الملاحظات المنشورة وحدها. لذلك فالملاحظة غير المنشورة ليست مخفيّة فحسب، بل لا يستطيع الزائر أن يفرّق بينها وبين ملاحظة لا وجود لها. وتتبع المرفقات ملاحظاتها: لافتة الملاحظة المنشورة وصورها المضمّنة يمكن جلبها، أما مرفقات الملاحظة غير المنشورة فلا.
وثمّة إعدادان يستطيعان تضييق ما يراه الزوار أكثر، من غير إلغاء نشر أي شيء: EXCLUDE_TAGS يخفي وسومًا معيّنة من قوائم المواضيع العامة، ومرشّح اللغة يخفي الملاحظات غير المكتوبة باللغة المختارة. وكلاهما ترتيب للواجهة لا أكثر. أما publish: false فهو المفتاح الذي يغلق الباب فعلًا.
ومفتاح واحد في المقدمة يوسّع بدل أن يضيّق: folders: يسمّي المجلدات العامة التي تنتمي إليها الملاحظة المنشورة، وهي مجموعات تعرّفها أنت على المدونة، إلى جانب المواضيع التي تصنعها وسومها. لا يؤثر في كون الملاحظة منشورة أو لا، ولا يفعل شيئًا حتى تعلن في الإعدادات مجلدًا بذلك العنوان.
ولا يصل شيء منشورًا من الخارج. فالمقالة المحفوظة من الخلاصات والملاحظة التي يجلبها معالج الاستيراد لا تحملان publish: true — والمستورِد يزيل ما وضعه Obsidian Publish — فتبقيان خاصتين حتى تنشرهما بنفسك.
المعاينة بوصفك زائرًا
وأنت مسجَّل الدخول تستطيع في أي وقت أن ترى الموقع كما يراه الزائر بالضبط: انقر أيقونة العين في شريط الحالة، أو شغّل معاينة كزائر من لوحة الأوامر. وهذه ليست محاكاة في المتصفح، بل يمرّ كل طلب على الخادم بالمسلك نفسه الذي يمرّ به طلب أي غريب (شجرة المنشور وحده، والبحث، والمخطط، وبثّ التغييرات)، فما تراه هو ما يقدّمه الموقع العام بايتًا بايتًا. ويعلّم الوضعَ شريطٌ ذهبي نحيل، وإنهاء المعاينة يعيدك إلى التطبيق الكامل على الملاحظة نفسها. وإعادة تحميل الصفحة تنهي المعاينة دائمًا.
وتبقى الملاحظة التي أنت عليها على الشاشة. فإذا كان الموقع العام يخفيها خلف مرشّح اللغة، كملاحظة عربية تعاينها على موقع إنجليزي مثلًا، عُرضت كما يراها قارئ تلك اللغة: تستعير المعاينة لغة ذلك القارئ ما دامت قائمة، وتقول ذلك في تنبيه، وتعيدها حين تخرج. أما الملاحظة غير المنشورة أصلًا فتظهر مكانها صفحة «لا شيء هنا»، تمامًا كما تظهر للزائر.
وعلى النسخة الخاصة (PUBLIC=false، وهكذا يشغّل تطبيق سطح المكتب الخزانة) لا شيء يُعاين: لن يلقى الزائر إلا صفحة تسجيل الدخول. فتقول العين ذلك في تنبيه وتتركك حيث أنت. عاين كتابتك على النسخة المستضافة التي تنشرها.
وضعه على الإنترنت
شغّل أسطرلاب خلف أي وكيل عكسي يعمل بـHTTPS، وهو خادم ويب يقف أمام التطبيق، يحمل شهادة HTTPS، ويمرّر الطلبات إلى localhost:6801. يصلح لذلك Caddy وnginx ونفق Cloudflare. والتطبيق كله على عنوان واحد ومنفذ واحد (الصفحات والواجهة البرمجية معًا)، فلا حاجة إلى قواعد خاصة في الوكيل. فقط تأكد من أنه لا يمكن الوصول إليه إلا عبر HTTPS، حتى لا تسافر كلمة المرور ولا ملف الجلسة مكشوفين.
وحين تضعه خلف وكيل اضبط أيضًا TRUSTED_PROXIES على عنوان الوكيل (مثل TRUSTED_PROXIES=127.0.0.1,::1). فعندئذ يُحسب حدّ محاولات الدخول على عنوان الزائر الحقيقي، مأخوذًا من الترويسة X-Forwarded-For، بدل أن يجمع الزوار جميعًا تحت عنوان الوكيل. ولا يُصدَّق ما في الترويسة إلا حين يأتي الاتصال من عنوان مذكور في القائمة؛ وإلا تُتجاهل، لأن أي أحد يستطيع تزويرها.
وأحجام الطلبات محدودة على الخادم قبل أن يقرأها أي شيء: 10 ميغابايت على أي طلب إلى /api، وحدّ أضيق كثيرًا هو 64 كيلوبايت على الشيئين اللذين يستطيع الزائر المجهول إرسالهما (التعليقات ومحاولات الدخول). وكل ما يتجاوز الحدّ يُرفض برمز الخطأ HTTP 413 (أي «الطلب أكبر من المسموح») بدل أن يشغل الذاكرة. وحدٌّ مماثل عند الوكيل طبقة حماية إضافية معقولة؛ في nginx مثلًا: client_max_body_size 10m;.
التعليقات
اضبط COMMENTS=on (أو شغّل المفتاح في الإعدادات ← التعليقات والإشارات)، فينبت تحت عرض القراءة لكل ملاحظة منشورة قسم هادئ اسمه الحواشي، يستطيع الزوار أن يتركوا فيه ملاحظة نصية. والاسم اختياري؛ ومن دونه يُنسب التعليق إلى «مجهول». وما دامت التعليقات معطَّلة (وهو الافتراضي) فلا أثر للميزة البتة: لا واجهة لها، ومساراتها في الواجهة البرمجية تجيب بـ404 كأنها غير موجودة.
والإشراف مدمج للمشرف. على كل تعليق زرّ حذف هادئ × (يسأل قبل أن يفعل شيئًا لا رجعة فيه)، ومفتاح على شكل عين يخفي التعليق بدل حذفه. والتعليق المخفي يختفي عن الزوار ويبقى في قاعدة البيانات؛ يراه المشرف باهتًا مع شارة «مخفي»، ويستطيع إظهاره في أي وقت. ويفتح أمر الإشراف على التعليقات في لوحة الأوامر لوحةً بأحدث التعليقات عبر كل الملاحظات: الكاتب، ومقتطف، والملاحظة التي ينتمي إليها التعليق (انقر للانتقال إليها)، مع أزرار الإخفاء والحذف نفسها.
وتُخزَّن التعليقات في ملف SQLite في ASTROLABE_DATA/comments.db (يُنشأ عند أول حاجة، ويتجاهله git)، عبر node:sqlite المدمج في Node، فلا شيء إضافي يُثبَّت. وضوابط الإساءة مدمجة أيضًا: طلب التعليق الذي يتجاوز 64 كيلوبايت يُرفض قبل أن يحلّله أي شيء؛ والنشر محدود بخمسة تعليقات في الدقيقة لكل عنوان IP (مع الأخذ بـTRUSTED_PROXIES لمعرفة العنوان الحقيقي، كما في تسجيل الدخول)؛ والتعليق ألفا حرف من النص العادي على الأكثر (يُعرض نصًّا خامًا دائمًا، فلا يُفسَّر فيه HTML ولا ماركداون)، والاسم أربعون حرفًا؛ ويحمل النموذج حقلًا مخفيًّا يبتلع في صمت ما ترسله الروبوتات. ولا يمكن قراءة التعليقات أو كتابتها إلا على ملاحظات تحمل publish: true؛ ولأي مسار آخر تجيب الواجهة البرمجية بالرمز 404 نفسه الذي تعطيه لملاحظة مفقودة، فتبقى المسارات غير المنشورة بمنأى عن التخمين. ومع PUBLIC=false (خزانة خاصة كليًّا) لا يستطيع الزوار قراءة التعليقات ولا كتابتها البتة.
ويمكن لمواقع أخرى أن تنضم إلى الحديث أيضًا. حين يعمل استقبال إشارات الويب أو الفيديفيرس (الإعدادات ← التعليقات والإشارات)، تصل الردود والإعجابات والإشارات المكتوبة في أماكن أخرى إلى لوحة الإشراف نفسها، موسومةً بمصدرها، وما توافق عليه منها يظهر تحت التعليقات في قسم صغير اسمه الإشارات. انظر إشارات الويب والفيديفيرس.
القشرات العامة الثلاث
القشرة هي الإطار الذي يرى الزائر ملاحظاتك بداخله. وثمّة ثلاث قشرات، تختار بينها بالمفتاح PUBLIC_LAYOUT:
PUBLIC_LAYOUT | ما يأخذه الزائر |
|---|---|
app (الافتراضي) | التطبيق نفسه للقراءة فقط: الشريط الجانبي والتبويبات والمخطط والبحث، بلا محرر |
blog | مدونة تقليدية: ترويسة وشريط مواضيع وصفحات مقالات وRSS وتعليقات؛ انظر وضع المدونة |
designed | صفحة رئيسية تركّبها بنفسك من أقسام؛ انظر المصمم |
ولا يغيّر أيٌّ منها شيئًا عندك أنت. فما دمت مسجَّل الدخول تأخذ التطبيق الكامل، بشريطه الجانبي وكل شيء. القشرة للزوار وحدهم.