Skip to content

Global Layout & SEO

The layout lives at src/layouts/Layout.astro. Every page wraps itself in this layout. All SEO, meta tags, JSON-LD, and analytics are wired here once.


export interface Props {
title?: string;
description?: string;
ogImage?: string;
canonical?: string;
noindex?: boolean;
}
const {
title = "Studio Name - Tagline",
description = "Default meta description.",
ogImage = `${siteUrl}/og-image.jpg`,
canonical = `${siteUrl}${Astro.url.pathname}`,
noindex = false,
} = Astro.props;

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<!-- Canonical: suppress on noindex pages -->
{!noindex && <link rel="canonical" href={canonical} />}
{noindex && <meta name="robots" content="noindex, nofollow" />}
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={canonical} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={siteName} />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
<!-- Geo (local businesses) -->
<meta name="geo.region" content="IN-MH" />
<meta name="geo.placename" content="City, State" />
<meta name="geo.position" content="lat;lng" />
<meta name="ICBM" content="lat, lng" />
<!-- Favicon: only reference files that actually exist -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Add these only after PNG files exist in /public/: -->
<!-- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" /> -->
<!-- <link rel="apple-touch-icon" sizes="256x256" href="/favicon-256.png" /> -->
<!-- JSON-LD -->
<script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />
<!-- Head slot (page-specific JSON-LD, etc.) -->
<slot name="head" />
<!-- GA4: uncomment after client provides ID -->
<!-- <script is:inline async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"></script> -->
<!-- <script is:inline>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-XXXXXXXX');</script> -->
</head>

const siteUrl = "https://domain.com";
const siteName = "Business Name";
const schema = {
"@context": "https://schema.org",
"@type": ["LocalBusiness", "ProfessionalService"],
name: siteName,
description: "One sentence description.",
url: siteUrl,
telephone: "+91XXXXXXXXXX",
email: "hello@domain.com",
address: {
"@type": "PostalAddress",
streetAddress: "Street, Locality",
addressLocality: "City",
addressRegion: "State",
postalCode: "XXXXXX",
addressCountry: "IN",
},
geo: {
"@type": "GeoCoordinates",
latitude: "XX.XXXX",
longitude: "XX.XXXX",
},
openingHoursSpecification: [{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
opens: "09:00",
closes: "19:00",
}],
areaServed: [{ "@type": "Country", name: "India" }],
priceRange: "$$",
aggregateRating: {
"@type": "AggregateRating",
ratingValue: "4.9", // <-- REAL numbers from client
reviewCount: "38", // <-- REAL numbers from client
bestRating: "5",
worstRating: "1",
},
logo: { "@type": "ImageObject", url: `${siteUrl}/favicon.svg` },
image: `${siteUrl}/og-image.jpg`,
sameAs: [
"https://instagram.com/handle",
"https://linkedin.com/company/slug",
],
};

Add to the homepage via the head slot:

---
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map(f => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
};
---
<Layout title="...">
<script slot="head" is:inline type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
<!-- page content -->
</Layout>

5-6 FAQ questions minimum to trigger Google’s accordion rich results.


Page type Canonical
Normal page ${siteUrl}${Astro.url.pathname}
noindex=true (checkout, payment-return) Suppressed - do not output a canonical

Reason: a canonical on a noindex page creates a “Non-Indexable Canonical” warning in Screaming Frog and Google Search Console.


  • File: public/og-image.jpg
  • Size: 1200 x 630 pixels
  • Format: JPG (smaller file size than PNG, social platforms accept it)
  • After changing: use the Facebook debugger to force WhatsApp/social re-scrape