رفعنا المشروع إلى استضافة مشتركة جديدة. كانت الملفات هي نفسها ملفات البيئة المحلية، وقاعدة البيانات مستوردة، وملف .env معبأ بالقيم الصحيحة. فتحنا النطاق فلم نرَ الصفحة الرئيسية للموقع بل قائمة بالمجلدات: app وbootstrap وconfig وvendor وتحتها public. كانت هذه أول مواجهة لنا مع المشكلة الدائمة لارافيل في public_html: جذر النطاق في cPanel هو المجلد public_html، بينما يجب أن يكون جذر الويب في لارافيل هو المجلد public.

كان رد الفعل الأول: حسنًا، العنوان الصحيح إذن هو example.com/public. جرّبناه فعمل الموقع فعلًا. فرحنا لحظة ثم أدركنا فورًا حجم ما حدث. إذا كان public ظاهرًا من الخارج فالمشروع نفسه ظاهر أيضًا. كتبنا العنوان example.com/.env في المتصفح فنزّل المتصفح الملف. كلمة مرور قاعدة البيانات وAPP_KEY ومفتاح بوابة الدفع، كلها في ملف نصي بسيط.

هذه المذكرة هي المسار نفسه: لماذا يوجد هذا التناقض أصلًا، والحلان الشائعان اللذان يختارهما الناس وكلاهما سيئ، والحل الصحيح وبديله عندما لا تسمح الاستضافة، وملف .htaccess القياسي في لارافيل سطرًا سطرًا، وفئة أخطاء 404 التي لا علاقة لها بكودك.

لماذا لا يتوافق لارافيل مع جذر public_html#

لدى لارافيل «متحكم أمامي» (Front Controller): الملف public/index.php. كل طلب يصل إلى الموقع يجب أن يصل إلى هذا الملف الواحد، ويتم باقي العمل داخل PHP. ولهذا التصميم قسّم لارافيل مجلداته إلى فئتين.

ما يجب أن يظهر للويب موجود داخل public فقط: index.php وملفات CSS وJS المبنية والصور وfavicon وrobots.txt.

أما ما يجب ألا يكون متاحًا للويب أبدًا فهو بقية المشروع:

  • ملف .env الذي يحتوي على كلمة مرور قاعدة البيانات وAPP_KEY ومفاتيح كل الخدمات الخارجية.
  • مجلد storage الذي يحفظ السجلات والكاش وملفات الجلسات والملفات الخاصة المرفوعة. يمكن لملف storage/logs/laravel.log عادي أن يكشف المسار الكامل للملفات وجزءًا من الاستعلامات وحتى قيم المتغيرات.
  • مجلد vendor بكامل كود الحزم؛ تُقرأ منه النسخة الدقيقة لكل حزمة، وهذا يسهّل على المهاجم العثور على ثغرة معروفة.
  • مجلد database بالترحيلات (Migrations) والبذور (Seeders)، وهو يكشف بنية الجداول وأحيانًا بيانات تجريبية.
  • مجلد .git إذا نقلت المشروع عبر جيت. يمكن من هنا إعادة بناء تاريخ الكود كاملًا.

الآن الطرف المقابل: في الاستضافة المشتركة مع cPanel يرتبط النطاق الرئيسي افتراضيًا بالمجلد ~/public_html. وكل ما تضعه بداخله يصبح متاحًا للويب ما لم تمنعه صراحةً. هنا يتشكل التناقض: الاستضافة تقول إن الجذر هو public_html، ولارافيل يقول إن الجذر يجب أن يكون public.

خلاصة هذا المقال جملة واحدة: بدل أن تجعل لارافيل يشبه الاستضافة، وجّه جذر الاستضافة إلى المجلد الصحيح.

مقارنة بين البنية الخطرة والبنية الصحيحة لمجلدات لارافيل على استضافة مشتركة

الطريقة السيئة الأولى: وضع المشروع كله في public_html#

