Compare commits
@@ -8,15 +8,15 @@ A modern, responsive photography portfolio website built with Astro, Tailwind CS
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid">
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid">
|
||||||
<br><em> More </em>
|
<br><em> More </em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect">
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect">
|
||||||
<br><em> At </em>
|
<br><em> At </em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img3.jpg" width="250" alt="Hero Section">
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img3.jpg" width="250" alt="Hero Section">
|
||||||
<br><em>Imagesbyolofsson.se</em>
|
<br><em>Imagesbyolofsson.se</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import { defineConfig } from 'astro/config';
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
// Kanonisk adress: canonical, og:url, sitemap och JSON-LD byggs från denna.
|
||||||
|
site: 'https://imagesbyolofsson.se',
|
||||||
|
// En URL per sida: /about (aldrig /about/). Servern 301:ar /about/ -> /about.
|
||||||
|
trailingSlash: 'never',
|
||||||
|
build: {
|
||||||
|
// about.astro -> dist/about.html (inte dist/about/index.html).
|
||||||
|
// nginx/Caddy serverar /about via try_files $uri.html.
|
||||||
|
format: 'file',
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
# # Specialregler (på Cleura: handle för /about, /contact, /posters -> kundportalen,
|
||||||
|
# # se kundportalens deploy/cleura/caddy/Caddyfile) ska ligga FÖRE importen.
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/install-portfolio-sync.sh – installs the portfolio sync on the Cleura server (run as root from a
|
||||||
|
# checkout of this repo). Needs /etc/portal/portfolio-sync.env (root 600) first; see portfolio-sync.sh for the keys.
|
||||||
|
# Undo: systemctl disable --now portfolio-sync.timer && rm /etc/systemd/system/portfolio-sync.{service,timer}
|
||||||
|
set -euo pipefail
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ENVF=/etc/portal/portfolio-sync.env
|
||||||
|
[ -f "$ENVF" ] || { echo "$ENVF is missing - copy IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY and GIT_URL there first"; exit 2; }
|
||||||
|
[ "$(stat -c %a "$ENVF")" = "600" ] || chmod 600 "$ENVF"
|
||||||
|
install -m 755 -o root -g root "$HERE/portfolio-sync.sh" /opt/portal/site/portfolio-sync.sh
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.service" /etc/systemd/system/portfolio-sync.service
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.timer" /etc/systemd/system/portfolio-sync.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now portfolio-sync.timer
|
||||||
|
systemctl list-timers portfolio-sync.timer --no-pager
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Immich portfolio tag -> imagesbyolofsson-se repo -> live site (deploy/cleura/portfolio-sync.sh)
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/opt/portal/site/portfolio-sync.sh
|
||||||
|
TimeoutStartSec=45min
|
||||||
|
Nice=10
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/portfolio-sync.sh – Immich "portfolio" tag -> this repo -> imagesbyolofsson.se, on the Cleura server.
|
||||||
|
#
|
||||||
|
# Replaces /opt/immich-sync/run-sync.sh on Nurem (started by n8n over SSH) when imagesbyolofsson.se moved to Cleura
|
||||||
|
# (Linus 2026-11-06: everything on the Cleura server). Same steps: fetch main, run scripts/portfolio-sync.mjs,
|
||||||
|
# commit + push to Gitea main when photos were added or removed, and then rebuild the live site with
|
||||||
|
# /opt/portal/site/build-live.sh (EXIF check included). Nothing is built when nothing changed.
|
||||||
|
#
|
||||||
|
# Installed by deploy/cleura/install-portfolio-sync.sh as /opt/portal/site/portfolio-sync.sh, run by
|
||||||
|
# portfolio-sync.timer every 20 minutes. Secrets: /etc/portal/portfolio-sync.env (root 600) with
|
||||||
|
# IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY (read-only Immich key) and GIT_URL (Gitea URL that may push main,
|
||||||
|
# with user:token@ in it, as on Nurem).
|
||||||
|
#
|
||||||
|
# The Gitea token never lands on disk outside that env file: the clone's remote URL is stored without it, git gets
|
||||||
|
# it from a credential helper at fetch/push time, and the Node container (npm install runs third-party code) gets
|
||||||
|
# an empty tmpfs over .git and only the Immich variables.
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
ENVF=${PORTFOLIO_SYNC_ENV:-/etc/portal/portfolio-sync.env}
|
||||||
|
DIR=${PORTFOLIO_SYNC_DIR:-/opt/portal/site/sync/repo}
|
||||||
|
NODE=node:22.22.0-alpine@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
||||||
|
|
||||||
|
exec 9>/run/lock/portfolio-sync.lock
|
||||||
|
flock -n 9 || { echo "[sync] already running"; exit 0; }
|
||||||
|
|
||||||
|
set -a; . "$ENVF"; set +a
|
||||||
|
export IMMICH_URL="${IMMICH_URL:-https://foto.imagesbyolofsson.se/api}"
|
||||||
|
export PORTFOLIO_TAG_ID="${PORTFOLIO_TAG_ID:-67d16b6c-0dea-42ef-a289-2c9e57fa5c5a}"
|
||||||
|
[ -n "${IMMICH_API_KEY:-}" ] || { echo "FATAL: IMMICH_API_KEY missing in $ENVF"; exit 2; }
|
||||||
|
[ -n "${GIT_URL:-}" ] || { echo "FATAL: GIT_URL missing in $ENVF"; exit 2; }
|
||||||
|
|
||||||
|
# Split GIT_URL into the clean URL (stored in .git/config) and user/token (kept in this process only).
|
||||||
|
read -r CLEAN_URL GIT_USER GIT_TOKEN < <(python3 - "$GIT_URL" <<'PY'
|
||||||
|
import sys, urllib.parse as u
|
||||||
|
p = u.urlsplit(sys.argv[1])
|
||||||
|
host = p.hostname + (":%d" % p.port if p.port else "")
|
||||||
|
print(u.urlunsplit((p.scheme, host, p.path, "", "")), u.unquote(p.username or "-"), u.unquote(p.password or "-"))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
unset GIT_URL
|
||||||
|
export GIT_USER GIT_TOKEN
|
||||||
|
# shellcheck disable=SC2016 # $GIT_USER/$GIT_TOKEN are expanded by the helper's shell, not here.
|
||||||
|
G=(git -c credential.helper= -c 'credential.helper=!f() { echo "username=$GIT_USER"; echo "password=$GIT_TOKEN"; }; f')
|
||||||
|
|
||||||
|
if [ ! -d "$DIR/.git" ]; then
|
||||||
|
mkdir -p "$(dirname "$DIR")"
|
||||||
|
"${G[@]}" clone -q "$CLEAN_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
chmod 700 "$(dirname "$DIR")" "$DIR"
|
||||||
|
git -C "$DIR" remote set-url origin "$CLEAN_URL"
|
||||||
|
echo "[sync] fetch + reset main"
|
||||||
|
"${G[@]}" -C "$DIR" fetch -q origin main
|
||||||
|
git -C "$DIR" checkout -q -B main origin/main
|
||||||
|
git -C "$DIR" reset -q --hard origin/main
|
||||||
|
|
||||||
|
echo "[sync] scripts/portfolio-sync.mjs"
|
||||||
|
# portfolio-sync.mjs exits 1 when a single photo fails but still writes the others; those are committed below, and the
|
||||||
|
# run ends with exit 1 so systemd shows that something needs a look (journalctl -u portfolio-sync).
|
||||||
|
SYNC_RC=0
|
||||||
|
docker run --rm --memory 3g --cpus 2 --network bridge \
|
||||||
|
-e IMMICH_URL -e IMMICH_API_KEY -e PORTFOLIO_TAG_ID \
|
||||||
|
-v "$DIR":/app --mount type=tmpfs,destination=/app/.git -w /app "$NODE" \
|
||||||
|
sh -c 'npm install --no-audit --no-fund --loglevel=error >/dev/null && node scripts/portfolio-sync.mjs' || SYNC_RC=$?
|
||||||
|
|
||||||
|
if git -C "$DIR" status --porcelain -- src/assets/images/portfolio | grep -q .; then
|
||||||
|
git -C "$DIR" add -A src/assets/images/portfolio
|
||||||
|
git -C "$DIR" -c user.name="portfolio-sync (Cleura)" -c user.email="Admin@imagesbyolofsson.se" \
|
||||||
|
commit -q -m "chore(portfolio): sync $(date -u +%Y-%m-%d)"
|
||||||
|
"${G[@]}" -C "$DIR" push -q origin main
|
||||||
|
echo "[sync] pushed $(git -C "$DIR" rev-parse --short HEAD), rebuilding the live site"
|
||||||
|
/opt/portal/site/build-live.sh main
|
||||||
|
else
|
||||||
|
echo "[sync] no changes"
|
||||||
|
fi
|
||||||
|
if [ "$SYNC_RC" != 0 ]; then
|
||||||
|
echo "[sync] WARNING: portfolio-sync.mjs exited $SYNC_RC - some photos failed (see the lines with '!' above)"
|
||||||
|
exit "$SYNC_RC"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Portfolio sync every 20 minutes (Immich has no event when a photo is tagged, so it polls)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/20
|
||||||
|
RandomizedDelaySec=60
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
map $sent_http_content_type $cache_control_strategy {
|
# nginx för imagesbyolofsson.se (Docker-image: dist/ i /usr/share/nginx/html).
|
||||||
default "public, max-age=2592000";
|
# Bygget använder build.format "file": /about -> about.html, 404 -> 404.html.
|
||||||
~^text/html "no-store, must-revalidate";
|
# Samma regler som Caddy-varianten i deploy/caddy-site.snippet.
|
||||||
~^text/css "public, max-age=31536000, immutable";
|
|
||||||
~^application/javascript "public, max-age=31536000, immutable";
|
# Cache-Control per sökväg (inte per Content-Type, så att /_astro/*.jpg också blir immutable).
|
||||||
~^application/json "no-store, must-revalidate";
|
map $uri $ibo_cache_control {
|
||||||
~^image/ "public, max-age=2592000";
|
default "public, max-age=0, must-revalidate";
|
||||||
~^font/ "public, max-age=31536000, immutable";
|
~^/_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 {
|
server {
|
||||||
@@ -14,33 +23,68 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.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-Content-Type-Options "nosniff" always;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
|
||||||
add_header Cache-Control $cache_control_strategy 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 / {
|
location / {
|
||||||
try_files $uri $uri/ $uri.html $uri/index.html =404;
|
try_files $uri $uri.html =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /_astro/ {
|
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
|
||||||
access_log off;
|
error_page 404 /404.html;
|
||||||
expires 1y;
|
location = /404.html {
|
||||||
}
|
internal;
|
||||||
|
|
||||||
location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|otf|eot)$ {
|
|
||||||
access_log off;
|
|
||||||
expires 30d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;
|
gzip_types text/plain text/css text/xml application/xml application/json application/javascript application/xml+rss image/svg+xml;
|
||||||
|
|
||||||
error_page 404 /404.html;
|
|
||||||
location = /404.html {
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 700 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 542 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
# robots.txt – https://imagesbyolofsson.se
|
||||||
|
#
|
||||||
|
# Policy (beslut av Linus 2026-10-27): ALLA botar är välkomna, även AI-crawlers
|
||||||
|
# och träningsbotar. Innehållet får användas av AI-assistenter med hänvisning
|
||||||
|
# till Images by Olofsson (se /llms.txt). Cloudflare-sidan (AI Crawl Control /
|
||||||
|
# "Block AI bots" / hanterad robots.txt) ställs in separat så att den inte
|
||||||
|
# blockerar eller skriver över detta.
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# AI-crawlers och träningsbotar: uttryckligen tillåtna.
|
||||||
|
User-agent: GPTBot
|
||||||
|
User-agent: OAI-SearchBot
|
||||||
|
User-agent: ChatGPT-User
|
||||||
|
User-agent: ClaudeBot
|
||||||
|
User-agent: Claude-User
|
||||||
|
User-agent: Claude-SearchBot
|
||||||
|
User-agent: anthropic-ai
|
||||||
|
User-agent: CCBot
|
||||||
|
User-agent: Google-Extended
|
||||||
|
User-agent: Applebot
|
||||||
|
User-agent: Applebot-Extended
|
||||||
|
User-agent: Bytespider
|
||||||
|
User-agent: Amazonbot
|
||||||
|
User-agent: meta-externalagent
|
||||||
|
User-agent: meta-externalfetcher
|
||||||
|
User-agent: FacebookBot
|
||||||
|
User-agent: PerplexityBot
|
||||||
|
User-agent: Perplexity-User
|
||||||
|
User-agent: cohere-ai
|
||||||
|
User-agent: Diffbot
|
||||||
|
User-agent: YouBot
|
||||||
|
User-agent: DuckAssistBot
|
||||||
|
User-agent: MistralAI-User
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://imagesbyolofsson.se/sitemap-index.xml
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Engangsjobb: skapa -s (800px) och -m (1600px) for redan synkade portfolio-bilder.
|
||||||
|
// Kallan ar den befintliga 3000px-filen i repot - ingen ny nedladdning fran Immich.
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const DIR = "src/assets/images/portfolio";
|
||||||
|
const SIZES = [{ suffix: "-s", dim: 800 }, { suffix: "-m", dim: 1600 }];
|
||||||
|
const QUALITY = 88;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(DIR).filter(
|
||||||
|
(f) => /^immich-[0-9a-f-]{36}-/i.test(f) && /\.jpg$/i.test(f) && !/-[sm]\.jpg$/i.test(f)
|
||||||
|
);
|
||||||
|
console.log(`hittade ${files.length} large-bilder`);
|
||||||
|
|
||||||
|
let skapade = 0, hoppade = 0;
|
||||||
|
for (const f of files) {
|
||||||
|
const src = path.join(DIR, f);
|
||||||
|
const base = src.replace(/\.jpg$/i, "");
|
||||||
|
for (const { suffix, dim } of SIZES) {
|
||||||
|
const dest = `${base}${suffix}.jpg`;
|
||||||
|
if (fs.existsSync(dest)) { hoppade++; continue; }
|
||||||
|
await sharp(src)
|
||||||
|
.rotate()
|
||||||
|
.resize(dim, dim, { fit: "inside", withoutEnlargement: true })
|
||||||
|
.withMetadata()
|
||||||
|
.jpeg({ quality: QUALITY })
|
||||||
|
.toFile(dest);
|
||||||
|
skapade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`klart: ${skapade} skapade, ${hoppade} fanns redan`);
|
||||||
@@ -77,7 +77,9 @@ function destName(asset) {
|
|||||||
async function downloadAndResize(asset, destPath) {
|
async function downloadAndResize(asset, destPath) {
|
||||||
const res = await api("GET", `/assets/${asset.id}/original`);
|
const res = await api("GET", `/assets/${asset.id}/original`);
|
||||||
const buf = Buffer.from(await res.arrayBuffer());
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
await sharp(buf)
|
// Pixel shift and the 15000 px Instagram squares can exceed sharp's default limit (268 MP). JPEGs are shrunk while
|
||||||
|
// they are decoded, so memory stays low; the sync container still has a memory cap.
|
||||||
|
await sharp(buf, { limitInputPixels: 1_000_000_000 })
|
||||||
.rotate() // respektera EXIF-orientering
|
.rotate() // respektera EXIF-orientering
|
||||||
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
||||||
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
|
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 937 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 1017 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 330 KiB |