SPA冲突路由改名+旧路径Navigate重定向(/api-docs→/open-api-docs、/api-detail→/open-api-detail、/article→/post、/user→/user-home);vite代理宽前缀改精确正则白名单;nginx.conf同步同口径;新增86路由回归脚本+Playwright 3项(86/86全绿)。
251 lines
10 KiB
Nginx Configuration File
251 lines
10 KiB
Nginx Configuration File
server {
|
||
listen 80;
|
||
server_name localhost;
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# 开启gzip压缩
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_min_length 1024;
|
||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
|
||
|
||
# WebSocket 反代(聊天 ws/chat、扫码登录 ws/qr-status → Granian + Channels)
|
||
location /ws/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 3600s;
|
||
proxy_send_timeout 3600s;
|
||
}
|
||
|
||
# API 反代 → Granian(ADRF 异步视图)
|
||
# C-01:精确收窄到后端真实存在的 API 前缀。SPA 路由(/open-api*、/post/*、
|
||
# /user-home*、/articles、/learn 列表页等)一律落到下面的 `location /` 回退,
|
||
# 不再被宽正则劫持。旧分享链接(/article/:id、/user/:id、/api-detail/* 等)
|
||
# 由前端 SPA 内 <Navigate> 重定向承接,nginx 直接放行到 index.html 即可。
|
||
location ~ ^/api/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_set_header Content-Type $http_content_type;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 user.urls(/user/<action>/):SPA 已迁到 /user-home*,旧 /user/:id 分享
|
||
# 链接由前端重定向承接;数字 id 形态放行到 SPA,其余代理到后端
|
||
location ~ ^/user/\d+/?$ {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
location ~ ^/user/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_set_header Content-Type $http_content_type;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 bug.urls 仅 /bug/reports/*;SPA /bug 与 /bug-detail 放行
|
||
location ~ ^/bug/reports/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 article.urls 仅 articles|my-articles|comments;SPA /post/*、/articles 放行
|
||
location ~ ^/article/(articles|my-articles|comments)/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 learn.urls 子段;SPA /learn 列表页放行
|
||
location ~ ^/learn/(courses|chapters|my-courses|my-progress|materials|favorites|cdn)/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 message.urls(/message/*);SPA /messages(多 s)天然不命中
|
||
location ~ ^/message/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 chat.urls 子段;SPA /chat 本体放行
|
||
location ~ ^/chat/(friend-requests|friends|users|conversations|messages|upload|favorite-stickers)/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 tool.urls(/tool/*);SPA /tool-detail 放行(不以 /tool/ 开头)
|
||
location ~ ^/tool/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 history.urls 仅 records/;SPA /history 列表页放行
|
||
location ~ ^/history/records/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 后端 search 查询串形态(GET /search/?q=);裸 /search(页)与无参 /search/ 放行到 SPA。
|
||
# location 按 URI 匹配、看不见 query string,故用 $args 分流。
|
||
location = /search/ {
|
||
error_page 418 = @spa_fallback;
|
||
if ($args = "") { return 418; }
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
}
|
||
location ~ ^/search/(suggestions|hot-keywords)/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
}
|
||
|
||
# 后端 app(changelog)/ logs / shorturl
|
||
location ~ ^/(app|logs/api|shorturl)/ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
client_max_body_size 50M;
|
||
}
|
||
|
||
# 短链跳转 /s/<code>/:仅短码形态代理,其余(/settings、/shorturl-detail)放行
|
||
location ~ ^/s/[^/]+/?$ {
|
||
proxy_pass http://backend:8000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
}
|
||
|
||
# Django admin / swagger / redoc / i18n / 测试页
|
||
location /admin/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 50M; }
|
||
location /swagger { proxy_pass http://backend:8000; proxy_set_header Host $host; }
|
||
location /swagger.json { proxy_pass http://backend:8000; proxy_set_header Host $host; }
|
||
location /redoc/ { proxy_pass http://backend:8000; proxy_set_header Host $host; }
|
||
location /i18n/ { proxy_pass http://backend:8000; proxy_set_header Host $host; }
|
||
location /test/ { proxy_pass http://backend:8000; proxy_set_header Host $host; }
|
||
|
||
# 带哈希指纹的构建产物:永久强缓存(immutable)
|
||
location ~* \assets/.*\.(js|css|png|jpg|jpeg|gif|webp|avif|ico|svg|woff|woff2|ttf|eot)$ {
|
||
expires 1y;
|
||
add_header Cache-Control "public, immutable";
|
||
access_log off;
|
||
}
|
||
|
||
# 其他静态资源(public/ 目录:图片/字体等无指纹文件):适中缓存
|
||
location ~* \.(png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
|
||
expires 7d;
|
||
add_header Cache-Control "public";
|
||
access_log off;
|
||
}
|
||
|
||
# index.html 不缓存,保证发版后立即生效
|
||
location = /index.html {
|
||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||
}
|
||
|
||
# C-05 SEO 预渲染优先:蜘蛛/首屏直接拿到含正文的静态 HTML,
|
||
# React hydrate 后照常接管交互(预渲染页内含 location.replace 跳转到 SPA 路由)。
|
||
# 首页用 index.seo.html(不覆盖 SPA 外壳 index.html);工具页/聚合页为目录 index.html。
|
||
location = / {
|
||
try_files /index.seo.html /index.html;
|
||
}
|
||
location ~ ^/(utility/[a-z-]+|qrcode-generator|color-picker|baidu-translate|utility|top)/?$ {
|
||
try_files $uri $uri/index.html $uri/ /index.html;
|
||
add_header Cache-Control "public, max-age=3600";
|
||
}
|
||
|
||
# SPA 路由回退(放在最后,避免拦截上面的 API 请求)
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
location @spa_fallback {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
}
|