Phone, prices, packages, delivery time and years stay null until Linus confirms them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
156 lines
4.9 KiB
Plaintext
156 lines
4.9 KiB
Plaintext
---
|
|
// src/components/SchemaOrg.astro
|
|
// JSON-LD (@graph): WebSite + Person (Linus) + ProfessionalService (Images by Olofsson)
|
|
// + WebPage för aktuell sida (+ FAQPage och BreadcrumbList när det är relevant).
|
|
// Allt hämtas från src/data/business.ts. null-värden utelämnas helt.
|
|
import { business, absoluteUrl, genreList } from '../data/business';
|
|
import type { FaqItem } from '../data/faq';
|
|
import { normalizePath } from '../lib/url';
|
|
|
|
export interface Props {
|
|
title: string;
|
|
description: string;
|
|
path?: string;
|
|
pageType?: 'WebPage' | 'AboutPage' | 'ContactPage' | 'CollectionPage';
|
|
/** Namn i brödsmulan för undersidor, t.ex. "Tjänster". */
|
|
breadcrumb?: string;
|
|
faq?: FaqItem[];
|
|
/** Absolut URL till sidans huvudbild (og:image). */
|
|
imageUrl?: string;
|
|
}
|
|
|
|
const { title, description, path, pageType = 'WebPage', breadcrumb, faq, imageUrl } = Astro.props;
|
|
|
|
const site = (Astro.site ?? new URL(business.url)).toString().replace(/\/$/, '');
|
|
const pagePath = normalizePath(path ?? Astro.url.pathname);
|
|
const pageUrl = absoluteUrl(pagePath);
|
|
|
|
const ids = {
|
|
website: `${site}/#website`,
|
|
person: `${site}/#linus-olofsson`,
|
|
business: `${site}/#images-by-olofsson`,
|
|
webpage: `${pageUrl}#webpage`,
|
|
breadcrumb: `${pageUrl}#breadcrumb`,
|
|
};
|
|
|
|
// Tar bort null/undefined/tomma listor så att okända fakta aldrig hamnar i JSON-LD.
|
|
function clean<T>(value: T): T {
|
|
if (Array.isArray(value)) {
|
|
return value.map(clean).filter((v) => v !== null && v !== undefined) as unknown as T;
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
if (v === null || v === undefined) continue;
|
|
const c = clean(v);
|
|
if (Array.isArray(c) && c.length === 0) continue;
|
|
out[k] = c;
|
|
}
|
|
return out as T;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Landet är känt (Sverige); ort och region läggs till när de fylls i business.ts.
|
|
const address = {
|
|
'@type': 'PostalAddress',
|
|
addressLocality: business.city,
|
|
addressRegion: business.region,
|
|
addressCountry: business.country.code,
|
|
};
|
|
|
|
const graph: Record<string, unknown>[] = [
|
|
{
|
|
'@type': 'WebSite',
|
|
'@id': ids.website,
|
|
url: `${site}/`,
|
|
name: business.brand,
|
|
inLanguage: 'sv-SE',
|
|
publisher: { '@id': ids.business },
|
|
},
|
|
{
|
|
'@type': 'Person',
|
|
'@id': ids.person,
|
|
name: business.person.name,
|
|
jobTitle: business.person.jobTitle,
|
|
url: absoluteUrl('/about'),
|
|
description: `${business.person.jobTitle} baserad i ${business.country.name}. ${business.recognitionThirdPerson}`,
|
|
knowsAbout: business.genres.map((g) => g.name),
|
|
worksFor: { '@id': ids.business },
|
|
sameAs: [business.social.linkedin, business.social.lightroom],
|
|
},
|
|
{
|
|
'@type': 'ProfessionalService',
|
|
'@id': ids.business,
|
|
name: business.brand,
|
|
url: `${site}/`,
|
|
description: `${business.person.name} fotograferar ${genreList()}.`,
|
|
email: business.email,
|
|
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 },
|
|
},
|
|
})),
|
|
},
|
|
},
|
|
];
|
|
|
|
const webpage: Record<string, unknown> = {
|
|
'@type': faq?.length ? [pageType, 'FAQPage'] : pageType,
|
|
'@id': ids.webpage,
|
|
url: pageUrl,
|
|
name: title,
|
|
description,
|
|
inLanguage: 'sv-SE',
|
|
isPartOf: { '@id': ids.website },
|
|
about: { '@id': ids.business },
|
|
primaryImageOfPage: imageUrl ? { '@type': 'ImageObject', url: imageUrl } : null,
|
|
breadcrumb: breadcrumb ? { '@id': ids.breadcrumb } : null,
|
|
mainEntity: faq?.length
|
|
? faq.map((item) => ({
|
|
'@type': 'Question',
|
|
name: item.question,
|
|
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
}))
|
|
: null,
|
|
};
|
|
graph.push(webpage);
|
|
|
|
if (breadcrumb) {
|
|
graph.push({
|
|
'@type': 'BreadcrumbList',
|
|
'@id': ids.breadcrumb,
|
|
itemListElement: [
|
|
{ '@type': 'ListItem', position: 1, name: 'Hem', item: `${site}/` },
|
|
{ '@type': 'ListItem', position: 2, name: breadcrumb, item: pageUrl },
|
|
],
|
|
});
|
|
}
|
|
|
|
const json = JSON.stringify(clean({ '@context': 'https://schema.org', '@graph': graph })).replace(/</g, '\\u003c');
|
|
---
|
|
|
|
<script type="application/ld+json" is:inline set:html={json} />
|