رفتن به محتوا
LoopX

سرو سایت استاتیک

توی این درس یاد می‌گیری یک سایت استاتیک را درست سرو کنی؛ یعنی فقط «باز شدن صفحه‌ی اول» کافی نیست. خروجی build همین سایت LoopX (که با Astro ساخته شده) را با Nginx بالا می‌آوری و همه‌ی آدرس‌هایش را آزمایش می‌کنی. صفحه‌ی خطای سفارشی (error_page) می‌سازی که کد درست (۴۰۴) را هم نگه دارد، برای یک SPA (اپ تک‌صفحه‌ای مثل React) fallback درست می‌کنی تا رفرش روی /products/42 خطا ندهد، و دام رایجش را می‌بینی. با autoindex فهرست یک پوشه‌ی دانلود را نشان می‌دهی، و جلوی لو رفتن فایل‌های حساسی مثل .git را می‌گیری.

مسئله: «روی لپ‌تاپم درست بود!»

Section titled “مسئله: «روی لپ‌تاپم درست بود!»”

سایت استاتیک ساده به نظر می‌رسد: چند فایل HTML، CSS و عکس در یک پوشه. ولی بعد از استقرار این‌ها پیش می‌آید:

  • آدرس‌های تمیز (/about/ به‌جای /about.html) گاهی کار می‌کنند و گاهی نه.
  • صفحه‌ی ۴۰۴ زشتِ پیش‌فرض Nginx نشان داده می‌شود، یا بدتر، صفحه‌ی ۴۰۴ خودت با کد ۲۰۰ برمی‌گردد و گوگل آن را صفحه‌ی واقعی ایندکس می‌کند.
  • در SPA، رفرش روی هر صفحه‌ای غیر از صفحه‌ی اول، ۴۰۴ می‌دهد.
  • پوشه‌ی .git یا فایل .env که با کد آمده، از اینترنت قابل دانلود است.

سایت استاتیک مثل ویترین یک مغازه است: هر چیزی که در ویترین (پوشه‌ی root) بگذاری، رهگذر می‌بیند. پس اول، فقط محصول نهایی (خروجی build) را در ویترین بگذار، نه انبار (کد منبع، .git، .env). دوم، برای قفسه‌ی خالی یک تابلوی محترمانه بگذار («این کالا موجود نیست»، یعنی صفحه‌ی ۴۰۴ سفارشی) ولی به مشتری دروغ نگو که کالا هست (کد ۲۰۰ برای صفحه‌ی ناموجود). و در یک SPA، ویترین فقط یک در ورودی دارد (index.html) که خودش مشتری را به هر بخشی هدایت می‌کند.

سایت چندصفحه‌ای در برابر SPA

Section titled “سایت چندصفحه‌ای در برابر SPA”
دو نوع سایت استاتیک. در سایت چندصفحه‌ای (مثل خروجی Astro یا Hugo) برای هر آدرس یک فایل HTML روی دیسک هست و Nginx همان را پیدا می‌کند؛ آدرسی که فایل ندارد واقعاً ۴۰۴ است. در SPA (React، Vue) فقط یک index.html هست و جاوااسکریپت در مرورگر آدرس را می‌خواند؛ پس Nginx باید برای هر آدرسی که فایل نیست، همان index.html را بدهد.

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده. خروجی build سایت LoopX را از همین مخزن (پوشه‌ی dist/ بعد از npm run build) روی آن کپی کرده‌ایم.

مثال ۱: یک سایت چندصفحه‌ای ساده

Section titled “مثال ۱: یک سایت چندصفحه‌ای ساده”
Terminal window
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>
EOF
echo '<!doctype html><title>About</title><h1>About us</h1>' > /var/www/site.test/about/index.html
echo 'h1 { color: teal; }' > /var/www/site.test/css/style.css
head -c 2048 /dev/urandom > /var/www/site.test/img/logo.png
cat > /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;
}
}
EOF
ln -s /etc/nginx/sites-available/site.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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"
done
خروجی
nginx: 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 است. همان را سرو می‌کنیم:

Terminal window
ls /opt/loopx-dist | head -n 12
echo "..."
echo "تعداد صفحه‌های HTML: $(find /opt/loopx-dist -name '*.html' | wc -l)"
du -sh /opt/loopx-dist | cut -f1
خروجی
404.html
_astro
about
apple-touch-icon.png
bash
docker
favicon-32.png
favicon.svg
git
icon-192.png
icon-512.png
index.html
...
تعداد صفحه‌های HTML: 164
36M
/etc/nginx/sites-available/loopx.test
server {
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;
}
Terminal window
cp -r /opt/loopx-dist /var/www/loopx.test
ln -s /etc/nginx/sites-available/loopx.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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"
done
خروجی
nginx: 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 یک صفحه‌ی ۴۰۴ و یک صفحه برای خطاهای سرور (۵۰۰ تا ۵۰۴) می‌سازیم:

Terminal window
mkdir -p /var/www/site.test/errors
echo '<!doctype html><title>Not found</title><h1>Sorry, this page does not exist.</h1>' > /var/www/site.test/errors/404.html
echo '<!doctype html><title>Down</title><h1>We will be back soon.</h1>' > /var/www/site.test/errors/50x.html
cat > /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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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/body
done
خروجی
nginx: 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/:

Terminal window
mkdir -p /var/www/spa.test/assets
cat > /var/www/spa.test/index.html <<'EOF'
<!doctype html><title>SPA</title><div id="app">SPA shell</div><script src="/assets/app.js"></script>
EOF
echo 'document.getElementById("app").textContent = "page: " + location.pathname;' > /var/www/spa.test/assets/app.js
cat > /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;
}
}
EOF
ln -s /etc/nginx/sites-available/spa.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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")"
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
/ 200
/products/42 404
/assets/app.js 200

صفحه‌ی اول باز می‌شود، ولی /products/42 (که کاربر با رفرش یا لینک مستقیم می‌خواهد) ۴۰۴ است؛ چون چنین فایلی روی دیسک نیست. مسیرها را جاوااسکریپت در مرورگر می‌سازد. راه‌حل استاندارد: هر آدرسی که فایل یا پوشه نبود، همان index.html را بده:

Terminal window
sed -i 's|try_files $uri $uri/ =404;|try_files $uri $uri/ /index.html;|' /etc/nginx/sites-available/spa.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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; echo
done
خروجی
nginx: 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) با ۴۰۴ واقعی:

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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")"
done
خروجی
nginx: 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 روشن کن:

Terminal window
mkdir -p /srv/downloads/2026
head -c 1500000 /dev/zero > /srv/downloads/backup-tool-1.2.tar.gz
echo "checksums" > /srv/downloads/SHA256SUMS
echo "notes" > /srv/downloads/2026/release-notes.txt
touch -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.txt
sed -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.test
grep -A4 'location /downloads/' /etc/nginx/sites-available/site.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -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 10
backup-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:

Terminal window
mkdir -p /srv/repo
cd /srv/repo
git init -q 2>/dev/null || mkdir -p .git
printf '[remote "origin"]\n\turl = git@github.com:company/shop-private.git\n' > .git/config
echo 'DB_PASSWORD=super-secret' > .env
echo '<h1>repo site</h1>' > index.html
cd ~
sed -i 's|root /var/www/site.test;|root /srv/repo;|' /etc/nginx/sites-available/site.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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' ' '; echo
done
خروجی
nginx: 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):

Terminal window
sed -i 's|^ location / {| location ~ /\\.(?!well-known) {\n deny all;\n }\n\n location / {|' /etc/nginx/sites-available/site.test
grep -A2 'well-known' /etc/nginx/sites-available/site.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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 بدون بدنه می‌دهد:

Terminal window
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.svg
echo "=== با If-None-Match (فایل عوض نشده):"
curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' -H "If-None-Match: $etag" http://loopx.test/favicon.svg
echo "=== با ETag قدیمی:"
curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' -H 'If-None-Match: "old-etag"' http://loopx.test/favicon.svg
خروجی
ETag: "6ac24be5-2a36"
=== درخواست اول:
200 10806 bytes
=== با If-None-Match (فایل عوض نشده):
304 0 bytes
=== با ETag قدیمی:
200 10806 bytes

