server { listen 80; listen [::]:80; server_name teknavglobal.com www.teknavglobal.com; return 301 https://www.teknavglobal.com$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name teknavglobal.com; return 301 https://www.teknavglobal.com$request_uri; ssl_certificate /etc/letsencrypt/live/teknavglobal.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/teknavglobal.com/privkey.pem; } server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name www.teknavglobal.com; ssl_certificate /etc/letsencrypt/live/teknavglobal.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/teknavglobal.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root /srv/teknav/global/dist; index index.html; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none';" always; server_tokens off; # Proxy API to same backend as teknav.ir location /api/ { proxy_pass http://127.0.0.1:3010; 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_connect_timeout 10s; proxy_read_timeout 60s; } # SSE — no buffering location ~ ^/api/v1/(auth/notifications|admin)/stream { proxy_pass http://127.0.0.1:3010; proxy_http_version 1.1; proxy_set_header Connection ''; proxy_set_header Cache-Control 'no-cache'; proxy_set_header X-Accel-Buffering no; proxy_buffering off; chunked_transfer_encoding on; } # Static article pages (pre-generated) location /article/ { try_files $uri $uri/index.html @spa; gzip_static on; brotli_static on; expires 1h; add_header Cache-Control "public, max-age=3600, stale-while-revalidate=86400"; } # Assets with long cache location /assets/ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; gzip_static on; brotli_static on; } location /fonts/ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; } location /images/ { expires 30d; add_header Cache-Control "public, max-age=2592000"; } # Block dotfiles and sensitive paths location ~ /\. { deny all; } location = /ACCOUNTS.md { deny all; return 404; } # SPA fallback location @spa { try_files /index.html =404; } location / { try_files $uri $uri/ @spa; gzip_static on; brotli_static on; } # Feeds and sitemaps — no cache location ~ \.(xml|json)$ { expires -1; add_header Cache-Control "no-cache, no-store"; } # Rate limiting limit_req_zone $binary_remote_addr zone=global_en:10m rate=300r/m; limit_req_zone $binary_remote_addr zone=api_en:10m rate=300r/m; limit_req zone=global_en burst=200 nodelay; location /api/ { limit_req zone=api_en burst=200 nodelay; } # Errors error_page 404 /index.html; error_page 429 @too_many; error_page 500 502 503 504 @server_error; location @too_many { default_type application/json; return 429 '{"error":"too_many_requests","retryAfter":60}'; } location @server_error { default_type application/json; return 503 '{"error":"service_unavailable"}'; } }