أكثر ما يُفعل شيوعًا هو: فك ضغط ملف المشروع داخل public_html ثم إما إعطاء العنوان مع /public أو تركيب ملف index.php على عجل يحوّل الطلب إلى public/index.php.

الموقع يعمل. المشكلة في مكان آخر. يكفي أمر بسيط لترى ما الذي انكشف:

Bash
curl -s https://example.com/.env | head -n 8

إذا كان الخرج شبيهًا بهذا فقد انتهت القضية هناك:

INI
APP_NAME=Debug
APP_ENV=production
APP_KEY=base64:0m3Zx8Qk1sT7vB2nR4pW9yL6cD5hJ0aE8fG3uI1oK2s=
APP_DEBUG=false
DB_DATABASE=user_shop
DB_USERNAME=user_shopadmin
DB_PASSWORD=***

وهذا ليس الملف الوحيد. وهذه عادةً مفتوحة أيضًا:

Bash
curl -sI https://example.com/storage/logs/laravel.log | head -n 1
curl -sI https://example.com/.git/config | head -n 1
curl -sI https://example.com/composer.lock | head -n 1

بواسطة APP_KEY يمكن تزوير كوكي الجلسة. وبكلمة مرور قاعدة البيانات، إذا سمحت الاستضافة بالاتصال من الخارج، يمكن الدخول إلى قاعدة البيانات مباشرة. ومن composer.lock تُعرف النسخة الدقيقة لكل حزمة. وسجل لارافيل، خاصة إذا بقي وضع التصحيح مفعّلًا، يُظهر مسارات الملفات وجزءًا من البيانات؛ وهذا هو السبب نفسه في وجوب إيقاف APP_DEBUG على الاستضافة المشتركة.

الجملة التي نسمعها دائمًا دفاعًا عن هذه الطريقة هي «لا أحد يعرف عنوان الملف». الماسحات الآلية تجرّب هذه المسارات الثابتة نفسها على كل النطاقات. إذا فتحت سجل الوصول في استضافتك فستجد على الأرجح طلبًا إلى /.env منذ الأسبوع الأول.

الطريقة السيئة الثانية: تعديل index.php وبقاء التطبيق تحت الجذر#

الطريقة الثانية تبدو أذكى قليلًا ولذلك هي أخطر. يضعون محتوى المجلد public داخل public_html، ويضعون بقية المشروع في public_html/laravel، ثم يغيّرون مسارات index.php:

PHP
require __DIR__.'/laravel/vendor/autoload.php';

$app = require_once __DIR__.'/laravel/bootstrap/app.php';

المظهر نظيف: عنوان الموقع صحيح وحُذف /public من الرابط. لكن مجلد التطبيق ما زال تحت جذر الويب. أي أن هذا العنوان ما زال يستجيب:

Bash
curl -s https://example.com/laravel/.env | head -n 3

من هنا يدخلون عادةً في سباق لا ينتهي: يغلقون مجلد laravel بـ .htaccess، ثم ينسون إغلاق المجلدات الفرعية، ثم تُفسد قاعدة إعادة كتابة أخرى الترتيب. إعداد خاطئ واحد في ملف واحد يرفع كل الحماية دون أي خطأ؛ فقط ينفتح بصمت.

وله ثلاث عيوب أخرى تزعج أكثر في الواقع العملي:

  • الملف public/index.php من ملفات إطار العمل نفسه. مع كل ترقية للارافيل أو كل git pull يُستبدل من جديد وعليك تعديله يدويًا في كل مرة. إذا نسيت مرة واحدة يتعطل الموقع بخطأ «الكلاس غير موجود».
  • الأمر php artisan يُنفَّذ من جذر المشروع لا من جذر الويب. بهذه البنية يختلط مفهوما «الجذر» وتُنفَّذ أوامر الكاش والترحيل من المسار الخطأ.
  • الدالة public_path() ما زالت تعيد المجلد public داخل المشروع، لا public_html. أي أن أي حزمة تكتب ملفًا في جذر الويب ستكتبه في مكان لا يراه خادم الويب. وهذا هو السبب نفسه في أن storage:link ينشئ الرابط في مكان لا نفع منه.

