# desfoto.de - static site behind the existing VPS Traefik (Docker provider). # # Truthfulness contract (see /datenschutz/): this server writes no access log, # sets no cookies, runs no analytics and loads no third-party assets. YouTube is # only embedded after an explicit click (youtube-nocookie.com). map $uri $desfoto_cache { default "public, max-age=0, must-revalidate"; ~^/assets/img/ "public, max-age=604800, stale-while-revalidate=86400"; ~^/assets/fonts/ "public, max-age=604800, stale-while-revalidate=86400"; ~^/assets/site\.(css|js)$ "public, max-age=0, must-revalidate"; ~^/(favicon\.svg|site\.webmanifest)$ "public, max-age=86400"; } server { listen 80; listen [::]:80; server_name _; root /usr/share/nginx/html; index index.html; charset utf-8; server_tokens off; # Redirects stay relative so they keep the https scheme terminated by Traefik. absolute_redirect off; # No stored visitor data: the privacy page promises log-free operation. # Access logging is off and error messages are discarded completely, so this # server never writes a log file that could contain a visitor IP address. access_log off; error_log /dev/null crit; etag on; gzip on; gzip_vary on; gzip_comp_level 6; gzip_min_length 512; gzip_types text/css text/javascript application/javascript application/json application/ld+json application/xml application/manifest+json image/svg+xml; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always; add_header Strict-Transport-Security "max-age=31536000" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self' mailto:; frame-ancestors 'none'; upgrade-insecure-requests" always; add_header Cache-Control $desfoto_cache always; error_page 404 /404.html; location = /404.html { internal; } location = / { try_files /index.html =404; } # Keep canonical URLs: no /index.html duplicates in the index. The check uses # $request_uri so the internal redirect from the index module (which still # carries the original URI) is not caught in a loop. if ($request_uri ~ ^/(.*/)?index\.html(\?.*)?$) { return 301 /$1; } location = /impressum.html { return 301 /impressum/; } location = /datenschutz.html { return 301 /datenschutz/; } location = /kontakt.html { return 301 /kontakt/; } location = /gtin { return 301 /gtin/; } location = /gtin/manifest.webmanifest { default_type application/manifest+json; try_files $uri =404; } # /fotografie.html used to redirect to /shootings/; it resolves to its own # canonical page now instead of turning into a 404. location = /fotografie.html { return 301 /fotografie/; } # Fotografie is the umbrella term for the photographic work; the old # /shootings/ URL stays reachable and points to the new page. # (/fotografie/index.html is already canonicalised to /fotografie/ above.) location = /shootings { return 301 /fotografie/; } location = /shootings/ { return 301 /fotografie/; } location = /shootings.html { return 301 /fotografie/; } # Familie keeps the photographic range that stays with desfoto; the old # combined Familien-und-Paare URL points here. location = /familien-und-paare { return 301 /familie/; } location = /familien-und-paare/ { return 301 /familie/; } location = /familien-und-paare.html { return 301 /familie/; } location ^~ /.well-known/ { allow all; autoindex off; types { } default_type text/plain; } location /assets/ { try_files $uri =404; } location / { try_files $uri $uri/ =404; } location ~ /\.(?!well-known/) { deny all; } }