feat(seo): facts in src/data/business.ts, SEO + JSON-LD components in layout
- business.ts: single source of truth; unknown facts (city, area, prices, packages, phone, delivery time, experience, testimonials) are null and are skipped everywhere. faq.ts builds answers only from known facts. - portfolio-meta.ts: data hook for real per-image alt texts. - SEO.astro: title, description, canonical, Open Graph, Twitter, og:image (EXIF-free JPG via getImage, absolute URL). - SchemaOrg.astro: @graph with WebSite, Person, ProfessionalService (sameAs, ReserveAction to the customer portal, offer catalog without prices), WebPage, optional FAQPage and BreadcrumbList. - MainLayout: lang=sv, Swedish nav (adds Tjänster), consent banner and footer in Swedish, lyxlimo/karleksguiden line removed, info@ address. 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
ce5676f552
commit
30e385ec7f
@@ -0,0 +1,181 @@
|
||||
// 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 – okänt */
|
||||
city: string | null;
|
||||
/** Län/region – okänt */
|
||||
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 };
|
||||
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: 'info@imagesbyolofsson.se',
|
||||
phone: null,
|
||||
country: { code: 'SE', name: 'Sverige' },
|
||||
city: null,
|
||||
region: null,
|
||||
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/' },
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user