الخلاصة: هذه الطريقة تربط الأمان بملف إعدادات هش وتجعل الصيانة ثقيلة إلى الأبد.

الطريقة الصحيحة: التطبيق خارج public_html والجذر على مجلد public#

الطريقة الصحيحة بسيطة. يبقى المشروع كله خارج جذر الويب وتوجّه جذر النطاق فقط إلى المجلد public في المشروع نفسه.

  1. ضع المشروع في مجلد خارج public_html، مثلًا ~/app:
Bash
cd ~
mkdir -p app
# ضع ملفات المشروع داخل ~/app
ls ~/app/public/index.php
  1. في cPanel اذهب إلى قسم Domains. افتح خيار الإدارة أو التعديل بجانب النطاق وغيّر قيمة Document Root من public_html إلى app/public. يُحسب المسار نسبةً إلى المجلد الرئيسي (Home)، فيكفي app/public.

حسب إصدار cPanel قد يكون هذا الخيار ضمن Domains أو Addon Domains أو Subdomains. في بعض الاستضافات يكون جذر النطاق الرئيسي مقفلًا ولا يمكن تغيير سوى النطاقات الإضافية والنطاقات الفرعية؛ في هذه الحالة انتقل إلى القسم التالي أو اطلب من الدعم الفني نقل الجذر. هذا بالنسبة للدعم تغيير من سطر واحد.

  1. تحقق من الصلاحيات. يكفي 755 للمجلدات و644 للملفات. ويجب أن يكون المجلدان storage وbootstrap/cache قابلين للكتابة لمستخدم PHP:
Bash
find ~/app -type d -exec chmod 755 {} \;
find ~/app -type f -exec chmod 644 {} \;
chmod -R 775 ~/app/storage ~/app/bootstrap/cache

في الاستضافة المشتركة يعمل PHP عادةً بمستخدم cPanel نفسه، فيكفي 755 للمجلد storage ولا حاجة لـ 777. لا تضع 777 على أي مجلد.

  1. أنشئ رابط مجلد الملفات العامة. كتبنا تفاصيل هذه الخطوة في cPanel وفخاخ المسار المطلق في مذكرة storage:link في cPanel:
Bash
cd ~/app && php artisan storage:link
  1. بعد نقل الجذر لم يعد المجلد public_html القديم مفيدًا. أفرغه أو غيّر اسمه حتى لا يُقدَّم منه شيء بالخطأ. وإذا كانت شهادة SSL التلقائية في الاستضافة تحتاج إلى public_html للتحقق فأبقِ المجلد لكن فارغًا.

إذا كنت للتو تشغّل مشروعًا على استضافة مشتركة فقد جمعنا بقية الخطوات اللازمة في قائمة فحص الاستضافة المشتركة للارافيل.

عندما يتعذر تغيير جذر النطاق: طريقة الرابط الرمزي#

بعض الاستضافات لا تسمح بتغيير Document Root للنطاق الرئيسي. في هذه الحالة يمكن ربط public_html برابط رمزي (symlink) بالمجلد ~/app/public:

Bash
mv ~/public_html ~/public_html.bak
ln -s ~/app/public ~/public_html
ls -l ~ | grep public_html

يجب أن يُظهر الخرج أن public_html رابط وأنه يشير إلى app/public.

