-
Notifications
You must be signed in to change notification settings - Fork 525
Expand file tree
/
Copy pathnginx.conf
More file actions
175 lines (157 loc) · 6.76 KB
/
Copy pathnginx.conf
File metadata and controls
175 lines (157 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# ---- SSG runtime flag: opt into pre-rendered pages via `features=ssr` ----
# Read from the URL (?features=ssr) or the sticky `FEATURES` cookie that
# featureFlag.ts writes from it. Default off → the plain SPA shell (unchanged);
# on → serve the pre-rendered page. Build emits both `<route>/index.html`
# (shell) and `<route>/index.ssg.html` (content). No rebuild needed to toggle.
map $arg_features $ssr_from_query {
default 0;
"~(^|,)ssr(,|$)" 1;
}
map $cookie_FEATURES $ssr_from_cookie {
default 0;
"~ssr" 1;
}
map "$ssr_from_query$ssr_from_cookie" $ssr_on {
"00" 0;
default 1;
}
# (flag × route) → the .ssg.html to serve. Only pre-rendered routes have entries.
map "$ssr_on:$uri" $ssg_page {
default "";
"1:/auth/login" /auth/login/index.ssg.html;
"1:/auth/login/" /auth/login/index.ssg.html;
}
# ---- Gzip compression ----
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_static on;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
server_tokens off;
# The built SPA contains one hostname placeholder in its OG image tags. Rewrite
# HTML at request time so crawlers receive a tenant-specific URL without JS.
sub_filter_once off;
server {
listen 80;
server_name localhost;
# ---- Security headers ----
# Also included per-location wherever a local add_header appears — nginx
# drops inherited add_headers as soon as a location sets one of its own.
include /etc/nginx/security-headers.conf;
# ---- Set shared proxy headers once here ----
# Make sure to include these BEFORE your location blocks.
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
sub_filter '__OG_HOST__' '$host';
# Host must be the backend's own name, not $host: forwarding
# studio.acedata.cloud makes EdgeOne route the request back here, looping
# until it gives up (cdn-loop: loops=16 → 423 on every /api/v1 call).
# Same reason the /i/ block below sets an explicit Host.
location /api/v1/ {
proxy_pass https://platform.acedata.cloud;
proxy_set_header Host platform.acedata.cloud;
# Transport ceiling, NOT the user-facing limit — that is the backend's
# COS_MAX_UPLOAD_BYTES (100MB), which returns a clean JSON 413. This
# must stay well ABOVE it: nginx rejects an oversized body before
# reading it, and EdgeOne does not forward that early 413, so the
# browser hangs until EdgeOne's own 554. Headroom also covers the
# multipart envelope (~212B) so a file at exactly the limit is safe.
client_max_body_size 300m;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# ---- Static assets: hash-based filenames → immutable caching ----
location /assets/ {
root /usr/share/nginx/html;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
include /etc/nginx/security-headers.conf;
access_log off;
}
location = /index.html {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
include /etc/nginx/security-headers.conf;
}
# Serve /.well-known/llms.txt as alias to /llms.txt (llmstxt.org spec)
location = /.well-known/llms.txt {
root /usr/share/nginx/html;
try_files /llms.txt =404;
}
# ---- Deferred-deep-link referral attribution ----
# iOS Universal Links / Android App Links verification files. Served as
# static files (constants) from public/well-known/ — NOT proxied, so they
# don't hit the platform host's path allowlist or loop through EdgeOne.
location = /.well-known/apple-app-site-association {
alias /usr/share/nginx/html/well-known/apple-app-site-association;
default_type application/json;
add_header Cache-Control "public, max-age=3600" always;
include /etc/nginx/security-headers.conf;
}
location = /.well-known/assetlinks.json {
alias /usr/share/nginx/html/well-known/assetlinks.json;
default_type application/json;
add_header Cache-Control "public, max-age=3600" always;
include /etc/nginx/security-headers.conf;
}
# Invite landing (only hit when the app is NOT installed; otherwise the OS
# intercepts the App/Universal Link). Rewrite /i/<id> to the backend's
# /api/v1 landing — the platform host routes /api/v1/* to Django, and the
# explicit Host stops EdgeOne from bouncing the request back here (loop).
location /i/ {
rewrite ^/i/(.+)$ /api/v1/attribution/landing/$1 break;
proxy_pass https://platform.acedata.cloud;
proxy_set_header Host platform.acedata.cloud;
}
# dist/auth/ is an empty intermediate directory (the SSG build only emits
# dist/auth/login/{index,index.ssg}.html), so `try_files $uri/` reaches a
# directory nginx refuses to list → 403, while a non-matching variant
# (/auth%20/) falls through to the SPA shell → 200. A scanner reads that
# pair as an access-control bypass (CASA scan 2026-07-20, findings #1, #10).
#
# Matches ONLY the bare directory, so the SSG rewrite in `location /` still
# owns /auth/login: rewriting here would re-enter this block and nginx
# would abort the redirect cycle with a 500.
location ~ "^/auth/?$" {
root /usr/share/nginx/html;
try_files /index.html =404;
add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
include /etc/nginx/security-headers.conf;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# features=ssr opt-in → serve the pre-rendered page; default → SPA shell.
if ($ssg_page != '') {
rewrite ^ $ssg_page last;
}
# `$uri/index.html` rather than `$uri/`: a directory without an
# index.html would 403 instead of falling through (see /auth above).
try_files $uri $uri/index.html /index.html;
add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
include /etc/nginx/security-headers.conf;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}