با ETag درست، پاسخ 304 و صفر بایت بدنه بود. برای سایتی با صدها کاربر برگشتی، این یعنی صرفه‌جویی بزرگ در پهنای باند. Nginx همه‌ی این‌ها را برای فایل‌های استاتیک خودکار انجام می‌دهد؛ در درس عملکرد، با هدرهای Cache-Control و expires می‌گوییم مرورگر حتی همین سؤال را هم تا مدتی نپرسد.

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;

۱) root روی پوشه‌ی پروژه به‌جای dist/

Section titled “۱) root روی پوشه‌ی پروژه به‌جای dist/”

مثال ۶: .git، .env، کد منبع و node_modules همه قابل دانلود می‌شوند. راه‌حل: root فقط روی خروجی build؛ و قانون location ~ /\.(?!well-known) { deny all; }.

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -o /dev/null -w 'GET /nothing -> %{http_code}\n' http://localhost:8088/nothing
rm /etc/nginx/conf.d/lx-soft404.conf
systemctl reload nginx
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
GET /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 {} +.

فهرست همه‌ی پوشه‌های بدون index، از جمله آن‌هایی که نباید دیده شوند. راه‌حل: فقط داخل location پوشه‌ی دانلود.

✎ تمرینآسان

برای site.test یک صفحه‌ی ۴۰۴ سفارشی بساز که اسم مسیر درخواستی را هم نشان دهد (راهنمایی: به‌جای فایل، با return 404 "..." و متغیر $uri در یک named location). ثابت کن کد ۴۰۴ حفظ می‌شود.

دیدن جواب
Terminal window
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.test
grep -A4 'error_page 404' /etc/nginx/sites-available/site.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -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 successful
Sorry, /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 سایت را از روی فایل‌ها پیدا کند، به هر کدام درخواست بفرستد و تعداد کدهای وضعیت را گزارش کند (همه باید ۲۰۰ باشند)، و یک آدرس ناموجود را هم امتحان کند (باید ۴۰۴ با صفحه‌ی ۴۰۴ سایت باشد).

دیدن جواب
Terminal window
cd /var/www/loopx.test
find . -name 'index.html' -printf '%h/\n' | sed 's|^\.||; s|^$|/|' | sort > /tmp/urls.txt
echo "صفحه‌ها: $(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 -c
echo "--- آدرس ناموجود:"
curl -s -o /tmp/body -w '%{http_code} ' http://loopx.test/this/does/not/exist/
grep -o '<title>[^<]*</title>' /tmp/body
cd ~
خروجی
صفحه‌ها: 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؛ ۴) فایل‌های مخفی بسته. یک جدول آزمایش با ۶ آدرس بساز که هر قانون را ثابت کند.

دیدن جواب
Terminal window
mkdir -p /srv/lx-api/api
echo '{"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/.env
cat > /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;
}
}
EOF
sleep 1
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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'; echo
done
خروجی
nginx: 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)؛ فایل مخفی ← ۴۰۳.

⚡ بررسی سریع

در یک SPA، رفرش روی /products/42 خطای ۴۰۴ می‌دهد. چه تنظیمی لازم است؟

؟ آزمونک
  1. error_page 404 /404.html; با چه کدی صفحه‌ی ۴۰۴ سفارشی را برمی‌گرداند؟

  2. internal; در یک location چه می‌کند؟

  3. با try_files $uri $uri/ /index.html; در یک SPA، درخواست /assets/old.js (حذف‌شده) چه جوابی می‌گیرد؟

  4. مخزن کد را در پوشه‌ی سایت clone کرده‌ای و root همان‌جاست. بزرگ‌ترین خطر؟

  5. چرا در قانون بستن فایل‌های مخفی، /.well-known/ را استثنا می‌کنیم؟

  6. مرورگر با If-None-Match و ETag درست درخواست می‌دهد. Nginx چه جوابی می‌دهد؟

  • سایت استاتیک = 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