ثلاثة تحذيرات يجب أن تعرفها قبل هذه الخطوة:

  • يجب أن يُسمح لأباتشي باتباع الروابط. في معظم استضافات cPanel يكون Options +SymLinksIfOwnerMatch مفعّلًا، وبما أن المسارين لهما المالك نفسه فلا مشكلة. إذا كانت إعدادات الخادم أكثر تشددًا فالنتيجة هي الخطأ 403.
  • بعض أدوات cPanel، مثل المثبّتات التلقائية أو أداة شهادة SSL، قد تعيد إنشاء public_html كمجلد وتُتلف الرابط. بعد أي عملية إدارية في اللوحة تحقق من حالة الرابط.
  • النسخ الاحتياطي التلقائي للاستضافة قد لا يتبع الرابط، أو على العكس قد ينسخ الملفات مرتين. اطلع مرة على مخرجات النسخة الاحتياطية.

إذا تعذّر تغيير الجذر والرابط الرمزي معًا فالخيار الأخير هو: ضع محتوى public في public_html لكن أبقِ بقية المشروع خارج الجذر ووجّه مسارات index.php إلى الخارج. هذا يختلف عن الطريقة السيئة الثانية في أمر أساسي: في تلك الطريقة يبقى مجلد التطبيق تحت الجذر، وهنا لا يبقى. ما زلت تدفع ثمنها — مع كل ترقية يجب تعديل index.php من جديد — لكن على الأقل لا يظهر .env للويب.

ملف htaccess القياسي في لارافيل، سطرًا سطرًا#

الملف public/.htaccess الذي يأتي مع لارافيل هو تقريبًا دائمًا هذا:

Apache
<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

الآن سطرًا سطرًا:

<IfModule mod_rewrite.c> — كل محتوى الملف داخل هذا الشرط. إذا لم تكن وحدة إعادة الكتابة مفعّلة على الخادم فإن أباتشي يتجاهل الكتلة كلها بدل أن يعطي خطأ. هذا «اللطف» هو بالضبط مصدر جزء كبير من أخطاء 404 الغريبة، لأنه لا يترك أي أثر.

Options -MultiViews -Indexes — مهمتان منفصلتان. -MultiViews يوقف التفاوض على المحتوى؛ وإلا فقد يُربط الطلب /report بنفسه بـ report.php أو report.html ولا يُنفَّذ مسار لارافيل أبدًا. و-Indexes يمنع عرض قائمة ملفات المجلد.

RewriteEngine On — يشغّل محرك إعادة الكتابة. بدون هذا السطر لا تُنفَّذ بقية القواعد.

قاعدة Authorization — بعض الإعدادات لا تمرر ترويسة Authorization إلى PHP. هذان السطران يقولان: إذا كان للترويسة Authorization قيمة فضع القيمة نفسها في متغير البيئة HTTP_AUTHORIZATION. إذا كان لديك تسجيل دخول بالرمز المميز أو Bearer وترى المستخدم «غير موثّق» في كل مكان، فغياب هذين السطرين هو أول ما يجب فحصه.

قاعدة الشرطة المائلة الأخيرة — هذه الأسطر الثلاثة تحذف الشرطة المائلة الأخيرة ولا تضيفها. الشرط الأول يقول: فقط عندما لا يكون المسار المطلوب مجلدًا حقيقيًا. الشرط الثاني يلتقط العناوين المنتهية بـ / ويحتفظ بالجزء الذي يسبقها في %1. والقاعدة تعيد التوجيه بـ R=301 توجيهًا دائمًا إلى العنوان نفسه بدون الشرطة. والنتيجة ألا يصبح /blog/post/ و/blog/post عنوانين منفصلين لمحركات البحث.

القاعدة النهائية — قلب الموضوع. إذا لم يكن المسار المطلوب مجلدًا حقيقيًا (!-d) ولا ملفًا حقيقيًا (!-f) فأرسل الطلب إلى index.php. الراية [L] تعني: انتهى الأمر هنا. لاحظ أن المسار الأصلي في الرابط يبقى كما هو؛ يقرؤه لارافيل من REQUEST_URI لا من معامل إعادة الكتابة.

