توی این درس یاد میگیری یک سایت استاتیک را درست سرو کنی؛ یعنی فقط «باز شدن صفحهی اول» کافی نیست. خروجی build همین سایت LoopX (که با Astro ساخته شده) را با Nginx بالا میآوری و همهی آدرسهایش را آزمایش میکنی. صفحهی خطای سفارشی (error_page) میسازی که کد درست (۴۰۴) را هم نگه دارد، برای یک SPA (اپ تکصفحهای مثل React) fallback درست میکنی تا رفرش روی /products/42 خطا ندهد، و دام رایجش را میبینی. با autoindex فهرست یک پوشهی دانلود را نشان میدهی، و جلوی لو رفتن فایلهای حساسی مثل .git را میگیری.
مسئله: «روی لپتاپم درست بود!»
Section titled “مسئله: «روی لپتاپم درست بود!»”سایت استاتیک ساده به نظر میرسد: چند فایل HTML، CSS و عکس در یک پوشه. ولی بعد از استقرار اینها پیش میآید:
- آدرسهای تمیز (
/about/بهجای/about.html) گاهی کار میکنند و گاهی نه. - صفحهی ۴۰۴ زشتِ پیشفرض Nginx نشان داده میشود، یا بدتر، صفحهی ۴۰۴ خودت با کد ۲۰۰ برمیگردد و گوگل آن را صفحهی واقعی ایندکس میکند.
- در SPA، رفرش روی هر صفحهای غیر از صفحهی اول، ۴۰۴ میدهد.
- پوشهی
.gitیا فایل.envکه با کد آمده، از اینترنت قابل دانلود است.
تشبیه: ویترین مغازه
Section titled “تشبیه: ویترین مغازه”سایت استاتیک مثل ویترین یک مغازه است: هر چیزی که در ویترین (پوشهی root) بگذاری، رهگذر میبیند. پس اول، فقط محصول نهایی (خروجی build) را در ویترین بگذار، نه انبار (کد منبع، .git، .env). دوم، برای قفسهی خالی یک تابلوی محترمانه بگذار («این کالا موجود نیست»، یعنی صفحهی ۴۰۴ سفارشی) ولی به مشتری دروغ نگو که کالا هست (کد ۲۰۰ برای صفحهی ناموجود). و در یک SPA، ویترین فقط یک در ورودی دارد (index.html) که خودش مشتری را به هر بخشی هدایت میکند.
سایت چندصفحهای در برابر SPA
Section titled “سایت چندصفحهای در برابر SPA”مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده. خروجی build سایت LoopX را از همین مخزن (پوشهی dist/ بعد از npm run build) روی آن کپی کردهایم.
مثال ۱: یک سایت چندصفحهای ساده
Section titled “مثال ۱: یک سایت چندصفحهای ساده”mkdir -p /var/www/site.test/{css,img,about}cat > /var/www/site.test/index.html <<'EOF'<!doctype html><title>Home</title><link rel="stylesheet" href="/css/style.css"><h1>Home</h1>EOFecho '<!doctype html><title>About</title><h1>About us</h1>' > /var/www/site.test/about/index.htmlecho 'h1 { color: teal; }' > /var/www/site.test/css/style.csshead -c 2048 /dev/urandom > /var/www/site.test/img/logo.pngcat > /etc/nginx/sites-available/site.test <<'EOF'server { listen 80; server_name site.test; root /var/www/site.test; index index.html;
location / { try_files $uri $uri/ =404; }}EOFln -s /etc/nginx/sites-available/site.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /about/ /about /css/style.css /img/logo.png /contact/; do printf '%-16s ' "$p" curl -s -o /dev/null -w '%{http_code} %{content_type} %{redirect_url}\n' "http://site.test$p"donenginx: configuration file /etc/nginx/nginx.conf test is successful/ 200 text/html/about/ 200 text/html/about 301 text/html http://site.test/about//css/style.css 200 text/css/img/logo.png 200 image/png/contact/ 404 text/html- هر فایل با
Content-Typeدرست رفت (text/html،text/css،image/png). Nginx نوع را از پسوند فایل و جدول/etc/nginx/mime.typesمیفهمد. نوع اشتباه یعنی مرورگر CSS را اعمال نمیکند یا JS را اجرا نمیکند. /about(بدون اسلش) با301به/about/رفت، چون پوشه است./contact/وجود ندارد:404واقعی.
مثال ۲: خروجی build همین سایت LoopX
Section titled “مثال ۲: خروجی build همین سایت LoopX”سایت LoopX با Astro ساخته شده و npm run build یک پوشهی dist/ میدهد: یک سایت چندصفحهای که هر درس در آن یک پوشه با index.html است. همان را سرو میکنیم:
ls /opt/loopx-dist | head -n 12echo "..."echo "تعداد صفحههای HTML: $(find /opt/loopx-dist -name '*.html' | wc -l)"du -sh /opt/loopx-dist | cut -f1404.html_astroaboutapple-touch-icon.pngbashdockerfavicon-32.pngfavicon.svggiticon-192.pngicon-512.pngindex.html...تعداد صفحههای HTML: 16436Mserver { listen 80; server_name loopx.test; root /var/www/loopx.test; index index.html;
location / { try_files $uri $uri/ =404; }
error_page 404 /404.html;}cp -r /opt/loopx-dist /var/www/loopx.testln -s /etc/nginx/sites-available/loopx.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /docker/what-is-docker/ /nginx/static-sites/ /favicon.svg /sitemap-index.xml /og/docker.png /no/such/page/; do printf '%-26s ' "$p" curl -s -o /tmp/body -w '%{http_code} %{content_type} ' "http://loopx.test$p" title=$(grep -o '<title>[^<]*</title>' /tmp/body | head -n 1) echo "$title"donenginx: configuration file /etc/nginx/nginx.conf test is successful/ 200 text/html <title>LoopX | آموزش فارسی لینوکس، Bash، Git، داکر و Nginx</title>/docker/what-is-docker/ 200 text/html <title>داکر چیست؟ — آموزش داکر | LoopX</title>/nginx/static-sites/ 200 text/html <title>سرو سایت استاتیک — آموزش Nginx | LoopX</title>/favicon.svg 200 image/svg+xml/sitemap-index.xml 200 text/xml/og/docker.png 200 image/png/no/such/page/ 404 text/html <title>صفحه پیدا نشد | LoopX</title>همهی صفحههای درسها، آیکون SVG، نقشهی سایت (sitemap-index.xml) و تصویر Open Graph با نوع درست آمدند. آدرس ناموجود 404 داد ولی بدنهاش صفحهی ۴۰۴ خود سایت (با عنوان «صفحه پیدا نشد») است، نه صفحهی پیشفرض Nginx؛ این کار error_page 404 /404.html است: کد ۴۰۴ میماند و فقط بدنه عوض میشود.
مثال ۳: صفحهی خطای سفارشی و internal
Section titled “مثال ۳: صفحهی خطای سفارشی و internal”برای سایت سادهی site.test یک صفحهی ۴۰۴ و یک صفحه برای خطاهای سرور (۵۰۰ تا ۵۰۴) میسازیم:
mkdir -p /var/www/site.test/errorsecho '<!doctype html><title>Not found</title><h1>Sorry, this page does not exist.</h1>' > /var/www/site.test/errors/404.htmlecho '<!doctype html><title>Down</title><h1>We will be back soon.</h1>' > /var/www/site.test/errors/50x.htmlcat > /etc/nginx/sites-available/site.test <<'EOF'server { listen 80; server_name site.test; root /var/www/site.test; index index.html;
location / { try_files $uri $uri/ =404; } location = /broken { return 503; }
error_page 404 /errors/404.html; error_page 500 502 503 504 /errors/50x.html; location ^~ /errors/ { internal; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in /nothing /broken /errors/404.html; do printf '%-18s ' "$p" curl -s -o /tmp/body -w '%{http_code} ' "http://site.test$p" grep -o '<h1>.*</h1>' /tmp/bodydonenginx: configuration file /etc/nginx/nginx.conf test is successful/nothing 404 <h1>Sorry, this page does not exist.</h1>/broken 503 <h1>We will be back soon.</h1>/errors/404.html 404 <h1>Sorry, this page does not exist.</h1>/nothing← کد ۴۰۴ با صفحهی خودمان./broken(شبیهسازی سرویس از کار افتاده) ← کد ۵۰۳ با صفحهی «برمیگردیم»./errors/404.htmlاگر مستقیم خواسته شود ← ۴۰۴.internal;یعنی این location فقط برای redirect داخلی (مثلerror_page) قابل استفاده است، نه برای کاربر. پس کسی نمیتواند صفحهی خطا را با کد ۲۰۰ ببیند (و موتور جستوجو هم آن را بهعنوان یک صفحهی واقعی ایندکس نمیکند).
مثال ۴: SPA و fallback به index.html
Section titled “مثال ۴: SPA و fallback به index.html”یک SPA کوچک: یک index.html که با جاوااسکریپت آدرس را میخواند (مثل React Router) و یک فایل JS در /assets/:
mkdir -p /var/www/spa.test/assetscat > /var/www/spa.test/index.html <<'EOF'<!doctype html><title>SPA</title><div id="app">SPA shell</div><script src="/assets/app.js"></script>EOFecho 'document.getElementById("app").textContent = "page: " + location.pathname;' > /var/www/spa.test/assets/app.jscat > /etc/nginx/sites-available/spa.test <<'EOF'server { listen 80; server_name spa.test; root /var/www/spa.test; index index.html;
location / { try_files $uri $uri/ =404; }}EOFln -s /etc/nginx/sites-available/spa.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /products/42 /assets/app.js; do printf '%-16s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "http://spa.test$p")"donenginx: configuration file /etc/nginx/nginx.conf test is successful/ 200/products/42 404/assets/app.js 200صفحهی اول باز میشود، ولی /products/42 (که کاربر با رفرش یا لینک مستقیم میخواهد) ۴۰۴ است؛ چون چنین فایلی روی دیسک نیست. مسیرها را جاوااسکریپت در مرورگر میسازد. راهحل استاندارد: هر آدرسی که فایل یا پوشه نبود، همان index.html را بده:
sed -i 's|try_files $uri $uri/ =404;|try_files $uri $uri/ /index.html;|' /etc/nginx/sites-available/spa.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /products/42 /account/settings /assets/app.js /assets/missing.js; do printf '%-20s ' "$p" curl -s -o /tmp/body -w '%{http_code} %{content_type} ' "http://spa.test$p" head -c 45 /tmp/body; echodonenginx: configuration file /etc/nginx/nginx.conf test is successful/ 200 text/html <!doctype html><title>SPA</title><div id="app/products/42 200 text/html <!doctype html><title>SPA</title><div id="app/account/settings 200 text/html <!doctype html><title>SPA</title><div id="app/assets/app.js 200 application/javascript document.getElementById("app").textContent =/assets/missing.js 200 text/html <!doctype html><title>SPA</title><div id="appحالا /products/42 و /account/settings با ۲۰۰ همان index.html را میگیرند و روتر SPA در مرورگر صفحهی درست را نشان میدهد. ولی به خط آخر نگاه کن: /assets/missing.js (فایلی که واقعاً نیست، مثلاً بعد از یک build جدید) هم با ۲۰۰ و محتوای HTML جواب گرفت! مرورگر آن را بهعنوان JS اجرا میکند و خطای گیجکنندهی Unexpected token '<' میدهد. راهحل: fallback فقط برای مسیرهای صفحه، و داراییها (assets) با ۴۰۴ واقعی:
cat > /etc/nginx/sites-available/spa.test <<'EOF'server { listen 80; server_name spa.test; root /var/www/spa.test; index index.html;
location /assets/ { try_files $uri =404; } location / { try_files $uri $uri/ /index.html; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in /products/42 /assets/app.js /assets/missing.js; do printf '%-20s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code} %{content_type}' "http://spa.test$p")"donenginx: configuration file /etc/nginx/nginx.conf test is successful/products/42 200 text/html/assets/app.js 200 application/javascript/assets/missing.js 404 text/htmlمثال ۵: فهرست پوشه با autoindex
Section titled “مثال ۵: فهرست پوشه با autoindex”بهصورت پیشفرض، پوشهی بدون index.html جواب ۴۰۳ میگیرد (درس server block). برای یک پوشهی دانلود که میخواهی فهرستش دیده شود، autoindex را فقط برای همان location روشن کن:
mkdir -p /srv/downloads/2026head -c 1500000 /dev/zero > /srv/downloads/backup-tool-1.2.tar.gzecho "checksums" > /srv/downloads/SHA256SUMSecho "notes" > /srv/downloads/2026/release-notes.txttouch -d '2026-09-01 10:00' /srv/downloads/backup-tool-1.2.tar.gz /srv/downloads/SHA256SUMS /srv/downloads/2026 /srv/downloads/2026/release-notes.txtsed -i 's|^ error_page 404 /errors/404.html;| location /downloads/ {\n alias /srv/downloads/;\n autoindex on;\n autoindex_exact_size off;\n }\n\n error_page 404 /errors/404.html;|' /etc/nginx/sites-available/site.testgrep -A4 'location /downloads/' /etc/nginx/sites-available/site.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s http://site.test/downloads/ | sed -n '/<pre>/,/<\/pre>/p' | sed -E 's/<a href="[^"]*">//; s/<\/a>//; s/<\/?pre>//g' | grep -v '^$' location /downloads/ { alias /srv/downloads/; autoindex on; autoindex_exact_size off; }nginx: configuration file /etc/nginx/nginx.conf test is successful<h1>Index of /downloads/</h1><hr>../2026/ 01-Sep-2026 10:00 -SHA256SUMS 01-Sep-2026 10:00 10backup-tool-1.2.tar.gz 01-Sep-2026 10:00 1M<hr></body>Nginx یک صفحهی HTML ساده با فهرست فایلها، تاریخ و اندازه ساخت (autoindex_exact_size off اندازه را خوانا، مثل 1.4M، نشان میدهد). autoindex را هرگز روی کل سایت روشن نکن؛ هر فایلی که اشتباهی در پوشه بماند، فهرست و قابل دانلود میشود.
مثال ۶: بستن فایلهای مخفی (.git، .env)
Section titled “مثال ۶: بستن فایلهای مخفی (.git، .env)”سناریوی اول درس را بازسازی کنیم: یک مخزن Git که مستقیم در پوشهی سایت clone شده و یک .env:
mkdir -p /srv/repocd /srv/repogit init -q 2>/dev/null || mkdir -p .gitprintf '[remote "origin"]\n\turl = git@github.com:company/shop-private.git\n' > .git/configecho 'DB_PASSWORD=super-secret' > .envecho '<h1>repo site</h1>' > index.htmlcd ~sed -i 's|root /var/www/site.test;|root /srv/repo;|' /etc/nginx/sites-available/site.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /.git/config /.env; do printf '%-14s ' "$p" curl -s -o /tmp/body -w '%{http_code} ' "http://site.test$p" head -n 2 /tmp/body | tr '\n' ' '; echodonenginx: configuration file /etc/nginx/nginx.conf test is successful/ 200 <h1>repo site</h1>/.git/config 200 [remote "origin"] url = git@github.com:company/shop-private.git/.env 200 DB_PASSWORD=super-secret.git/config (آدرس مخزن خصوصی) و .env (رمز دیتابیس) هر دو با ۲۰۰ دانلود شدند. ابزارهای آمادهای هستند که از همین .git باز، کل کد و تاریخچه را بازسازی میکنند. دو راهحل، با هم:
۱. root فقط روی خروجی build باشد (dist/)، نه پوشهی پروژه.
۲. یک قانون عمومی که هر مسیری را که با نقطه شروع میشود (فایل یا پوشهی مخفی) ببندد، بهجز /.well-known/ (که برای گواهی HTTPS لازم است؛ درس Let’s Encrypt):
sed -i 's|^ location / {| location ~ /\\.(?!well-known) {\n deny all;\n }\n\n location / {|' /etc/nginx/sites-available/site.testgrep -A2 'well-known' /etc/nginx/sites-available/site.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in / /.git/config /.git/HEAD /.env /.well-known/test; do printf '%-18s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "http://site.test$p")"done location ~ /\.(?!well-known) { deny all; }nginx: configuration file /etc/nginx/nginx.conf test is successful/ 200/.git/config 403/.git/HEAD 403/.env 403/.well-known/test 404حالا .git و .env با ۴۰۳ (deny all) بستهاند. regex /\.(?!well-known) یعنی «اسلشی که بعدش نقطه آمده، به شرطی که ادامهاش well-known نباشد» ((?!...) در regex یعنی «نه این»). /.well-known/test بسته نشد (۴۰۴ چون فایلی نیست). چون این یک location regex است، بر location / پیشوندی برنده میشود (درس قبل).
پشت پرده: Nginx فایل را چطور میفرستد؟ (sendfile و 304)
Section titled “پشت پرده: Nginx فایل را چطور میفرستد؟ (sendfile و 304)”برای یک فایل استاتیک، Nginx محتوا را حتی وارد حافظهی خودش هم نمیکند: با sendfile on; (که در nginx.conf Ubuntu روشن است) از هستهی لینوکس میخواهد فایل را مستقیم از دیسک (یا کش صفحهی هسته) به سوکت شبکه بریزد. این یکی از دلیلهای سرعت Nginx در فایل استاتیک است.
دومین بهینهسازی، درخواست شرطی است. مرورگر بار دوم که فایلی را میخواهد، ETag یا Last-Modified قبلی را میفرستد و میپرسد «عوض شده؟». اگر نه، Nginx فقط 304 Not Modified بدون بدنه میدهد:
etag=$(curl -s -I http://loopx.test/favicon.svg | grep -i '^etag' | cut -d' ' -f2 | tr -d '\r')echo "ETag: $etag"echo "=== درخواست اول:"curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' http://loopx.test/favicon.svgecho "=== با If-None-Match (فایل عوض نشده):"curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' -H "If-None-Match: $etag" http://loopx.test/favicon.svgecho "=== با ETag قدیمی:"curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' -H 'If-None-Match: "old-etag"' http://loopx.test/favicon.svgETag: "6ac24be5-2a36"=== درخواست اول:200 10806 bytes=== با If-None-Match (فایل عوض نشده):304 0 bytes=== با ETag قدیمی:200 10806 bytesبا ETag درست، پاسخ 304 و صفر بایت بدنه بود. برای سایتی با صدها کاربر برگشتی، این یعنی صرفهجویی بزرگ در پهنای باند. Nginx همهی اینها را برای فایلهای استاتیک خودکار انجام میدهد؛ در درس عملکرد، با هدرهای Cache-Control و expires میگوییم مرورگر حتی همین سؤال را هم تا مدتی نپرسد.
جدولهای مرجع
Section titled “جدولهای مرجع”directive های این درس:
| directive | کار | نمونه |
|---|---|---|
root |
پوشهی سایت | root /var/www/site; |
index |
فایل پیشفرض پوشه | index index.html; |
try_files |
ترتیب امتحان فایلها | try_files $uri $uri/ /index.html; |
error_page |
صفحهی خطای سفارشی (کد حفظ میشود) | error_page 404 /404.html; |
internal |
location فقط برای redirect داخلی | location ^~ /errors/ { internal; } |
autoindex |
فهرست پوشه | autoindex on; |
autoindex_exact_size |
اندازهی دقیق یا خوانا | autoindex_exact_size off; |
deny all |
ممنوع (۴۰۳) | location ~ /\.(?!well-known) { deny all; } |
sendfile |
ارسال فایل با کمک هسته | sendfile on; |
default_type |
نوع پیشفرض پسوندهای ناشناخته | default_type application/octet-stream; |
try_files برای انواع سایت:
| نوع | تنظیم |
|---|---|
| چندصفحهای (Astro، Hugo، Jekyll) | try_files $uri $uri/ =404; + error_page 404 /404.html; |
| SPA (React، Vue، Angular) | location /assets/ { try_files $uri =404; } و location / { try_files $uri $uri/ /index.html; } |
آدرس بدون .html |
try_files $uri $uri.html $uri/ =404; |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) root روی پوشهی پروژه بهجای dist/
Section titled “۱) root روی پوشهی پروژه بهجای dist/”مثال ۶: .git، .env، کد منبع و node_modules همه قابل دانلود میشوند. راهحل: root فقط روی خروجی build؛ و قانون location ~ /\.(?!well-known) { deny all; }.
۲) صفحهی ۴۰۴ با کد ۲۰۰
Section titled “۲) صفحهی ۴۰۴ با کد ۲۰۰”cat > /etc/nginx/conf.d/lx-soft404.conf <<'EOF'server { listen 8088; root /var/www/site.test; location / { try_files $uri $uri/ /errors/404.html; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -o /dev/null -w 'GET /nothing -> %{http_code}\n' http://localhost:8088/nothingrm /etc/nginx/conf.d/lx-soft404.confsystemctl reload nginxnginx: configuration file /etc/nginx/nginx.conf test is successfulGET /nothing -> 200اگر صفحهی خطا را بهجای error_page در آخر try_files بگذاری، کد ۲۰۰ برمیگردد (به آن «soft 404» میگویند): موتورهای جستوجو صفحههای ناموجود را واقعی میدانند و ابزارهای مانیتورینگ خطا را نمیبینند. راهحل: try_files ... =404; و error_page 404 /404.html;.
۳) SPA fallback که داراییهای گمشده را هم میبلعد
Section titled “۳) SPA fallback که داراییهای گمشده را هم میبلعد”مثال ۴: /assets/missing.js با HTML و کد ۲۰۰ جواب گرفت. راهحل: location جدا برای /assets/ با try_files $uri =404;.
۴) مجوز فایلها بعد از کپی
Section titled “۴) مجوز فایلها بعد از کپی”اگر فایلها را با کاربری کپی کنی که umask سختگیرانه دارد (مثلاً 077)، www-data نمیتواند بخواندشان و همهچیز ۴۰۳ میشود. راهحل: بعد از استقرار find /var/www/site -type d -exec chmod 755 {} + و find /var/www/site -type f -exec chmod 644 {} +.
۵) autoindex on در server
Section titled “۵) autoindex on در server”فهرست همهی پوشههای بدون index، از جمله آنهایی که نباید دیده شوند. راهحل: فقط داخل location پوشهی دانلود.
برای site.test یک صفحهی ۴۰۴ سفارشی بساز که اسم مسیر درخواستی را هم نشان دهد (راهنمایی: بهجای فایل، با return 404 "..." و متغیر $uri در یک named location). ثابت کن کد ۴۰۴ حفظ میشود.
دیدن جواب
sed -i 's|root /srv/repo;|root /var/www/site.test;|; s| error_page 404 /errors/404.html;| error_page 404 @notfound;\n location @notfound {\n default_type text/plain;\n return 404 "Sorry, $uri was not found on this site.\\n";\n }|' /etc/nginx/sites-available/site.testgrep -A4 'error_page 404' /etc/nginx/sites-available/site.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s -w '(HTTP %{http_code})\n' http://site.test/old/page.html error_page 404 @notfound; location @notfound { default_type text/plain; return 404 "Sorry, $uri was not found on this site.\n"; }nginx: configuration file /etc/nginx/nginx.conf test is successfulSorry, /old/page.html was not found on this site.(HTTP 404)error_page 404 @notfound بهجای فایل، یک named location (با @) را صدا میزند؛ named location فقط داخلی است و از بیرون قابل درخواست نیست. $uri همان مسیر درخواستی (/old/page.html) است و return 404 کد را حفظ میکند.
تمرین اصلی درس: یک سایت Astro ساختهشده را با Nginx سرو کن. ما از خروجی build همین سایت LoopX استفاده کردیم (/var/www/loopx.test)؛ تو میتوانی پروژهی Astro خودت را با npm run build بسازی و dist/ را کپی کنی. یک اسکریپت بنویس که همهی صفحههای HTML سایت را از روی فایلها پیدا کند، به هر کدام درخواست بفرستد و تعداد کدهای وضعیت را گزارش کند (همه باید ۲۰۰ باشند)، و یک آدرس ناموجود را هم امتحان کند (باید ۴۰۴ با صفحهی ۴۰۴ سایت باشد).
دیدن جواب
cd /var/www/loopx.testfind . -name 'index.html' -printf '%h/\n' | sed 's|^\.||; s|^$|/|' | sort > /tmp/urls.txtecho "صفحهها: $(wc -l < /tmp/urls.txt)"while IFS= read -r u; do curl -s -o /dev/null -w '%{http_code}\n' "http://loopx.test$u"done < /tmp/urls.txt | sort | uniq -cecho "--- آدرس ناموجود:"curl -s -o /tmp/body -w '%{http_code} ' http://loopx.test/this/does/not/exist/grep -o '<title>[^<]*</title>' /tmp/bodycd ~صفحهها: 163 163 200--- آدرس ناموجود:404 <title>صفحه پیدا نشد | LoopX</title>find -printf '%h/\n' برای هر index.html پوشهاش را میدهد (./docker/what-is-docker/)؛ sed نقطهی اول را برمیدارد تا آدرس شود. همهی صفحهها 200 دادند و آدرس ناموجود 404 با صفحهی ۴۰۴ خود سایت. (برای استقرار واقعی، همین اسکریپت بعد از هر deploy یک «آزمون دود (smoke test)» خوب است.)
برای spa.test اینها را بساز: ۱) /api/ به یک سرور پایتون روی 127.0.0.1:9201 برود و اگر فایلی نبود ۴۰۴ خود API را بدهد (نه index.html)؛ ۲) /assets/ با ۴۰۴ واقعی؛ ۳) بقیه fallback به index.html؛ ۴) فایلهای مخفی بسته. یک جدول آزمایش با ۶ آدرس بساز که هر قانون را ثابت کند.
دیدن جواب
mkdir -p /srv/lx-api/apiecho '{"id": 42, "name": "book"}' > /srv/lx-api/api/product-42.json(cd /srv/lx-api && exec python3 -m http.server 9201 --bind 127.0.0.1) > /dev/null 2>&1 &echo 'SECRET=1' > /var/www/spa.test/.envcat > /etc/nginx/sites-available/spa.test <<'EOF'server { listen 80; server_name spa.test; root /var/www/spa.test; index index.html;
location ~ /\.(?!well-known) { deny all; } location /api/ { proxy_pass http://127.0.0.1:9201; } location /assets/ { try_files $uri =404; } location / { try_files $uri $uri/ /index.html; }}EOFsleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in /orders/7 /assets/app.js /assets/old.js /api/product-42.json /api/missing.json /.env; do printf '%-22s ' "$p" curl -s -o /tmp/body -w '%{http_code} %{content_type} ' "http://spa.test$p" head -c 40 /tmp/body | tr -d '\r\n'; echodonenginx: configuration file /etc/nginx/nginx.conf test is successful/orders/7 200 text/html <!doctype html><title>SPA</title><div id/assets/app.js 200 application/javascript document.getElementById("app").textConte/assets/old.js 404 text/html <html><head><title>404 Not Found</titl/api/product-42.json 200 application/json {"id": 42, "name": "book"}/api/missing.json 404 text/html;charset=utf-8 <!DOCTYPE HTML><html lang="en"> <he/.env 403 text/html <html><head><title>403 Forbidden</titlهر قانون با یک آدرس ثابت شد: مسیر صفحه ← index.html؛ دارایی موجود ← JS؛ دارایی گمشده ← ۴۰۴؛ API موجود ← JSON از پایتون؛ API ناموجود ← ۴۰۴ پایتون (HTML خطای http.server، نه index.html SPA)؛ فایل مخفی ← ۴۰۳.
آزمونک
Section titled “آزمونک”در یک SPA، رفرش روی /products/42 خطای ۴۰۴ میدهد. چه تنظیمی لازم است؟
روتر SPA در مرورگر آدرس را میخواند؛ Nginx فقط باید همان index.html را بدهد. برای /assets/ ولی ۴۰۴ واقعی نگه دار.
error_page 404 /404.html; با چه کدی صفحهی ۴۰۴ سفارشی را برمیگرداند؟
اگر صفحهی خطا را در آخر try_files بگذاری، 200 میگیری (soft 404).
internal; در یک location چه میکند؟
برای پوشهی صفحههای خطا.
با try_files $uri $uri/ /index.html; در یک SPA، درخواست /assets/old.js (حذفشده) چه جوابی میگیرد؟
location جدا: location /assets/ { try_files $uri =404; }.
مخزن کد را در پوشهی سایت clone کردهای و root همانجاست. بزرگترین خطر؟
root فقط روی خروجی build؛ و location ~ /\.(?!well-known) { deny all; }.
چرا در قانون بستن فایلهای مخفی، /.well-known/ را استثنا میکنیم؟
گرفتن گواهی HTTPS از همین مسیر انجام میشود.
مرورگر با If-None-Match و ETag درست درخواست میدهد. Nginx چه جوابی میدهد؟
صرفهجویی در پهنای باند؛ Nginx خودکار برای فایلهای استاتیک انجام میدهد.
جمعبندی
Section titled “جمعبندی”- سایت استاتیک =
root+index+try_files؛ نوع فایل از پسوند وmime.typesمیآید.sendfileفایل را مستقیم از هسته میفرستد و ETag/304بار تکراری را حذف میکند. - چندصفحهای (Astro مثل همین سایت):
try_files $uri $uri/ =404;وerror_page 404 /404.html;(کد ۴۰۴ حفظ میشود). صفحههای خطا را باinternalاز دسترسی مستقیم ببند. - SPA:
try_files $uri $uri/ /index.html;برای صفحهها، ولیlocation /assets/ { try_files $uri =404; }برای داراییها. autoindexفقط در location پوشهی دانلود.- امنیت:
rootفقط روی خروجی build؛location ~ /\.(?!well-known) { deny all; }برای.git،.envو هر فایل مخفی. - بعد از هر استقرار، یک آزمون دود: همهی صفحهها ۲۰۰، آدرس ناموجود ۴۰۴.
| دستور | کاری که میکند |
|---|---|
try_files $uri $uri/ =404; | سایت چندصفحهای |
error_page 404 /404.html; | صفحهی ۴۰۴ سفارشی با کد ۴۰۴ |
error_page 500 502 503 504 /50x.html; | صفحهی خطای سرور |
location ^~ /errors/ { internal; } | بستن دسترسی مستقیم به صفحههای خطا |
location / { try_files $uri $uri/ /index.html; } | SPA fallback |
location /assets/ { try_files $uri =404; } | ۴۰۴ واقعی برای داراییهای SPA |
location /downloads/ { alias /srv/downloads/; autoindex on; } | فهرست پوشهی دانلود |
location ~ /\.(?!well-known) { deny all; } | بستن .git، .env و فایلهای مخفی |
curl -s -o /dev/null -w '%{http_code} %{content_type}' URL | کد و نوع پاسخ |
curl -H "If-None-Match: $etag" URL | آزمایش 304 |