Methodology

JSON-LD Schema Checklist by Page Type — the eleven page types, schema each emits, validators in CI

Copy-pasteable checklist of the JSON-LD schema each of the eleven canonical page types must emit. Home, services hub, service detail, pricing, case studies hub, case study detail, about, blog hub, blog detail/pillar, contact. Includes the Schema.org Validator and Google Rich Results Test wiring for CI.

Schema is the one thing on your page the buyer never sees and the crawler reads first. Get it wrong and you get under-cited without ever knowing why; get it right on all eleven page types and the clarity compounds into citations.

Structured data is the first of the five AI-native properties from AI-Native Website in 4 Weeks. The page-type model says what each page is for; this checklist says what JSON-LD each page emits. Three reasons it matters: schema must be generated, not hand-authored, or it drifts the first time your catalog changes; AI answer engines (Perplexity, AI Overviews, ChatGPT search, Gemini) read structured data to disambiguate intent, and complete schema gets cited more than partial; and rich-result eligibility carries real organic-CTR lift — Glenn Gabe's rich-results case studies are full of it on properly marked pages.

The eleven page types — schema bundle per type

The mapping is canonical and small. Each row is a copy-pasteable checklist for the bundle that page type emits.

Page typeSchema types emittedNotes
homeOrganization, BreadcrumbList, FAQPage, WebSiteOrganization carries logo, sameAs, contactPoint
services_hubOrganization, BreadcrumbList, ItemList (Service nodes)ItemList enumerates the service detail pages
service_detailService, BreadcrumbList, FAQPage, AggregateRating (if reviews exist)Service carries provider, areaServed, serviceType
pricingService or Product, Offer, BreadcrumbList, FAQPageOffer carries price, priceCurrency, availability
case_studies_hubOrganization, BreadcrumbList, ItemList (Article nodes)ItemList enumerates case study detail pages
case_study_detailArticle, BreadcrumbList, mentions (client Organization), about (Service)Article author is the operator; client is mentions
aboutOrganization, BreadcrumbList, AboutPage, FAQPageAboutPage is the page type; Organization is the entity
blog_hubBlog, BreadcrumbList, ItemList (BlogPosting nodes)Blog carries name, publisher
blog_detailArticle, BreadcrumbList, FAQPage, HowTo (if procedural)Article carries author, datePublished, dateModified
pillarArticle, BreadcrumbList, FAQPage, HowTo (if implementation plan present)Same as blog_detail; distinguished by length + cluster role
contactContactPage, Organization, BreadcrumbList, ContactPointContactPoint carries contactType, telephone, email

Worked example — a service-detail page

The service_detail bundle is the most consequential for conversion. Four nodes in a single @graph:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Service",
      "@id": "https://nobrainermedia.com/services/google-ads-api-integration/#service",
      "name": "Google Ads API Integration",
      "description": "Server-side conversion attribution via the Google Ads API. Four-week implementation.",
      "provider": { "@type": "Organization", "@id": "https://nobrainermedia.com/#organization" },
      "serviceType": "Conversion Attribution",
      "areaServed": { "@type": "Country", "name": "United States" },
      "offers": {
        "@type": "Offer",
        "priceSpecification": { "@type": "PriceSpecification", "priceCurrency": "USD" }
      }
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://nobrainermedia.com/" },
        { "@type": "ListItem", "position": 2, "name": "Services", "item": "https://nobrainermedia.com/services/" },
        { "@type": "ListItem", "position": 3, "name": "Google Ads API Integration", "item": "https://nobrainermedia.com/services/google-ads-api-integration/" }
      ]
    },
    {
      "@type": "FAQPage",
      "mainEntity": [
        {
          "@type": "Question",
          "name": "How long does the implementation take?",
          "acceptedAnswer": {
            "@type": "Answer",
            "text": "Four weeks: milestone map (week 1), Worker scaffold (week 2), platform API wire-up (week 3), launch + verification (week 4)."
          }
        }
      ]
    },
    {
      "@type": "Organization",
      "@id": "https://nobrainermedia.com/#organization",
      "name": "No Brainer Media",
      "url": "https://nobrainermedia.com/",
      "logo": "https://nobrainermedia.com/logo.svg"
    }
  ]
}
</script>

Three patterns: a single @graph wrapper (cleaner and less fragile than multiple script blocks); @id references between nodes (the Service points at the Organization by @id instead of embedding it, deduplicating the org across the whole site); and an explicit @id on every canonical node (stable URIs like …/#organization give engines a canonical identifier for cross-page entity recognition).

The generator — fed by frontmatter

Generated, never hand-authored. Per-page-type Astro components read frontmatter and emit JSON-LD:

// src/components/schema/ServiceSchema.astro
---
const { service, organization, breadcrumbs, faqs } = Astro.props

const graph = [
  {
    "@type": "Service",
    "@id": `${service.canonical_url}#service`,
    name: service.title,
    description: service.summary,
    provider: { "@type": "Organization", "@id": `${organization.canonical_url}#organization` },
    serviceType: service.service_type,
    areaServed: { "@type": "Country", name: service.area_served },
  },
  {
    "@type": "BreadcrumbList",
    itemListElement: breadcrumbs.map((b, i) => ({
      "@type": "ListItem", position: i + 1, name: b.name, item: b.url,
    })),
  },
  faqs && {
    "@type": "FAQPage",
    mainEntity: faqs.map(f => ({
      "@type": "Question", name: f.question,
      acceptedAnswer: { "@type": "Answer", text: f.answer },
    })),
  },
].filter(Boolean)
---
<script type="application/ld+json" set:html={JSON.stringify({ "@context": "https://schema.org", "@graph": graph })} />

About thirty lines per type; eleven generators cover the taxonomy. Each page imports its generator and passes frontmatter; the schema emits at build time and is validated in CI.

Want this schema layer correct on every page from day one? Talk to the team. →

CI validation

Two validators fire before deploy: the Schema.org Validator against every page's HTML, and the Google Rich Results Test against a handful of sampled URLs (its API is rate-limited). Both must pass for the deploy to proceed; locally, pnpm validate:schema runs the Schema.org check without the CI overhead.

The five failure modes

  • Missing @context. Every block needs @context: "https://schema.org". The generator always emits it — never hand-author.
  • Invalid @type values. Typos like "Servce" or "FAQpage" fail silently. TypeScript types enforce valid values at compile time.
  • Type mismatches. A JS Date instead of an ISO 8601 string; "$" instead of "USD"; a phone without country code. A Zod schema validates every frontmatter field at build time.
  • Missing required properties. Service needs name + provider; Article needs author + headline; Offer needs priceCurrency. The generator throws a build error rather than emit incomplete schema.
  • Inconsistent @id across pages. The Organization @id on a service page must match the one on the home page. Both come from one canonical-URL helper, never per-page interpolation.

Closing

JSON-LD is the first AI-native property because it is the most consequential signal a page sends to crawlers and retrieval systems alike. Eleven page types, eleven generators, two CI validators, all fed from frontmatter so it never drifts — that is the foundation the rest of the stack stands on. The boring stack that prints does not skip schema. Variations away from the pattern above should be justified at code review.

Ready to ship the schema layer correctly on every page from day one? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.