ترتيب هذين الشرطين السلبيين هو أهم نقطة في هذا الملف: الملف والمجلد الحقيقيان لهما دائمًا أولوية على مسار لارافيل. وهذا هو منبع أحد أخطاء 404 التي ستأتي في القسم التالي.

أخطاء 404 الغريبة وجذورها#

قبل أي شيء، تفريق يختصر نصف العمل: من الذي أنشأ الـ 404؟

  • إذا كانت صفحة الخطأ بمظهر أباتشي البسيط وتعرض نصًا مثل «Not Found — The requested URL was not found on this server» فالطلب لم يصل إلى PHP أصلًا. المسؤول في طبقة خادم الويب.
  • إذا كانت صفحة 404 هي صفحة لارافيل نفسه (أو قالبك errors/404.blade.php) فقد وصل الطلب ولم يجد لارافيل مسارًا له. المسؤول في طبقة التطبيق.
مسار الطلب من أباتشي إلى موجّه لارافيل والنقاط التي تنتج فيها أخطاء 404

خمسة جذور شائعة لـ 404 من جهة خادم الويب:

1. غياب AllowOverride. إذا كان في إعدادات أباتشي لهذا المسار AllowOverride None فإن ملف .htaccess لا يُقرأ أصلًا. وعلامته المميزة أن الصفحة الرئيسية تعمل بشكل صحيح (لأن DirectoryIndex يختار index.php بنفسه) بينما كل عنوان آخر يعطي 404. والاختبار القاطع بسيط: أضف سطرًا بلا معنى في أول .htaccess:

Apache
ThisIsNotADirective

الآن افتح الموقع. إذا ظهر الخطأ 500 فالملف يُقرأ وAllowOverride سليم. وإذا عمل الموقع دون تغيير فالملف لا يُقرأ أصلًا. احذف السطر بعد الاختبار بلا تأخير.

2. تعطيل mod_rewrite. كما قلنا، يتجاوز <IfModule> هذه الحالة بصمت. وعلامتها هي نفسها في الحالة السابقة. للتشخيص، أزل مؤقتًا الشرط <IfModule mod_rewrite.c> والسطر الختامي </IfModule>. إذا كانت الوحدة غير موجودة فعلًا فسيعطي أباتشي الخطأ 500 عند RewriteEngine On وهذا الخطأ هو جوابك. في الاستضافة المشتركة تكون هذه الوحدة مفعّلة تقريبًا دائمًا؛ وإن لم تكن فلا شيء بيدك سوى فتح تذكرة دعم.

3. ملف .htaccess لم يُرفع أصلًا. لأن اسمه يبدأ بنقطة فهو ملف مخفي. برنامج FTP لا يعرض الملفات المخفية افتراضيًا ولا يرفعها. وفي مدير ملفات cPanel أيضًا يجب تفعيل خيار إظهار الملفات المخفية من الإعدادات لكي يظهر. والتحقق منه أمر واحد:

Bash
ls -la ~/app/public/ | grep htaccess

إذا عاد فارغًا فالملف غير موجود. هذا كل شيء.

4. حساسية حالة الأحرف. بيئتك المحلية على الأرجح ويندوز أو ماك، وفيهما Logo.png وlogo.png ملف واحد. أما لينكس فلا. الملف الذي استدعيته في القالب باسم logo.png وهو على القرص Logo.png يعطي 404 على الاستضافة. والقاعدة نفسها تنطبق على أسماء ملفات العروض (Views): resources/views/Home.blade.php مع view('home') يعمل محليًا ويعطي على الاستضافة خطأ «view not found». للعثور على الحالات المشبوهة:

Bash
find ~/app/public ~/app/resources -name '*[A-Z]*' -type f

5. تعارض المسار مع ملف أو مجلد حقيقي. تذكّر أن القاعدة الأخيرة قالت «إذا لم يكن ملفًا أو مجلدًا حقيقيًا». إذا عرّفت مسارًا باسم /build وكان في public مجلد build أيضًا (مخرجات بناء الأصول) فلا شأن لأباتشي بلارافيل وسيقدّم المجلد بنفسه. وبما أن -Indexes مفعّل والمجلد لا يحتوي ملف index فالنتيجة 403 من أباتشي، لا تنفيذ مسارك. وهذا ما يحدث مع /storage عندما يكون الرابط public/storage مكسورًا أو لم يُنشأ أصلًا. قارن أسماء المسارات بمحتوى المجلد public:

Bash
ls ~/app/public
php artisan route:list --columns=uri

حماية إضافية: الملفات المخفية وقوائم المجلدات#

حتى عندما تكون البنية صحيحة ولا يقع تحت الجذر سوى public فإن بضعة أسطر إضافية في .htaccess تستحق الإضافة. مثل ملف .htaccess نفسه، أو الملفات المخفية التي تتركها أدوات أخرى.

Apache
Options -Indexes

<FilesMatch "^\.">
    Require all denied
</FilesMatch>

Options -Indexes يمنع عرض قائمة الملفات في المجلدات التي لا تحتوي index. والنمط ^\. يغلق أي ملف يبدأ اسمه بنقطة؛ أي .env و.htaccess وأمثالهما.

لكن انتبه: FilesMatch يتعامل مع اسم الملف فقط لا مع المسار. أي أن هذه الكتلة لا تمنع /.git/config لأن اسم ذلك الملف config ولا يبدأ بنقطة. وللمجلدات التي تبدأ بنقطة تحتاج إلى قاعدة تعتمد المسار:

Apache
RedirectMatch 404 /\.(git|svn|env)

ملاحظة عن الإصدار: Require all denied صياغة أباتشي 2.4. وعلى الخوادم الأقدم بأباتشي 2.2 يجب أن تكتب Deny from all. معظم الاستضافات اليوم 2.4، لكن إذا حصلت على الخطأ 500 بعد إضافة هذه الكتلة فأول ما يجب تغييره هو هذا.

وإذا أردت إغلاق امتدادات معينة فانتبه للآثار الجانبية:

Apache
<FilesMatch "\.(env|log|sql|sh|bak|lock|yml|yaml)$">
    Require all denied
</FilesMatch>

إضافة json إلى هذه القائمة مغرية لكنها خطيرة: إذا كان موقعك يقرأ site.webmanifest أو manifest.json أو أي ملف JSON آخر من جهة المتصفح فسيُغلق هو أيضًا ويتعطل بصمت.

لماذا يجب ألا يكون htaccess عمود الأمان الوحيد#

ملف .htaccess طبقة مريحة وليس حدًّا فاصلًا. ثلاثة أسباب واضحة:

  • لا يعمل إلا إذا كان خادم الويب أباتشي وسمح AllowOverride. إذا انتقل الموقع يومًا إلى LiteSpeed بإعدادات مختلفة أو خلف Nginx فقد يفقد هذا الملف بعض قواعده. LiteSpeed يفهم معظم أوامر أباتشي، لكن «معظم» ليست «كل».
  • إذا حُذف الملف أو نُقل أو غيّرت قاعدة إعادة كتابة سابقة المسار فإن الحماية تزول بصمت. لا ترى أي خطأ؛ فقط في يوم ما يكون ملف لا يجب أن يكون مفتوحًا مفتوحًا.
  • الحماية بـ .htaccess من جنس «الإخفاء» لا «العدم». الشيء الوحيد الذي لا يعتمد على ملف إعدادات هو ألا يكون الملف الحساس تحت جذر الويب أصلًا.

لذا اكتب هذه القواعد كحزام ثانٍ، لا كحل. الحل هو البنية التي ذكرناها في قسم «الطريقة الصحيحة».

الكاش والضغط في الملف نفسه#

