Overview

JSON-LD is the structured-data format Google recommends, and a static build can generate it from page frontmatter so markup and content cannot drift. This guide chooses the types for a typical content site, emits the blocks into <head>, keeps URLs consistent, and validates them. The rules are in structured-data; the long menu of types is in schema-markup-deep.

Prerequisites

  • A build step that controls <head> (Astro, Next.js static export, Quartz, Hugo, Eleventy).
  • Frontmatter with title, summary, a published date, a modified date, and an author.
  • One canonical URL per page; every URL in the markup must match it exactly.

Steps

1. Pick the types

  • WebSite once, on the homepage, with name and alternateName for site-name display. Google retired the sitelinks search box in November 2024, so SearchAction earns nothing.
  • Organization once, on the homepage or /about, with logo and sameAs.
  • BreadcrumbList on every page below the homepage.
  • Article or BlogPosting on editorial pages.
  • FAQPage and HowTo no longer produce Google rich results; ship them only when the page really is Q&A or steps.

Map each template to one primary entity. Do not stack five types on a page.

2. Generate the block from frontmatter

export function articleJsonLd(page: Page, site: Site) {
  return {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: page.title,
    description: page.summary,
    author: { "@type": "Person", name: page.author },
    datePublished: page.datePublished,
    dateModified: page.lastUpdated,
    mainEntityOfPage: `${site.baseUrl}/${page.path}`,
  }
}

Keep one helper per entity type and compose them in the template.

3. Inject it server-side

<script type="application/ld+json">{ ...serialized object... }</script>

The block must be in the HTML the server sends. Many crawlers do not run client JavaScript. Emit it from a head component or template, not from markdown, and use one <script> per entity.

4. Make three URLs match

<link rel="canonical">, og:url, and mainEntityOfPage must be the same string, including the trailing-slash form. Build all three from one canonicalUrl variable.

5. Validate

Run the Rich Results Test (search.google.com/test/rich-results) for eligibility and the Schema Markup Validator (validator.schema.org) for syntax. Automate a check that parses every ld+json block in the build output and asserts required fields; this repo does it in scripts/validate-jsonld.mjs.

Verify it worked

curl -s https://yourdomain.com/foo | grep -c 'application/ld+json'     # at least 1
curl -s https://yourdomain.com/foo | python3 -c "import sys,re,json; [json.loads(m) for m in re.findall(r'<script[^>]*ld\+json[^>]*>(.*?)</script>', sys.stdin.read(), re.S)]; print('parses')"

Common errors

  • The Rich Results Test reports the page as not eligible for rich results but lists no errors. The type has no Google rich result (for example FAQPage, HowTo, or WebSite with SearchAction); use the Schema Markup Validator to confirm the markup parses. Google lists no required properties for Article; headline, author, datePublished, and image are recommended.
  • Markup describes content the page does not show. Google treats that as a spam signal; mark up only visible content.
  • Canonical forms differ (trailing slash). Normalize site-wide.
  • Two Article blocks on one page. Keep one entity per type.
  • Block injected by client-side React or Vue. Render it on the server.

FAQ

Does FAQPage schema still get rich results in Google?

No. FAQPage and HowTo no longer produce Google rich results, so ship them only when the page really is Q&A or steps.

Should I add a SearchAction to my WebSite schema?

No. Google retired the sitelinks search box in November 2024, so SearchAction earns nothing; give WebSite a name and alternateName instead.

Can I inject JSON-LD with client-side JavaScript?

Avoid it. The block must be in the HTML the server sends because many crawlers do not run client JavaScript, so emit it from a head component or template.

Which URLs in structured data have to match?

<link rel="canonical">, og:url, and mainEntityOfPage must be the same string, including the trailing-slash form. Build all three from one canonicalUrl variable.

Which tool should I use to validate JSON-LD?

Use the Rich Results Test for eligibility and the Schema Markup Validator for syntax, then automate a build check that parses every ld+json block and asserts required fields.