Compare commits
5
Commits
d841cf98b5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e499ae533e | ||
|
|
69cb075618 | ||
|
|
63baa35b92 | ||
|
|
63a4b3a848 | ||
|
|
c760e11c4c |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 714 KiB |
@@ -12,7 +12,7 @@ export interface Props {
|
||||
description: string;
|
||||
path?: string;
|
||||
pageType?: 'WebPage' | 'AboutPage' | 'ContactPage' | 'CollectionPage';
|
||||
/** Namn i brödsmulan för undersidor, t.ex. "Tjänster". */
|
||||
/** Namn i brödsmulan för undersidor, t.ex. "Portfolio". */
|
||||
breadcrumb?: string;
|
||||
faq?: FaqItem[];
|
||||
/** Absolut URL till sidans huvudbild (og:image). */
|
||||
@@ -80,7 +80,7 @@ const graph: Record<string, unknown>[] = [
|
||||
sameAs: [business.social.linkedin, business.social.lightroom],
|
||||
},
|
||||
{
|
||||
'@type': 'ProfessionalService',
|
||||
'@type': 'Organization',
|
||||
'@id': ids.business,
|
||||
name: business.brand,
|
||||
url: `${site}/`,
|
||||
@@ -89,31 +89,9 @@ const graph: Record<string, unknown>[] = [
|
||||
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 },
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
// src/components/T.astro – en text på svenska och engelska (Linus 2026-11-06: en svensk och en engelsk version,
|
||||
// SV/EN-knappen i menyn växlar). Båda finns i sidan; CSS i MainLayout visar den som hör till <html lang>.
|
||||
// <T sv="Om mig" en="About me" /> enkel text
|
||||
// <T><Fragment slot="sv">…</Fragment><Fragment slot="en">…</Fragment></T> text med länkar eller taggar
|
||||
export interface Props {
|
||||
sv?: string;
|
||||
en?: string;
|
||||
}
|
||||
const { sv, en } = Astro.props;
|
||||
---
|
||||
<span class="i18n-sv"><slot name="sv">{sv}</slot></span><span class="i18n-en" lang="en"><slot name="en">{en}</slot></span>
|
||||
+2
-60
@@ -1,63 +1,5 @@
|
||||
// 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';
|
||||
// Typ för vanliga frågor (FAQPage-JSON-LD). Frågorna om bokning, offert och gallerier togs bort 2026-11-06:
|
||||
// Linus erbjuder inga tjänster, bara posters och samarbeten.
|
||||
|
||||
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}).`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ImageMetadata } from 'astro';
|
||||
import { getImage } from 'astro:assets';
|
||||
import SEO from '../components/SEO.astro';
|
||||
import SchemaOrg from '../components/SchemaOrg.astro';
|
||||
import T from '../components/T.astro';
|
||||
import type { FaqItem } from '../data/faq';
|
||||
import { business } from '../data/business';
|
||||
import defaultOgImage from '../assets/images/3d-scroll/img5.jpg';
|
||||
@@ -13,7 +14,7 @@ export interface Props {
|
||||
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. */
|
||||
/** Kanonisk sökväg, t.ex. "/portfolio". Utelämnas = härleds från URL:en. */
|
||||
path?: string;
|
||||
image?: ImageMetadata;
|
||||
imageAlt?: string;
|
||||
@@ -43,13 +44,14 @@ const ogImageUrl = new URL(og.src, site).toString();
|
||||
const ogWidth = Number(og.attributes.width) || undefined;
|
||||
const ogHeight = Number(og.attributes.height) || undefined;
|
||||
|
||||
// Samma meny på alla sidor (Linus 2026-11-06): Portfolio, Posters, Om mig, Kontakt.
|
||||
const nav = [
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/posters', label: 'Posters' },
|
||||
{ href: '/tjanster', label: 'Tjänster' },
|
||||
{ href: '/about', label: 'Om mig' },
|
||||
{ href: '/contact', label: 'Kontakt' },
|
||||
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
|
||||
{ href: '/posters', sv: 'Posters', en: 'Posters' },
|
||||
{ href: '/about', sv: 'Om mig', en: 'About me' },
|
||||
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
|
||||
];
|
||||
const langBtn = 'rounded-full px-2.5 py-1 text-xs font-semibold transition-colors text-gray-400 hover:text-white aria-pressed:bg-white aria-pressed:text-neutral-900';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -62,6 +64,24 @@ const nav = [
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180.png" />
|
||||
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
|
||||
<!-- Språket väljs innan sidan ritas (ingen blinkning): ?lang=, sedan besökarens val (samma nyckel som kundportalen),
|
||||
sedan webbläsaren. Svenska/nordiska webbläsare får svenska. -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var lang = null;
|
||||
try { var q = new URLSearchParams(location.search).get('lang'); if (q === 'sv' || q === 'en') lang = q; } catch (e) {}
|
||||
if (!lang) { try { var s = localStorage.getItem('ibo_lang'); if (s === 'sv' || s === 'en') lang = s; } catch (e) {} }
|
||||
if (!lang) {
|
||||
var b = ((navigator.languages && navigator.languages[0]) || navigator.language || 'sv').toLowerCase();
|
||||
lang = /^(sv|nb|no|da|nn)/.test(b) ? 'sv' : 'en';
|
||||
}
|
||||
document.documentElement.lang = lang;
|
||||
})();
|
||||
</script>
|
||||
<style is:global>
|
||||
html[lang="en"] .i18n-sv { display: none !important; }
|
||||
html:not([lang="en"]) .i18n-en { display: none !important; }
|
||||
</style>
|
||||
<SEO
|
||||
title={title}
|
||||
description={description}
|
||||
@@ -93,11 +113,17 @@ const nav = [
|
||||
Images by Olofsson
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-4 md:gap-6">
|
||||
<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>
|
||||
<li><a href={item.href} class="hover:text-white transition"><T sv={item.sv} en={item.en} /></a></li>
|
||||
))}
|
||||
</ul>
|
||||
<div class="inline-flex items-center rounded-full border border-neutral-700 p-0.5" role="group" aria-label="Språk / Language">
|
||||
<button type="button" data-lang-set="sv" class={langBtn} aria-label="Svenska">SV</button>
|
||||
<button type="button" data-lang-set="en" class={langBtn} aria-label="English">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -107,26 +133,49 @@ const nav = [
|
||||
|
||||
<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">
|
||||
{business.brand} – {business.person.jobTitle.toLowerCase()} {business.person.name}, {business.country.name}
|
||||
{business.brand} – <T sv={`fotograf ${business.person.name}, Sverige`} en={`photographer ${business.person.name}, Sweden`} />
|
||||
· <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()} {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>
|
||||
<p>© {new Date().getFullYear()} {business.brand}. <T sv="Alla rättigheter förbehållna." en="All rights reserved." /> · <a href="/privacy" class="text-gray-300 hover:text-white transition-colors"><T sv="Integritetspolicy" en="Privacy policy" /></a> · <button type="button" data-consent-open class="text-gray-300 hover:text-white transition-colors"><T sv="Cookie-inställningar" en="Cookie settings" /></button></p>
|
||||
</footer>
|
||||
|
||||
<!-- 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">
|
||||
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>
|
||||
<T sv="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."
|
||||
en="I use the Meta Pixel to see how visitors from Instagram and Facebook use the site. It sets cookies and sends data to Meta." />
|
||||
<a href="/privacy" class="text-white underline hover:text-gray-300"><T sv="Läs mer" en="Read more" /></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">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>
|
||||
<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"><T sv="Avböj" en="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"><T sv="Godkänn" en="Accept" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
// SV/EN-knappen: sparar valet (samma nyckel som kundportalen), byter <html lang> och meddelar sidans skript.
|
||||
(function () {
|
||||
function show(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
document.querySelectorAll('[data-lang-set]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', b.getAttribute('data-lang-set') === lang ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('[data-aria-sv]').forEach(function (el) {
|
||||
el.setAttribute('aria-label', el.getAttribute(lang === 'en' ? 'data-aria-en' : 'data-aria-sv'));
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('ibo-lang', { detail: lang }));
|
||||
}
|
||||
show(document.documentElement.lang === 'en' ? 'en' : 'sv');
|
||||
document.querySelectorAll('[data-lang-set]').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var lang = b.getAttribute('data-lang-set');
|
||||
try { localStorage.setItem('ibo_lang', lang); } catch (e) {}
|
||||
show(lang);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var KEY = 'ibo-consent';
|
||||
var banner = document.querySelector('[data-consent-banner]');
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// src/lib/url.ts
|
||||
// Gör om en byggsökväg till kanonisk form: "/", "/about", "/tjanster".
|
||||
// Gör om en byggsökväg till kanonisk form: "/", "/about", "/portfolio".
|
||||
// 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$/, '');
|
||||
|
||||
+10
-10
@@ -2,33 +2,33 @@
|
||||
// src/pages/404.astro -> dist/404.html (servern svarar med status 404)
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import T from '../components/T.astro';
|
||||
|
||||
const links = [
|
||||
{ href: '/', label: 'Startsidan' },
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/posters', label: 'Posters' },
|
||||
{ href: '/tjanster', label: 'Tjänster och bokning' },
|
||||
{ href: '/contact', label: 'Kontakt' },
|
||||
{ href: '/', sv: 'Startsidan', en: 'Home' },
|
||||
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
|
||||
{ href: '/posters', sv: 'Posters', en: 'Posters' },
|
||||
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
|
||||
];
|
||||
---
|
||||
|
||||
<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."
|
||||
description="Sidan du letade efter finns inte. Gå till startsidan, portfolion eller posters av 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-sm text-gray-400 uppercase tracking-widest mb-4"><T sv="Fel 404" en="Error 404" /></p>
|
||||
<h1 class="text-4xl md:text-6xl font-bold mb-6"><T sv="Sidan hittades inte" en="Page not found" /></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:
|
||||
<T sv="Sidan du letade efter finns inte längre, eller så har adressen ändrats. Här kan du hitta tillbaka:" en="The page you were looking for no longer exists, or its address has changed. Find your way back here:" />
|
||||
</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>
|
||||
<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"><T sv={l.sv} en={l.en} /></a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
+23
-43
@@ -3,12 +3,21 @@
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import ThreeDScroll from '../components/ThreeDScroll.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import T from '../components/T.astro';
|
||||
import { business, basedIn, genreList } from '../data/business';
|
||||
|
||||
const footerButtons = [
|
||||
{ href: '/posters', sv: 'Posters', en: 'Posters' },
|
||||
{ href: '/contact', sv: 'Samarbeten', en: 'Collaborations' },
|
||||
{ href: '/about', sv: 'Mer om mig', en: 'More about me' },
|
||||
{ href: '/portfolio', sv: 'Portfolio', en: 'Portfolio' },
|
||||
{ href: '/contact', sv: 'Kontakt', en: 'Contact' },
|
||||
];
|
||||
---
|
||||
|
||||
<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.`}
|
||||
description={`Fotograf Linus Olofsson (Images by Olofsson) i ${basedIn()}: porträtt, natur och djur, stad, makro, norrsken och familjefester. Fotografierna finns som posters.`}
|
||||
path="/"
|
||||
>
|
||||
|
||||
@@ -43,7 +52,7 @@ import { business, basedIn, genreList } from '../data/business';
|
||||
|
||||
</div>
|
||||
|
||||
<p class="text-white/80 mt-4 text-base md:text-lg tracking-widest uppercase">Fotograf {business.person.name} · {basedIn()}</p>
|
||||
<p class="text-white/80 mt-4 text-base md:text-lg tracking-widest uppercase"><T sv={`Fotograf ${business.person.name} · ${basedIn()}`} en={`Photographer ${business.person.name} · Örebro, Sweden`} /></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>
|
||||
|
||||
@@ -58,9 +67,9 @@ import { business, basedIn, genreList } from '../data/business';
|
||||
<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">Letar du inspiration?</h2>
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6"><T sv="Letar du inspiration?" en="Looking for 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">
|
||||
Se min portfolio
|
||||
<T sv="Se min portfolio" en="See my portfolio" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -86,52 +95,23 @@ import { business, basedIn, genreList } from '../data/business';
|
||||
</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">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
|
||||
<h2 class="text-3xl md:text-4xl text-white font-bold mb-6"><T sv="Redo att samarbeta?" en="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">
|
||||
<T sv="Kontakta mig" en="Contact me" />
|
||||
</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>
|
||||
<!-- Länkar längst ner (Linus 2026-11-06): bara knappar, avlånga och raka, ingen text och inga textlänkar. -->
|
||||
<nav id="lankar" aria-label="Mer på sajten" data-aria-sv="Mer på sajten" data-aria-en="More on the site" class="relative z-30 bg-neutral-900 border-t border-white/10 py-12 md:py-16">
|
||||
<ul class="container mx-auto px-4 flex flex-wrap justify-center gap-3 md:gap-4">
|
||||
{footerButtons.map((b) => (
|
||||
<li><a href={b.href} class="inline-flex min-w-[11rem] items-center justify-center rounded-md border border-white/20 bg-white/[0.03] px-8 py-3 text-xs md:text-sm font-semibold uppercase tracking-[0.2em] text-white transition-colors hover:border-white hover:bg-white hover:text-neutral-900"><T sv={b.sv} en={b.en} /></a></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>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
// SVG Längd
|
||||
|
||||
+1
-13
@@ -2,15 +2,13 @@
|
||||
// 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';
|
||||
import { business, basedIn, genreList } from '../data/business';
|
||||
|
||||
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}`,
|
||||
@@ -22,31 +20,21 @@ export const GET: APIRoute = ({ site }) => {
|
||||
`- 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`,
|
||||
`- [Posters](${url('/posters')}): fotografier som posters, välj storlek och material`,
|
||||
`- [Kontakt och samarbeten](${url('/contact')}): samarbeten, reklam och sponsring`,
|
||||
'',
|
||||
'## Vanliga frågor',
|
||||
'',
|
||||
...faq.map((f) => `- ${f.question} ${f.answer}`),
|
||||
'',
|
||||
'## Profiler',
|
||||
'',
|
||||
`- [Instagram](${business.social.instagram.url})`,
|
||||
|
||||
+68
-30
@@ -6,6 +6,7 @@ import exifr from 'exifr';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import T from '../components/T.astro';
|
||||
import { business, genreList } from '../data/business';
|
||||
import { portfolioMeta } from '../data/portfolio-meta';
|
||||
|
||||
@@ -74,6 +75,9 @@ const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([re
|
||||
const fullDate = hasDate
|
||||
? dateObj.toLocaleDateString('sv-SE', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
: '';
|
||||
const fullDateEn = hasDate
|
||||
? dateObj.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
: '';
|
||||
|
||||
const shutterSpeed = formatShutterSpeed(exifData?.ExposureTime);
|
||||
const focalLength = exifData?.FocalLength ? `${Math.round(exifData.FocalLength)}mm` : null;
|
||||
@@ -100,7 +104,7 @@ const galleryItems = await Promise.all(Object.entries(imageFiles).map(async ([re
|
||||
|
||||
return {
|
||||
img: file.default, key, alt, title: meta.title ?? null,
|
||||
year, month, monthIndex, fullDate, model: exifData?.Model,
|
||||
year, month, monthIndex, fullDate, fullDateEn, model: exifData?.Model,
|
||||
fStop, shutterSpeed, iso, focalLength,
|
||||
large, srcset: jpg.srcSet.attribute,
|
||||
statKey,
|
||||
@@ -111,7 +115,8 @@ const uniqueYears = [...new Set(galleryItems.map((item: any) => item.year))].sor
|
||||
const allMonths = galleryItems.map((item: any) => ({ name: item.month, index: item.monthIndex }));
|
||||
const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).values()]
|
||||
.sort((a: any, b: any) => a.index - b.index)
|
||||
.map((item: any) => item.name);
|
||||
.map((item: any) => ({ sv: item.name, en: new Date(2000, item.index, 1).toLocaleString('en-GB', { month: 'long' }) }));
|
||||
const genresEn = 'portraits and people, nature and animals, city and architecture, macro and details, northern lights, and celebrations and family occasions';
|
||||
---
|
||||
|
||||
<MainLayout
|
||||
@@ -130,32 +135,35 @@ 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-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>
|
||||
<p class="text-gray-300 max-w-2xl mx-auto"><T sv={`Ett urval av mina bilder i olika stilar – ${genreList()}.`} en={`A selection of my photos in different styles – ${genresEn}.`} /></p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-6 mb-12">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<button type="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"><T sv="Alla år" en="All years" /></button>
|
||||
{uniqueYears.map(year => (
|
||||
<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 type="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"><T sv="Alla månader" en="All months" /></button>
|
||||
{uniqueMonths.map(month => (
|
||||
<button type="button" class="filter-btn capitalize" data-filter={month} data-type="month">{month}</button>
|
||||
<button type="button" class="filter-btn capitalize" data-filter={month.sv} data-type="month"><T sv={month.sv} en={month.en} /></button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Bilder</h2>
|
||||
<h2 class="sr-only"><T sv="Bilder" en="Photos" /></h2>
|
||||
<div class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6" id="gallery-grid">
|
||||
{galleryItems.map((item: any, index: number) => (
|
||||
<a
|
||||
href={item.large}
|
||||
class="gallery-item block break-inside-avoid relative group overflow-hidden rounded-lg cursor-pointer bg-neutral-800 shadow-md"
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={item.fullDate ? `Visa bilden från ${item.fullDate}` : 'Visa bilden'}
|
||||
data-aria-sv={item.fullDate ? `Visa bilden från ${item.fullDate}` : 'Visa bilden'}
|
||||
data-aria-en={item.fullDateEn ? `View the photo from ${item.fullDateEn}` : 'View the photo'}
|
||||
class="gallery-item protected 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.large}
|
||||
@@ -170,6 +178,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
data-shutter={item.shutterSpeed}
|
||||
data-focal={item.focalLength}
|
||||
data-date={item.fullDate}
|
||||
data-date-en={item.fullDateEn}
|
||||
>
|
||||
<Image
|
||||
src={item.img}
|
||||
@@ -179,20 +188,23 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
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"
|
||||
draggable="false"
|
||||
class="pointer-events-none select-none w-full h-auto object-cover transform transition duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<!-- Genomskinligt lager över bilden (som på Posters): högerklick och långt tryck träffar inte bilden. -->
|
||||
<div class="absolute inset-0" aria-hidden="true"></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>
|
||||
{item.fullDate && <p class="text-white font-bold text-lg"><T sv={item.fullDate} en={item.fullDateEn} /></p>}
|
||||
<p class="text-xs text-gray-300 uppercase tracking-widest mt-1"><T sv="Visa bild" en="View photo" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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 type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300">Visa alla bilder</button>
|
||||
<p class="text-xl"><T sv="Inga bilder hittades för denna tidpunkt." en="No photos found for this time." /></p>
|
||||
<button type="button" id="reset-btn" class="mt-4 text-white underline hover:text-gray-300"><T sv="Visa alla bilder" en="Show all photos" /></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -200,16 +212,17 @@ 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 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">
|
||||
<button type="button" id="lb-close" aria-label="Stäng" data-aria-sv="Stäng" data-aria-en="Close" 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="" 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">
|
||||
<div class="protected flex-1 relative bg-black flex items-center justify-center overflow-hidden group">
|
||||
<img id="lb-img" src="" alt="" sizes="100vw" draggable="false" class="pointer-events-none select-none max-h-full max-w-full w-full h-full object-contain p-2 md:p-4" />
|
||||
<div class="absolute inset-0" aria-hidden="true"></div>
|
||||
<button type="button" id="lb-prev" aria-label="Föregående bild" data-aria-sv="Föregående bild" data-aria-en="Previous photo" 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 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">
|
||||
<button type="button" id="lb-next" aria-label="Nästa bild" data-aria-sv="Nästa bild" data-aria-en="Next photo" 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>
|
||||
@@ -220,13 +233,10 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
<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">
|
||||
<a href="/posters" class="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">
|
||||
Köp som poster
|
||||
</a>
|
||||
<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-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"><T sv="Brännvidd" en="Focal length" /></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"><T sv="Bländare" en="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-400 uppercase text-[10px] font-bold"><T sv="Slutartid" en="Shutter speed" /></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>
|
||||
@@ -334,7 +344,8 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
lbImg.alt = item.dataset.alt || '';
|
||||
stat('open', statsItem(item));
|
||||
|
||||
if(lbDate) lbDate.innerText = [item.dataset.title, item.dataset.date].filter(Boolean).join(' – ');
|
||||
const en = document.documentElement.lang === 'en';
|
||||
if(lbDate) lbDate.innerText = (en ? item.dataset.dateEn : item.dataset.date) || '';
|
||||
if(lbModel) lbModel.innerText = item.dataset.model || '';
|
||||
if(lbFocal) lbFocal.innerText = item.dataset.focal || '--';
|
||||
if(lbFstop) lbFstop.innerText = item.dataset.fstop || '--';
|
||||
@@ -352,8 +363,28 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
updateLightboxContent();
|
||||
}
|
||||
|
||||
// Skydd som på Posters: ingen högerklick-meny, ingen dragning, inget långt tryck på bilderna.
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
if ((e.target as HTMLElement).closest('.protected')) e.preventDefault();
|
||||
});
|
||||
document.addEventListener('dragstart', (e) => {
|
||||
if ((e.target as HTMLElement).closest('.protected')) e.preventDefault();
|
||||
});
|
||||
// Byter besökaren språk med ljuslådan öppen följer datumet med.
|
||||
document.addEventListener('ibo-lang', () => {
|
||||
if (!lightbox?.classList.contains('hidden')) updateLightboxContent();
|
||||
});
|
||||
|
||||
// Event Listeners
|
||||
items.forEach((item) => {
|
||||
item.addEventListener('keydown', (e) => {
|
||||
const key = (e as KeyboardEvent).key;
|
||||
if (key !== 'Enter' && key !== ' ') return;
|
||||
e.preventDefault();
|
||||
updateVisibleItems();
|
||||
const index = visibleItems.indexOf(item);
|
||||
if (index !== -1) openLightbox(index);
|
||||
});
|
||||
item.classList.add('visible-item');
|
||||
item.addEventListener('click', (e) => {
|
||||
// Länken pekar på den stora bilden (crawlbar/öppningsbar i ny flik),
|
||||
@@ -385,7 +416,7 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
// Filter Listeners
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const target = btn as HTMLElement;
|
||||
const type = target.dataset.type;
|
||||
const value = target.dataset.filter;
|
||||
if (!type || !value) return;
|
||||
@@ -418,6 +449,13 @@ const uniqueMonths = [...new Map(allMonths.map(item => [item.name, item])).value
|
||||
<style>
|
||||
@reference "../styles/global.css";
|
||||
|
||||
.protected, .protected img {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
+82
-5
@@ -2,6 +2,7 @@
|
||||
// src/pages/privacy.astro
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import WaveBackground from '../components/WaveBackground.astro';
|
||||
import T from '../components/T.astro';
|
||||
import { business } from '../data/business';
|
||||
|
||||
const link = 'text-white underline hover:text-gray-300';
|
||||
@@ -19,10 +20,10 @@ const link = 'text-white underline hover:text-gray-300';
|
||||
<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">Integritetspolicy</h1>
|
||||
<p class="text-gray-400 mb-10">Senast uppdaterad: 27 oktober 2026</p>
|
||||
<h1 class="text-4xl md:text-5xl font-bold leading-tight mb-2"><T sv="Integritetspolicy" en="Privacy policy" /></h1>
|
||||
<p class="text-gray-400 mb-10"><T sv="Senast uppdaterad: 6 november 2026" en="Last updated: 6 November 2026" /></p>
|
||||
|
||||
<div class="space-y-8 text-gray-300 text-lg leading-relaxed">
|
||||
<div class="i18n-sv space-y-8 text-gray-300 text-lg leading-relaxed">
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Vem som är ansvarig</h2>
|
||||
@@ -35,7 +36,7 @@ const link = 'text-white underline hover:text-gray-300';
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Den här webbplatsen</h2>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>Det finns inga användarkonton och inga kontaktformulär på den här webbplatsen. Bokningar görs i den separata kundportalen.</li>
|
||||
<li>Det finns inga användarkonton och inga kontaktformulär på den här webbplatsen. Beställningar av posters och samarbeten sköts 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>
|
||||
Webbplatsen levereras via Cloudflare, som behandlar tekniska uppgifter som din IP-adress för att leverera
|
||||
@@ -63,7 +64,7 @@ const link = 'text-white underline hover:text-gray-300';
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Anonym statistik för portfolion</h2>
|
||||
<p>
|
||||
För att se vilka bilder som uppskattas räknar portfolion hur många gånger varje bild öppnas eller laddas ner, per dag.
|
||||
För att se vilka bilder som uppskattas räknar portfolion hur många gånger varje bild öppnas, 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>
|
||||
@@ -101,6 +102,82 @@ const link = 'text-white underline hover:text-gray-300';
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="i18n-en space-y-8 text-gray-300 text-lg leading-relaxed" lang="en">
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Who is responsible</h2>
|
||||
<p>
|
||||
{business.brand} is run by {business.person.name}, Sweden. Questions about privacy:
|
||||
<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>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>There are no user accounts and no contact forms on this website. Poster orders and collaborations are handled in the separate customer portal.</li>
|
||||
<li>If you e-mail me, your address and message are only used to answer you and are not shared with anyone.</li>
|
||||
<li>
|
||||
The website is delivered through Cloudflare, which processes technical data such as your IP address to deliver
|
||||
and protect the website. See <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer" class={link}>Cloudflare's privacy policy</a>.
|
||||
</li>
|
||||
<li>If you appear in one of my photos and want it removed, e-mail me and I will remove it.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Meta Pixel (only with your consent)</h2>
|
||||
<p>
|
||||
If you click "Accept" in the cookie box, the website loads the Meta Pixel from Meta Platforms Ireland Ltd.
|
||||
I use it to see how visitors coming from my Instagram and Facebook use the website.
|
||||
</p>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>It sets cookies (for example <code>_fbp</code>) and sends data to Meta, such as the pages you visit, your IP address and information about your browser.</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={link}>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 through "Cookie settings" at the bottom of every page. Your choice is stored in your own browser.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Anonymous statistics for the portfolio</h2>
|
||||
<p>
|
||||
To see which photos are appreciated, the portfolio counts how many times each photo is opened, per day.
|
||||
Only the count is stored, on my own server in Sweden. No cookies, no IP address, no browser information and nothing
|
||||
that can identify you. This happens without consent because no data about you is collected.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Photo backup app (Google)</h2>
|
||||
<p>
|
||||
"townhouse-backup" is a private tool that only I use to back up my own photos to my own Google account.
|
||||
</p>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>Google Drive: the tool only accesses files it has created itself (a backup of my photo archive).</li>
|
||||
<li>Google Photos: the tool only uploads my photos and puts them in albums it has created itself; it cannot see anything else in the library.</li>
|
||||
<li>No data about anyone else is collected. Nothing is sold, shared with third parties, used for advertising or used to train AI models.</li>
|
||||
<li>
|
||||
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>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-2xl font-bold text-white">Your rights</h2>
|
||||
<p>
|
||||
Under the GDPR you can ask what personal data I hold about you and have it corrected or deleted – just e-mail me.
|
||||
You can also complain to the Swedish Authority for Privacy Protection
|
||||
(<a href="https://www.imy.se/" target="_blank" rel="noopener noreferrer" class={link}>IMY</a>).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
// 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