diff --git a/README.md b/README.md index 1655dbb..d6ec4f1 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ A modern, responsive photography portfolio website built with Astro, Tailwind CS diff --git a/astro.config.mjs b/astro.config.mjs index 2b45c70..df833d6 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,6 +2,15 @@ import { defineConfig } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; 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: { plugins: [tailwindcss()], }, diff --git a/deploy/caddy-site.snippet b/deploy/caddy-site.snippet new file mode 100644 index 0000000..550c8c4 --- /dev/null +++ b/deploy/caddy-site.snippet @@ -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 + } + } + } +} diff --git a/nginx.conf b/nginx.conf index 242f4a1..58fd60e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -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 ~ ^(?/.+)\.html$ { + return 301 $ibo_noext$is_args$args; + } + + # Inga dubbletter med avslutande snedstreck: /about/ -> /about. + location ~ ^(?/.+)/$ { + 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; } diff --git a/public/3d-scroll/img1.jpg b/public/3d-scroll/img1.jpg deleted file mode 100644 index aaa04dd..0000000 Binary files a/public/3d-scroll/img1.jpg and /dev/null differ diff --git a/public/3d-scroll/img2.jpg b/public/3d-scroll/img2.jpg deleted file mode 100644 index fd3ced4..0000000 Binary files a/public/3d-scroll/img2.jpg and /dev/null differ diff --git a/public/3d-scroll/img3.jpg b/public/3d-scroll/img3.jpg deleted file mode 100644 index 07ec679..0000000 Binary files a/public/3d-scroll/img3.jpg and /dev/null differ diff --git a/public/3d-scroll/img4.jpg b/public/3d-scroll/img4.jpg deleted file mode 100644 index 4d61f9d..0000000 Binary files a/public/3d-scroll/img4.jpg and /dev/null differ diff --git a/public/3d-scroll/img5.jpg b/public/3d-scroll/img5.jpg deleted file mode 100644 index 6947880..0000000 Binary files a/public/3d-scroll/img5.jpg and /dev/null differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..20c0216 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,41 @@ +# 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: / +# Lösenordsskyddad nedladdning av originalfiler – inget att indexera. +Disallow: /download/ + +# 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: / +Disallow: /download/ + +Sitemap: https://imagesbyolofsson.se/sitemap-index.xml diff --git a/src/components/SEO.astro b/src/components/SEO.astro new file mode 100644 index 0000000..cfe2c32 --- /dev/null +++ b/src/components/SEO.astro @@ -0,0 +1,46 @@ +--- +// src/components/SEO.astro +// , meta description, canonical, Open Graph och Twitter-kort för en sida. +import { business } from '../data/business'; +import { normalizePath } from '../lib/url'; + +export interface Props { + title: string; + description: string; + /** Sökväg utan avslutande snedstreck, t.ex. "/about". Annars härleds den från URL:en. */ + path?: string; + /** Absolut URL till og:image (MainLayout tar fram en EXIF-fri JPG via getImage). */ + imageUrl: string; + imageWidth?: number; + imageHeight?: number; + imageAlt: string; + type?: 'website' | 'profile' | 'article'; + noindex?: boolean; +} + +const { title, description, path, imageUrl, imageWidth, imageHeight, imageAlt, type = 'website', noindex = false } = Astro.props; + +const site = Astro.site ?? new URL(business.url); +const canonical = new URL(normalizePath(path ?? Astro.url.pathname), site).toString(); +--- + +<title>{title} + +{noindex ? : } + + + + + + + + +{imageWidth && } +{imageHeight && } + + + + + + + diff --git a/src/components/SchemaOrg.astro b/src/components/SchemaOrg.astro new file mode 100644 index 0000000..297af28 --- /dev/null +++ b/src/components/SchemaOrg.astro @@ -0,0 +1,155 @@ +--- +// src/components/SchemaOrg.astro +// JSON-LD (@graph): WebSite + Person (Linus) + ProfessionalService (Images by Olofsson) +// + WebPage för aktuell sida (+ FAQPage och BreadcrumbList när det är relevant). +// Allt hämtas från src/data/business.ts. null-värden utelämnas helt. +import { business, absoluteUrl, genreList } from '../data/business'; +import type { FaqItem } from '../data/faq'; +import { normalizePath } from '../lib/url'; + +export interface Props { + title: string; + description: string; + path?: string; + pageType?: 'WebPage' | 'AboutPage' | 'ContactPage' | 'CollectionPage'; + /** Namn i brödsmulan för undersidor, t.ex. "Tjänster". */ + breadcrumb?: string; + faq?: FaqItem[]; + /** Absolut URL till sidans huvudbild (og:image). */ + imageUrl?: string; +} + +const { title, description, path, pageType = 'WebPage', breadcrumb, faq, imageUrl } = Astro.props; + +const site = (Astro.site ?? new URL(business.url)).toString().replace(/\/$/, ''); +const pagePath = normalizePath(path ?? Astro.url.pathname); +const pageUrl = absoluteUrl(pagePath); + +const ids = { + website: `${site}/#website`, + person: `${site}/#linus-olofsson`, + business: `${site}/#images-by-olofsson`, + webpage: `${pageUrl}#webpage`, + breadcrumb: `${pageUrl}#breadcrumb`, +}; + +// Tar bort null/undefined/tomma listor så att okända fakta aldrig hamnar i JSON-LD. +function clean(value: T): T { + if (Array.isArray(value)) { + return value.map(clean).filter((v) => v !== null && v !== undefined) as unknown as T; + } + if (value && typeof value === 'object') { + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + if (v === null || v === undefined) continue; + const c = clean(v); + if (Array.isArray(c) && c.length === 0) continue; + out[k] = c; + } + return out as T; + } + return value; +} + +// Landet är känt (Sverige); ort och region läggs till när de fylls i business.ts. +const address = { + '@type': 'PostalAddress', + addressLocality: business.city, + addressRegion: business.region, + addressCountry: business.country.code, +}; + +const graph: Record[] = [ + { + '@type': 'WebSite', + '@id': ids.website, + url: `${site}/`, + name: business.brand, + inLanguage: 'sv-SE', + publisher: { '@id': ids.business }, + }, + { + '@type': 'Person', + '@id': ids.person, + name: business.person.name, + jobTitle: business.person.jobTitle, + url: absoluteUrl('/about'), + description: `${business.person.jobTitle} baserad i ${business.country.name}. ${business.recognitionThirdPerson}`, + knowsAbout: business.genres.map((g) => g.name), + worksFor: { '@id': ids.business }, + sameAs: [business.social.linkedin, business.social.lightroom], + }, + { + '@type': 'ProfessionalService', + '@id': ids.business, + name: business.brand, + url: `${site}/`, + description: `${business.person.name} fotograferar ${genreList()}.`, + email: business.email, + telephone: business.phone, + image: imageUrl, + address, + areaServed: business.serviceArea, + priceRange: business.priceRange, + founder: { '@id': ids.person }, + employee: { '@id': ids.person }, + sameAs: [business.social.instagram.url, business.social.facebook, business.social.flickr, business.social.lightroom], + potentialAction: { + '@type': 'ReserveAction', + name: 'Skicka en bokningsförfrågan', + target: business.portal.bookingUrl, + }, + hasOfferCatalog: { + '@type': 'OfferCatalog', + name: 'Fotografering', + itemListElement: business.genres.map((g) => ({ + '@type': 'Offer', + priceCurrency: business.priceFromSek != null ? 'SEK' : null, + price: business.priceFromSek, + itemOffered: { + '@type': 'Service', + name: g.name, + description: g.description, + provider: { '@id': ids.business }, + }, + })), + }, + }, +]; + +const webpage: Record = { + '@type': faq?.length ? [pageType, 'FAQPage'] : pageType, + '@id': ids.webpage, + url: pageUrl, + name: title, + description, + inLanguage: 'sv-SE', + isPartOf: { '@id': ids.website }, + about: { '@id': ids.business }, + primaryImageOfPage: imageUrl ? { '@type': 'ImageObject', url: imageUrl } : null, + breadcrumb: breadcrumb ? { '@id': ids.breadcrumb } : null, + mainEntity: faq?.length + ? faq.map((item) => ({ + '@type': 'Question', + name: item.question, + acceptedAnswer: { '@type': 'Answer', text: item.answer }, + })) + : null, +}; +graph.push(webpage); + +if (breadcrumb) { + graph.push({ + '@type': 'BreadcrumbList', + '@id': ids.breadcrumb, + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Hem', item: `${site}/` }, + { '@type': 'ListItem', position: 2, name: breadcrumb, item: pageUrl }, + ], + }); +} + +const json = JSON.stringify(clean({ '@context': 'https://schema.org', '@graph': graph })).replace(/ diff --git a/src/components/ThreeDScroll.astro b/src/components/ThreeDScroll.astro index e5a00c2..20c3f50 100644 --- a/src/components/ThreeDScroll.astro +++ b/src/components/ThreeDScroll.astro @@ -1,13 +1,22 @@ --- // src/components/ThreeDScroll.astro +// Bilderna ligger i src/assets så att Astro skalar om dem (AVIF/WebP/JPG, srcset) +// och rensar bort EXIF (GPS, serienummer m.m.). Originalen publiceras inte. +import { Picture } from 'astro:assets'; +import img1 from '../assets/images/3d-scroll/img1.jpg'; +import img2 from '../assets/images/3d-scroll/img2.jpg'; +import img3 from '../assets/images/3d-scroll/img3.jpg'; +import img4 from '../assets/images/3d-scroll/img4.jpg'; +import img5 from '../assets/images/3d-scroll/img5.jpg'; const images = [ - { src: "/3d-scroll/img1.jpg", alt: "Architecture Dark" }, - { src: "/3d-scroll/img2.jpg", alt: "Shadows Light" }, - { src: "/3d-scroll/img3.jpg", alt: "Portrait High End" }, - { src: "/3d-scroll/img4.jpg", alt: "Minimalist Landscape" }, - { src: "/3d-scroll/img5.jpg", alt: "Foggy Forest" }, + { src: img1, alt: 'Närbild av en gammal läderinbunden bok med slitna pappersark i varmt motljus' }, + { src: img2, alt: 'Veteranbil med tända strålkastare på en stadsgata i kvällsljus' }, + { src: img3, alt: 'Överblommade tistlar bland höga grässtrån i dämpade bruna toner' }, + { src: img4, alt: 'Järnvägsspår som slingrar sig mellan träd i motljus, i sepiaton' }, + { src: img5, alt: 'Svartvit bild av en man som bär ett litet barn bland såpbubblor i motljus' }, ]; +const last = images.length - 1; ---
@@ -20,9 +29,20 @@ const images = [ class="gallery-item absolute transform-style-3d will-change-transform flex items-center justify-center" data-index={index} > - {img.alt}
diff --git a/src/data/business.ts b/src/data/business.ts new file mode 100644 index 0000000..05be817 --- /dev/null +++ b/src/data/business.ts @@ -0,0 +1,183 @@ +// src/data/business.ts +// ALLA fakta om Images by Olofsson på ett ställe. Sidor, JSON-LD, llms.txt och +// sitemap läser härifrån – skriv aldrig in fakta direkt i en sida. +// +// Regel: det vi inte vet är `null`. Allt som läser filen hoppar över null-värden +// (ingen "TODO", inga påhittade siffror syns på sajten). När Linus lämnar ett +// värde: fyll i det här, så dyker det upp på /tjanster, i FAQ och i JSON-LD. + +export type Genre = { + /** Kort id, används som ankare på /tjanster (#portratt osv.) */ + id: string; + name: string; + description: string; +}; + +export type Business = { + brand: string; + url: string; + person: { + name: string; + jobTitle: string; + }; + email: string; + /** Telefonnummer i format +46... – okänt */ + phone: string | null; + country: { code: string; name: string }; + /** Ort Linus utgår från (hans publika Flickr- och LinkedIn-profil, 2026-10-27) */ + city: string | null; + /** Län/region (LinkedIn, 2026-10-27) */ + region: string | null; + /** Område han tar uppdrag i (fritext, t.ex. "Stockholm med omnejd") – okänt */ + serviceArea: string | null; + /** Lägsta pris i SEK för en fotografering – okänt */ + priceFromSek: number | null; + /** schema.org priceRange, t.ex. "$$" eller "1500–5000 kr" – okänt */ + priceRange: string | null; + /** Paket och vad som ingår – okänt */ + packages: { name: string; includes: string[]; priceSek: number | null }[] | null; + /** Typisk leveranstid, fritext (t.ex. "inom två veckor") – okänt */ + deliveryTime: string | null; + /** Antal år som fotograf – okänt */ + yearsOfExperience: number | null; + /** Kundomdömen – inga kända */ + testimonials: { name: string; text: string }[] | null; + camera: string; + lenses: string[]; + editing: string; + /** Linus egen formulering: "featured by Adobe three times". Inga datum/detaljer. Jag-form för sidtext. */ + recognition: string; + /** Samma fakta i tredje person (llms.txt, JSON-LD, meta descriptions). */ + recognitionThirdPerson: string; + portal: { + url: string; + bookingUrl: string; + }; + social: { + instagram: { handle: string; url: string }; + facebook: string; + flickr: string; + lightroom: string; + linkedin: string; + github: string; + }; + genres: Genre[]; + bookingSteps: { title: string; text: string }[]; +}; + +export const business: Business = { + brand: 'Images by Olofsson', + url: 'https://imagesbyolofsson.se', + person: { + name: 'Linus Olofsson', + jobTitle: 'Fotograf', + }, + email: 'Admin@imagesbyolofsson.se', + phone: null, + country: { code: 'SE', name: 'Sverige' }, + city: 'Örebro', + region: 'Örebro län', + serviceArea: null, + priceFromSek: null, + priceRange: null, + packages: null, + deliveryTime: null, + yearsOfExperience: null, + testimonials: null, + camera: 'Sony A7R V', + lenses: ['Sony FE 70-200mm f/4 G OSS', 'Sony FE 20mm f/1.8 G'], + editing: 'Adobe Lightroom Classic', + recognition: 'Mina bilder har lyfts fram av Adobe tre gånger.', + recognitionThirdPerson: 'Hans bilder har lyfts fram av Adobe tre gånger.', + portal: { + url: 'https://customer.imagesbyolofsson.se', + bookingUrl: 'https://customer.imagesbyolofsson.se/boka', + }, + social: { + instagram: { handle: '@imagesbyolofsson', url: 'https://www.instagram.com/imagesbyolofsson/' }, + facebook: 'https://www.facebook.com/imagesbyolofsson', + flickr: 'https://www.flickr.com/photos/imagesbyolofsson/', + lightroom: 'https://lightroom.adobe.com/u/linusolofsson1', + linkedin: 'https://www.linkedin.com/in/linus-olofsson-/', + github: 'https://github.com/LinusOlofsson-maker', + }, + // Bara genrer som redan finns i sajtens texter och gamla sidnamn + // (/my-people, /animales-and-nature, /celebrations-with-family, /northen-lights). + genres: [ + { + id: 'portratt', + name: 'Porträtt och människor', + description: 'Porträtt och ögonblick med människor – uttryck som berättar något utan ord.', + }, + { + id: 'natur', + name: 'Natur och djur', + description: 'Skog, landskap och djur – naturens stillhet och de små detaljerna i den.', + }, + { + id: 'stad', + name: 'Stad och arkitektur', + description: 'Gator, byggnader, strukturer och kontraster – stadens energi och dess tomma stunder.', + }, + { + id: 'makro', + name: 'Makro och detaljer', + description: 'Mönster, texturer och små detaljer som de flesta går förbi.', + }, + { + id: 'norrsken', + name: 'Norrsken', + description: 'Norrskenets ljus och färger över himlen.', + }, + { + id: 'fest', + name: 'Fester och familjehögtider', + description: 'Bilder från fester och högtider tillsammans med familj och vänner.', + }, + ], + bookingSteps: [ + { + title: 'Skicka en förfrågan', + text: 'Berätta vad du vill ha fotograferat, när och var – via kundportalen.', + }, + { + title: 'Få en offert', + text: 'Jag återkommer med ett förslag och en offert för just ditt uppdrag.', + }, + { + title: 'Bokning', + text: 'När du har godkänt offerten bokar vi in fotograferingen.', + }, + { + title: 'Leverans i ett privat galleri', + text: 'Du får bilderna i ett eget, privat galleri i kundportalen.', + }, + { + title: 'Originalfilerna efter betalning', + text: 'När betalningen är klar kan du ladda ner originalfilerna i full upplösning.', + }, + ], +}; + +/** Genrer som läsbar lista: "a, b, c samt d". */ +export function genreList(lowercase = true): string { + const names = business.genres.map((g) => (lowercase ? g.name.toLowerCase() : g.name)); + if (names.length <= 1) return names.join(''); + // "samt" före sista ledet, eftersom genrenamnen själva innehåller "och". + return `${names.slice(0, -1).join(', ')} samt ${names[names.length - 1]}`; +} + +/** "Sverige", eller "Ort, Sverige" när orten är känd. */ +export function basedIn(): string { + return [business.city, business.country.name].filter(Boolean).join(', '); +} + +/** Formaterat pris ("1 500 kr") eller null. */ +export function formatSek(value: number | null): string | null { + if (value == null) return null; + return `${value.toLocaleString('sv-SE')} kr`; +} + +export function absoluteUrl(path: string): string { + return new URL(path, business.url).toString(); +} diff --git a/src/data/faq.ts b/src/data/faq.ts new file mode 100644 index 0000000..fe6f9eb --- /dev/null +++ b/src/data/faq.ts @@ -0,0 +1,63 @@ +// src/data/faq.ts +// Vanliga frågor för /tjanster. Samma text används på sidan och i FAQPage-JSON-LD. +// Svaren byggs ENBART av fakta i business.ts. Okända fakta (null) ger ett +// "kontakta mig"-svar i stället för en gissning. +import { business, basedIn, formatSek, genreList } from './business'; + +export type FaqItem = { question: string; answer: string }; + +function areaAnswer(): string { + if (business.serviceArea) { + return `Jag utgår från ${basedIn()} och tar uppdrag i ${business.serviceArea}. Hör av dig om du är osäker på om jag kan komma till dig.`; + } + return `Jag är baserad i ${basedIn()}. Kontakta mig så stämmer vi av om jag kan fotografera där du befinner dig.`; +} + +function priceAnswer(): string { + const from = formatSek(business.priceFromSek); + if (from) { + return `En fotografering kostar från ${from}. Det slutliga priset beror på uppdraget och står i offerten du får efter din förfrågan.`; + } + return 'Priset beror på uppdraget. Kontakta mig eller skicka en förfrågan i kundportalen, så får du en offert för just det du vill ha fotograferat.'; +} + +function deliveryAnswer(): string { + const base = 'Du får bilderna i ett privat galleri i kundportalen. När betalningen är klar kan du ladda ner originalfilerna i full upplösning.'; + if (business.deliveryTime) return `${base} Normal leveranstid är ${business.deliveryTime}.`; + return base; +} + +export const faq: FaqItem[] = [ + { + question: 'Vad fotograferar du?', + answer: `Jag fotograferar ${genreList()}.`, + }, + { + question: 'Var finns du och var kan du fotografera?', + answer: areaAnswer(), + }, + { + question: 'Vad kostar en fotografering?', + answer: priceAnswer(), + }, + { + question: 'Hur bokar jag en fotografering?', + answer: `Skicka en förfrågan i kundportalen (${business.portal.bookingUrl}). Jag återkommer med en offert, och när du har godkänt den bokar vi in fotograferingen.`, + }, + { + question: 'Hur får jag mina bilder?', + answer: deliveryAnswer(), + }, + { + question: 'Vilken utrustning använder du?', + answer: `Jag fotograferar med en ${business.camera} och redigerar i ${business.editing}.`, + }, + { + question: 'Har dina bilder uppmärksammats?', + answer: business.recognition, + }, + { + question: 'Hur kontaktar jag dig?', + answer: `Mejla ${business.email} eller skicka ett meddelande på Instagram (${business.social.instagram.handle}).`, + }, +]; diff --git a/src/data/portfolio-meta.ts b/src/data/portfolio-meta.ts new file mode 100644 index 0000000..e9faa8a --- /dev/null +++ b/src/data/portfolio-meta.ts @@ -0,0 +1,616 @@ +// src/data/portfolio-meta.ts +// DATAKROK för riktiga alt-texter (och ev. titel/kategori) per portfoliobild. +// Ifylld 2026-10-27: alt-texter skrivna med visionsmodellen gemma-4-31b-it-fp8 (Cleura) +// på nedskalade kopior, granskade manuellt mot bilderna. Saknas en post använder sidan +// en neutral svensk reservtext byggd på datum, aldrig kameramodell eller "undefined". +// +// Nyckel = bildens filnamn utan ändelse, exakt som i src/assets/images/portfolio/, +// t.ex. "immich-02533d10-07ca-4a39-8b8a-6648fe2ffcef-20260905-DSC07734" +// eller "20250510-WJ9E1439-Enhanced-NR". +// +// Exempel: +// "immich-02533d10-...-20260905-DSC07734": { +// alt: "Norrsken i grönt och lila över en mörk skogssiluett", +// title: "Norrsken i september", +// category: "Norrsken", +// }, + +export type PortfolioMeta = { + /** Beskriv vad bilden visar, på svenska, ca 8–16 ord. */ + alt?: string; + /** Kort bildtitel (visas i lightboxen om den finns). */ + title?: string; + /** Helst ett av namnen i business.genres, t.ex. "Natur och djur". */ + category?: string; +}; + +export const portfolioMeta: Record = { + "20240804-20241104-IMG_3861": { + alt: "En person sedd bakifrån under lövverk mot ett dimmigt fält och en lada i skymningen", + category: "Porträtt och människor", + }, + "20240819-Screenshot_2024-08-19-12-12-33-62_92460851df6f172a4592fca41cc2d2e6": { + alt: "Närbild av ett gulsvart bi med orange pollen på benet mot en mörk bakgrund", + category: "Makro och detaljer", + }, + "20240901-20240901-_J9E8565": { + alt: "En slingrande grusväg genom en mörk skog med höga björkar i dämpat ljus", + category: "Natur och djur", + }, + "20240901-20240901-_J9E8586": { + alt: "Ett djuprött löv i centrum mot en mörk och suddig grön bakgrund i dämpat ljus", + category: "Makro och detaljer", + }, + "20240901-_J9E8446": { + alt: "Vita fröställningar i ett fält av torrt gräs med varma färger och mörk bakgrund", + category: "Natur och djur", + }, + "20240903-20240903-WJ9E8891": { + alt: "Kurviga järnvägsspår i sepiafärgat ljus omgivna av mörka träd och en byggnad i bakgrunden", + category: "Stad och arkitektur", + }, + "20240914-20240914-WJ9E0318": { + alt: "Silhuett av en person med långt hår mot en gyllene solnedgång över öppna fält", + category: "Porträtt och människor", + }, + "20240921-WJ9E1119": { + alt: "En man med rödbrunt lockigt hår och skägg i mörka kläder i en dämpad stadsmiljö", + category: "Porträtt och människor", + }, + "20241021-IMG_9545-Enhanced-NR": { + alt: "Närbild på torra bruna ekblad omgivna av gula och orangea höstfärger med mörk vinjettering", + category: "Makro och detaljer", + }, + "20241101-_MG_0023-Enhanced-NR": { + alt: "Ett brunt höstlöv med glittrande vattendroppar mot en svart bakgrund i dramatiskt ljus", + category: "Makro och detaljer", + }, + "20241111-_MG_9832": { + alt: "En mörk stadsgata med byggnader under en lila skymningshimmel och en lysande måne", + category: "Stad och arkitektur", + }, + "20241117-_J9E0597": { + alt: "En brun häst med grimma i profil mot en gyllene solnedgång och suddiga träd", + category: "Natur och djur", + }, + "20241117-_J9E0620": { + alt: "Närbild av en vit häst med brunt läderträns i ett varmt och mörkt motljus", + category: "Natur och djur", + }, + "20241117-_J9E0621": { + alt: "En vit häst med brun grimma i ett mjukt ljus vid en solnedgång", + category: "Natur och djur", + }, + "20241124-_MG_1066-Enhanced-NR": { + alt: "En närbild av vita, glittrande iskristaller mot en mörk och suddig bakgrund", + category: "Makro och detaljer", + }, + "20250217-_J9E5859-Enhanced-NR": { + alt: "Torkade fröställningar i mörkt, stämningsfullt ljus med dova bruna och lila färger", + category: "Makro och detaljer", + }, + "20250217-_J9E5864-Enhanced-NR": { + alt: "En rödbrun torkad blomställning i mjukt ljus mot en mörk suddig bakgrund", + category: "Makro och detaljer", + }, + "20250217-_J9E5897": { + alt: "Gyllene ax av torkat gräs i varmt ljus mot en mörkblå suddig bakgrund", + category: "Natur och djur", + }, + "20250315-WJ9E1852": { + alt: "Ett barn i grön vinterjacka och mörk mössa håller i något i dämpat utomhusljus", + category: "Porträtt och människor", + }, + "20250315-WJ9E1919": { + alt: "En man i mörkblå mössa och dunjacka i dämpat ljus mot en suddig bakgrund", + category: "Porträtt och människor", + }, + "20250315-WJ9E1967-Enhanced-NR": { + alt: "En kvinna i vit mössa och ett barn i grön jacka blåser såpbubblor utomhus", + category: "Porträtt och människor", + }, + "20250330-_MG_1223-Enhanced-NR": { + alt: "En person håller en svart kamera mot en suddig bakgrund av gula höstlöv", + category: "Makro och detaljer", + }, + "20250413-WJ9E0212": { + alt: "En man kör en vagn dragen av två hästar på en stadsgata i sepia", + category: "Stad och arkitektur", + }, + "20250510-WJ9E1439-Enhanced-NR": { + alt: "Ett barn i randig mössa och grön jacka håller en röd spade i en sandlåda", + category: "Porträtt och människor", + }, + "20250510-WJ9E1492-Enhanced-NR": { + alt: "En kvinna med glasögon och brun handske håller en grön mugg i dämpat ljus", + category: "Porträtt och människor", + }, + "20250510-WJ9E1546-Enhanced-NR": { + alt: "Ett barn med blont hår och blå ögon i mörkblå fleecejacka med fingret vid munnen", + category: "Porträtt och människor", + }, + "20250510-WJ9E1598-Enhanced-NR": { + alt: "Två vuxna och ett barn går hand i hand över en parkering med bilar", + category: "Porträtt och människor", + }, + "20250510-WJ9E1673-Enhanced-NR": { + alt: "Ett barn i grön väst håller i ratten på ett rött fordon med en man bakom", + category: "Porträtt och människor", + }, + "20250510-WJ9E1806-Enhanced-NR": { + alt: "En man håller ett barn vid ett stängsel i en grön utomhusmiljö med röda byggnader", + category: "Porträtt och människor", + }, + "20250510-WJ9E1817-Enhanced-NR": { + alt: "En brun höna med röd kam i profil mot en suddig grön bakgrund", + category: "Natur och djur", + }, + "20250513-WJ9E0143-Enhanced-NR": { + alt: "En blå klassisk bil på en stadsgata i varmt kvällsljus med mörka skuggor", + category: "Stad och arkitektur", + }, + "20250513-WJ9E0211-Enhanced-NR": { + alt: "Gula och röda hus vid ett stilla vatten med speglingar i varmt kvällsljus", + category: "Stad och arkitektur", + }, + "20250726-20250726-WJ9E0854": { + alt: "En ryttare i mörk rustning på en vit häst framför en ljus byggnad och publik", + category: "Porträtt och människor", + }, + "20250726-20250726-WJ9E0979": { + alt: "En man i blå och gul rustning med ett svärd rider en brun häst utomhus", + category: "Porträtt och människor", + }, + "20250802-WJ9E1325": { + alt: "En humla och en vit fjäril på lila blommor i ett mjukt gyllene ljus", + category: "Makro och detaljer", + }, + "20250802-WJ9E1390": { + alt: "En vit fjäril med svarta fläckar på vingarna vilar på en lila blomstängel", + category: "Natur och djur", + }, + "20250802-WJ9E1396": { + alt: "En vit fjäril med svarta prickar vilar på en lila blomma i starkt solljus", + category: "Natur och djur", + }, + "20250806-IMG_5565": { + alt: "En orange och svart humla på en lila klotformad blomma mot grön bakgrund", + category: "Makro och detaljer", + }, + "20251017-WJ9E0289": { + alt: "En tom träbänk i en höstskog omgiven av gula löv och varmt ljus", + category: "Natur och djur", + }, + "20251227-WJ9E0356": { + alt: "Frostiga växter och en tunn gren i varmt motljus från en sol i skogen", + category: "Natur och djur", + }, + "20251227-WJ9E0361": { + alt: "Frostig växt i motljus med en gyllene sol mot en mörk och suddig skog", + category: "Natur och djur", + }, + "20251227-WJ9E0362": { + alt: "Varmt solljus strömmar genom träden bakom ett trästaket i en mörk skog", + category: "Natur och djur", + }, + "20251227-WJ9E0388": { + alt: "Gyllene solljus som lyser upp frostiga torra löv och grässtrån på en mark", + category: "Makro och detaljer", + }, + "20251227-WJ9E0434": { + alt: "En stadsgata i varmt gyllene ljus med suddiga byggnader och träd vid solnedgången", + category: "Stad och arkitektur", + }, + "20251227-WJ9E0450": { + alt: "Gyllene solljus över en tom stadsgata med silhuetter av träd och en husfasad", + category: "Stad och arkitektur", + }, + "20251230-IMG20251230141046": { + alt: "Snöig gränd med stenhus prydd med stora röda julkulor och små gröna granar i krukor", + category: "Stad och arkitektur", + }, + "20251231-IMG20251231113121": { + alt: "Gyllene solljus över en snöig kullerstensgata omgiven av färgglada hus i en stad", + category: "Stad och arkitektur", + }, + "20251231-IMG20251231113417": { + alt: "En smal kullerstensgata mellan en vit husfasad och en hög stenmur under en molnig himmel", + category: "Stad och arkitektur", + }, + "20251231-WJ9E0465": { + alt: "En mörk lykta i förgrunden med en suddig ljus byggnad och spiror i bakgrunden", + category: "Stad och arkitektur", + }, + "20251231-WJ9E0490": { + alt: "Snöklädda granris med små varma och kalla ljuspunkter i en mörk vintermiljö", + category: "Makro och detaljer", + }, + "20251231-WJ9E0495": { + alt: "Snötäckta barr med små varma lampor mot en mörk bakgrund med suddiga orangea ljus", + category: "Natur och djur", + }, + "20251231-WJ9E0512": { + alt: "En smal kullerstensgata i en stad med gamla hus, snöfläckar och svala, mörka färger", + category: "Stad och arkitektur", + }, + "20251231-WJ9E0552": { + alt: "Smal stadsgata med ett högt torn, en julgran och silhuetter av människor i varmt ljus", + category: "Stad och arkitektur", + }, + "20260102-IMG20260102132702": { + alt: "Vy genom en mörk stenvägg mot ett snöigt landskap och en gyllene himmel", + category: "Stad och arkitektur", + }, + "20260102-IMG20260102141620": { + alt: "Grå stenmur med trägallerier och ett torn med rött tak under ett snöfall", + category: "Stad och arkitektur", + }, + "20260102-IMG20260102142048": { + alt: "En trägång längs en grå stenmur under ett snöfall med kala grenar i förgrunden", + category: "Stad och arkitektur", + }, + "immich-02533d10-07ca-4a39-8b8a-6648fe2ffcef-20260905-DSC07734": { + alt: "Två barn springer och skrattar framför ett hus med rött plåttak och högt gräs", + category: "Porträtt och människor", + }, + "immich-04342446-23b4-4600-a93e-352d32b6eac1-20260905-DSC07933": { + alt: "Två barn leker med en grön vattenpump och en hink på en solig gräsmatta", + category: "Porträtt och människor", + }, + "immich-0617ce88-3f9c-4b5b-bec4-1a30cfd65602-20260905-DSC07895": { + alt: "Ett blont barn i grön tröja vid en grön vattenpump i varmt gyllene ljus", + category: "Porträtt och människor", + }, + "immich-07328282-e321-4216-8710-ee87dc65496e-20260905-DSC07730": { + alt: "Ett leende barn i blå t-shirt står på en grön gräsmatta omgiven av gröna träd", + category: "Porträtt och människor", + }, + "immich-07578a7d-3d6e-4dbe-8fec-0accff45258c-20260920-DSC08719-2": { + alt: "En gulsvart humla samlar pollen från en mörkröd blomma mot en suddig grön bakgrund", + category: "Natur och djur", + }, + "immich-0be0c122-4e81-4e92-b229-4736e66b9c70-20260920-DSC08717-2": { + alt: "En svartgul humla på en djupröd blomma omgiven av gröna stjälkar i varmt solljus", + category: "Makro och detaljer", + }, + "immich-0d7ccea4-22c2-4208-b487-ec09eb88158c-20260704-DSC03171": { + alt: "En man och en kvinna med solglasögon poserar bredvid en vit limousine i en stad", + category: "Porträtt och människor", + }, + "immich-0f559874-9212-45e1-8860-a9ebfe6d95c0-20260704-DSC03081": { + alt: "En man i vit skjorta och mörka byxor vid en vit limousine under gröna träd", + category: "Porträtt och människor", + }, + "immich-10a42a42-e7af-4f02-a0ca-d07a960bd094-20260605-_J9E5983": { + alt: "Närbild på en svartvit hund med bruna ögon bland suddiga rosa blommor och grönska", + category: "Natur och djur", + }, + "immich-146b119f-7a18-47ca-b255-38242adc290e-20260621-Kopia_av_IMG_7877": { + alt: "En röd ekorre på en grov trädgren med mjukt ljus och suddig bakgrund", + category: "Natur och djur", + }, + "immich-17c30f0a-3dea-425d-9fbe-336cc1ad88e2-20260621-Kopia_av_IMG_8045": { + alt: "En rödbrun ekorre på en trädgren omgiven av ljusgröna blad i ett ljust skogsparti", + category: "Natur och djur", + }, + "immich-18db801d-5db9-49d0-83c0-cda75832be93-20260920-DSC08726": { + alt: "En gulsvart humla i flykt mot en mörkröd blomma med orange centrum i grönska", + category: "Natur och djur", + }, + "immich-1ab2b2fe-dea6-4aa4-81c9-73fb8d4918c3-20240816-WJ9E37214520240816": { + alt: "En man och ett barn i solljus med suddiga gröna träd och färgglada ljusreflexer", + category: "Porträtt och människor", + }, + "immich-1ccacf42-3f11-46c6-a6ac-4e38c6ff96b5-20250413-WJ9E0212": { + alt: "Två hästar drar en vit vagn med människor längs en solig gata i en stad", + category: "Stad och arkitektur", + }, + "immich-1dc2a516-5026-4f97-861d-3e450acb0948-20260606-_J9E6089": { + alt: "Orange och svart fjäril som vilar på vita blommor i mörklila bladverk", + category: "Natur och djur", + }, + "immich-1e4b55d1-10c0-4431-bcbf-0b6207208e1c-20260905-DSC07775": { + alt: "En man i vit jacka hjälper ett barn med blå hjälm och röd cykel utomhus", + category: "Porträtt och människor", + }, + "immich-23f04539-f8eb-47f2-8d39-3d3e548e0f9b-20260905-DSC07842": { + alt: "Ett leende barn med blont hår i en grön pikétröja utomhus i mjukt ljus", + category: "Porträtt och människor", + }, + "immich-27924d4a-0c2e-4421-b8cd-067b857549ba-20240912-20240912-WJ9E9866": { + alt: "En bäck med vitt skummande vatten, mossiga stenar och torra löv under en mörk trädstam", + category: "Natur och djur", + }, + "immich-2be918bc-db83-4024-a066-bf300e2bebc5-20240912-20240912-WJ9E9861": { + alt: "Rinnande vatten över gröna mossiga stenar omgivna av gräs och torra bruna löv", + category: "Natur och djur", + }, + "immich-2db21106-a3be-4f2b-82a7-eb69eb0038eb-20240912-20240912-WJ9E0194": { + alt: "Ett rödbrunt höstlöv som vilar på en mörk texturerad yta mot en mörkblå bakgrund", + category: "Makro och detaljer", + }, + "immich-302dfa7d-fe44-45a4-9dd7-6e2d0ceaf39d-20260905-DSC07999": { + alt: "En kvinna med guldglasögon och blont hår i en denimjacka mot en suddig bakgrund", + category: "Porträtt och människor", + }, + "immich-30952208-1c57-48b0-8917-8176ee374efe-20260905-DSC07908": { + alt: "Ett barn i grön tröja vattnar lila blommor i en trädgård bredvid en ljus bod", + category: "Porträtt och människor", + }, + "immich-30a4d915-e794-40d0-8517-73c385a5d9c9-20260621-squareblur_1781332089807_7": { + alt: "En person i mörk mantel sedd bakifrån genom lövverk mot ett grönt fält", + category: "Porträtt och människor", + }, + "immich-3539cda6-7495-4a51-b2d5-61d528fece0f-20260621-Kopia_av_IMG_7804": { + alt: "En brun ekorre med fluffig svans och vitt bröst mot en ljus bakgrund", + category: "Natur och djur", + }, + "immich-38146653-9b12-4333-a4f7-b72b86c58f87-20250416-WJ9E0243": { + alt: "En beige skoter parkerad på en kullerstensgata i en stad i varmt, gyllene ljus", + category: "Stad och arkitektur", + }, + "immich-39b95c2c-eeb7-4334-8771-47e726ca00a3-20240816-WJ9E37204620240816": { + alt: "En man och ett barn i naturligt ljus med en suddig grön bakgrund av träd", + category: "Porträtt och människor", + }, + "immich-3b58d401-1b27-4c89-8094-c5025e24b160-20260905-DSC07838": { + alt: "En man i ljus skjorta kramar ett barn med blå hjälm bland färgglada blommor", + category: "Porträtt och människor", + }, + "immich-3d77f859-13be-462b-92be-767aa1f74ee6-20260920-DSC08731": { + alt: "Ett svartgult bi på en mörkröd blomma med suddig bakgrund i varma färger", + category: "Natur och djur", + }, + "immich-3eccb3dc-707f-4f8d-837b-d0db61358e31-20260905-DSC07598": { + alt: "En man med solglasögon och ljus skjorta står bredvid en röd bil mot grön bakgrund", + category: "Porträtt och människor", + }, + "immich-415532f5-deec-43d5-8cb5-a47f4f0abfbc-20260606-_J9E6061": { + alt: "En gulsvart humla i flykt mot rosa blomklasar mot en ljusblå himmel", + category: "Natur och djur", + }, + "immich-46430956-07b8-4306-86a2-daf98daab847-20241018-20241018-IMG_8965": { + alt: "Gammal stenbyggnad med runt torn och tända lyktor reflekteras i mörkt vatten under grå himmel", + category: "Stad och arkitektur", + }, + "immich-490a149a-df25-4b81-96fd-3dc2fee7bc24-20241018-20241018-IMG_8926": { + alt: "En mörk stenbro med valv över vatten med blå toner och orangea reflektioner", + category: "Stad och arkitektur", + }, + "immich-4abbfa4b-6eba-466d-88f4-57c2e867df82-20260920-DSC08912-2": { + alt: "En röd blomma i fokus mot en mörk bakgrund med varma gyllene ljuscirklar", + category: "Natur och djur", + }, + "immich-4bcab51e-f51f-460d-8df7-7096e70a76bb-20260905-DSC07811": { + alt: "Ett leende barn med blå hjälm på en röd cykel i en suddig utomhusmiljö", + category: "Porträtt och människor", + }, + "immich-4f60de4c-4e1f-445c-afd9-f911e1acf995-20260605-_J9E5968": { + alt: "En svartvit prickig hund i profil omgiven av suddigt grönt gräs i naturligt ljus", + category: "Natur och djur", + }, + "immich-4f8a9f96-16bb-4275-887b-7958a34addab-20250413-WJ9E0196": { + alt: "En gammal astronomisk klocka med gyllene detaljer i blått och rött mot mörk bakgrund", + category: "Stad och arkitektur", + }, + "immich-50bce1ec-f162-4fa8-957c-4819689f70c4-20260905-DSC07617": { + alt: "Ett barn med blont hår håller en vuxens hand utomhus bredvid en grön gräsmatta", + category: "Porträtt och människor", + }, + "immich-50e9e443-c475-4da0-b438-1a6b3f740a8c-20250414-WJ9E0143-Enhanced-NR": { + alt: "Människor med röda paraplyer i en solig stad med blå och beige arkitektur", + category: "Stad och arkitektur", + }, + "immich-54d22882-3bdc-4bd5-b4d5-7dd2e5d71656-20260905-DSC07825": { + alt: "En man i ljus skjorta håller ett barn i gröna kläder på en gräsmatta vid klippor", + category: "Porträtt och människor", + }, + "immich-590158f5-f459-4c16-b6dd-b4354c0ab512-20260824-DSC07025__2_": { + alt: "Närbild av ett hjortdjur med lurviga, basttäckta horn och mörk päls mot svart bakgrund", + category: "Natur och djur", + }, + "immich-5a60699d-bf05-4c18-bb43-5969af8f0106-20260905-DSC07930": { + alt: "Ett barn i grön tröja leker med en grön vattenpump på en solig gräsmatta", + category: "Porträtt och människor", + }, + "immich-5f79107b-ffbe-4e32-8b9d-b37689b3352f-20260905-DSC07639": { + alt: "En man, en kvinna och två barn vid en träbyggnad med gula blommor utomhus", + category: "Porträtt och människor", + }, + "immich-61040612-f637-4bdb-893c-901171857f83-20260621-squareblur_1781332134108_13": { + alt: "Röd boj vid en vit träbom i stilla vatten med näckrosblad och vass i solnedgången", + category: "Natur och djur", + }, + "immich-638a305e-2d06-4d4e-9166-5f3a80c906a2-20260905-DSC07710": { + alt: "Ett barn i blå tröja med brandbilar står i en grönskande miljö framför en man", + category: "Porträtt och människor", + }, + "immich-69aa5b22-f80d-4cb5-b022-e77132560985-20260606-_J9E6105": { + alt: "En orange och svart fjäril på vita blommor bland mörklila blad i starkt solljus", + category: "Natur och djur", + }, + "immich-6ee56b75-ff51-4817-98c8-0fb3c4485931-20250415-WJ9E1163-Enhanced-NR": { + alt: "En stenbro full av människor över en flod mellan röda tak och torn", + category: "Stad och arkitektur", + }, + "immich-6f8ae1a4-fc2d-478d-ba2e-8452db0a08f0-20240912-20240912-WJ9E9894": { + alt: "En liten bäck med mjukt vatten som flyter över mörka stenar och grönska", + category: "Natur och djur", + }, + "immich-71cea45b-73c3-4e9a-9810-c0e070b4f5d0-20240914-20240914-WJ9E0284": { + alt: "En person i jeansjacka sedd bakifrån i ett solrosfält i solnedgångens varma ljus", + category: "Porträtt och människor", + }, + "immich-73a61049-65e8-47ed-9904-220bf316e351-20260605-_J9E5985": { + alt: "Närbild av en svartvit hund i en grön trädgård med suddiga rosa blommor i förgrunden", + category: "Natur och djur", + }, + "immich-7c1fc70b-c766-4c97-ba30-7fbd321ac702-20260905-DSC07953": { + alt: "Ett barn med blont hår skrattar i en blå t-shirt med brandbilar framför trävägg", + category: "Porträtt och människor", + }, + "immich-7c32da7b-9a9a-4675-a813-4fa3d026bcf5-20260905-DSC07665": { + alt: "Ett leende barn i blå t-shirt i en dörröppning med suddiga lila blommor i förgrunden", + category: "Porträtt och människor", + }, + "immich-8113f930-24bf-4920-adc6-734e397bd3e5-20260905-DSC07800": { + alt: "Ett barn med blå hjälm på en röd cykel framför ett beige hus i dagsljus", + category: "Porträtt och människor", + }, + "immich-81d60de8-8a13-43e1-b549-ced73e326368-20250415-WJ9E1324": { + alt: "Mörkt fönster med utsikt mot en molnig himmel och skuggspel på en grå vägg", + category: "Stad och arkitektur", + }, + "immich-827bde98-bbf1-4f1f-bd38-4ffffb8c6f03-20260920-DSC08725-2": { + alt: "En gulsvart humla på en mörkröd blomma med suddig grön och röd bakgrund", + category: "Makro och detaljer", + }, + "immich-87fbfcab-81f8-4ccf-a80e-e097e9a0614d-20250415-WJ9E1448": { + alt: "En kvinna med vindpustat brunt hår och mörkblå jacka mot en bakgrund av vita blommor", + category: "Porträtt och människor", + }, + "immich-9ad43795-2df0-4ca4-8a69-f8f99954b7e8-20260605-_J9E5944": { + alt: "En svartvit hund med lockig päls sitter på grönt gräs bredvid en vit fotboll", + category: "Natur och djur", + }, + "immich-a5086bb9-3868-442c-9715-f3d626ec0123-20260605-_J9E6018": { + alt: "Klara vattendroppar på ett mörkgrönt blad med tydliga ådror mot en svart bakgrund", + category: "Makro och detaljer", + }, + "immich-a5129e40-445b-4fcf-a5c5-3a5202590f27-20260920-DSC08720-2": { + alt: "En gulsvart humla på en mörkröd blomma omgiven av gröna blad i dagsljus", + category: "Natur och djur", + }, + "immich-a6fa98f0-ba73-42c2-91a6-07709e195015-20260905-DSC07869": { + alt: "Två barn sedda bakifrån bredvid en vuxen och en vit flätad korg i varmt ljus", + category: "Porträtt och människor", + }, + "immich-a71dc5cd-ede5-4ae2-94cb-07f06d074846-20260606-_J9E6073": { + alt: "Orange och svart fjäril på en ljusgrå träbit med blå färgfläckar mot suddig grön bakgrund", + category: "Natur och djur", + }, + "immich-a8bd05fb-864a-4c87-8a6c-7cbfcc047d3d-20250415-WJ9E1249-Enhanced-NR": { + alt: "En person med långt brunt hår som blåser i vinden, sedd bakifrån i dämpat ljus", + category: "Porträtt och människor", + }, + "immich-ac8360da-9fbd-45e0-b740-d091356037fe-20241018-20241018-IMG_8978": { + alt: "Ett runt stentorn och en gul byggnad vid ett vatten med varma kvällsljus", + category: "Stad och arkitektur", + }, + "immich-b241db80-28b8-4430-b08b-c3ce589a85a3-20260621-Kopia_av_IMG_8714": { + alt: "En rödbrun ekorre på en grov gren med suddiga gröna barr i bakgrunden", + category: "Natur och djur", + }, + "immich-b2780899-4269-44be-8dd3-fed23d3e89d3-20260621-squareblur_1781332107423_9": { + alt: "En blå vintagebil på en mörk stadsgata med varma gula ljusreflektioner i asfalten", + category: "Stad och arkitektur", + }, + "immich-b2f52935-d52a-46f1-9262-78a7b1e19a90-20260905-DSC07604": { + alt: "Ett barn i grön tröja och ljus keps framför en suddig röd bil", + category: "Porträtt och människor", + }, + "immich-b3c19299-0e5d-4261-a98a-1ed08f01a0a3-20260905-DSC07702": { + alt: "Ett barn i grön tröja sparkar på en fotboll på en gräsmatta framför ett staket", + category: "Porträtt och människor", + }, + "immich-b7ddab61-cbfc-4c5f-9354-b56bab0b38a1-20241018-20241018-IMG_8925-2": { + alt: "Orangea höstlöv hänger över mörkt vatten med tydliga speglingar och en stenvägg i bakgrunden", + category: "Natur och djur", + }, + "immich-b99369cb-622e-4f31-9a9f-a33bc4e0030a-20260605-_J9E5916": { + alt: "En mörk hund med lockiga öron tittar uppåt mot en mörkgrön suddig bakgrund", + category: "Natur och djur", + }, + "immich-bbf62d80-754f-4243-a71b-a5abe1e0340c-20260905-DSC07890": { + alt: "Ett barn i grön pikétröja pumpar vatten ur en gammal grön handpump i ett varmt motljus", + category: "Porträtt och människor", + }, + "immich-c3ab7ee4-8a84-42a4-a3e9-662469569b51-20260605-_J9E5888": { + alt: "En person håller om en svart och vit hund med lockig päls i dämpat inomhusljus", + category: "Natur och djur", + }, + "immich-c6489154-53d1-47e2-8572-521dd851137a-20260905-DSC07823": { + alt: "En man i ljus skjorta lyfter ett barn på en gräsmatta bredvid en stor sten", + category: "Porträtt och människor", + }, + "immich-cbbd628e-bf5a-4a9e-85e0-d5229680f492-20260905-DSC07657": { + alt: "Ett barn i blå t-shirt står i dörröppningen till en vit träbyggnad med mörkt inre", + category: "Porträtt och människor", + }, + "immich-d058e797-861a-4cd3-86a1-63c616bf248f-20260905-DSC07711": { + alt: "Ett barn i blå tröja går mot en man i vit jacka i en grön trädgård", + category: "Porträtt och människor", + }, + "immich-d2d2eb01-597b-47ed-8055-f193a629c472-20260605-_J9E5860": { + alt: "En springande svartvit hund med fladdrande öron på en grön gräsmatta mot mörk bakgrund", + category: "Natur och djur", + }, + "immich-d34b0499-d2c0-40fd-848a-8e06329b7bf5-20260905-DSC07607": { + alt: "En kvinna och ett barn går hand i hand framför ett hus i varmt ljus", + category: "Porträtt och människor", + }, + "immich-d8cb2d51-2ead-4cf0-9215-a59b07f26f6d-20260905-DSC07704": { + alt: "Ett barn i grön tröja och blå jeans sparkar en fotboll på gräs framför en stenvägg", + category: "Porträtt och människor", + }, + "immich-db3ffd50-7e6a-4319-8815-b388e52e9aa9-20260905-DSC07896": { + alt: "Ett barn i grön tröja pumpar vatten ur en mörk handpump på en grön gräsmatta", + category: "Porträtt och människor", + }, + "immich-ddb13056-990c-48dc-ab92-e03baadb7a44-20260905-DSC07647": { + alt: "En kvinna i blå kläder håller i händerna på ett barn utomhus på grön gräsmatta", + category: "Porträtt och människor", + }, + "immich-debf1a27-f337-4fc8-a556-c8b08ad98404-20260621-squareblur_1781332127640_12": { + alt: "En tom träbänk i en höstskog omgiven av gula och orangea löv", + category: "Natur och djur", + }, + "immich-e20af65c-c05b-4585-81b4-dcad6b52e450-20240914-20240914-WJ9E0319": { + alt: "En person i motljus framför ett öppet landskap under en gyllene solnedgång", + category: "Porträtt och människor", + }, + "immich-e25d4130-b3d3-4c63-8c82-e0750efa41b8-20260621-squareblur_1781332028758_1": { + alt: "Mörka stentrappor med orangea löv som leder upp från en spegelblank vattenyta", + category: "Stad och arkitektur", + }, + "immich-e3a7d5bc-9ee1-4d29-8673-65922736295f-20260920-DSC08813": { + alt: "En svartgul humla i flykt mot en lila blomma omgiven av mörkgröna blad", + category: "Makro och detaljer", + }, + "immich-e7048f3d-da0d-432e-b833-8e766df5708b-20240914-20240914-WJ9E0285": { + alt: "En person med långt hår sedd bakifrån i ett solrosfält i varmt kvällsljus", + category: "Porträtt och människor", + }, + "immich-f1f02aac-7ef7-4cc0-818a-e06e0ecdf7f7-20241018-20241018-IMG_9028": { + alt: "En stenbro med valv som reflekteras i mörkt vatten under ett varmt gyllene ljus", + category: "Stad och arkitektur", + }, + "immich-f2d03140-50d5-4e94-94e8-425fb605a110-20260905-DSC07904": { + alt: "Ett barn i grön tröja pumpar vatten ur en grön handpump på en gräsmatta", + category: "Porträtt och människor", + }, + "immich-f836d606-e331-4277-9500-b9906305544e-20260621-Kopia_av_IMG_8222": { + alt: "En rödbrun ekorre sitter på marken mot en grov grå trädstam i dagsljus", + category: "Natur och djur", + }, + "immich-f8943f55-4cba-4521-b9a5-4e735f31a336-20241018-20241018-IMG_9057": { + alt: "Stadsmiljö i skymning med upplysta hus och färgglada ljusreflektioner i ett mörkt vatten", + category: "Stad och arkitektur", + }, + "immich-f9dd008c-3445-4394-b4f4-329b0034fd2c-20241018-20241018-IMG_8996": { + alt: "En mörk stenbro över vatten med röda höstlöv och en byggnad i bakgrunden", + category: "Stad och arkitektur", + }, + "immich-faee21c0-1609-4a35-aab5-0b5d68c4cab4-20260905-DSC07618": { + alt: "Ett leende barn i blå tröja håller en vuxens hand mot en grön bakgrund", + category: "Porträtt och människor", + }, + "immich-fbb8346a-5992-4136-a099-596c06bbde30-20260905-DSC07723": { + alt: "En man i ljus jacka och två barn springer över en grön gräsmatta under träd", + category: "Porträtt och människor", + }, + "immich-fed1e776-2242-468a-9800-8a8901df2be0-20250413-WJ9E0284": { + alt: "En kvinna i blå jeansjacka med en kamera i en park med gula tulpaner", + category: "Porträtt och människor", + }, +}; diff --git a/src/layouts/MainLayout.astro b/src/layouts/MainLayout.astro index d026ac6..0132447 100644 --- a/src/layouts/MainLayout.astro +++ b/src/layouts/MainLayout.astro @@ -1,56 +1,127 @@ --- import "../styles/global.css"; -const { title } = Astro.props; +import type { ImageMetadata } from 'astro'; +import { getImage } from 'astro:assets'; +import SEO from '../components/SEO.astro'; +import SchemaOrg from '../components/SchemaOrg.astro'; +import type { FaqItem } from '../data/faq'; +import { business } from '../data/business'; +import defaultOgImage from '../assets/images/3d-scroll/img5.jpg'; + +export interface Props { + /** Unik svensk titel för sidan. */ + title: string; + /** Unik svensk meta description, ca 120–160 tecken. */ + description: string; + /** Kanonisk sökväg, t.ex. "/tjanster". Utelämnas = härleds från URL:en. */ + path?: string; + image?: ImageMetadata; + imageAlt?: string; + pageType?: 'WebPage' | 'AboutPage' | 'ContactPage' | 'CollectionPage'; + /** Namn i brödsmulan (BreadcrumbList) för undersidor. */ + breadcrumb?: string; + faq?: FaqItem[]; + noindex?: boolean; +} + +const { + title, + description, + path, + image = defaultOgImage, + imageAlt = 'Svartvit bild av en man som bär ett litet barn bland såpbubblor i motljus', + pageType, + breadcrumb, + faq, + noindex = false, +} = Astro.props; + +// og:image: ny JPG via Astros bildtjänst (EXIF/GPS rensas bort), absolut URL. +const site = Astro.site ?? new URL(business.url); +const og = await getImage({ src: image, width: 1200, format: 'jpg', quality: 82 }); +const ogImageUrl = new URL(og.src, site).toString(); +const ogWidth = Number(og.attributes.width) || undefined; +const ogHeight = Number(og.attributes.height) || undefined; + +const nav = [ + { href: '/portfolio', label: 'Portfolio' }, + { href: '/tjanster', label: 'Tjänster' }, + { href: '/about', label: 'Om mig' }, + { href: '/contact', label: 'Kontakt' }, +]; --- - + + + - + + - {title} + + + {!noindex && ( + + )} - -
- Portfolio Grid + Portfolio Grid
More
- 3D Scroll Effect + 3D Scroll Effect
At
- Hero Section + Hero Section
Imagesbyolofsson.se