ما دمت تعمل على .htaccess فهناك كتلتان أخريان تستحقان الاهتمام. أولًا الضغط:

Apache
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
    AddOutputFilterByType DEFLATE application/javascript application/json
    AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>

القائمة تقتصر عمدًا على الأنواع النصية. صور jpg وpng وwebp مضغوطة أصلًا، ووضعها في هذه القائمة يستهلك المعالج دون تقليل الحجم. أما image/svg+xml فاستثناء لأنه في الحقيقة نص.

ثم كاش المتصفح:

Apache
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 month"
    ExpiresByType image/webp "access plus 1 month"
    ExpiresDefault "access plus 1 hour"
</IfModule>

وإذا كانت أصولك بأسماء تحتوي على هاش فمن المنطقي أيضًا استخدام الترويسة immutable:

Apache
<IfModule mod_headers.c>
    <FilesMatch "\.(css|js|woff2)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

هنا يلزم تحذير جدي: ضع هذه الترويسة فقط على الملفات التي يتغير اسمها مع كل تعديل — مثل مخرجات Vite التي تضع الهاش داخل اسم الملف. إذا طُبّقت القاعدة نفسها على ملف باسم ثابت مثل app.css فسيحتفظ متصفح المستخدم بالنسخة القديمة لمدة سنة ولن ينقذه Ctrl+F5 العادي. وهذا هو الفخ نفسه الذي شرحناه في مذكرة كاش immutable وعدم وصول CSS.

الاختبار بعد النشر#

بعد كل عملية نقل نفّذ هذه الأوامر القليلة. تستغرق أقل من دقيقة وتمنع أسوأ ما قد يحدث.

أولًا، تأكد أن الملفات الحساسة غير متاحة من الويب. الجواب الصحيح 403 أو 404:

Bash
for p in /.env /storage/logs/laravel.log /composer.json /.git/config /vendor/autoload.php; do
  echo -n "$p -> "
  curl -s -o /dev/null -w "%{http_code}\n" "https://example.com$p"
done

إذا أعاد أي منها 200 فتوقف هنا وأصلح البنية. وبعد ذلك غيّر APP_KEY وأعد إنشاء كلمة مرور قاعدة البيانات ومفاتيح الخدمات من جديد؛ ما خرج مرة فقد خرج.

ثانيًا، تأكد أن المسارات تمر فعلًا عبر .htaccess. اختبر الصفحة الرئيسية وعنوانًا داخليًا كلًّا على حدة:

Bash
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/login

إذا أعطى الأول 200 والثاني 404 فالمشكلة في قواعد إعادة الكتابة لا في مساراتك. ارجع إلى قسم 404.

ثالثًا، افحص رابط الملفات العامة:

Bash
ls -l ~/app/public/storage
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/storage/test.txt

يجب أن يُظهر خرج ls -l رابطًا يشير إلى ../storage/app/public. إذا كان مجلدًا عاديًا أو غير موجود فنفّذ php artisan storage:link من جديد.

رابعًا، اطلع على ترويسة الكاش لأحد الأصول ذات الهاش:

Bash
curl -sI https://example.com/build/assets/app-8f2b1c9d.css | grep -i cache-control

الدرس الرئيسي#

مسألة public_html مسألة أمنية ترتدي ثوب مسألة إعداد. أي حل عمله «إخفاء» ملفات المشروع عن الويب سيفشل عاجلًا أو آجلًا لأنه يعتمد على ملف إعدادات ووحدة اختيارية. الحل الوحيد الذي يعمل دائمًا هو ألا يكون الملف الحساس تحت جذر الويب أصلًا.

وبخصوص أخطاء 404: قبل أن تفتح routes/web.php، حدّد أولًا هل صفحة الخطأ من أباتشي أم من لارافيل. هذه النظرة الواحدة تقسم مسار التشخيص إلى فرعين وتحوّل عادةً ساعات إلى دقائق.