# ============================================================================= # Nginx Configuration for FlowWink (SPA) # ============================================================================= # Optimized for: # - Single Page Application routing (all routes -> index.html) # - Static asset caching with proper headers # - Gzip compression # - Security headers # - Social media crawler detection for SSR meta tags # ============================================================================= # Detect social media crawlers for SSR meta tag rendering # These bots need server-rendered OG tags since they don't execute JavaScript map $http_user_agent $is_social_crawler { default 0; "~*facebookexternalhit" 1; "~*Facebot" 1; "~*Twitterbot" 1; "~*LinkedInBot" 1; "~*WhatsApp" 1; "~*Slackbot" 1; "~*Discordbot" 1; "~*TelegramBot" 1; "~*Pinterestbot" 1; "~*Googlebot" 1; "~*bingbot" 1; } server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Static assets with long-term caching (hashed filenames from Vite) location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # Favicon and other root assets location ~* \.(ico|png|jpg|jpeg|gif|svg|webp|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # index.html - no cache (always fetch latest) location = /index.html { add_header Cache-Control "no-cache, must-revalidate"; try_files $uri =404; } # SPA routing with social crawler detection # Social crawlers get redirected to render-page edge function for SSR meta tags # Regular users get the SPA location / { # Route social crawlers to SSR endpoint for proper OG tags # IMPORTANT: Replace SUPABASE_PROJECT_URL with your actual Supabase URL # Example: https://abcdefghijklmnop.supabase.co if ($is_social_crawler) { # Uncomment and configure the following lines for production: # rewrite ^(.*)$ /functions/v1/render-page?path=$1 break; # proxy_pass SUPABASE_PROJECT_URL; # proxy_set_header Host $proxy_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; # For now, fall through to SPA (social sharing will use index.html defaults) # Configure the proxy_pass above with your Supabase URL to enable SSR } # Normal SPA routing try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache, must-revalidate"; } # The MCP gateway at the site's own address — /mcp, the shape every other # integration uses (mcp.stripe.com, mcp.linear.app/mcp). On Vercel api/mcp.ts # does this; here it is one proxy_pass to the instance's mcp-server, query # string and headers (Authorization, Mcp-Session-Id, Accept) intact, with # buffering OFF so Streamable HTTP's event stream reaches the client as it # is written. Without it the gateway is still reachable at # SUPABASE_PROJECT_URL/functions/v1/mcp-server, which the admin shows. # IMPORTANT: Replace SUPABASE_PROJECT_URL with your actual Supabase URL location /mcp { # Uncomment and configure for production: # rewrite ^/mcp(/.*)?$ /functions/v1/mcp-server$1 break; # proxy_pass SUPABASE_PROJECT_URL; # proxy_http_version 1.1; # proxy_set_header Host $proxy_host; # proxy_set_header X-Forwarded-Host $host; # proxy_set_header X-Forwarded-Proto $scheme; # proxy_buffering off; # proxy_read_timeout 150s; # add_header X-FlowWink-MCP proxy always; return 404; } # Proxy sitemap.xml to Supabase edge function # IMPORTANT: Replace SUPABASE_PROJECT_URL with your actual Supabase URL # Example: https://abcdefghijklmnop.supabase.co location = /sitemap.xml { # Uncomment and configure for production: # proxy_pass SUPABASE_PROJECT_URL/functions/v1/sitemap-xml; # proxy_set_header Host $proxy_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 X-Forwarded-Host $host; # proxy_cache_valid 200 1h; # Fallback: return empty sitemap until proxy is configured default_type application/xml; return 200 '\n'; } # The blog's RSS feed (the archive's RSS button) — api/blog-rss.ts on Vercel. # Pass the site's own address as base_url, or every link names the functions host. location = /api/blog-rss { # Uncomment and configure for production: # rewrite ^ /functions/v1/blog-rss?base_url=$scheme://$host&$args break; # proxy_pass SUPABASE_PROJECT_URL; # proxy_set_header Host $proxy_host; # proxy_set_header apikey SUPABASE_ANON_KEY; # proxy_set_header Authorization "Bearer SUPABASE_ANON_KEY"; # Fallback: an empty feed until the proxy is configured default_type application/rss+xml; return 200 '\nBloghttps://$host/blog'; } # Proxy robots.txt (optional: can also be a static file) location = /robots.txt { default_type text/plain; return 200 'User-agent: *\nAllow: /\nSitemap: https://$host/sitemap.xml\n'; } # Health check endpoint for container orchestration location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } }