feat(server): redirects, real 404, caching and security headers (nginx + Caddy)

- www -> apex 301, old URLs (/my-people, /animales-and-nature,
  /celebrations-with-family, /northen-lights) -> /portfolio 301,
  trailing slash and .html -> clean URL 301.
- try_files $uri $uri.html, otherwise 404.html with status 404.
- /_astro/*: public, max-age=31536000, immutable (path-based map, no
  duplicate Cache-Control headers); HTML must-revalidate.
- HSTS, Referrer-Policy, Permissions-Policy, X-Frame-Options, CSP that
  allows the Meta Pixel and the stats beacon to customer.imagesbyolofsson.se.
- deploy/caddy-site.snippet: the same rules as importable Caddy snippets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
This commit is contained in:
Linus Olofsson
2026-10-27 19:13:07 +01:00
co-authored by Claude Opus 5.5
parent 206d31d9c2
commit 4f1b468f75
2 changed files with 168 additions and 25 deletions
+99
View File
@@ -0,0 +1,99 @@
# deploy/caddy-site.snippet
# Caddy-motsvarigheten till nginx.conf för imagesbyolofsson.se (statisk dist/ från `astro build`,
# build.format "file": /about -> about.html, 404 -> 404.html).
#
# Användning i Caddyfile (Caddy >= 2.7):
#
# import /etc/caddy/snippets/caddy-site.snippet
#
# www.imagesbyolofsson.se {
# import ibo_www_redirect
# }
# imagesbyolofsson.se {
# # Befintliga specialregler (t.ex. handle /download/original* -> Immich-proxyn)
# # ska ligga FÖRE importen, så att de vinner över try_files/file_server.
# import ibo_site /srv/imagesbyolofsson/dist
# }
#
# Argumentet är sökvägen till dist-katalogen på servern (justera till den riktiga).
(ibo_www_redirect) {
redir https://imagesbyolofsson.se{uri} permanent
}
(ibo_site) {
root * {args[0]}
encode zstd gzip
# --- Säkerhetsheaders ---
# CSP: tillåter Meta Pixel (laddas först efter samtycke), anonym statistik via sendBeacon
# till customer.imagesbyolofsson.se och Cloudflares egna skript (/cdn-cgi, ev. Web Analytics).
# 'unsafe-inline' behövs för samtyckesskriptet och Astros inlinade skript/stilar.
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
-Server
}
# --- Cache-Control (ömsesidigt uteslutande matchers) ---
@ibo_immutable path /_astro/*
header @ibo_immutable Cache-Control "public, max-age=31536000, immutable"
@ibo_media {
path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf
not path /_astro/*
}
header @ibo_media Cache-Control "public, max-age=2592000"
@ibo_textfiles path *.xml *.txt
header @ibo_textfiles Cache-Control "public, max-age=3600"
@ibo_pages {
not path /_astro/*
not path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf *.xml *.txt
}
header @ibo_pages Cache-Control "public, max-age=0, must-revalidate"
# --- 301:or ---
# Gamla indexerade adresser -> portfolion.
@ibo_old path /my-people /my-people/ /animales-and-nature /animales-and-nature/ /celebrations-with-family /celebrations-with-family/ /northen-lights /northen-lights/
redir @ibo_old /portfolio permanent
# /index.html -> /
redir /index.html / permanent
# /about.html -> /about (404-sidan hanteras separat nedan)
@ibo_htmlext {
path_regexp ibo_htmlext ^(/.+)\.html$
not path /404.html
}
redir @ibo_htmlext {re.ibo_htmlext.1}{?query} permanent
# Avslutande snedstreck: /about/ -> /about
@ibo_trailing path_regexp ibo_trailing ^(/.+)/$
redir @ibo_trailing {re.ibo_trailing.1}{?query} permanent
# 404-sidan ska bara synas som svar på okända adresser, inte som egen URL med 200.
@ibo_404page path /404 /404.html
error @ibo_404page 404
# --- Statiska filer ---
try_files {path} {path}.html
file_server
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
handle_errors {
@ibo_notfound expression {err.status_code} == 404
handle @ibo_notfound {
root * {args[0]}
rewrite * /404.html
file_server {
status 404
}
}
}
}
+69 -25
View File
@@ -1,11 +1,20 @@
map $sent_http_content_type $cache_control_strategy {
default "public, max-age=2592000";
~^text/html "no-store, must-revalidate";
~^text/css "public, max-age=31536000, immutable";
~^application/javascript "public, max-age=31536000, immutable";
~^application/json "no-store, must-revalidate";
~^image/ "public, max-age=2592000";
~^font/ "public, max-age=31536000, immutable";
# nginx för imagesbyolofsson.se (Docker-image: dist/ i /usr/share/nginx/html).
# Bygget använder build.format "file": /about -> about.html, 404 -> 404.html.
# Samma regler som Caddy-varianten i deploy/caddy-site.snippet.
# Cache-Control per sökväg (inte per Content-Type, så att /_astro/*.jpg också blir immutable).
map $uri $ibo_cache_control {
default "public, max-age=0, must-revalidate";
~^/_astro/ "public, max-age=31536000, immutable";
~*\.(?:jpe?g|png|webp|avif|gif|svg|ico|woff2?|ttf|otf)$ "public, max-age=2592000";
~*\.(?:xml|txt)$ "public, max-age=3600";
}
# www -> apex (om www-trafik når den här containern).
server {
listen 80;
server_name www.imagesbyolofsson.se;
return 301 https://imagesbyolofsson.se$request_uri;
}
server {
@@ -14,33 +23,68 @@ server {
root /usr/share/nginx/html;
index index.html;
# Relativa Location-headers: bakom Cloudflare/proxy blir det aldrig http://host:80/...
absolute_redirect off;
server_tokens off;
# Säkerhetsheaders. OBS: inga add_header i location-blocken nedan, annars ärvs inte dessa.
# CSP: tillåter Meta Pixel (connect.facebook.net/facebook.com, laddas först efter samtycke),
# den anonyma statistiken (sendBeacon till customer.imagesbyolofsson.se) och Cloudflares
# egna skript (/cdn-cgi, ev. Web Analytics). 'unsafe-inline' behövs för samtyckesskriptet
# och Astros inlinade skript/stilar; byt mot hashar (Astro experimental.csp) i ett senare steg.
add_header Strict-Transport-Security "max-age=31536000" 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 "geolocation=(), microphone=(), camera=()" always;
add_header Cache-Control $cache_control_strategy always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
add_header Cache-Control $ibo_cache_control always;
# Gamla indexerade adresser -> närmaste nya sida.
location ~ ^/(?:my-people|animales-and-nature|celebrations-with-family|northen-lights)/?$ {
return 301 /portfolio;
}
# /index.html och /about.html -> rena URL:er (bara externa anrop; try_files nedan
# serverar filerna internt utan att gå hit).
location = /index.html {
return 301 /;
}
location ~ ^(?<ibo_noext>/.+)\.html$ {
return 301 $ibo_noext$is_args$args;
}
# Inga dubbletter med avslutande snedstreck: /about/ -> /about.
location ~ ^(?<ibo_noslash>/.+)/$ {
return 301 $ibo_noslash$is_args$args;
}
# Startsidan serveras direkt (ingen intern omdirigering till /index.html).
location = / {
try_files /index.html =404;
}
# Hashade Astro-filer: cachas ett år (immutable sätts via map ovan).
location ^~ /_astro/ {
access_log off;
try_files $uri =404;
}
# Originalnedladdning (/download/original) proxas till Immich någon annanstans
# (Cloudflare/Caddy). Finns den regeln i den här nginx: lägg den här ovanför.
location / {
try_files $uri $uri/ $uri.html $uri/index.html =404;
try_files $uri $uri.html =404;
}
location /_astro/ {
access_log off;
expires 1y;
}
location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|otf|eot)$ {
access_log off;
expires 30d;
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
error_page 404 /404.html;
location = /404.html {
internal;
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;
error_page 404 /404.html;
location = /404.html {
internal;
}
gzip_types text/plain text/css text/xml application/xml application/json application/javascript application/xml+rss image/svg+xml;
}