- Start page: the text block ("Fotograf Linus Olofsson ..." and "Posters och samarbeten ...") is gone; the buttons
Posters, Samarbeten, Mer om mig, Portfolio and Kontakt sit at the bottom as long, straight buttons (no round pills, no
text links). The second scroll box is Linus's own from May again (Redo att samarbeta? -> Kontakta mig) instead of
"Vill du ha en bild på väggen?".
- Portfolio: "Vill du ha en bild på väggen? Se posters." and every "Köp som poster" button are gone. Photos are
protected like on Posters: no link to the large file (nothing to open in a new tab), a transparent layer over every
photo, no context menu, no drag, no long-press save.
- SV/EN: SV/EN switch in the menu; every visible text in both languages (menu, footer, cookie box, start page,
portfolio with dates and months, privacy page, 404). The choice uses the portal's key (ibo_lang), so all pages on
imagesbyolofsson.se follow the same choice; it is set before the page is drawn.
- Privacy: the statistics count openings only (there are no downloads any more).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linus 2026-11-06: he offers no services; the site has posters and collaborations only. /tjanster went live with the
move to Cleura (it came with the SEO pull request #1) and was redirected to the start page in Caddy the same morning.
- tjanster.astro deleted; Tjänster out of the menu and the 404 page.
- Start page: "Vill du boka en fotografering?" -> "Vill du ha en bild på väggen?" (Se posters); "Så bokar du" ->
"Posters och samarbeten"; description without "Boka via kundportalen".
- Portfolio intro points to the posters; privacy page: orders of posters and collaborations instead of bookings.
- JSON-LD: Organization instead of ProfessionalService, no ReserveAction, offer catalog, price range or service area.
- llms.txt: no prices, booking, booking steps, services page or booking FAQ; faq.ts keeps only the type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seen on the first run on Cleura 2026-11-06: 13 photos tagged since 2026-09-29 were never synced. portfolio-sync.mjs
sets exit code 1 when one photo fails (DSC00342_PSMS16-square.jpg: "Input image exceeds pixel limit", sharp's default
268 MP) and set -e then stopped before commit/push - on Nurem as well. Now the photos that worked are committed and
built, the run still ends with exit 1 so systemd shows it, and sharp's limit is 1 000 MP (JPEGs shrink while decoding;
the container gets 3 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review: GIT_URL (user:token@) was stored as the clone's remote URL, and the repo, .git included, was mounted
into the container that runs npm install. Now: umask 077 + 700 directories, the remote is the clean URL, git gets the
token from a credential helper at fetch/push time, and the container sees an empty tmpfs over .git.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deploy/cleura/portfolio-sync.sh: same steps as /opt/immich-sync/run-sync.sh on Nurem (fetch main, portfolio-sync.mjs,
commit + push when photos changed) and then build-live.sh main on Cleura; flock against overlapping runs, the sync
runs in the pinned Node container. install-portfolio-sync.sh puts it in place; secrets stay in
/etc/portal/portfolio-sync.env (root 600). The snippet comment no longer mentions the removed /download/original.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linus 2026-11-06: everything public lives on imagesbyolofsson.se (Cleura), the customer
subdomain is only for ordering and the logged-in portal; photos are sold as posters
instead of being downloadable.
- Portfolio: download menu (S/M/L), the original password dialog and /download/original
are gone; the lightbox has "Köp som poster" -> /posters instead.
- about.astro and contact.astro removed: Caddy on Cleura sends /about, /contact and
/posters to the portal, so the About text and collaborations are edited once in admin.
They stay in sitemap-0.xml (PORTAL_PAGES); the sitemap index lists /posters/sitemap.xml.
- Menu and 404 page link Posters; privacy, robots.txt and llms.txt no longer mention
password downloads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Homepage images: <Picture> from src/assets (AVIF/WebP/JPG srcset, width/
height, first image eager + fetchpriority high, rest lazy), Swedish alt
texts. (The duplicate EXIF-laden copies in public/3d-scroll were deleted
in 30e385e; README links now point at src/assets.)
- Portfolio: originals are no longer referenced (no .src access), so Astro
does not copy them to dist/. Small/Medium/Large downloads and the lightbox
srcset are EXIF-free JPGs from getImage. Old -s/-m files are no longer
imported.
- Alt texts: Swedish fallback from date (EXIF or filename), never camera
model or "undefined"; portfolio-meta.ts overrides per image.
- Gallery items are <a href> to the large image (lightbox on click);
download options are buttons; no empty H3, no H4 for the password box.
- Stats beacon unchanged; item keys computed at build time (same format).
- Script moved inside the layout so it is no longer rendered after </html>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- 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
- New /tjanster: genres from existing copy, where/price (contact for offer
while unknown), booking flow via customer portal, delivery in a private
gallery, originals after payment, FAQ (also emitted as FAQPage).
- Homepage: tagline + text section on who/what/where/how to book.
- About: Swedish, photographer framing instead of "by day/by night",
Adobe line. Contact: Swedish, plain mailto info@, booking CTA, fixed
unclosed div. Privacy: Swedish (Google Limited Use line kept in English).
- Unique Swedish title and description per page; scripts moved inside the
layout (no <script> after </html>).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- astro.config: site, trailingSlash "never", build.format "file".
- /sitemap-index.xml + /sitemap-0.xml as prerendered endpoints (no new
dependency); pages are derived from src/pages/*.astro, 404 excluded.
- robots.txt: all bots allowed, AI crawlers/training bots explicitly
allowed (Linus's decision), /download/ disallowed, sitemap line.
- /llms.txt generated from business.ts, incl. usage-with-attribution note.
- 404.astro in Swedish with links back, noindex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- business.ts: single source of truth; unknown facts (city, area, prices,
packages, phone, delivery time, experience, testimonials) are null and are
skipped everywhere. faq.ts builds answers only from known facts.
- portfolio-meta.ts: data hook for real per-image alt texts.
- SEO.astro: title, description, canonical, Open Graph, Twitter, og:image
(EXIF-free JPG via getImage, absolute URL).
- SchemaOrg.astro: @graph with WebSite, Person, ProfessionalService
(sameAs, ReserveAction to the customer portal, offer catalog without
prices), WebPage, optional FAQPage and BreadcrumbList.
- MainLayout: lang=sv, Swedish nav (adds Tjänster), consent banner and
footer in Swedish, lyxlimo/karleksguiden line removed, info@ address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
Counts go to customer.imagesbyolofsson.se/api/stats/hit via sendBeacon. No cookie, IP or id stored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Meta Pixel (2453598878495749) loads only after the visitor clicks Accept;
choice stored in localStorage (ibo-consent).
- Decline after a previous Accept clears _fbp/_fbc and reloads.
- "Cookie settings" link in the footer reopens the banner.
- Privacy page: removed "no tracking cookies", added Meta Pixel section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- storleksraderna far en mork hinna vid hover i stallet for ljus
- pilen i knappen gar gra medan menyn ar oppen, vit igen nar den stangs
(bade vid val av storlek och nar man klickar bort menyn)
- knappen trycks ner lite vid klick (active:scale-95)
- neutral-700 i stallet for helvit sa den inte skriker mot bilden
- tunn svart kant + ring/skugga for en sleak kontur
- knappen grupperad med EXIF i en hogergrupp; footern ar justify-between,
darfor hamnade den tidigare mitt i raden
- menyn forankras till hoger pa desktop sa den inte gar utanfor kortet
Knappen var kantig medan allt runt omkring ar rundat. Nu:
- rounded-full + hover:scale-105 som sajtens ovriga CTA-knappar
- nedladdningsikon, uppercase tracking-widest
- menyn rundad med backdrop-blur och mjuk in/ut-overgang
- losenordsrutan rundad med samma kort-kansla som lightboxen
- Escape stanger menyn innan lightboxen
DOWNLOAD-knapp i lightboxen oppnar en storleksmeny. S/M/L pekar pa filer i
repot; Original gar till /download/original och oppnar en losenordsruta.
Losenordet sparas i sessionStorage (bara fliken), och HEAD-anrop anvands sa
fel losenord ger 401 utan att dra igang en 70 MB-nedladdning.
🔴 Globen filtrerar nu bort -s/-m ur galleriet - utan det visades varje bild
tre ganger. Variantkartan (assetId -> hashade URL:er) genereras i frontmattern.
Genererar -s (800px) och -m (1600px) vid sidan av befintlig 3000px-fil.
En nedladdning fran Immich -> tre storlekar. Befintliga 82 bilder backfillade.
Original (27-70 MB/st, ~4 GB totalt) ligger INTE i repot utan hamtas live
via original-proxyn pa /download/original, skyddad av losenord + kontroll
att assetet faktiskt bar portfolio-taggen.
RAW formats (.CR2/.ARW/etc) can't be converted by sharp/libvips. Skip them
with a clear log instead of counting as failures. Tag the JPEG export instead.
- portfolio-sync: GitHub-runner kan ej nå Tailscale-Coolify, förlitar
på Coolifys auto-deploy-on-push istället
- ta bort codeql.yml (tom matrix, ogiltig, failade på varje push)
The previous favicon was the dark logo on transparent which was nearly
invisible. This uses the proper logo with purple-cyan gradient glow that
shows clearly against both light and dark browser tabs.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render logo to square PNG favicons (32/180/512px) and link them in
MainLayout.astro head. Previously the active layout had no favicon.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MainLayout footer now includes attribution: 'Crafting digital
experiences for lyxlimo.se & karleksguiden.se' visible on all pages.
On index page only: hovering lyxlimo.se tints hero-logo with
green (#4fd1c5 teal), hovering karleksguiden.se tints with
magenta (#c026d3). Mouseleave restores default white smoky-path
animation. Mechanism reuses existing data-logo-tint attribute on
#text-wrapper.
Removed logo-pulse functionality entirely (threadLogoIn/Out paths,
runLogoPulse function, LOGO_PROBABILITY, updateLogoThreadPaths,
resize/scroll listeners for logo threads). Hover-based UX replaces
the random pulse approach per Linus design preference.
Threads now lock to logo bracket pin endpoints (where vertical legs
of ::before / ::after bracket-corners end):
- Rosa tråd: från (0, viewport.height) → (logoLeft, logoTop + 40% height)
Quadratic Bezier with cp=(endX, startY) = x^2 curve, går rakt först
och accelererar upp i slutet
- Cyan tråd: från (logoRight, logoBottom - 40% height) → (viewport.width, 0)
Quadratic Bezier with cp=(startX, endY) = -x^2 curve, accelererar upp
i början och decelererar mot slutet
Permanent paths som uppdateras vid resize + scroll + 500ms/1500ms
post-load. runLogoPulse triggar update före varje pulse.
Pulse-blob is inside the SAME SVG as the path, so applyTransform
should use raw SVG coords (pt.x, pt.y) — not getScreenCTM-converted
ones. Double-transform was pushing pulse to SVG-X 9794+ which renders
off-screen-right.
Also phase2 now uses entryPath-end + exitPath-start positions in
SVG-coord-space so the inside-logo glide is continuous with phase1
and phase3.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(logo-pulse): entry/exit threads now properly visible
Previous version used gradient strokes (gradPurple/gradMain) which
have stop-opacity 0 at ends + 0.45 mid, multiplied by path-opacity
0.55 = ~0.25 effective opacity. Practically invisible.
Now solid stroke colors (#e879f9 pink, #4fd1c5 cyan), stroke-width 2
(was 0.8), opacity 0.85 (was 0.55), plus pulseGlowMid filter for soft
halo. Should be clearly visible from off-screen edges to logo.
* feat(logo-pulse): permanent visible threads (no dynamic creation)
Previously dynamic paths were built/destroyed per pulse, were too
thick (stroke-width 2 + glow), and persisted incorrectly causing
visual artifacts.
Now:
- threadLogoIn: permanent purple wave-thread, same style as other
wave-threads (gradPurple stroke, stroke-width 0.7, opacity 0.55)
- threadLogoOut: permanent cyan wave-thread (gradMain stroke 0.7,
opacity 0.50)
- runLogoPulse uses these permanent paths via getPointAtLength
- getScreenCTM converts SVG-coords to screen-coords so pulse follows
thread even with scroll/viewport variance
* fix(logo-threads): align Y-coords to actually cross logo
Logo Y-center is approx 287 in screen-coords. Previous thread Y-coords
(400-600) only grazed logo-bottom or sat entirely below it. Adjusted:
- threadLogoIn: M-1000 500 Q 200 350 850 287 T 2600 200
Kommer från nedre-vänster (Y=500), kurvar UPP till logo-center
(Y=287), fortsätter upp till övre-höger (Y=200)
- threadLogoOut: M-1000 350 Q 400 200 850 287 T 2600 500
Kommer från övre-vänster (Y=350), genom logo-center (Y=287), ner
till nedre-höger (Y=500) — vertikalt motsatt entry-tråd