Merge pull request 'SEO & AEO: everything from the PDF review (robots, sitemap, 404, meta/OG, JSON-LD, Swedish text, /tjanster, EXIF clean, alt texts, Admin@, Örebro)' (#1) from seo-aeo-2026-10-27 into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -8,15 +8,15 @@ A modern, responsive photography portfolio website built with Astro, Tailwind CS
|
||||
<table>
|
||||
<tr>
|
||||
<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>
|
||||
</td>
|
||||
<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>
|
||||
</td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# deploy/caddy-site.snippet
|
||||
# Caddy-motsvarigheten till nginx.conf för imagesbyolofsson.se (statisk dist/ från `astro build`,
|
||||
# build.format "file": /about -> about.html, 404 -> 404.html).
|
||||
#
|
||||
# Användning i Caddyfile (Caddy >= 2.7):
|
||||
#
|
||||
# import /etc/caddy/snippets/caddy-site.snippet
|
||||
#
|
||||
# www.imagesbyolofsson.se {
|
||||
# import ibo_www_redirect
|
||||
# }
|
||||
# imagesbyolofsson.se {
|
||||
# # Befintliga specialregler (t.ex. handle /download/original* -> Immich-proxyn)
|
||||
# # ska ligga FÖRE importen, så att de vinner över try_files/file_server.
|
||||
# import ibo_site /srv/imagesbyolofsson/dist
|
||||
# }
|
||||
#
|
||||
# Argumentet är sökvägen till dist-katalogen på servern (justera till den riktiga).
|
||||
|
||||
(ibo_www_redirect) {
|
||||
redir https://imagesbyolofsson.se{uri} permanent
|
||||
}
|
||||
|
||||
(ibo_site) {
|
||||
root * {args[0]}
|
||||
encode zstd gzip
|
||||
|
||||
# --- Säkerhetsheaders ---
|
||||
# CSP: tillåter Meta Pixel (laddas först efter samtycke), anonym statistik via sendBeacon
|
||||
# till customer.imagesbyolofsson.se och Cloudflares egna skript (/cdn-cgi, ev. Web Analytics).
|
||||
# 'unsafe-inline' behövs för samtyckesskriptet och Astros inlinade skript/stilar.
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
|
||||
-Server
|
||||
}
|
||||
|
||||
# --- Cache-Control (ömsesidigt uteslutande matchers) ---
|
||||
@ibo_immutable path /_astro/*
|
||||
header @ibo_immutable Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
@ibo_media {
|
||||
path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf
|
||||
not path /_astro/*
|
||||
}
|
||||
header @ibo_media Cache-Control "public, max-age=2592000"
|
||||
|
||||
@ibo_textfiles path *.xml *.txt
|
||||
header @ibo_textfiles Cache-Control "public, max-age=3600"
|
||||
|
||||
@ibo_pages {
|
||||
not path /_astro/*
|
||||
not path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf *.xml *.txt
|
||||
}
|
||||
header @ibo_pages Cache-Control "public, max-age=0, must-revalidate"
|
||||
|
||||
# --- 301:or ---
|
||||
# Gamla indexerade adresser -> portfolion.
|
||||
@ibo_old path /my-people /my-people/ /animales-and-nature /animales-and-nature/ /celebrations-with-family /celebrations-with-family/ /northen-lights /northen-lights/
|
||||
redir @ibo_old /portfolio permanent
|
||||
|
||||
# /index.html -> /
|
||||
redir /index.html / permanent
|
||||
|
||||
# /about.html -> /about (404-sidan hanteras separat nedan)
|
||||
@ibo_htmlext {
|
||||
path_regexp ibo_htmlext ^(/.+)\.html$
|
||||
not path /404.html
|
||||
}
|
||||
redir @ibo_htmlext {re.ibo_htmlext.1}{?query} permanent
|
||||
|
||||
# Avslutande snedstreck: /about/ -> /about
|
||||
@ibo_trailing path_regexp ibo_trailing ^(/.+)/$
|
||||
redir @ibo_trailing {re.ibo_trailing.1}{?query} permanent
|
||||
|
||||
# 404-sidan ska bara synas som svar på okända adresser, inte som egen URL med 200.
|
||||
@ibo_404page path /404 /404.html
|
||||
error @ibo_404page 404
|
||||
|
||||
# --- Statiska filer ---
|
||||
try_files {path} {path}.html
|
||||
file_server
|
||||
|
||||
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
|
||||
handle_errors {
|
||||
@ibo_notfound expression {err.status_code} == 404
|
||||
handle @ibo_notfound {
|
||||
root * {args[0]}
|
||||
rewrite * /404.html
|
||||
file_server {
|
||||
status 404
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-25
@@ -1,11 +1,20 @@
|
||||
map $sent_http_content_type $cache_control_strategy {
|
||||
default "public, max-age=2592000";
|
||||
~^text/html "no-store, must-revalidate";
|
||||
~^text/css "public, max-age=31536000, immutable";
|
||||
~^application/javascript "public, max-age=31536000, immutable";
|
||||
~^application/json "no-store, must-revalidate";
|
||||
~^image/ "public, max-age=2592000";
|
||||
~^font/ "public, max-age=31536000, immutable";
|
||||
# nginx för imagesbyolofsson.se (Docker-image: dist/ i /usr/share/nginx/html).
|
||||
# Bygget använder build.format "file": /about -> about.html, 404 -> 404.html.
|
||||
# Samma regler som Caddy-varianten i deploy/caddy-site.snippet.
|
||||
|
||||
# Cache-Control per sökväg (inte per Content-Type, så att /_astro/*.jpg också blir immutable).
|
||||
map $uri $ibo_cache_control {
|
||||
default "public, max-age=0, must-revalidate";
|
||||
~^/_astro/ "public, max-age=31536000, immutable";
|
||||
~*\.(?:jpe?g|png|webp|avif|gif|svg|ico|woff2?|ttf|otf)$ "public, max-age=2592000";
|
||||
~*\.(?:xml|txt)$ "public, max-age=3600";
|
||||
}
|
||||
|
||||
# www -> apex (om www-trafik når den här containern).
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.imagesbyolofsson.se;
|
||||
return 301 https://imagesbyolofsson.se$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -14,33 +23,68 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Relativa Location-headers: bakom Cloudflare/proxy blir det aldrig http://host:80/...
|
||||
absolute_redirect off;
|
||||
server_tokens off;
|
||||
|
||||
# Säkerhetsheaders. OBS: inga add_header i location-blocken nedan, annars ärvs inte dessa.
|
||||
# CSP: tillåter Meta Pixel (connect.facebook.net/facebook.com, laddas först efter samtycke),
|
||||
# den anonyma statistiken (sendBeacon till customer.imagesbyolofsson.se) och Cloudflares
|
||||
# egna skript (/cdn-cgi, ev. Web Analytics). 'unsafe-inline' behövs för samtyckesskriptet
|
||||
# och Astros inlinade skript/stilar; byt mot hashar (Astro experimental.csp) i ett senare steg.
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
add_header Cache-Control $cache_control_strategy always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
|
||||
add_header Cache-Control $ibo_cache_control always;
|
||||
|
||||
# Gamla indexerade adresser -> närmaste nya sida.
|
||||
location ~ ^/(?:my-people|animales-and-nature|celebrations-with-family|northen-lights)/?$ {
|
||||
return 301 /portfolio;
|
||||
}
|
||||
|
||||
# /index.html och /about.html -> rena URL:er (bara externa anrop; try_files nedan
|
||||
# serverar filerna internt utan att gå hit).
|
||||
location = /index.html {
|
||||
return 301 /;
|
||||
}
|
||||
location ~ ^(?<ibo_noext>/.+)\.html$ {
|
||||
return 301 $ibo_noext$is_args$args;
|
||||
}
|
||||
|
||||
# Inga dubbletter med avslutande snedstreck: /about/ -> /about.
|
||||
location ~ ^(?<ibo_noslash>/.+)/$ {
|
||||
return 301 $ibo_noslash$is_args$args;
|
||||
}
|
||||
|
||||
# Startsidan serveras direkt (ingen intern omdirigering till /index.html).
|
||||
location = / {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
# Hashade Astro-filer: cachas ett år (immutable sätts via map ovan).
|
||||
location ^~ /_astro/ {
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Originalnedladdning (/download/original) proxas till Immich någon annanstans
|
||||
# (Cloudflare/Caddy). Finns den regeln i den här nginx: lägg den här ovanför.
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html $uri/index.html =404;
|
||||
try_files $uri $uri.html =404;
|
||||
}
|
||||
|
||||
location /_astro/ {
|
||||
access_log off;
|
||||
expires 1y;
|
||||
}
|
||||
|
||||
location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|otf|eot)$ {
|
||||
access_log off;
|
||||
expires 30d;
|
||||
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
gzip_types text/plain text/css text/xml application/xml application/json application/javascript application/xml+rss image/svg+xml;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 700 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 542 KiB |
@@ -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
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
// src/components/SEO.astro
|
||||
// <title>, 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}</title>
|
||||
<meta name="description" content={description} />
|
||||
{noindex ? <meta name="robots" content="noindex, follow" /> : <link rel="canonical" href={canonical} />}
|
||||
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:site_name" content={business.brand} />
|
||||
<meta property="og:locale" content="sv_SE" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={imageUrl} />
|
||||
{imageWidth && <meta property="og:image:width" content={String(imageWidth)} />}
|
||||
{imageHeight && <meta property="og:image:height" content={String(imageHeight)} />}
|
||||
<meta property="og:image:alt" content={imageAlt} />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={imageUrl} />
|
||||
<meta name="twitter:image:alt" content={imageAlt} />
|
||||
@@ -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<T>(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<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
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<string, unknown>[] = [
|
||||
{
|
||||
'@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<string, unknown> = {
|
||||
'@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(/</g, '\\u003c');
|
||||
---
|
||||
|
||||
<script type="application/ld+json" is:inline set:html={json} />
|
||||
@@ -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;
|
||||
---
|
||||
|
||||
<div id="scroll-container" class="relative h-[400vh] w-full">
|
||||
@@ -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
|
||||
{/* Första bilden: eager + hög prioritet. Den sista ligger överst innan man
|
||||
scrollat (alla bilder står på samma plats tills scriptet flyttar dem),
|
||||
så den laddas också direkt. Övriga laddas lat. */}
|
||||
<Picture
|
||||
src={img.src}
|
||||
alt={img.alt}
|
||||
widths={[640, 1024, 1600, 2048]}
|
||||
sizes="80vw"
|
||||
formats={['avif', 'webp']}
|
||||
fallbackFormat="jpg"
|
||||
quality={78}
|
||||
loading={index === 0 || index === last ? 'eager' : 'lazy'}
|
||||
fetchpriority={index === 0 ? 'high' : 'auto'}
|
||||
decoding="async"
|
||||
class="floating-animation max-w-[80vw] max-h-[70vh] w-auto h-auto object-contain rounded-lg shadow-[0_20px_60px_-15px_rgba(0,0,0,0.8)] border border-white/5 grayscale hover:grayscale-0 transition-all duration-700 ease-out"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}).`,
|
||||
},
|
||||
];
|
||||
@@ -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<string, PortfolioMeta> = {
|
||||
"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",
|
||||
},
|
||||
};
|
||||
@@ -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' },
|
||||
];
|
||||
---
|
||||
|
||||
<html lang="sv" class="scroll-smooth"> <head>
|
||||
<!doctype html>
|
||||
<html lang="sv" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#171717" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180.png" />
|
||||
<title>{title}</title>
|
||||
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
|
||||
<SEO
|
||||
title={title}
|
||||
description={description}
|
||||
path={path}
|
||||
imageUrl={ogImageUrl}
|
||||
imageWidth={ogWidth}
|
||||
imageHeight={ogHeight}
|
||||
imageAlt={imageAlt}
|
||||
noindex={noindex}
|
||||
/>
|
||||
{!noindex && (
|
||||
<SchemaOrg
|
||||
title={title}
|
||||
description={description}
|
||||
path={path}
|
||||
pageType={pageType}
|
||||
breadcrumb={breadcrumb}
|
||||
faq={faq}
|
||||
imageUrl={ogImageUrl}
|
||||
/>
|
||||
)}
|
||||
</head>
|
||||
<body class="bg-neutral-900 text-white font-sans antialiased min-h-screen flex flex-col">
|
||||
|
||||
<nav class="fixed top-0 w-full z-50 border-b border-white/10 py-6 bg-neutral-900/80 backdrop-blur-md transition-all duration-300">
|
||||
<div class="container mx-auto px-4 flex justify-between items-center">
|
||||
<nav class="fixed top-0 w-full z-50 border-b border-white/10 py-4 md:py-6 bg-neutral-900/80 backdrop-blur-md transition-all duration-300">
|
||||
<div class="container mx-auto px-4 flex flex-wrap gap-y-2 justify-between items-center">
|
||||
|
||||
<a href="/" class="text-2xl font-bold tracking-wider uppercase hover:text-gray-300 transition relative z-50">
|
||||
<a href="/" class="text-lg md:text-2xl font-bold tracking-wider uppercase hover:text-gray-300 transition relative z-50">
|
||||
Images by Olofsson
|
||||
</a>
|
||||
|
||||
<ul class="flex space-x-6 text-sm font-medium text-gray-400">
|
||||
<li><a href="/portfolio" class="hover:text-white transition">Portfolio</a></li>
|
||||
<li><a href="/about" class="hover:text-white transition">About</a></li>
|
||||
<li><a href="/contact" class="hover:text-white transition">Contact</a></li>
|
||||
<ul class="flex space-x-4 md:space-x-6 text-sm font-medium text-gray-300">
|
||||
{nav.map((item) => (
|
||||
<li><a href={item.href} class="hover:text-white transition">{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex-grow pt-24">
|
||||
<main class="flex-grow pt-28 md:pt-24">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-neutral-800 py-8 text-center text-gray-500 text-sm bg-neutral-900 relative z-40">
|
||||
<footer class="border-t border-neutral-800 py-8 text-center text-gray-400 text-sm bg-neutral-900 relative z-40">
|
||||
<p class="mb-2">
|
||||
Crafting digital experiences for
|
||||
<a href="https://lyxlimo.se/" target="_blank" rel="noopener noreferrer" data-glow-link="green" class="text-gray-400 hover:text-white transition-colors">lyxlimo.se</a>
|
||||
&
|
||||
<a href="https://karleksguiden.se/" target="_blank" rel="noopener noreferrer" data-glow-link="magenta" class="text-gray-400 hover:text-white transition-colors">karleksguiden.se</a>
|
||||
{business.brand} – {business.person.jobTitle.toLowerCase()} {business.person.name}, {business.country.name}
|
||||
· <a href={`mailto:${business.email}`} class="text-gray-300 hover:text-white transition-colors">{business.email}</a>
|
||||
· <a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="text-gray-300 hover:text-white transition-colors">Instagram</a>
|
||||
</p>
|
||||
<p>© {new Date().getFullYear()} Images by Olofsson. All rights reserved. · <a href="/privacy" class="text-gray-400 hover:text-white transition-colors">Privacy</a> · <button type="button" data-consent-open class="text-gray-400 hover:text-white transition-colors">Cookie settings</button></p>
|
||||
<p>© {new Date().getFullYear()} {business.brand}. Alla rättigheter förbehållna. · <a href="/privacy" class="text-gray-300 hover:text-white transition-colors">Integritetspolicy</a> · <button type="button" data-consent-open class="text-gray-300 hover:text-white transition-colors">Cookie-inställningar</button></p>
|
||||
</footer>
|
||||
|
||||
<!-- Cookie consent: Meta Pixel laddas forst efter "Accept" -->
|
||||
<!-- Cookie consent: Meta Pixel laddas först efter "Godkänn" -->
|
||||
<div data-consent-banner hidden class="fixed bottom-4 inset-x-4 md:left-auto md:right-6 md:max-w-md z-[60] bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-2xl p-5 shadow-2xl text-sm text-gray-300">
|
||||
<p class="mb-4">
|
||||
I use the Meta Pixel to see how visitors from Instagram and Facebook use this site. It sets cookies and sends data to Meta.
|
||||
<a href="/privacy" class="text-white underline hover:text-gray-300">Read more</a>
|
||||
Jag använder Meta Pixel för att se hur besökare från Instagram och Facebook använder sajten. Den sätter cookies och skickar data till Meta.
|
||||
<a href="/privacy" class="text-white underline hover:text-gray-300">Läs mer</a>
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors">Decline</button>
|
||||
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors">Accept</button>
|
||||
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors">Avböj</button>
|
||||
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors">Godkänn</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +149,7 @@ const { title } = Astro.props;
|
||||
/* End Meta Pixel Code */
|
||||
}
|
||||
|
||||
// Aterkallat samtycke: ta bort Metas forsta-parts-cookies.
|
||||
// Återkallat samtycke: ta bort Metas förstapartscookies.
|
||||
function clearMetaCookies() {
|
||||
['_fbp', '_fbc'].forEach(function (name) {
|
||||
document.cookie = name + '=; Max-Age=0; path=/';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// src/lib/pages.ts
|
||||
// Sajtens riktiga sidor, härledda från src/pages/*.astro (404 och _-filer exkluderas).
|
||||
// Nya sidor hamnar automatiskt i sitemap.
|
||||
const pageFiles = import.meta.glob('../pages/*.astro');
|
||||
|
||||
export function sitePaths(): string[] {
|
||||
return Object.keys(pageFiles)
|
||||
.map((file) => file.replace(/^\.\.\/pages\//, '').replace(/\.astro$/, ''))
|
||||
.filter((name) => name !== '404' && !name.startsWith('_') && !name.startsWith('['))
|
||||
.map((name) => (name === 'index' ? '/' : `/${name}`))
|
||||
.sort((a, b) => (a === '/' ? -1 : b === '/' ? 1 : a.localeCompare(b)));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// src/lib/url.ts
|
||||
// Gör om en byggsökväg till kanonisk form: "/", "/about", "/tjanster".
|
||||
// Klarar både build.format "file" (/about.html) och "directory" (/about/).
|
||||
export function normalizePath(p: string): string {
|
||||
let out = p.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
|
||||
if (out.length > 1) out = out.replace(/\/+$/, '');
|
||||
return out || '/';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
// src/pages/404.astro -> dist/404.html (servern svarar med status 404)
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
const links = [
|
||||
{ href: '/', label: 'Startsidan' },
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/tjanster', label: 'Tjänster och bokning' },
|
||||
{ href: '/contact', label: 'Kontakt' },
|
||||
];
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title="Sidan hittades inte | Images by Olofsson"
|
||||
description="Sidan du letade efter finns inte. Gå till startsidan, portfolion eller tjänsterna hos fotograf Linus Olofsson."
|
||||
noindex
|
||||
>
|
||||
<WaveBackground opacity="opacity-60" />
|
||||
|
||||
<div class="container mx-auto px-4 py-20 md:py-32 relative z-10 text-center">
|
||||
<p class="text-sm text-gray-400 uppercase tracking-widest mb-4">Fel 404</p>
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6">Sidan hittades inte</h1>
|
||||
<p class="text-gray-300 text-lg max-w-xl mx-auto mb-10">
|
||||
Sidan du letade efter finns inte längre, eller så har adressen ändrats. Här kan du hitta tillbaka:
|
||||
</p>
|
||||
<ul class="flex flex-wrap justify-center gap-4">
|
||||
{links.map((l) => (
|
||||
<li>
|
||||
<a href={l.href} class="inline-block px-6 py-3 rounded-full border border-white/20 text-white hover:bg-white hover:text-black transition-colors">{l.label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</MainLayout>
|
||||
+34
-24
@@ -4,9 +4,18 @@ import MainLayout from '../layouts/MainLayout.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import profileImg from '../assets/images/about/profile.jpg';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business, basedIn } from '../data/business';
|
||||
---
|
||||
|
||||
<MainLayout title="About | Images by Olofsson">
|
||||
<MainLayout
|
||||
title="Om Linus Olofsson – fotograf | Images by Olofsson"
|
||||
description={`Linus Olofsson är fotografen bakom Images by Olofsson, baserad i ${basedIn()}. ${business.recognitionThirdPerson} Läs om hans bildspråk och utrustning.`}
|
||||
path="/about"
|
||||
image={profileImg}
|
||||
imageAlt="Porträtt av fotografen Linus Olofsson"
|
||||
pageType="AboutPage"
|
||||
breadcrumb="Om mig"
|
||||
>
|
||||
|
||||
<WaveBackground opacity="opacity-80" />
|
||||
|
||||
@@ -21,7 +30,7 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
<div class="relative rounded-2xl overflow-hidden shadow-2xl border border-white/10 transform transition duration-500 group-hover:scale-[1.01]">
|
||||
<Image
|
||||
src={profileImg}
|
||||
alt="Linus Olofsson Photographer"
|
||||
alt="Porträtt av fotografen Linus Olofsson"
|
||||
width={800}
|
||||
height={1000}
|
||||
class="w-full h-auto object-cover grayscale hover:grayscale-0 transition-all duration-700 ease-in-out"
|
||||
@@ -31,25 +40,24 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
<div class="w-full lg:w-1/2 space-y-6">
|
||||
<h1 class="text-4xl md:text-5xl font-bold leading-tight">
|
||||
Dynamic moments, <br/>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500">Frozen in high resolution.</span>
|
||||
Levande ögonblick, <br/>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-400">fångade i hög upplösning.</span>
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4 text-gray-300 text-lg leading-relaxed">
|
||||
<p>
|
||||
Hi — I’m Linus Olofsson.
|
||||
Cloud specialist by day, passionate photographer by night.
|
||||
Hej, jag heter {business.person.name} och är fotografen bakom {business.brand}.
|
||||
</p>
|
||||
<p>
|
||||
Based in Sweden, my journey began with a simple curiosity for how light shapes the way we perceive the world. Over time, that curiosity evolved into an obsession with capturing the subtle details most people overlook — textures, structures, contrasts and moments that flicker by unnoticed.
|
||||
Jag är baserad i {basedIn()}. Allt började med en enkel nyfikenhet på hur ljuset formar sättet vi ser världen på. Med tiden blev nyfikenheten en besatthet av att fånga detaljerna som de flesta missar – texturer, strukturer, kontraster och ögonblick som passerar obemärkt förbi.
|
||||
</p>
|
||||
<p>
|
||||
I find beauty in the small things: a stone, a tree, an empty street, a quiet forest path, or the expression of a stranger passing by.
|
||||
Jag hittar skönheten i det lilla: en sten, ett träd, en tom gata, en stilla skogsstig eller uttrycket hos en främling som går förbi.
|
||||
</p>
|
||||
<p>
|
||||
Whether it’s the intricate patterns of a macro shot, the vast silence of nature, or the energy of a city, my goal is
|
||||
always the same — to create images that convey emotion and tell a story without words.
|
||||
Oavsett om det är de invecklade mönstren i en makrobild, naturens stora tystnad eller stadens energi är målet alltid detsamma – att skapa bilder som förmedlar känslor och berättar en historia utan ord.
|
||||
</p>
|
||||
<p>{business.recognition}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
@@ -58,10 +66,13 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
<div class="flex flex-wrap gap-4 pt-6">
|
||||
<a href="/contact" class="inline-block bg-white text-black px-8 py-3 rounded-full font-bold hover:bg-gray-200 transition-colors shadow-lg">
|
||||
Get in touch
|
||||
Kontakta mig
|
||||
</a>
|
||||
<a href="/portfolio" class="inline-block border border-white text-white px-8 py-3 rounded-full font-bold hover:bg-white/10 transition-colors">
|
||||
View the world through my eyes
|
||||
Se världen genom mina ögon
|
||||
</a>
|
||||
<a href="/tjanster" class="inline-block border border-white/40 text-white px-8 py-3 rounded-full font-bold hover:bg-white/10 transition-colors">
|
||||
Tjänster och bokning
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +80,7 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-white/10 pt-16">
|
||||
<h2 class="text-3xl font-bold mb-10 text-center text-white">What's in my bag?</h2>
|
||||
<h2 class="text-3xl font-bold mb-10 text-center text-white">Vad har jag i kameraväskan?</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
|
||||
@@ -77,20 +88,19 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
<div class="icon-box">
|
||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Camera Body</h3>
|
||||
<p class="text-gray-400">Sony A7RV</p>
|
||||
<p class="text-gray-500 text-sm mt-2">A trusty companion that has been with me on countless adventures.</p>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Kamerahus</h3>
|
||||
<p class="text-gray-300">{business.camera}</p>
|
||||
<p class="text-gray-400 text-sm mt-2">En pålitlig följeslagare som har varit med mig på otaliga äventyr.</p>
|
||||
</div>
|
||||
|
||||
<div data-trail-box class="gear-card">
|
||||
<div class="icon-box">
|
||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Lenses</h3>
|
||||
<ul class="text-gray-400 text-sm space-y-1">
|
||||
<li>Sony FE 70-200mm f/4 G OSS</li>
|
||||
<li>Sony FE 20mm f/1.8 G</li>
|
||||
<li>and a couple of extra batteries</li>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Objektiv</h3>
|
||||
<ul class="text-gray-300 text-sm space-y-1">
|
||||
{business.lenses.map((lens) => <li>{lens}</li>)}
|
||||
<li>och ett par extra batterier</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -98,9 +108,9 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
<div class="icon-box">
|
||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Editing</h3>
|
||||
<p class="text-gray-400">Adobe Lightroom Classic</p>
|
||||
<p class="text-gray-500 text-sm mt-2">Where the raw captures come to life. I focus on natural tones and contrast.</p>
|
||||
<h3 class="text-xl font-bold mb-2 text-white">Redigering</h3>
|
||||
<p class="text-gray-300">{business.editing}</p>
|
||||
<p class="text-gray-400 text-sm mt-2">Här får råfilerna liv. Jag fokuserar på naturliga toner och kontrast.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+51
-38
@@ -2,55 +2,66 @@
|
||||
// src/pages/contact.astro
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business } from '../data/business';
|
||||
---
|
||||
|
||||
<MainLayout title="Contact | Images by Olofsson">
|
||||
<MainLayout
|
||||
title="Kontakt och bokning | Images by Olofsson"
|
||||
description={`Kontakta fotograf Linus Olofsson på ${business.email} eller Instagram ${business.social.instagram.handle}, eller skicka en bokningsförfrågan via kundportalen.`}
|
||||
path="/contact"
|
||||
pageType="ContactPage"
|
||||
breadcrumb="Kontakt"
|
||||
>
|
||||
<div>
|
||||
<WaveBackground opacity="opacity-80" theme="purple" />
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10">
|
||||
<div class="container mx-auto px-4 py-12 md:py-20">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 max-w-6xl mx-auto items-start">
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-6">Get in touch</h1>
|
||||
<p class="text-gray-400 text-lg leading-relaxed">
|
||||
Do you have questions about any of my photos, collaborations, or just want to say hi?
|
||||
Connect with me on social media or send me a direct email.
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-6">Kontakta mig</h1>
|
||||
<p class="text-gray-300 text-lg leading-relaxed">
|
||||
Har du frågor om någon av mina bilder, vill du samarbeta eller bara säga hej?
|
||||
Hör av dig via sociala medier eller mejla mig direkt.
|
||||
</p>
|
||||
<p class="text-gray-300 text-lg leading-relaxed mt-4">
|
||||
Vill du boka en fotografering? Skicka en förfrågan i kundportalen så återkommer jag med en offert.
|
||||
Läs mer om hur det går till under <a href="/tjanster" class="text-white underline underline-offset-4 hover:text-gray-300">Tjänster</a>.
|
||||
</p>
|
||||
<a href={business.portal.bookingUrl} class="inline-block mt-6 px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Skicka en bokningsförfrågan</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-white font-semibold text-xl mb-4">You can also find me on...</h2>
|
||||
<h2 class="text-white font-semibold text-xl mb-4">Du hittar mig också på</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
|
||||
<a href="https://www.instagram.com/imagesbyolofsson/" target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Instagram">
|
||||
<a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Instagram">
|
||||
<svg class="w-8 h-8 group-hover:text-pink-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="https://www.flickr.com/photos/imagesbyolofsson/" target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Flickr">
|
||||
<a href={business.social.flickr} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Flickr">
|
||||
<svg class="w-8 h-8 group-hover:text-blue-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M24 4v16h-24v-16h24zm-16.146 11.908c1.652 0 2.992-1.339 2.992-2.991 0-1.652-1.34-2.991-2.992-2.991-1.651 0-2.99 1.339-2.99 2.991 0 1.652 1.339 2.991 2.99 2.991zm8.008 0c1.652 0 2.992-1.339 2.992-2.991 0-1.652-1.34-2.991-2.992-2.991-1.652 0-2.992 1.339-2.992 2.991 0 1.652 1.34 2.991 2.992 2.991z"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="https://lightroom.adobe.com/u/linusolofsson1" target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Adobe Lightroom">
|
||||
<a href={business.social.lightroom} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Adobe Lightroom">
|
||||
<svg class="w-8 h-8 group-hover:text-cyan-400 transition-colors" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 4v16h11v-4H8V4H4zm13 4c-2.21 0-4 1.79-4 4v8h4v-7c0-1.1.9-2 2-2v-3h-2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/LinusOlofsson-maker" target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="Github">
|
||||
<a href={business.social.github} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="GitHub">
|
||||
<svg class="w-8 h-8 group-hover:text-purple-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="https://www.linkedin.com/in/linus-olofsson-/" target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="LinkedIn">
|
||||
<a href={business.social.linkedin} target="_blank" rel="noopener noreferrer" class="social-card group" aria-label="LinkedIn">
|
||||
<svg class="w-8 h-8 group-hover:text-blue-400 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="mailto:Admin@imagesbyolofsson.se" class="social-card group" aria-label="Email">
|
||||
<a href={`mailto:${business.email}`} class="social-card group" aria-label="E-post">
|
||||
<svg class="w-8 h-8 group-hover:text-green-500 transition-colors" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-11.174l4.623 5.462zm5.377 2.738l-5.354-6.324-3.791 3.214 9.145 10.829 9.145-10.829-3.791-3.214-5.354 6.324zm5.377-2.738l4.623-5.462v11.174l-4.623-5.712z"/></svg>
|
||||
</a>
|
||||
|
||||
@@ -63,8 +74,8 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
<div data-trail-box class="relative bg-neutral-900 ring-1 ring-white/10 rounded-xl p-8 md:p-12 text-center space-y-10">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-white mb-2">Waiting to hear from you</h3>
|
||||
<p class="text-gray-400">Feel free to reach out for inquiries or just a chat.</p>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Jag ser fram emot att höra av dig</h2>
|
||||
<p class="text-gray-300">Hör gärna av dig med frågor, förfrågningar eller bara för att prata.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -74,18 +85,21 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 uppercase tracking-widest mb-2">Email me at</p>
|
||||
<button id="email-btn" class="text-xl md:text-3xl font-bold text-white hover:text-purple-400 transition-colors break-all" data-email="Admin@imagesbyolofsson.se">
|
||||
Admin@imagesbyolofsson.se
|
||||
</button>
|
||||
<p id="copy-feedback" class="h-4 text-xs text-green-400 mt-2 opacity-0 transition-opacity">Copied to clipboard!</p>
|
||||
<p class="text-sm text-gray-400 uppercase tracking-widest mb-2">Mejla mig på</p>
|
||||
<a href={`mailto:${business.email}`} class="text-xl md:text-3xl font-bold text-white hover:text-purple-400 transition-colors break-all">
|
||||
{business.email}
|
||||
</a>
|
||||
<div>
|
||||
<button type="button" id="email-btn" class="mt-3 text-xs uppercase tracking-widest text-gray-400 hover:text-white transition-colors" data-email={business.email}>Kopiera adressen</button>
|
||||
</div>
|
||||
<p id="copy-feedback" class="h-4 text-xs text-green-400 mt-2 opacity-0 transition-opacity" aria-live="polite">Kopierad!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<p class="text-sm text-gray-500 uppercase tracking-widest mb-4">Or DM me on Instagram</p>
|
||||
<a href="https://www.instagram.com/imagesbyolofsson/" target="_blank" class="inline-flex items-center gap-2 text-xl font-bold text-white hover:text-pink-500 transition-colors">
|
||||
@imagesbyolofsson
|
||||
<p class="text-sm text-gray-400 uppercase tracking-widest mb-4">Eller skriv till mig på Instagram</p>
|
||||
<a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 text-xl font-bold text-white hover:text-pink-500 transition-colors">
|
||||
{business.social.instagram.handle}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,21 +107,6 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</MainLayout>
|
||||
|
||||
<style>
|
||||
@reference "../styles/global.css";
|
||||
|
||||
.social-card {
|
||||
@apply flex items-center justify-center p-4 rounded-xl bg-neutral-800 border border-white/5 text-gray-400 transition-all duration-300;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.social-card:hover {
|
||||
@apply bg-neutral-700 border-white/20 transform -translate-y-1 shadow-lg shadow-white/5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const emailBtn = document.getElementById('email-btn');
|
||||
const feedback = document.getElementById('copy-feedback');
|
||||
@@ -126,3 +125,17 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</MainLayout>
|
||||
|
||||
<style>
|
||||
@reference "../styles/global.css";
|
||||
|
||||
.social-card {
|
||||
@apply flex items-center justify-center p-4 rounded-xl bg-neutral-800 border border-white/5 text-gray-400 transition-all duration-300;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.social-card:hover {
|
||||
@apply bg-neutral-700 border-white/20 transform -translate-y-1 shadow-lg shadow-white/5;
|
||||
}
|
||||
</style>
|
||||
|
||||
+135
-105
@@ -3,9 +3,14 @@
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import ThreeDScroll from '../components/ThreeDScroll.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business, basedIn, genreList } from '../data/business';
|
||||
---
|
||||
|
||||
<MainLayout title="Home | Images by Olofsson">
|
||||
<MainLayout
|
||||
title="Images by Olofsson – fotograf Linus Olofsson i Sverige"
|
||||
description={`Fotograf Linus Olofsson (Images by Olofsson) i ${basedIn()}: porträtt, natur och djur, stad, makro, norrsken och familjefester. Boka via kundportalen.`}
|
||||
path="/"
|
||||
>
|
||||
|
||||
<div id="wave-wrapper" class="fixed inset-0 z-0 pointer-events-none transition-opacity duration-1000 ease-in-out" style="opacity: 0.4;">
|
||||
<WaveBackground opacity="opacity-100" theme="cyan" />
|
||||
@@ -38,7 +43,8 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
|
||||
</div>
|
||||
|
||||
<p id="scroll-arrow" class="text-white/70 mt-6 text-lg font-light tracking-widest uppercase transition-opacity duration-300">↓</p>
|
||||
<p class="text-white/80 mt-4 text-base md:text-lg tracking-widest uppercase">Fotograf {business.person.name} · {basedIn()}</p>
|
||||
<p id="scroll-arrow" aria-hidden="true" class="text-white/70 mt-6 text-lg font-light tracking-widest uppercase transition-opacity duration-300">↓</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -52,14 +58,14 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
<div class="flex flex-col md:flex-row gap-12 md:gap-24 items-center text-center">
|
||||
|
||||
<div data-trail-box class="flex flex-col items-center px-6 py-4 rounded-2xl">
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Looking for inspiration?</h2>
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Letar du inspiration?</h2>
|
||||
<a href="/portfolio" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
|
||||
My Portfolio
|
||||
Se min portfolio
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row md:flex-col gap-6 items-center justify-center">
|
||||
<a href="https://www.instagram.com/imagesbyolofsson/" target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Instagram">
|
||||
<a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Instagram">
|
||||
<svg class="outline-ring" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="48" />
|
||||
</svg>
|
||||
@@ -69,7 +75,7 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://lightroom.adobe.com/u/linusolofsson1" target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Lightroom">
|
||||
<a href={business.social.lightroom} target="_blank" rel="noopener noreferrer" class="circle-btn group" aria-label="Adobe Lightroom">
|
||||
<svg class="outline-ring" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="48" />
|
||||
</svg>
|
||||
@@ -80,15 +86,135 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
</div>
|
||||
|
||||
<div data-trail-box class="flex flex-col items-center px-6 py-4 rounded-2xl">
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Ready to collaborate?</h2>
|
||||
<a href="/contact" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
|
||||
Contact me
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6">Vill du boka en fotografering?</h2>
|
||||
<a href="/tjanster" class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">
|
||||
Tjänster och bokning
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fakta om vem, vad, var och bokning: läsbar text för besökare, sökmotorer och AI-assistenter. -->
|
||||
<section id="om-images-by-olofsson" class="relative z-30 bg-neutral-900 border-t border-white/10 py-16 md:py-24">
|
||||
<div class="container mx-auto px-4 max-w-5xl grid gap-12 md:grid-cols-2">
|
||||
<div class="space-y-5 text-gray-300 text-lg leading-relaxed">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white">Fotograf {business.person.name}</h2>
|
||||
<p>
|
||||
Jag heter {business.person.name} och fotograferar under namnet {business.brand}.
|
||||
Jag är baserad i {basedIn()} och fotograferar {genreList()}.
|
||||
</p>
|
||||
<p>{business.recognition}</p>
|
||||
<p>
|
||||
Jag fotograferar med en {business.camera} och redigerar i {business.editing}, med fokus på naturliga toner och kontrast.
|
||||
</p>
|
||||
<p class="flex flex-wrap gap-x-6 gap-y-2 text-base">
|
||||
<a href="/about" class="text-white underline underline-offset-4 hover:text-gray-300">Mer om mig</a>
|
||||
<a href="/portfolio" class="text-white underline underline-offset-4 hover:text-gray-300">Portfolio</a>
|
||||
<a href={`mailto:${business.email}`} class="text-white underline underline-offset-4 hover:text-gray-300">{business.email}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white">Så bokar du</h2>
|
||||
<ol class="space-y-4 text-gray-300">
|
||||
{business.bookingSteps.map((step, i) => (
|
||||
<li class="flex gap-4">
|
||||
<span class="shrink-0 w-8 h-8 rounded-full border border-white/30 flex items-center justify-center text-sm font-bold text-white">{i + 1}</span>
|
||||
<span><strong class="text-white">{step.title}.</strong> {step.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<a href={business.portal.bookingUrl} class="inline-block px-6 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Skicka en förfrågan</a>
|
||||
<a href="/tjanster" class="inline-block px-6 py-3 border border-white text-white font-bold rounded-full hover:bg-white/10 transition">Vanliga frågor</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// SVG Längd
|
||||
const path = document.getElementById('outline-path') as unknown as SVGPathElement;
|
||||
if (path) {
|
||||
const length = path.getTotalLength();
|
||||
path.closest('svg')?.style.setProperty('--length', length.toString());
|
||||
}
|
||||
|
||||
const heroText = document.getElementById('hero-text');
|
||||
const textWrapper = document.getElementById('text-wrapper');
|
||||
const arrow = document.getElementById('scroll-arrow');
|
||||
const smokyPath = document.querySelector('.smoky-path') as SVGElement;
|
||||
const waveWrapper = document.getElementById('wave-wrapper'); // Hämta bakgrunden
|
||||
|
||||
// LOGIK FÖR GRADVIS FADE BASERAT PÅ SCROLL
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollY = window.scrollY;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (arrow) {
|
||||
arrow.style.opacity = scrollY > 50 ? '0' : '1';
|
||||
}
|
||||
|
||||
if (heroText && !footerIsVisible) {
|
||||
const fadeProgress = Math.min(scrollY / (windowHeight * 3), 1);
|
||||
heroText.style.opacity = fadeProgress.toString();
|
||||
}
|
||||
|
||||
if (scrollY > 2500) {
|
||||
if (smokyPath) smokyPath.style.animationDuration = '4s';
|
||||
} else {
|
||||
if (smokyPath) smokyPath.style.animationDuration = '6s';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// FOOTER INTERAKTION
|
||||
const footer = document.getElementById('footer-section');
|
||||
const imagesContainer = document.getElementById('images-container');
|
||||
let footerIsVisible = false;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
footerIsVisible = entry.isIntersecting;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// -- FOOTER SYNS --
|
||||
if (imagesContainer) imagesContainer.style.opacity = '0';
|
||||
if (heroText) {
|
||||
heroText.style.opacity = '1';
|
||||
heroText.style.transform = 'translateY(-25vh)';
|
||||
}
|
||||
|
||||
// Tänd Brackets
|
||||
if (textWrapper) textWrapper.classList.add('show-brackets');
|
||||
|
||||
// HÖJ OPACITY TILL 90% (Hårdkodat via style)
|
||||
if (waveWrapper) {
|
||||
waveWrapper.style.opacity = '0.9';
|
||||
}
|
||||
|
||||
} else {
|
||||
// -- FOOTER SYNS INTE --
|
||||
if (imagesContainer) imagesContainer.style.opacity = '1';
|
||||
if (heroText) {
|
||||
heroText.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
// Släck Brackets
|
||||
if (textWrapper) textWrapper.classList.remove('show-brackets');
|
||||
|
||||
// ÅTERSTÄLL OPACITY TILL 40% (Hårdkodat via style)
|
||||
if (waveWrapper) {
|
||||
waveWrapper.style.opacity = '0.4';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
if (footer) observer.observe(footer);
|
||||
</script>
|
||||
|
||||
</MainLayout>
|
||||
|
||||
<style>
|
||||
@@ -213,99 +339,3 @@ import WaveBackground from '../components/WaveBackground.astro';
|
||||
.outline-ring circle { fill: none; stroke: white; stroke-width: 2; stroke-dasharray: 302; stroke-dashoffset: 302; transition: stroke-dashoffset 0.6s cubic-bezier(0.25, 1, 0.5, 1); opacity: 0.8; }
|
||||
.circle-btn:hover .outline-ring circle { stroke-dashoffset: 0; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// SVG Längd
|
||||
const path = document.getElementById('outline-path') as unknown as SVGPathElement;
|
||||
if (path) {
|
||||
const length = path.getTotalLength();
|
||||
path.closest('svg')?.style.setProperty('--length', length.toString());
|
||||
}
|
||||
|
||||
const heroText = document.getElementById('hero-text');
|
||||
const textWrapper = document.getElementById('text-wrapper');
|
||||
const arrow = document.getElementById('scroll-arrow');
|
||||
const smokyPath = document.querySelector('.smoky-path') as SVGElement;
|
||||
const waveWrapper = document.getElementById('wave-wrapper'); // Hämta bakgrunden
|
||||
|
||||
// LOGIK FÖR GRADVIS FADE BASERAT PÅ SCROLL
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollY = window.scrollY;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (arrow) {
|
||||
arrow.style.opacity = scrollY > 50 ? '0' : '1';
|
||||
}
|
||||
|
||||
if (heroText && !footerIsVisible) {
|
||||
const fadeProgress = Math.min(scrollY / (windowHeight * 3), 1);
|
||||
heroText.style.opacity = fadeProgress.toString();
|
||||
}
|
||||
|
||||
if (scrollY > 2500) {
|
||||
if (smokyPath) smokyPath.style.animationDuration = '4s';
|
||||
} else {
|
||||
if (smokyPath) smokyPath.style.animationDuration = '6s';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// FOOTER INTERAKTION
|
||||
const footer = document.getElementById('footer-section');
|
||||
const imagesContainer = document.getElementById('images-container');
|
||||
let footerIsVisible = false;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
footerIsVisible = entry.isIntersecting;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// -- FOOTER SYNS --
|
||||
if (imagesContainer) imagesContainer.style.opacity = '0';
|
||||
if (heroText) {
|
||||
heroText.style.opacity = '1';
|
||||
heroText.style.transform = 'translateY(-25vh)';
|
||||
}
|
||||
|
||||
// Tänd Brackets
|
||||
if (textWrapper) textWrapper.classList.add('show-brackets');
|
||||
|
||||
// HÖJ OPACITY TILL 90% (Hårdkodat via style)
|
||||
if (waveWrapper) {
|
||||
waveWrapper.style.opacity = '0.9';
|
||||
}
|
||||
|
||||
} else {
|
||||
// -- FOOTER SYNS INTE --
|
||||
if (imagesContainer) imagesContainer.style.opacity = '1';
|
||||
if (heroText) {
|
||||
heroText.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
// Släck Brackets
|
||||
if (textWrapper) textWrapper.classList.remove('show-brackets');
|
||||
|
||||
// ÅTERSTÄLL OPACITY TILL 40% (Hårdkodat via style)
|
||||
if (waveWrapper) {
|
||||
waveWrapper.style.opacity = '0.4';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
if (footer) observer.observe(footer);
|
||||
|
||||
// FOOTER-LÄNKAR HOVER: ändra logo-glow när användare hovrar lyxlimo/karleksguiden-länkar
|
||||
const glowLinks = document.querySelectorAll<HTMLElement>('[data-glow-link]');
|
||||
glowLinks.forEach(link => {
|
||||
link.addEventListener('mouseenter', () => {
|
||||
if (!textWrapper) return;
|
||||
const tint = link.getAttribute('data-glow-link');
|
||||
if (tint) textWrapper.setAttribute('data-logo-tint', tint);
|
||||
});
|
||||
link.addEventListener('mouseleave', () => {
|
||||
if (!textWrapper) return;
|
||||
textWrapper.removeAttribute('data-logo-tint');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
// src/pages/llms.txt.ts -> /llms.txt (https://llmstxt.org/)
|
||||
// Byggs från src/data/business.ts så att fakta bara finns på ett ställe.
|
||||
// Okända fakta (null) skrivs som "lämnas på förfrågan", aldrig som gissningar.
|
||||
import type { APIRoute } from 'astro';
|
||||
import { business, basedIn, formatSek, genreList } from '../data/business';
|
||||
import { faq } from '../data/faq';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const base = site ?? new URL(business.url);
|
||||
const url = (p: string) => new URL(p, base).toString();
|
||||
const price = formatSek(business.priceFromSek);
|
||||
|
||||
const lines: (string | null)[] = [
|
||||
`# ${business.brand}`,
|
||||
'',
|
||||
`> ${business.brand} är namnet som ${business.person.jobTitle.toLowerCase()}en ${business.person.name} fotograferar under. Han är baserad i ${basedIn()} och fotograferar ${genreList()}. ${business.recognitionThirdPerson}`,
|
||||
'',
|
||||
'## Fakta',
|
||||
'',
|
||||
`- Fotograf: ${business.person.name}`,
|
||||
`- Namn/varumärke: ${business.brand}`,
|
||||
`- Baserad i: ${basedIn()}`,
|
||||
business.serviceArea ? `- Tar uppdrag i: ${business.serviceArea}` : '- Upptagningsområde: stäms av vid förfrågan',
|
||||
price ? `- Pris från: ${price}` : '- Priser: lämnas som offert efter förfrågan',
|
||||
business.deliveryTime ? `- Leveranstid: ${business.deliveryTime}` : null,
|
||||
business.phone ? `- Telefon: ${business.phone}` : null,
|
||||
`- E-post: ${business.email}`,
|
||||
`- Instagram: ${business.social.instagram.handle} (${business.social.instagram.url})`,
|
||||
`- Facebook: ${business.social.facebook}`,
|
||||
`- Kamera: ${business.camera}`,
|
||||
`- Redigering: ${business.editing}`,
|
||||
`- Bokning: ${business.portal.bookingUrl}`,
|
||||
`- Bokningsgång: ${business.bookingSteps.map((s) => s.title.toLowerCase()).join(' → ')}`,
|
||||
'',
|
||||
'## Sidor',
|
||||
'',
|
||||
`- [Startsida](${url('/')}): vem ${business.person.name} är och vad han fotograferar`,
|
||||
`- [Tjänster och vanliga frågor](${url('/tjanster')}): vad du kan boka, hur bokningen går till och svar på vanliga frågor`,
|
||||
`- [Portfolio](${url('/portfolio')}): urval av fotografier`,
|
||||
`- [Om mig](${url('/about')}): ${business.person.name}, bakgrund och utrustning`,
|
||||
`- [Kontakt](${url('/contact')}): e-post, Instagram och bokning`,
|
||||
'',
|
||||
'## Vanliga frågor',
|
||||
'',
|
||||
...faq.map((f) => `- ${f.question} ${f.answer}`),
|
||||
'',
|
||||
'## Profiler',
|
||||
'',
|
||||
`- [Instagram](${business.social.instagram.url})`,
|
||||
`- [Flickr](${business.social.flickr})`,
|
||||
`- [Adobe Lightroom](${business.social.lightroom})`,
|
||||
`- [LinkedIn](${business.social.linkedin})`,
|
||||
'',
|
||||
'## Användning av innehållet',
|
||||
'',
|
||||
`AI-assistenter och sökmotorer får läsa och använda innehållet på den här sajten, inklusive för träning, med hänvisning till ${business.brand} (${url('/')}). Fotografierna är upphovsrättsskyddade av ${business.person.name}.`,
|
||||
'',
|
||||
];
|
||||
|
||||
const body = lines.filter((l): l is string => l !== null).join('\n');
|
||||
return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
||||
};
|
||||
+192
-112
@@ -1,31 +1,32 @@
|
||||
---
|
||||
// src/pages/portfolio.astro
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import { Image, getImage } from 'astro:assets';
|
||||
import exifr from 'exifr';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business, genreList } from '../data/business';
|
||||
import { portfolioMeta } from '../data/portfolio-meta';
|
||||
|
||||
// --- SERVER-SIDE LOGIC ---
|
||||
// SERVER-SIDE LOGIC (byggtid)
|
||||
|
||||
// 1. Hämta alla bilder
|
||||
const allImageFiles = import.meta.glob('../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}', { eager: true });
|
||||
// -s (800px) och -m (1600px) ar NEDLADDNINGSVARIANTER, inte egna galleribilder.
|
||||
// Utan filtret visas varje bild tre ganger.
|
||||
const imageFiles = Object.fromEntries(
|
||||
Object.entries(allImageFiles).filter(([pathKey]) => !/-[sm]\.(jpe?g|png|webp)$/i.test(pathKey))
|
||||
// 1. Hämta alla galleribilder.
|
||||
// -s (800px) och -m (1600px) i mappen är gamla nedladdningsvarianter med EXIF kvar.
|
||||
// De importeras INTE längre (då hamnar de inte i dist/); nedladdningsstorlekarna
|
||||
// tas i stället fram av Astros bildtjänst nedan, vilket också rensar bort EXIF
|
||||
// (GPS, kamerans serienummer, Artist m.m.).
|
||||
const imageFiles = import.meta.glob(
|
||||
[
|
||||
'../assets/images/portfolio/*.{jpeg,jpg,png,webp,JPG,JPEG,PNG}',
|
||||
'!../assets/images/portfolio/*-{s,m}.{jpeg,jpg,png,webp,JPG,JPEG,PNG}',
|
||||
],
|
||||
{ eager: true }
|
||||
);
|
||||
|
||||
// Karta assetId -> { s, m } med Astros hashade URL:er, sa klienten kan lanka
|
||||
// till ratt nedladdningsvariant. Large ar galleribilden sjalv, original gar via proxy.
|
||||
const downloadVariants: Record<string, Record<string, string>> = {};
|
||||
for (const [pathKey, file] of Object.entries(allImageFiles) as [string, any][]) {
|
||||
const m = pathKey.match(/immich-([0-9a-f-]{36})-.*-([sm])\.(?:jpe?g|png|webp)$/i);
|
||||
if (!m) continue;
|
||||
const id = m[1].toLowerCase();
|
||||
(downloadVariants[id] ||= {})[m[2].toLowerCase()] = file.default.src;
|
||||
}
|
||||
// OBS: läs aldrig .src/.width/.height direkt på de importerade bilderna här.
|
||||
// Då räknar Astro originalet som "använt" och kopierar originalfilen (med EXIF)
|
||||
// till dist/. Gå alltid via <Image>/getImage.
|
||||
|
||||
function formatShutterSpeed(exposureTime: number) {
|
||||
if (!exposureTime) return null;
|
||||
@@ -34,40 +35,78 @@ function formatShutterSpeed(exposureTime: number) {
|
||||
return `1/${fraction}`;
|
||||
}
|
||||
|
||||
// Datum ur filnamnet: "immich-<id>-20260905-..." eller "20250510-...".
|
||||
function dateFromFilename(name: string): Date | null {
|
||||
const m = name.match(/(?:^|-)((?:19|20)\d{2})(\d{2})(\d{2})-/);
|
||||
if (!m) return null;
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
const widthOf = (descriptor: string) => parseInt(descriptor, 10) || 0;
|
||||
|
||||
const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([relativePath, file]: [string, any]) => {
|
||||
let exifData: any = {};
|
||||
let dateTaken = null;
|
||||
let dateTaken: Date | null = null;
|
||||
const absolutePath = path.resolve('./src/pages', relativePath);
|
||||
const fileName = path.basename(relativePath);
|
||||
// Stabil nyckel = filnamn utan ändelse. Används för alt-texter (portfolio-meta.ts) och statistik.
|
||||
const key = fileName.replace(/\.[^.]+$/, '');
|
||||
|
||||
try {
|
||||
const fileBuffer = fs.readFileSync(absolutePath);
|
||||
exifData = await exifr.parse(fileBuffer, [
|
||||
'ISO', 'Model', 'FNumber', 'ExposureTime', 'FocalLength', 'DateTimeOriginal', 'CreateDate'
|
||||
]);
|
||||
dateTaken = exifData?.DateTimeOriginal || exifData?.CreateDate;
|
||||
if (!dateTaken) dateTaken = new Date();
|
||||
const exifDate = exifData?.DateTimeOriginal || exifData?.CreateDate;
|
||||
if (exifDate) dateTaken = new Date(exifDate);
|
||||
} catch (e) {
|
||||
console.error(`Kunde inte läsa EXIF för ${relativePath}`);
|
||||
dateTaken = new Date();
|
||||
}
|
||||
if (!dateTaken || isNaN(dateTaken.getTime())) dateTaken = dateFromFilename(fileName);
|
||||
const hasDate = !!dateTaken;
|
||||
const dateObj = dateTaken ?? new Date();
|
||||
|
||||
const dateObj = new Date(dateTaken);
|
||||
const year = dateObj.getFullYear().toString();
|
||||
const month = dateObj.toLocaleString('sv-SE', { month: 'long' }).toLowerCase();
|
||||
const monthIndex = dateObj.getMonth();
|
||||
|
||||
const fullDate = dateObj.toLocaleDateString('sv-SE', {
|
||||
day: 'numeric', month: 'long', year: 'numeric'
|
||||
});
|
||||
const fullDate = hasDate
|
||||
? dateObj.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
: '';
|
||||
|
||||
const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime);
|
||||
const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null;
|
||||
const fStop = exifData?.FNumber ? `f/${exifData.FNumber}` : null;
|
||||
const iso = exifData?.ISO ? `ISO ${exifData.ISO}` : null;
|
||||
|
||||
// Alt-text: riktig text från portfolio-meta.ts om den finns, annars en neutral
|
||||
// svensk reservtext. Aldrig kameramodell, aldrig "undefined".
|
||||
const meta = portfolioMeta[key] ?? {};
|
||||
const fallbackAlt = [
|
||||
meta.category ? `${meta.category}:` : null,
|
||||
`fotografi av ${business.person.name}`,
|
||||
fullDate ? `taget ${fullDate}` : null,
|
||||
].filter(Boolean).join(' ');
|
||||
const alt = meta.alt?.trim() || fallbackAlt.charAt(0).toUpperCase() + fallbackAlt.slice(1);
|
||||
|
||||
// Nedladdning/lightbox: nya, EXIF-fria JPG:er i 800/1600/3000 px
|
||||
// (Astro hoppar över bredder större än originalet).
|
||||
const jpg = await getImage({ src: file.default, format: 'jpg', quality: 88, width: 800, widths: [800, 1600, 3000] });
|
||||
const variants = [...jpg.srcSet.values].sort((a, b) => widthOf(a.descriptor ?? '') - widthOf(b.descriptor ?? ''));
|
||||
const small = variants[0]?.url ?? jpg.src;
|
||||
const large = variants[variants.length - 1]?.url ?? jpg.src;
|
||||
const medium = variants.find((v) => widthOf(v.descriptor ?? '') === 1600)?.url ?? large;
|
||||
|
||||
const assetId = key.match(/^immich-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-/i)?.[1]?.toLowerCase() ?? null;
|
||||
const statKey = ('portfolio/' + key.replace(/[^\w\-.]/g, '_')).slice(0, 150);
|
||||
|
||||
return {
|
||||
...file, year, month, monthIndex, fullDate, model: exifData?.Model,
|
||||
fStop, shutterSpeed, iso, focalLength
|
||||
img: file.default, key, alt, title: meta.title ?? null,
|
||||
year, month, monthIndex, fullDate, model: exifData?.Model,
|
||||
fStop, shutterSpeed, iso, focalLength,
|
||||
dlS: small, dlM: medium, dlL: large, srcset: jpg.srcSet.attribute,
|
||||
assetId, statKey,
|
||||
};
|
||||
}));
|
||||
|
||||
@@ -78,7 +117,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
.map((item: any) => item.name);
|
||||
---
|
||||
|
||||
<MainLayout title="Portfolio | Images by Olofsson">
|
||||
<MainLayout
|
||||
title="Portfolio – fotografier av Linus Olofsson | Images by Olofsson"
|
||||
description={`Portfolio med fotografier av Linus Olofsson: ${genreList()}. Filtrera på år och månad och öppna bilderna i större format.`}
|
||||
path="/portfolio"
|
||||
pageType="CollectionPage"
|
||||
breadcrumb="Portfolio"
|
||||
>
|
||||
|
||||
<div>
|
||||
<WaveBackground opacity="opacity-55" />
|
||||
@@ -88,32 +133,44 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-4">Portfolio</h1>
|
||||
<p class="text-gray-400">A curated selection of my work across different styles.</p>
|
||||
<p class="text-gray-300 max-w-2xl mx-auto">Ett urval av mina bilder i olika stilar – {genreList()}.
|
||||
Vill du boka en fotografering? Läs om <a href="/tjanster" class="text-white underline underline-offset-4 hover:text-gray-300">tjänster och bokning</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-6 mb-12">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<button class="filter-btn active" data-filter="all" data-type="year">Alla år</button>
|
||||
<button type="button" class="filter-btn active" data-filter="all" data-type="year">Alla år</button>
|
||||
{uniqueYears.map(year => (
|
||||
<button class="filter-btn" data-filter={year} data-type="year">{year}</button>
|
||||
<button type="button" class="filter-btn" data-filter={year} data-type="year">{year}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<button class="filter-btn active" data-filter="all" data-type="month">Alla månader</button>
|
||||
<button type="button" class="filter-btn active" data-filter="all" data-type="month">Alla månader</button>
|
||||
{uniqueMonths.map(month => (
|
||||
<button class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
|
||||
<button type="button" class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Bilder</h2>
|
||||
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
|
||||
{galleryItems.map((item: any) => (
|
||||
<div
|
||||
class="gallery-item break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
||||
{galleryItems.map((item: any, index: number) => (
|
||||
<a
|
||||
href={item.dlL}
|
||||
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
||||
data-year={item.year}
|
||||
data-month={item.month}
|
||||
data-src={item.default.src}
|
||||
data-src={item.dlL}
|
||||
data-srcset={item.srcset}
|
||||
data-dl-s={item.dlS}
|
||||
data-dl-m={item.dlM}
|
||||
data-dl-l={item.dlL}
|
||||
data-key={item.key}
|
||||
data-asset={item.assetId}
|
||||
data-stat={item.statKey}
|
||||
data-alt={item.alt}
|
||||
data-title={item.title}
|
||||
data-model={item.model}
|
||||
data-iso={item.iso}
|
||||
data-fstop={item.fStop}
|
||||
@@ -122,23 +179,27 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
data-date={item.fullDate}
|
||||
>
|
||||
<Image
|
||||
src={item.default}
|
||||
alt={`Foto taget med ${item.model}`}
|
||||
src={item.img}
|
||||
alt={item.alt}
|
||||
width={800}
|
||||
widths={[480, 800]}
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
||||
loading={index < 3 ? 'eager' : 'lazy'}
|
||||
decoding="async"
|
||||
class="w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition duration-300 flex flex-col justify-end p-6 pointer-events-none">
|
||||
<p class="text-white font-bold text-lg">{item.fullDate}</p>
|
||||
<p class="text-xs text-gray-400 uppercase tracking-widest mt-1">Visa bild</p>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition duration-300 flex flex-col justify-end p-6 pointer-events-none">
|
||||
{(item.title || item.fullDate) && <p class="text-white font-bold text-lg">{item.title ?? item.fullDate}</p>}
|
||||
<p class="text-xs text-gray-300 uppercase tracking-widest mt-1">Visa bild</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div id="no-results" class="hidden text-center text-gray-500 mt-20">
|
||||
<div id="no-results" class="hidden text-center text-gray-400 mt-20">
|
||||
<p class="text-xl">Inga bilder hittades för denna tidpunkt.</p>
|
||||
<button id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
|
||||
<button type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -146,84 +207,85 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
<div id="lightbox" class="fixed inset-0 z-50 hidden bg-black/95 backdrop-blur-md flex items-center justify-center opacity-0 transition-opacity duration-300 p-2 md:p-8">
|
||||
<div class="bg-neutral-900 w-full max-w-6xl h-[85vh] md:h-[90vh] flex flex-col rounded-xl overflow-hidden shadow-2xl relative border border-white/10">
|
||||
|
||||
<button id="lb-close" class="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/80 text-white p-2 rounded-full transition-colors">
|
||||
<button type="button" id="lb-close" aria-label="Stäng" class="absolute top-4 right-4 z-50 bg-black/50 hover:bg-black/80 text-white p-2 rounded-full transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 relative bg-black flex items-center justify-center overflow-hidden group">
|
||||
<img id="lb-img" src="" alt="Full size" class="max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
|
||||
<button id="lb-prev" class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||||
<img id="lb-img" src="" alt="" sizes="100vw" class="max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
|
||||
<button type="button" id="lb-prev" aria-label="Föregående bild" class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
<button id="lb-next" class="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||||
<button type="button" id="lb-next" aria-label="Nästa bild" class="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 bg-black/40 hover:bg-black/70 text-white p-3 rounded-full transition-all">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-800 p-4 md:p-6 shrink-0 border-t border-white/5 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div class="text-left">
|
||||
<h3 id="lb-date" class="text-white font-bold text-lg md:text-xl capitalize"></h3>
|
||||
<p id="lb-date" class="text-white font-bold text-lg md:text-xl"></p>
|
||||
<p id="lb-model" class="text-gray-400 text-sm font-medium tracking-wide mt-1"></p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4 md:gap-6">
|
||||
<div class="relative">
|
||||
<button id="lb-download" class="group inline-flex items-center gap-2 px-6 py-2.5 bg-neutral-700/80 hover:bg-neutral-600/80 text-gray-200 hover:text-white font-semibold text-xs uppercase tracking-widest rounded-full border border-black/60 shadow-lg shadow-black/30 ring-1 ring-white/5 transition-all duration-200 transform hover:scale-105 active:scale-95 active:duration-75">
|
||||
<button type="button" id="lb-download" aria-haspopup="true" aria-expanded="false" class="group inline-flex items-center gap-2 px-6 py-2.5 bg-neutral-700/80 hover:bg-neutral-600/80 text-gray-200 hover:text-white font-semibold text-xs uppercase tracking-widest rounded-full border border-black/60 shadow-lg shadow-black/30 ring-1 ring-white/5 transition-all duration-200 transform hover:scale-105 active:scale-95 active:duration-75">
|
||||
<svg id="lb-dl-arrow" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 transition-colors duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 19h16"/></svg>
|
||||
Download
|
||||
Ladda ner
|
||||
</button>
|
||||
<div id="lb-sizes" class="hidden absolute bottom-full mb-3 left-0 md:left-auto md:right-0 min-w-[260px] bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden origin-bottom opacity-0 translate-y-2 transition-all duration-200 ease-out">
|
||||
<a data-size="s" class="dl-opt group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Small</span><span class="text-gray-500 text-xs font-mono group-hover:text-gray-300 transition-colors">800 px</span></a>
|
||||
<a data-size="m" class="dl-opt group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Medium</span><span class="text-gray-500 text-xs font-mono group-hover:text-gray-300 transition-colors">1600 px</span></a>
|
||||
<a data-size="l" class="dl-opt group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Large</span><span class="text-gray-500 text-xs font-mono group-hover:text-gray-300 transition-colors">3000 px</span></a>
|
||||
<a data-size="orig" class="dl-opt group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors "><span class="font-medium uppercase tracking-widest text-xs">Original<span class="block text-[10px] text-gray-500 normal-case tracking-normal mt-0.5">Kräver lösenord</span></span><span class="text-gray-500 text-xs font-mono group-hover:text-gray-300 transition-colors">full storlek</span></a>
|
||||
<button type="button" data-size="s" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Liten</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">800 px</span></button>
|
||||
<button type="button" data-size="m" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Mellan</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">1600 px</span></button>
|
||||
<button type="button" data-size="l" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors border-b border-white/5"><span class="font-medium uppercase tracking-widest text-xs">Stor</span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">3000 px</span></button>
|
||||
<button type="button" data-size="orig" class="dl-opt w-full text-left group flex items-center justify-between px-5 py-3.5 text-sm text-gray-200 hover:bg-black/40 hover:text-white cursor-pointer transition-colors "><span class="font-medium uppercase tracking-widest text-xs">Original<span class="block text-[10px] text-gray-400 normal-case tracking-normal mt-0.5">Kräver lösenord</span></span><span class="text-gray-400 text-xs font-mono group-hover:text-gray-300 transition-colors">full storlek</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 md:gap-6 text-xs md:text-sm font-mono text-gray-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">Focal</span><span id="lb-focal">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">Aperture</span><span id="lb-fstop">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">Shutter</span><span id="lb-shutter">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-500 uppercase text-[10px] font-bold">ISO</span><span id="lb-iso">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Brännvidd</span><span id="lb-focal">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Bländare</span><span id="lb-fstop">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">Slutartid</span><span id="lb-shutter">--</span></div>
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-1"><span class="text-gray-400 uppercase text-[10px] font-bold">ISO</span><span id="lb-iso">--</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="dl-variants" set:html={JSON.stringify(downloadVariants)}></script>
|
||||
|
||||
<!-- Losenordsruta for ORIGINAL. Originalen ligger inte i repot (27-70 MB/st)
|
||||
utan hamtas fran Immich via /download/original, som kraver losenord. -->
|
||||
<div id="pw-modal" class="hidden fixed inset-0 z-[60] bg-black/85 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<!-- Lösenordsruta för ORIGINAL. Originalen ligger inte i repot (27–70 MB/st)
|
||||
utan hämtas från Immich via /download/original, som kräver lösenord. -->
|
||||
<div id="pw-modal" role="dialog" aria-modal="true" aria-labelledby="pw-title" class="hidden fixed inset-0 z-[60] bg-black/85 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-neutral-900 border border-white/10 rounded-xl shadow-2xl p-7 w-full max-w-sm">
|
||||
<h4 class="text-white font-bold text-lg tracking-tight mb-1">Ladda ner original</h4>
|
||||
<p id="pw-title" class="text-white font-bold text-lg tracking-tight mb-1">Ladda ner original</p>
|
||||
<p class="text-gray-400 text-sm mb-4">Originalfilen är i full upplösning och kräver lösenord.</p>
|
||||
<input id="pw-input" type="password" placeholder="Lösenord" autocomplete="current-password"
|
||||
<input id="pw-input" type="password" aria-label="Lösenord" placeholder="Lösenord" autocomplete="current-password"
|
||||
class="w-full bg-black/60 border border-white/20 rounded-lg text-white px-4 py-2.5 mb-2 focus:outline-none focus:border-white/60 focus:ring-1 focus:ring-white/20 transition" />
|
||||
<p id="pw-error" class="hidden text-red-400 text-xs tracking-wide mb-2">Fel lösenord — försök igen.</p>
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<button id="pw-cancel" class="px-5 py-2.5 text-xs uppercase tracking-widest text-gray-400 hover:text-white rounded-full transition-colors">Avbryt</button>
|
||||
<button id="pw-ok" class="px-5 py-2.5 text-xs uppercase tracking-widest bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">Ladda ner</button>
|
||||
<button type="button" id="pw-cancel" class="px-5 py-2.5 text-xs uppercase tracking-widest text-gray-400 hover:text-white rounded-full transition-colors">Avbryt</button>
|
||||
<button type="button" id="pw-ok" class="px-5 py-2.5 text-xs uppercase tracking-widest bg-white text-black font-bold rounded-full hover:bg-gray-200 transition transform hover:scale-105">Ladda ner</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</MainLayout>
|
||||
|
||||
<style>
|
||||
@reference "../styles/global.css";
|
||||
|
||||
.filter-btn {
|
||||
@apply px-4 py-1.5 rounded-full border border-white/10 bg-neutral-800/50 text-sm text-gray-400 transition hover:bg-white hover:text-black hover:border-white;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
@apply bg-white text-black border-white font-medium shadow-[0_0_15px_rgba(255,255,255,0.3)];
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// --- ANONYM STATISTIK ---
|
||||
// Räknare per dag och bild i kundportalen: ingen cookie, ingen IP, inget id.
|
||||
const STATS_URL = 'https://customer.imagesbyolofsson.se/api/stats/hit';
|
||||
// Bildens statistiknyckel räknas ut vid bygget (data-stat) och är densamma som
|
||||
// tidigare ("portfolio/<filnamn utan ändelse>"), så historiken fortsätter.
|
||||
function statsItem(item?: HTMLElement): string {
|
||||
return item?.dataset.stat || 'portfolio/okand';
|
||||
}
|
||||
function stat(kind: 'view' | 'open' | 'download', item: string) {
|
||||
try {
|
||||
const body = new Blob([JSON.stringify({ kind, item })], { type: 'text/plain' });
|
||||
navigator.sendBeacon?.(STATS_URL, body);
|
||||
} catch { /* statistik får aldrig störa sidan */ }
|
||||
}
|
||||
stat('view', 'page/portfolio');
|
||||
|
||||
// --- FILTER LOGIK ---
|
||||
// MULTI-select: flera ar + manader samtidigt. 'all' = samtligt.
|
||||
// MULTI-select: flera år + månader samtidigt. 'all' = samtligt.
|
||||
let selectedYears = new Set(['all']);
|
||||
let selectedMonths = new Set(['all']);
|
||||
|
||||
@@ -293,28 +355,22 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
const pwInput = document.getElementById('pw-input') as HTMLInputElement;
|
||||
const pwError = document.getElementById('pw-error');
|
||||
|
||||
// Astro hashar filnamnen, men assetId:t finns kvar i dem:
|
||||
// /_astro/immich-<assetId>-<datum>-<namn>.<hash>.jpg
|
||||
function assetIdFor(src: string): string | null {
|
||||
const m = src.match(/immich-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-/i);
|
||||
return m ? m[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
// Large ar filen galleriet redan visar. -s/-m ligger bredvid i samma katalog,
|
||||
// men med egen Astro-hash - darfor laser vi dem ur en genererad karta.
|
||||
const VARIANTS: Record<string, Record<string, string>> = JSON.parse(
|
||||
document.getElementById('dl-variants')?.textContent || '{}'
|
||||
);
|
||||
|
||||
// Small/Medium/Large är EXIF-fria JPG:er som Astro tar fram vid bygget
|
||||
// (data-dl-s/m/l). Original hämtas från Immich via proxyn och kräver lösenord.
|
||||
function urlForSize(size: string): string | null {
|
||||
const item = visibleItems[currentImageIndex];
|
||||
if (!item) return null;
|
||||
const large = item.dataset.src || '';
|
||||
if (size === 'l') return large;
|
||||
const id = assetIdFor(large);
|
||||
if (!id) return null;
|
||||
if (size === 'orig') return `/download/original?asset=${id}`;
|
||||
return VARIANTS[id]?.[size] || null;
|
||||
if (size === 's') return item.dataset.dlS || null;
|
||||
if (size === 'm') return item.dataset.dlM || null;
|
||||
if (size === 'l') return item.dataset.dlL || null;
|
||||
if (size === 'orig') return item.dataset.asset ? `/download/original?asset=${item.dataset.asset}` : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function fileNameFor(size: string): string {
|
||||
const item = visibleItems[currentImageIndex];
|
||||
const key = (item?.dataset.key || 'bild').replace(/[^\w\-]/g, '_');
|
||||
return `imagesbyolofsson-${key}-${size}.jpg`;
|
||||
}
|
||||
|
||||
function startDownload(url: string, filnamn?: string) {
|
||||
@@ -330,9 +386,10 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
|
||||
function visaMeny(visa: boolean) {
|
||||
if (!dlMenu) return;
|
||||
// Pilen gar gra medan menyn ar oppen och tillbaka till vanligt nar den stangs
|
||||
// (bade vid val av storlek och nar man klickar bort menyn).
|
||||
// Pilen går grå medan menyn är öppen och tillbaka till vanligt när den stängs
|
||||
// (både vid val av storlek och när man klickar bort menyn).
|
||||
dlArrow?.classList.toggle('text-gray-500', visa);
|
||||
dlBtn?.setAttribute('aria-expanded', visa ? 'true' : 'false');
|
||||
if (visa) {
|
||||
dlMenu.classList.remove('hidden');
|
||||
requestAnimationFrame(() => dlMenu.classList.remove('opacity-0', 'translate-y-2'));
|
||||
@@ -357,9 +414,10 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
const size = (opt as HTMLElement).dataset.size || 'l';
|
||||
visaMeny(false);
|
||||
const url = urlForSize(size);
|
||||
if (!url) { alert('Den storleken saknas for den har bilden.'); return; }
|
||||
if (!url) { alert('Den storleken saknas för den här bilden.'); return; }
|
||||
stat('download', statsItem(visibleItems[currentImageIndex]));
|
||||
if (size === 'orig') {
|
||||
// Losenordet sparas bara for fliken, aldrig i localStorage.
|
||||
// Lösenordet sparas bara för fliken, aldrig i localStorage.
|
||||
const sparat = sessionStorage.getItem('dlpw');
|
||||
if (sparat) { startDownload(`${url}&pw=${encodeURIComponent(sparat)}`); return; }
|
||||
pwError?.classList.add('hidden');
|
||||
@@ -367,7 +425,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
pwModal?.classList.remove('hidden');
|
||||
setTimeout(() => pwInput?.focus(), 50);
|
||||
} else {
|
||||
startDownload(url);
|
||||
startDownload(url, fileNameFor(size));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -377,12 +435,12 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
if (!pw) return;
|
||||
const url = urlForSize('orig');
|
||||
if (!url) return;
|
||||
// HEAD forst: da far vi 401 utan att dra igang en 70 MB-nedladdning.
|
||||
// HEAD först: då får vi 401 utan att dra igång en 70 MB-nedladdning.
|
||||
try {
|
||||
const r = await fetch(`${url}&pw=${encodeURIComponent(pw)}`, { method: 'HEAD' });
|
||||
if (r.status === 401) { pwError?.classList.remove('hidden'); return; }
|
||||
if (!r.ok) { alert('Kunde inte hamta originalet just nu.'); return; }
|
||||
} catch { /* natverksfel -> forsok anda nedan */ }
|
||||
if (!r.ok) { alert('Kunde inte hämta originalet just nu.'); return; }
|
||||
} catch { /* nätverksfel -> försök ändå nedan */ }
|
||||
sessionStorage.setItem('dlpw', pw);
|
||||
pwModal?.classList.add('hidden');
|
||||
startDownload(`${url}&pw=${encodeURIComponent(pw)}`);
|
||||
@@ -412,9 +470,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
if (!item) return;
|
||||
visaMeny(false);
|
||||
|
||||
// srcset först så att webbläsaren väljer 800/1600/3000 px efter skärmen.
|
||||
lbImg.srcset = item.dataset.srcset || '';
|
||||
lbImg.src = item.dataset.src || '';
|
||||
lbImg.alt = item.dataset.alt || '';
|
||||
stat('open', statsItem(item));
|
||||
|
||||
if(lbDate) lbDate.innerText = item.dataset.date || '';
|
||||
if(lbDate) lbDate.innerText = [item.dataset.title, item.dataset.date].filter(Boolean).join(' – ');
|
||||
if(lbModel) lbModel.innerText = item.dataset.model || '';
|
||||
if(lbFocal) lbFocal.innerText = item.dataset.focal || '--';
|
||||
if(lbFstop) lbFstop.innerText = item.dataset.fstop || '--';
|
||||
@@ -435,7 +497,12 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
// Event Listeners
|
||||
items.forEach((item) => {
|
||||
item.classList.add('visible-item');
|
||||
item.addEventListener('click', () => {
|
||||
item.addEventListener('click', (e) => {
|
||||
// Länken pekar på den stora bilden (crawlbar/öppningsbar i ny flik),
|
||||
// men ett vanligt klick öppnar lightboxen.
|
||||
const me = e as MouseEvent;
|
||||
if (me.metaKey || me.ctrlKey || me.shiftKey || me.button === 1) return;
|
||||
e.preventDefault();
|
||||
updateVisibleItems();
|
||||
const index = visibleItems.indexOf(item);
|
||||
if (index !== -1) openLightbox(index);
|
||||
@@ -488,3 +555,16 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
filterImages();
|
||||
});
|
||||
</script>
|
||||
</MainLayout>
|
||||
|
||||
<style>
|
||||
@reference "../styles/global.css";
|
||||
|
||||
.filter-btn {
|
||||
@apply px-4 py-1.5 rounded-full border border-white/10 bg-neutral-800/50 text-sm text-gray-400 transition hover:bg-white hover:text-black hover:border-white;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
@apply bg-white text-black border-white font-medium shadow-[0_0_15px_rgba(255,255,255,0.3)];
|
||||
}
|
||||
</style>
|
||||
|
||||
+56
-34
@@ -2,80 +2,102 @@
|
||||
// src/pages/privacy.astro
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business } from '../data/business';
|
||||
|
||||
const link = 'text-white underline hover:text-gray-300';
|
||||
---
|
||||
|
||||
<MainLayout title="Privacy Policy | Images by Olofsson">
|
||||
<MainLayout
|
||||
title="Integritetspolicy | Images by Olofsson"
|
||||
description="Hur imagesbyolofsson.se hanterar personuppgifter: Meta Pixel endast med samtycke, anonym bildstatistik utan cookies och dina rättigheter enligt GDPR."
|
||||
path="/privacy"
|
||||
breadcrumb="Integritetspolicy"
|
||||
>
|
||||
|
||||
<WaveBackground opacity="opacity-80" />
|
||||
|
||||
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10">
|
||||
<div data-trail-box class="bg-neutral-900/60 backdrop-blur-md border border-white/10 rounded-3xl p-8 md:p-12 shadow-2xl mb-20 max-w-3xl mx-auto">
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold leading-tight mb-2">Privacy Policy</h1>
|
||||
<p class="text-gray-500 mb-10">Last updated: 25 October 2026</p>
|
||||
<h1 class="text-4xl md:text-5xl font-bold leading-tight mb-2">Integritetspolicy</h1>
|
||||
<p class="text-gray-400 mb-10">Senast uppdaterad: 27 oktober 2026</p>
|
||||
|
||||
<div class="space-y-8 text-gray-300 text-lg leading-relaxed">
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Who is responsible</h2>
|
||||
<h2 class="text-2xl font-bold text-white">Vem som är ansvarig</h2>
|
||||
<p>
|
||||
Images by Olofsson is run by Linus Olofsson, Sweden. Questions about privacy:
|
||||
<a href="mailto:Admin@imagesbyolofsson.se" class="text-white underline hover:text-gray-300">Admin@imagesbyolofsson.se</a>.
|
||||
{business.brand} drivs av {business.person.name}, {business.country.name}. Frågor om integritet:
|
||||
<a href={`mailto:${business.email}`} class={link}>{business.email}</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">This website</h2>
|
||||
<h2 class="text-2xl font-bold text-white">Den här webbplatsen</h2>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>There are no user accounts and no contact forms.</li>
|
||||
<li>If you email me, your address and message are used only to answer you and are not shared with anyone.</li>
|
||||
<li>Password-protected galleries keep the password only in your open browser tab; it is not stored after you close it.</li>
|
||||
<li>Det finns inga användarkonton och inga kontaktformulär på den här webbplatsen. Bokningar görs i den separata kundportalen.</li>
|
||||
<li>Om du mejlar mig används din adress och ditt meddelande bara för att svara dig och delas inte med någon.</li>
|
||||
<li>Lösenordsskyddade nedladdningar sparar lösenordet bara i din öppna webbläsarflik; det finns inte kvar när du stänger den.</li>
|
||||
<li>
|
||||
The site is delivered through Cloudflare, which processes technical data such as your IP address to deliver
|
||||
and protect the site. See <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class="text-white underline hover:text-gray-300">Cloudflare's privacy policy</a>.
|
||||
Webbplatsen levereras via Cloudflare, som behandlar tekniska uppgifter som din IP-adress för att leverera
|
||||
och skydda webbplatsen. Se <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class={link}>Cloudflares integritetspolicy</a>.
|
||||
</li>
|
||||
<li>If you appear in one of my photos and want it removed, email me and I will take it down.</li>
|
||||
<li>Om du syns på någon av mina bilder och vill att den tas bort, mejla mig så tar jag bort den.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Meta Pixel (only with your consent)</h2>
|
||||
<h2 class="text-2xl font-bold text-white">Meta Pixel (bara med ditt samtycke)</h2>
|
||||
<p>
|
||||
If you click "Accept" in the cookie box, the site loads the Meta Pixel from Meta Platforms Ireland Ltd.
|
||||
I use it to see how visitors coming from my Instagram and Facebook use the site.
|
||||
Om du klickar på ”Godkänn” i cookierutan laddar webbplatsen Meta Pixel från Meta Platforms Ireland Ltd.
|
||||
Jag använder den för att se hur besökare som kommer från mitt Instagram och Facebook använder webbplatsen.
|
||||
</p>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>It sets cookies (such as <code>_fbp</code>) and sends Meta information like the pages you visit, your IP address and browser details.</li>
|
||||
<li>Meta may link this to your Facebook or Instagram account and may transfer data outside the EU. See
|
||||
<a href="https://www.facebook.com/privacy/policy/" target="_blank" rel="noopener noreferrer" class="text-white underline hover:text-gray-300">Meta's privacy policy</a>.</li>
|
||||
<li>If you click "Decline" or do nothing, the pixel is never loaded.</li>
|
||||
<li>You can change your choice at any time via "Cookie settings" at the bottom of every page. Your choice is stored in your own browser.</li>
|
||||
<li>Den sätter cookies (till exempel <code>_fbp</code>) och skickar uppgifter till Meta, som vilka sidor du besöker, din IP-adress och information om din webbläsare.</li>
|
||||
<li>Meta kan koppla detta till ditt Facebook- eller Instagramkonto och kan föra över uppgifter utanför EU. Se
|
||||
<a href="https://www.facebook.com/privacy/policy/" target="_blank" rel="noopener noreferrer" class={link}>Metas integritetspolicy</a>.</li>
|
||||
<li>Om du klickar på ”Avböj” eller inte gör något laddas pixeln aldrig.</li>
|
||||
<li>Du kan ändra ditt val när som helst via ”Cookie-inställningar” längst ner på varje sida. Ditt val sparas i din egen webbläsare.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Photo backup app (Google)</h2>
|
||||
<h2 class="text-2xl font-bold text-white">Anonym statistik för portfolion</h2>
|
||||
<p>
|
||||
"townhouse-backup" is a private tool used only by me to back up my own photos to my own Google account.
|
||||
För att se vilka bilder som uppskattas räknar portfolion hur många gånger varje bild öppnas eller laddas ner, per dag.
|
||||
Bara antalet sparas, på min egen server i Sverige. Inga cookies, ingen IP-adress, ingen webbläsarinformation och inget
|
||||
som kan identifiera dig. Det sker utan samtycke eftersom inga uppgifter om dig samlas in.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">App för säkerhetskopiering av bilder (Google)</h2>
|
||||
<p>
|
||||
”townhouse-backup” är ett privat verktyg som bara jag använder för att säkerhetskopiera mina egna bilder till mitt eget Google-konto.
|
||||
</p>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>Google Drive: it only accesses files it has created itself (a backup copy of my photo archive).</li>
|
||||
<li>Google Photos: it only uploads my photos and adds them to albums it created; it cannot see anything else in the library.</li>
|
||||
<li>No data from anyone else is collected. Nothing is sold, shared with third parties, used for advertising or used to train AI models.</li>
|
||||
<li>Google Drive: verktyget kommer bara åt filer som det själv har skapat (en säkerhetskopia av mitt bildarkiv).</li>
|
||||
<li>Google Foto: verktyget laddar bara upp mina bilder och lägger dem i album som det själv har skapat; det kan inte se något annat i biblioteket.</li>
|
||||
<li>Inga uppgifter om någon annan samlas in. Ingenting säljs, delas med tredje part, används för reklam eller används för att träna AI-modeller.</li>
|
||||
<li>
|
||||
Its use of information received from Google APIs adheres to the
|
||||
<a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener noreferrer" class="text-white underline hover:text-gray-300">Google API Services User Data Policy</a>,
|
||||
Verktygets användning av information från Googles API:er följer
|
||||
<a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener noreferrer" class={link}>Google API Services User Data Policy</a>,
|
||||
inklusive kraven på begränsad användning (Limited Use).
|
||||
</li>
|
||||
</ul>
|
||||
<p lang="en" class="text-base text-gray-400">
|
||||
In English: townhouse-backup's use of information received from Google APIs adheres to the
|
||||
<a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener noreferrer" class={link}>Google API Services User Data Policy</a>,
|
||||
including the Limited Use requirements.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Your rights</h2>
|
||||
<h2 class="text-2xl font-bold text-white">Dina rättigheter</h2>
|
||||
<p>
|
||||
Under the GDPR you can ask what personal data I hold about you, and have it corrected or deleted — just email me.
|
||||
You can also complain to the Swedish Authority for Privacy Protection
|
||||
(<a href="https://www.imy.se/" target="_blank" rel="noopener noreferrer" class="text-white underline hover:text-gray-300">IMY</a>).
|
||||
Enligt GDPR kan du fråga vilka personuppgifter jag har om dig och få dem rättade eller raderade – mejla mig bara.
|
||||
Du kan också klaga hos Integritetsskyddsmyndigheten
|
||||
(<a href="https://www.imy.se/" target="_blank" rel="noopener noreferrer" class={link}>IMY</a>).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// src/pages/sitemap-0.xml.ts -> /sitemap-0.xml
|
||||
// Listar sajtens riktiga sidor (inte 404). Ingen <lastmod>: vi har inget
|
||||
// pålitligt ändringsdatum per sida, och ett påhittat datum gör mer skada än nytta.
|
||||
import type { APIRoute } from 'astro';
|
||||
import { sitePaths } from '../lib/pages';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const escapeXml = (s: string) =>
|
||||
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const base = site ?? new URL('https://imagesbyolofsson.se');
|
||||
const urls = sitePaths()
|
||||
.map((p) => ` <url><loc>${escapeXml(new URL(p, base).toString())}</loc></url>`)
|
||||
.join('\n');
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls}
|
||||
</urlset>
|
||||
`;
|
||||
return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// src/pages/sitemap-index.xml.ts -> /sitemap-index.xml
|
||||
// Egen sitemap utan @astrojs/sitemap (inga nya beroenden).
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const base = site ?? new URL('https://imagesbyolofsson.se');
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap><loc>${new URL('/sitemap-0.xml', base).toString()}</loc></sitemap>
|
||||
</sitemapindex>
|
||||
`;
|
||||
return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
// src/pages/tjanster.astro
|
||||
// Vad Linus fotograferar, för vem, var, pris, leverans och hur man bokar + FAQ.
|
||||
// Allt från src/data/business.ts och src/data/faq.ts – okända fakta visas inte.
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import { business, basedIn, formatSek } from '../data/business';
|
||||
import { faq } from '../data/faq';
|
||||
import heroImg from '../assets/images/3d-scroll/img2.jpg';
|
||||
|
||||
const priceFrom = formatSek(business.priceFromSek);
|
||||
const packages = business.packages ?? [];
|
||||
const portalHost = new URL(business.portal.url).host;
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
title="Tjänster och bokning – fotografering | Images by Olofsson"
|
||||
description={`Fotograf Linus Olofsson i ${basedIn()}: porträtt, natur, stad, makro, norrsken och familjefester. Så bokar du via kundportalen, leverans i privat galleri och vanliga frågor.`}
|
||||
path="/tjanster"
|
||||
image={heroImg}
|
||||
imageAlt="Veteranbil med tända strålkastare på en stadsgata i kvällsljus"
|
||||
breadcrumb="Tjänster"
|
||||
faq={faq}
|
||||
>
|
||||
<WaveBackground opacity="opacity-70" />
|
||||
|
||||
<div class="container mx-auto px-4 py-12 md:py-20 relative z-10 max-w-5xl">
|
||||
|
||||
<header class="text-center mb-16">
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6">Tjänster och bokning</h1>
|
||||
<p class="text-gray-300 text-lg md:text-xl leading-relaxed max-w-3xl mx-auto">
|
||||
Jag heter {business.person.name} och är fotografen bakom {business.brand}, baserad i {basedIn()}.
|
||||
Här ser du vad jag fotograferar, hur en bokning går till och hur du får dina bilder.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4 mt-8">
|
||||
<a href={business.portal.bookingUrl} class="inline-block px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Skicka en förfrågan</a>
|
||||
<a href="#vanliga-fragor" class="inline-block px-8 py-3 border border-white text-white font-bold rounded-full hover:bg-white/10 transition">Vanliga frågor</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="vad" class="mb-20">
|
||||
<h2 id="vad" class="text-3xl font-bold text-white mb-8">Det här fotograferar jag</h2>
|
||||
<ul class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{business.genres.map((g) => (
|
||||
<li id={g.id} data-trail-box class="bg-neutral-900/80 backdrop-blur-sm p-6 rounded-xl border border-white/10">
|
||||
<h3 class="text-xl font-bold text-white mb-2">{g.name}</h3>
|
||||
<p class="text-gray-300">{g.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p class="text-gray-300 mt-6">
|
||||
Se exempel i min <a href="/portfolio" class="text-white underline underline-offset-4 hover:text-gray-300">portfolio</a>.
|
||||
Har du något annat i tankarna? Beskriv det i din förfrågan, så återkommer jag med ett förslag.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="var-pris" class="mb-20 grid gap-6 md:grid-cols-2">
|
||||
<div data-trail-box class="bg-neutral-900/80 backdrop-blur-sm p-6 md:p-8 rounded-xl border border-white/10">
|
||||
<h2 id="var-pris" class="text-2xl font-bold text-white mb-3">Var</h2>
|
||||
{business.serviceArea ? (
|
||||
<p class="text-gray-300">Jag utgår från {basedIn()} och tar uppdrag i {business.serviceArea}.</p>
|
||||
) : (
|
||||
<p class="text-gray-300">Jag är baserad i {basedIn()}. Kontakta mig så stämmer vi av om jag kan fotografera där du befinner dig.</p>
|
||||
)}
|
||||
</div>
|
||||
<div data-trail-box class="bg-neutral-900/80 backdrop-blur-sm p-6 md:p-8 rounded-xl border border-white/10">
|
||||
<h2 class="text-2xl font-bold text-white mb-3">Pris</h2>
|
||||
{priceFrom ? (
|
||||
<p class="text-gray-300">Fotografering från <strong class="text-white">{priceFrom}</strong>. Det slutliga priset står i offerten.</p>
|
||||
) : (
|
||||
<p class="text-gray-300">Priset beror på uppdraget. Kontakta mig eller skicka en förfrågan, så får du en offert för just det du vill ha fotograferat.</p>
|
||||
)}
|
||||
{packages.length > 0 && (
|
||||
<ul class="mt-4 space-y-3">
|
||||
{packages.map((p) => (
|
||||
<li class="text-gray-300">
|
||||
<strong class="text-white">{p.name}</strong>
|
||||
{formatSek(p.priceSek) && <span> – {formatSek(p.priceSek)}</span>}
|
||||
{p.includes.length > 0 && <span class="block text-sm text-gray-400">{p.includes.join(', ')}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="boka" class="mb-20">
|
||||
<h2 id="boka" class="text-3xl font-bold text-white mb-8">Så går en bokning till</h2>
|
||||
<ol class="space-y-4">
|
||||
{business.bookingSteps.map((step, i) => (
|
||||
<li class="flex gap-4 bg-neutral-900/70 backdrop-blur-sm p-5 rounded-xl border border-white/10">
|
||||
<span class="shrink-0 w-10 h-10 rounded-full border border-white/30 flex items-center justify-center font-bold text-white">{i + 1}</span>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-white">{step.title}</h3>
|
||||
<p class="text-gray-300">{step.text}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p class="text-gray-300 mt-6">
|
||||
Leverans: bilderna levereras i ett privat galleri i kundportalen
|
||||
({portalHost}).{business.deliveryTime && ` Normal leveranstid är ${business.deliveryTime}.`}
|
||||
</p>
|
||||
<a href={business.portal.bookingUrl} class="inline-block mt-6 px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-200 transition">Skicka en förfrågan i kundportalen</a>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="vanliga-fragor" class="mb-12">
|
||||
<h2 id="vanliga-fragor" class="text-3xl font-bold text-white mb-8">Vanliga frågor</h2>
|
||||
<div class="space-y-4">
|
||||
{faq.map((item) => (
|
||||
<details class="group bg-neutral-900/80 backdrop-blur-sm rounded-xl border border-white/10 p-5 open:border-white/20">
|
||||
<summary class="cursor-pointer list-none flex justify-between items-center gap-4">
|
||||
<h3 class="text-lg font-bold text-white">{item.question}</h3>
|
||||
<span aria-hidden="true" class="text-gray-400 transition-transform group-open:rotate-45 text-2xl leading-none">+</span>
|
||||
</summary>
|
||||
<p class="text-gray-300 mt-3">{item.answer}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
<p class="text-gray-300 mt-8">
|
||||
Fler frågor? Mejla <a href={`mailto:${business.email}`} class="text-white underline underline-offset-4 hover:text-gray-300">{business.email}</a>
|
||||
eller skriv på Instagram <a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="text-white underline underline-offset-4 hover:text-gray-300">{business.social.instagram.handle}</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</MainLayout>
|
||||
Reference in New Issue
Block a user