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;Full <head> template
Section titled “Full <head> template”<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>JSON-LD schema object
Section titled “JSON-LD schema object”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", ],};FAQPage schema (homepage only)
Section titled “FAQPage schema (homepage only)”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.
Canonical URL rules
Section titled “Canonical URL rules”| 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.
OG image
Section titled “OG image”- 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