feat(seo): centralize sitewide JSON-LD in SEO.astro with Organization reviews
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m27s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Security Header Tests (push) Failing after 10m21s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m27s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m13s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m5s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 12m58s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 13m50s
E2E Test Suite / Form Interaction Tests (push) Failing after 14m43s
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled

Move the base Organization + LocalBusiness and WebSite structured data out of
BaseLayout's inline scripts into SEO.astro behind a new `base` prop, rendered
once from BaseLayout's <head> so every route (incl. pages without a
page-specific <SEO>) carries the canonical company schema exactly once.

- Organization/LocalBusiness now includes aggregateRating + Review nodes built
  from the three real homepage testimonials (Ajay Pruthi, Dhirendra Singh,
  Dilip Raj — all 5-star, reviewCount 3, rating 5.0).
- foundingDate 2022, telephone +91-9561417403, email admin@workroot.in,
  areaServed Bihar (AdministrativeArea) + India (Country).
- Description keeps the canonical proof numbers identical to the site:
  20+ projects, 11 districts, 50,000+ records, 99.9% uptime.
- Reusable Service node now links provider via @id to #organization and
  serves Bihar/India; page-specific schema (Service/FAQ/WebPage/...) stays
  opt-in and is gated off the base instance to prevent duplication.
- Homepage still emits a single FAQPage (from HomepageFaq) — no duplicate.
- Add jsdom devDependency required by scripts/validate-schema.js.

Verified: build succeeds; scripts/validate-schema.js passes (24 schemas, 0
issues, 0 warnings); rendered ld+json inspected on homepage, services, and
schema-less pages (privacy/terms/cookies/404).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
platform-mcp
2026-06-19 08:47:50 +05:30
co-authored by Claude Opus 4.8
parent be80d63f04
commit dccc5d3b7f
4 changed files with 735 additions and 116 deletions
+202 -11
View File
@@ -9,6 +9,11 @@ interface BreadcrumbItem {
interface Props {
type?: 'WebPage' | 'BlogPosting' | 'Article' | 'Service' | 'AboutPage' | 'ContactPage' | 'FAQPage';
// When true this instance emits ONLY the site-wide base schema
// (Organization + LocalBusiness and WebSite). Rendered once from
// BaseLayout's <head> so every route carries the canonical company data,
// independently of whether the page adds its own page-specific <SEO>.
base?: boolean;
breadcrumbs?: BreadcrumbItem[];
article?: {
author: string;
@@ -29,11 +34,173 @@ interface Props {
}>;
}
const { type = 'WebPage', breadcrumbs, article, service, faq } = Astro.props;
const { type = 'WebPage', base = false, breadcrumbs, article, service, faq } = Astro.props;
const baseUrl = 'https://workroot.in';
const currentUrl = Astro.url.href;
const orgName = 'WorkRoot IT Solutions LLP';
const orgId = `${baseUrl}/#organization`;
// ---------------------------------------------------------------------------
// Site-wide base schema — Organization + LocalBusiness and WebSite.
//
// Emitted once per page via `<SEO base />` rendered from BaseLayout's <head>,
// so every route (including pages without their own page-specific <SEO>)
// carries WorkRoot's canonical structured data.
//
// `review` / `aggregateRating` are built from the three real, attributable
// client testimonials shown on the homepage (see the `testimonials` array in
// src/pages/index.astro). All three are 5★, so the aggregate rating is 5.0
// over reviewCount = 3.
//
// Headline proof numbers in the description (20+ projects · 11 districts ·
// 50,000+ records · 99.9% uptime) mirror the canonical set in
// ProofNumbers.astro and the /services stats — keep them identical site-wide.
// ---------------------------------------------------------------------------
const testimonialReviews = [
{
author: 'Ajay Pruthi',
body: 'WorkRoot IT Solutions is very professional and responsive. They built PLNR Financing platform with great attention to detail. The Angular + Spring Boot stack works flawlessly and their ongoing support is excellent.',
},
{
author: 'Dhirendra Singh',
body: 'The FPC Cart e-commerce platform delivered by WorkRoot exceeded our expectations. Built with Astro and Golang, it loads incredibly fast and handles our product catalog seamlessly. Their team understood our business needs perfectly.',
},
{
author: 'Dilip Raj',
body: 'WorkRoot delivered our University Management System for IIT Patna on time and within budget. The Next.js + Spring Boot architecture is robust and scalable. They are a reliable technology partner for complex enterprise projects.',
},
];
const reviewNodes = testimonialReviews.map((review) => ({
"@type": "Review",
"author": {
"@type": "Person",
"name": review.author
},
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5",
"worstRating": "1"
},
"reviewBody": review.body
}));
const organizationSchema = {
"@context": "https://schema.org",
"@type": ["Organization", "LocalBusiness"],
"@id": orgId,
"name": orgName,
"alternateName": ["WorkRoot", "WorkRoot IT Solutions"],
"url": baseUrl,
"logo": {
"@type": "ImageObject",
"url": `${baseUrl}/logo.png`,
"width": "512",
"height": "512"
},
"image": `${baseUrl}/og-image.jpg`,
"description": "WorkRoot IT Solutions LLP builds production-grade web, mobile, ERP, and government IT systems — 20+ projects delivered across 11 districts, 50,000+ records managed, and 99.9% uptime on mission-critical government deployments.",
"foundingDate": "2022",
"founder": {
"@type": "Person",
"name": "Ajay Ghanwat"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
"addressLocality": "Satara",
"addressRegion": "MH",
"postalCode": "415503",
"addressCountry": "IN"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "17.7733632",
"longitude": "74.3309312"
},
"hasMap": "https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5",
"telephone": "+91-9561417403",
"email": "admin@workroot.in",
"contactPoint": [{
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "customer service",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "IN"
}, {
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "sales",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "IN"
}],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00",
"closes": "18:00"
}
],
"priceRange": "$$",
"currenciesAccepted": "INR",
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
"sameAs": [
"https://x.com/GhanwatAjay",
"https://www.linkedin.com/company/workroot",
"https://git.workroot.in/WorkRoot/",
"https://www.facebook.com/profile.php?id=61576503897897"
],
"areaServed": [
{
"@type": "AdministrativeArea",
"name": "Bihar"
},
{
"@type": "Country",
"name": "India"
}
],
"serviceType": ["Web Development", "Mobile App Development", "ERP & Enterprise Systems", "Government IT Solutions"],
"knowsAbout": ["Web Development", "Software Engineering", "Cloud Computing", "Artificial Intelligence", "Machine Learning", "Mobile Development"],
"slogan": "Transforming Ideas Into Digital Reality",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5.0",
"reviewCount": String(reviewNodes.length),
"bestRating": "5",
"worstRating": "1"
},
"review": reviewNodes
};
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": orgName,
"url": baseUrl,
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"publisher": {
"@type": "Organization",
"@id": orgId,
"name": orgName
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://workroot.in/blog?search={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "en-US"
};
// Get page title and description from parent layout props
const pageTitle = (Astro.props as any).title || 'WorkRoot IT Solutions';
const pageDescription = (Astro.props as any).description || 'Professional IT services including web development, cloud solutions, and digital transformation for businesses.';
@@ -90,7 +257,9 @@ const blogPostingSchema = article && type === 'BlogPosting' ? {
"inLanguage": "en-US"
} : null;
// Generate Service schema
// Generate Service schema — reusable node; each service page passes its own
// name/description via the `service` prop. Provider is linked by @id to the
// site-wide Organization node so the structured-data graph stays connected.
const serviceSchema = service ? {
"@context": "https://schema.org",
"@type": "Service",
@@ -98,10 +267,20 @@ const serviceSchema = service ? {
"description": service.description,
"provider": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"@id": orgId,
"name": orgName,
"url": baseUrl
},
"areaServed": "Worldwide",
"areaServed": [
{
"@type": "AdministrativeArea",
"name": "Bihar"
},
{
"@type": "Country",
"name": "India"
}
],
"serviceType": service.name
} : null;
@@ -214,30 +393,42 @@ const contactPageSchema = type === 'ContactPage' ? {
} : null;
---
{breadcrumbSchema && (
{/* Site-wide base schema — emitted only by the single `<SEO base />` rendered
from BaseLayout's <head>, so it appears exactly once on every page. */}
{base && (
<script type="application/ld+json" set:html={JSON.stringify(organizationSchema)} />
)}
{base && (
<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />
)}
{/* Page-specific schema — never emitted by the base instance, so there is no
duplication with the site-wide Organization/WebSite nodes above. */}
{!base && breadcrumbSchema && (
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
)}
{blogPostingSchema && (
{!base && blogPostingSchema && (
<script type="application/ld+json" set:html={JSON.stringify(blogPostingSchema)} />
)}
{serviceSchema && (
{!base && serviceSchema && (
<script type="application/ld+json" set:html={JSON.stringify(serviceSchema)} />
)}
{webPageSchema && (
{!base && webPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(webPageSchema)} />
)}
{faqPageSchema && (
{!base && faqPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(faqPageSchema)} />
)}
{aboutPageSchema && (
{!base && aboutPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(aboutPageSchema)} />
)}
{contactPageSchema && (
{!base && contactPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(contactPageSchema)} />
)}
+9 -105
View File
@@ -4,6 +4,7 @@
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import SEO from '../components/SEO.astro';
import Analytics from '../components/Analytics.astro';
import PWAInstallPrompt from '../components/PWAInstallPrompt.astro';
import WhatsAppFab from '../components/WhatsAppFab.astro';
@@ -103,111 +104,14 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#0891b2" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f172a" />
<!-- JSON-LD Structured Data - Organization + LocalBusiness Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": ["Organization", "LocalBusiness"],
"name": "WorkRoot IT Solutions LLP",
"alternateName": ["WorkRoot", "WorkRoot IT Solutions"],
"url": "https://workroot.in",
"logo": {
"@type": "ImageObject",
"url": "https://workroot.in/logo.png",
"width": "512",
"height": "512"
},
"image": "https://workroot.in/og-image.jpg",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"foundingDate": "2022",
"founder": {
"@type": "Person",
"name": "Ajay Ghanwat"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
"addressLocality": "Satara",
"addressRegion": "MH",
"postalCode": "415503",
"addressCountry": "IN"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "17.7733632",
"longitude": "74.3309312"
},
"hasMap": "https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5",
"telephone": "+91-9561417403",
"email": "admin@workroot.in",
"contactPoint": [{
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "customer service",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "Worldwide"
}, {
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "sales",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "Worldwide"
}],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00",
"closes": "18:00"
}
],
"priceRange": "$$",
"currenciesAccepted": "INR",
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
"sameAs": [
"https://x.com/GhanwatAjay",
"https://www.linkedin.com/company/workroot",
"https://git.workroot.in/WorkRoot/",
"https://www.facebook.com/profile.php?id=61576503897897"
],
"areaServed": {
"@type": "GeoShape",
"name": "Worldwide"
},
"serviceType": ["Web Development", "Mobile App Development", "AI & Machine Learning", "Cloud Services"],
"knowsAbout": ["Web Development", "Software Engineering", "Cloud Computing", "Artificial Intelligence", "Machine Learning", "Mobile Development"],
"slogan": "Transforming Ideas Into Digital Reality",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5.0",
"reviewCount": "20",
"bestRating": "5",
"worstRating": "1"
}
})} />
<!-- JSON-LD Structured Data - WebSite Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "WorkRoot IT Solutions LLP",
"url": "https://workroot.in",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"publisher": {
"@type": "Organization",
"name": "WorkRoot IT Solutions LLP"
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://workroot.in/blog?search={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "en-US"
})} />
<!-- JSON-LD Structured Data — site-wide base schema (Organization +
LocalBusiness and WebSite). Centralised in SEO.astro and emitted here
in `base` mode so it appears exactly once on EVERY route, including
pages that don't add their own page-specific <SEO> (404, privacy,
terms, cookies, offline). Page-specific schema (Service, FAQ,
BlogPosting, breadcrumbs, …) is opt-in via the slotted <SEO> each
page renders into the head. -->
<SEO base />
<!-- Preconnect to Google Fonts (must come before font stylesheet) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />