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
@@ -1,56 +1,127 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
const { title } = Astro.props;
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import { getImage } from 'astro:assets';
|
||||
import SEO from '../components/SEO.astro';
|
||||
import SchemaOrg from '../components/SchemaOrg.astro';
|
||||
import type { FaqItem } from '../data/faq';
|
||||
import { business } from '../data/business';
|
||||
import defaultOgImage from '../assets/images/3d-scroll/img5.jpg';
|
||||
|
||||
export interface Props {
|
||||
/** Unik svensk titel för sidan. */
|
||||
title: string;
|
||||
/** Unik svensk meta description, ca 120–160 tecken. */
|
||||
description: string;
|
||||
/** Kanonisk sökväg, t.ex. "/tjanster". Utelämnas = härleds från URL:en. */
|
||||
path?: string;
|
||||
image?: ImageMetadata;
|
||||
imageAlt?: string;
|
||||
pageType?: 'WebPage' | 'AboutPage' | 'ContactPage' | 'CollectionPage';
|
||||
/** Namn i brödsmulan (BreadcrumbList) för undersidor. */
|
||||
breadcrumb?: string;
|
||||
faq?: FaqItem[];
|
||||
noindex?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
path,
|
||||
image = defaultOgImage,
|
||||
imageAlt = 'Svartvit bild av en man som bär ett litet barn bland såpbubblor i motljus',
|
||||
pageType,
|
||||
breadcrumb,
|
||||
faq,
|
||||
noindex = false,
|
||||
} = Astro.props;
|
||||
|
||||
// og:image: ny JPG via Astros bildtjänst (EXIF/GPS rensas bort), absolut URL.
|
||||
const site = Astro.site ?? new URL(business.url);
|
||||
const og = await getImage({ src: image, width: 1200, format: 'jpg', quality: 82 });
|
||||
const ogImageUrl = new URL(og.src, site).toString();
|
||||
const ogWidth = Number(og.attributes.width) || undefined;
|
||||
const ogHeight = Number(og.attributes.height) || undefined;
|
||||
|
||||
const nav = [
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/tjanster', label: 'Tjänster' },
|
||||
{ href: '/about', label: 'Om mig' },
|
||||
{ href: '/contact', label: 'Kontakt' },
|
||||
];
|
||||
---
|
||||
|
||||
<html lang="sv" class="scroll-smooth"> <head>
|
||||
<!doctype html>
|
||||
<html lang="sv" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#171717" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180.png" />
|
||||
<title>{title}</title>
|
||||
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml" />
|
||||
<SEO
|
||||
title={title}
|
||||
description={description}
|
||||
path={path}
|
||||
imageUrl={ogImageUrl}
|
||||
imageWidth={ogWidth}
|
||||
imageHeight={ogHeight}
|
||||
imageAlt={imageAlt}
|
||||
noindex={noindex}
|
||||
/>
|
||||
{!noindex && (
|
||||
<SchemaOrg
|
||||
title={title}
|
||||
description={description}
|
||||
path={path}
|
||||
pageType={pageType}
|
||||
breadcrumb={breadcrumb}
|
||||
faq={faq}
|
||||
imageUrl={ogImageUrl}
|
||||
/>
|
||||
)}
|
||||
</head>
|
||||
<body class="bg-neutral-900 text-white font-sans antialiased min-h-screen flex flex-col">
|
||||
|
||||
<nav class="fixed top-0 w-full z-50 border-b border-white/10 py-6 bg-neutral-900/80 backdrop-blur-md transition-all duration-300">
|
||||
<div class="container mx-auto px-4 flex justify-between items-center">
|
||||
|
||||
<a href="/" class="text-2xl font-bold tracking-wider uppercase hover:text-gray-300 transition relative z-50">
|
||||
|
||||
<nav class="fixed top-0 w-full z-50 border-b border-white/10 py-4 md:py-6 bg-neutral-900/80 backdrop-blur-md transition-all duration-300">
|
||||
<div class="container mx-auto px-4 flex flex-wrap gap-y-2 justify-between items-center">
|
||||
|
||||
<a href="/" class="text-lg md:text-2xl font-bold tracking-wider uppercase hover:text-gray-300 transition relative z-50">
|
||||
Images by Olofsson
|
||||
</a>
|
||||
|
||||
<ul class="flex space-x-6 text-sm font-medium text-gray-400">
|
||||
<li><a href="/portfolio" class="hover:text-white transition">Portfolio</a></li>
|
||||
<li><a href="/about" class="hover:text-white transition">About</a></li>
|
||||
<li><a href="/contact" class="hover:text-white transition">Contact</a></li>
|
||||
<ul class="flex space-x-4 md:space-x-6 text-sm font-medium text-gray-300">
|
||||
{nav.map((item) => (
|
||||
<li><a href={item.href} class="hover:text-white transition">{item.label}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex-grow pt-24">
|
||||
<slot />
|
||||
<main class="flex-grow pt-28 md:pt-24">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-neutral-800 py-8 text-center text-gray-500 text-sm bg-neutral-900 relative z-40">
|
||||
<footer class="border-t border-neutral-800 py-8 text-center text-gray-400 text-sm bg-neutral-900 relative z-40">
|
||||
<p class="mb-2">
|
||||
Crafting digital experiences for
|
||||
<a href="https://lyxlimo.se/" target="_blank" rel="noopener noreferrer" data-glow-link="green" class="text-gray-400 hover:text-white transition-colors">lyxlimo.se</a>
|
||||
&
|
||||
<a href="https://karleksguiden.se/" target="_blank" rel="noopener noreferrer" data-glow-link="magenta" class="text-gray-400 hover:text-white transition-colors">karleksguiden.se</a>
|
||||
{business.brand} – {business.person.jobTitle.toLowerCase()} {business.person.name}, {business.country.name}
|
||||
· <a href={`mailto:${business.email}`} class="text-gray-300 hover:text-white transition-colors">{business.email}</a>
|
||||
· <a href={business.social.instagram.url} target="_blank" rel="noopener noreferrer" class="text-gray-300 hover:text-white transition-colors">Instagram</a>
|
||||
</p>
|
||||
<p>© {new Date().getFullYear()} Images by Olofsson. All rights reserved. · <a href="/privacy" class="text-gray-400 hover:text-white transition-colors">Privacy</a> · <button type="button" data-consent-open class="text-gray-400 hover:text-white transition-colors">Cookie settings</button></p>
|
||||
<p>© {new Date().getFullYear()} {business.brand}. Alla rättigheter förbehållna. · <a href="/privacy" class="text-gray-300 hover:text-white transition-colors">Integritetspolicy</a> · <button type="button" data-consent-open class="text-gray-300 hover:text-white transition-colors">Cookie-inställningar</button></p>
|
||||
</footer>
|
||||
|
||||
<!-- Cookie consent: Meta Pixel laddas forst efter "Accept" -->
|
||||
<!-- Cookie consent: Meta Pixel laddas först efter "Godkänn" -->
|
||||
<div data-consent-banner hidden class="fixed bottom-4 inset-x-4 md:left-auto md:right-6 md:max-w-md z-[60] bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-2xl p-5 shadow-2xl text-sm text-gray-300">
|
||||
<p class="mb-4">
|
||||
I use the Meta Pixel to see how visitors from Instagram and Facebook use this site. It sets cookies and sends data to Meta.
|
||||
<a href="/privacy" class="text-white underline hover:text-gray-300">Read more</a>
|
||||
Jag använder Meta Pixel för att se hur besökare från Instagram och Facebook använder sajten. Den sätter cookies och skickar data till Meta.
|
||||
<a href="/privacy" class="text-white underline hover:text-gray-300">Läs mer</a>
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors">Decline</button>
|
||||
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors">Accept</button>
|
||||
<button type="button" data-consent="denied" class="px-4 py-2 rounded-full border border-white/20 text-gray-300 hover:text-white hover:border-white/40 transition-colors">Avböj</button>
|
||||
<button type="button" data-consent="granted" class="px-4 py-2 rounded-full bg-white text-neutral-900 font-medium hover:bg-gray-200 transition-colors">Godkänn</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +149,7 @@ const { title } = Astro.props;
|
||||
/* End Meta Pixel Code */
|
||||
}
|
||||
|
||||
// Aterkallat samtycke: ta bort Metas forsta-parts-cookies.
|
||||
// Återkallat samtycke: ta bort Metas förstapartscookies.
|
||||
function clearMetaCookies() {
|
||||
['_fbp', '_fbc'].forEach(function (name) {
|
||||
document.cookie = name + '=; Max-Age=0; path=/';
|
||||
@@ -108,4 +179,4 @@ const { title } = Astro.props;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user