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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
This commit is contained in:
Linus Olofsson
2026-10-27 19:13:07 +01:00
co-authored by Claude Opus 5.5
parent 30e385ec7f
commit f452d6d237
7 changed files with 198 additions and 0 deletions
+9
View File
@@ -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()],
},
+41
View File
@@ -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
+12
View File
@@ -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)));
}
+35
View File
@@ -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>
+63
View File
@@ -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' } });
};
+23
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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' } });
};
+15
View File
@@ -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' } });
};