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:
co-authored by
Claude Opus 5.5
parent
30e385ec7f
commit
f452d6d237
@@ -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,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,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,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>
|
||||
@@ -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' } });
|
||||
};
|
||||
@@ -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' } });
|
||||
};
|
||||
Reference in New Issue
Block a user