From f452d6d2373f643c58e96cbc7bbc7fcefa14ce8b Mon Sep 17 00:00:00 2001 From: Linus Olofsson Date: Tue, 27 Oct 2026 19:13:07 +0100 Subject: [PATCH] feat(seo): site config, sitemap, robots.txt, llms.txt and a real 404 page - astro.config: site, trailingSlash "never", build.format "file". - /sitemap-index.xml + /sitemap-0.xml as prerendered endpoints (no new dependency); pages are derived from src/pages/*.astro, 404 excluded. - robots.txt: all bots allowed, AI crawlers/training bots explicitly allowed (Linus's decision), /download/ disallowed, sitemap line. - /llms.txt generated from business.ts, incl. usage-with-attribution note. - 404.astro in Swedish with links back, noindex. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7 --- astro.config.mjs | 9 +++++ public/robots.txt | 41 ++++++++++++++++++++++ src/lib/pages.ts | 12 +++++++ src/pages/404.astro | 35 +++++++++++++++++++ src/pages/llms.txt.ts | 63 ++++++++++++++++++++++++++++++++++ src/pages/sitemap-0.xml.ts | 23 +++++++++++++ src/pages/sitemap-index.xml.ts | 15 ++++++++ 7 files changed, 198 insertions(+) create mode 100644 public/robots.txt create mode 100644 src/lib/pages.ts create mode 100644 src/pages/404.astro create mode 100644 src/pages/llms.txt.ts create mode 100644 src/pages/sitemap-0.xml.ts create mode 100644 src/pages/sitemap-index.xml.ts diff --git a/astro.config.mjs b/astro.config.mjs index 2b45c70..df833d6 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,6 +2,15 @@ import { defineConfig } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ + // Kanonisk adress: canonical, og:url, sitemap och JSON-LD byggs från denna. + site: 'https://imagesbyolofsson.se', + // En URL per sida: /about (aldrig /about/). Servern 301:ar /about/ -> /about. + trailingSlash: 'never', + build: { + // about.astro -> dist/about.html (inte dist/about/index.html). + // nginx/Caddy serverar /about via try_files $uri.html. + format: 'file', + }, vite: { plugins: [tailwindcss()], }, diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..20c0216 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,41 @@ +# robots.txt – https://imagesbyolofsson.se +# +# Policy (beslut av Linus 2026-10-27): ALLA botar är välkomna, även AI-crawlers +# och träningsbotar. Innehållet får användas av AI-assistenter med hänvisning +# till Images by Olofsson (se /llms.txt). Cloudflare-sidan (AI Crawl Control / +# "Block AI bots" / hanterad robots.txt) ställs in separat så att den inte +# blockerar eller skriver över detta. + +User-agent: * +Allow: / +# Lösenordsskyddad nedladdning av originalfiler – inget att indexera. +Disallow: /download/ + +# AI-crawlers och träningsbotar: uttryckligen tillåtna. +User-agent: GPTBot +User-agent: OAI-SearchBot +User-agent: ChatGPT-User +User-agent: ClaudeBot +User-agent: Claude-User +User-agent: Claude-SearchBot +User-agent: anthropic-ai +User-agent: CCBot +User-agent: Google-Extended +User-agent: Applebot +User-agent: Applebot-Extended +User-agent: Bytespider +User-agent: Amazonbot +User-agent: meta-externalagent +User-agent: meta-externalfetcher +User-agent: FacebookBot +User-agent: PerplexityBot +User-agent: Perplexity-User +User-agent: cohere-ai +User-agent: Diffbot +User-agent: YouBot +User-agent: DuckAssistBot +User-agent: MistralAI-User +Allow: / +Disallow: /download/ + +Sitemap: https://imagesbyolofsson.se/sitemap-index.xml diff --git a/src/lib/pages.ts b/src/lib/pages.ts new file mode 100644 index 0000000..77379c8 --- /dev/null +++ b/src/lib/pages.ts @@ -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))); +} diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..484f2fc --- /dev/null +++ b/src/pages/404.astro @@ -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' }, +]; +--- + + + + +
+

Fel 404

+

Sidan hittades inte

+

+ Sidan du letade efter finns inte längre, eller så har adressen ändrats. Här kan du hitta tillbaka: +

+
    + {links.map((l) => ( +
  • + {l.label} +
  • + ))} +
+
+
diff --git a/src/pages/llms.txt.ts b/src/pages/llms.txt.ts new file mode 100644 index 0000000..9993512 --- /dev/null +++ b/src/pages/llms.txt.ts @@ -0,0 +1,63 @@ +// 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})`, + `- 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' } }); +}; diff --git a/src/pages/sitemap-0.xml.ts b/src/pages/sitemap-0.xml.ts new file mode 100644 index 0000000..cbb24fa --- /dev/null +++ b/src/pages/sitemap-0.xml.ts @@ -0,0 +1,23 @@ +// src/pages/sitemap-0.xml.ts -> /sitemap-0.xml +// Listar sajtens riktiga sidor (inte 404). Ingen : 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, '"'); + +export const GET: APIRoute = ({ site }) => { + const base = site ?? new URL('https://imagesbyolofsson.se'); + const urls = sitePaths() + .map((p) => ` ${escapeXml(new URL(p, base).toString())}`) + .join('\n'); + const body = ` + +${urls} + +`; + return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } }); +}; diff --git a/src/pages/sitemap-index.xml.ts b/src/pages/sitemap-index.xml.ts new file mode 100644 index 0000000..7c1c787 --- /dev/null +++ b/src/pages/sitemap-index.xml.ts @@ -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 = ` + + ${new URL('/sitemap-0.xml', base).toString()} + +`; + return new Response(body, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } }); +};