← back to blog · · essay

Build-time JSON-LD vs. runtime JSON-LD

Three failure modes of hand-rolled structured data — and the build-time fix that ships in your repo, not someone else's.

There’s a thread of category-leading marketing that argues: hand-rolled JSON-LD drifts, has coverage gaps, ships invalid graphs, and therefore you should subscribe to a runtime schema-generation API. The first half of the claim is correct. The conclusion is a non-sequitur. The same failure modes are solvable at build time, in your repo, with no runtime dependency on anyone.

The three failure modes are real

Drift is when the JSON-LD says one thing and the page renders another.

The Article.headline declares “How we ship in 8 weeks” but the <h1> reads “How we ship a product in 8 weeks.” The Organization.name is “Acme, Inc.” but the visible brand is “Acme.” The Offer.price says “$99” but the cart shows “$89” because the team forgot to update the schema after a sale.

AI assistants quoting the JSON-LD — and humans reading the page — disagree about what the site actually said. The model that cites the page now has a citation that doesn’t match its source.

Coverage gaps are when some pages have JSON-LD and others don’t.

The homepage emits Organization and WebSite. The pricing page emits Offer. The blog has Article on three of seven posts because the engineer who set them up finished four and ran out of time. The product pages have nothing because the schema was never wired into the catalog template.

A crawler that builds an entity graph from your site comes away with a partial picture. Every page that should have schema and doesn’t is a row missing from the dataset the crawler is trying to assemble.

Invalid graphs are silent failures.

The Organization is missing a required name field. The Article.author is a string instead of a Person object. The BreadcrumbList numbers its items starting from 0 instead of 1. The page parses fine in the browser — no JS error, no visible broken layout — and ships. A strict parser quietly drops the bad block; a permissive one tries to use a malformed graph. Either way, the content is invisible to whatever was supposed to read it.

These failure modes compound. A site that’s been live for three years with hand-rolled JSON-LD pasted into Liquid templates has all three problems by default.

Response A: runtime JSON-LD APIs

One answer goes: install a runtime service that inspects each URL’s rendered content and emits a fresh, valid JSON-LD graph on every request. Because the service generates the graph dynamically, it doesn’t drift; because it runs against every URL it configures coverage; because it validates before emitting, it can’t ship invalid.

All three failure modes — solved.

The cost is what the service costs you.

Cancel, and the schema disappears. The structured data is rendered by a service running on someone else’s infrastructure. When the subscription ends, every page that relied on the runtime emission has no JSON-LD. The fix is also the lock-in.

The schema-generation service is a third party doing work on the customer’s domain at request time. That means:

  • A single point of failure on every page load. If the schema API has an outage, the structured-data <script> tags are empty on every page request during the outage. Crawlers that hit your site during the window see a site with no JSON-LD.
  • Request-time latency. Every page either waits for the schema service or renders without it on cold cache. Either choice has a cost.
  • A third-party scraper on your domain. The service has to fetch every URL to inspect content. That’s outbound bandwidth and processing time bookmarked against the vendor’s plan tier.
  • A procurement objection. For any buyer running a security review — regulated industry, large enterprise, security-conscious infra team — a third-party service rendering content into the production HTML triggers DPA review, sub-processor list updates, and a sign-off cycle that takes weeks.

The runtime API isn’t wrong about the failure modes. It’s wrong that you need a vendor in your request path to fix them.

Response B: build-time JSON-LD with typed source

The build-time approach is older, plainer, and structurally stronger. It works like this.

Drift — fixed by typing the data source

Define the structured data the page is going to render as a typed TypeScript object that is the source of truth for both the JSON-LD block and the visible content:

// pricing.ts
export const SETUP_SKU: Sku = {
  name: 'Setup',
  priceUsdCents: 500000,
  description: 'One-time engagement. Adapts an existing codebase for AI-agent readiness.',
};

The pricing page reads this object to render its visible content. The JSON-LD builder reads the same object to emit the Offer:

buildOfferJsonLd({
  name: SETUP_SKU.name,
  price: SETUP_SKU.priceUsdCents / 100,
  priceCurrency: 'USD',
});

The JSON-LD cannot drift from the visible content, because both are derived from the same source. Update the source, both update at the next build. If the source is wrong, the page is wrong and the schema is wrong identically — which is at least honest.

Coverage gaps — fixed by layout-level enforcement

Make JSON-LD a layout-level concern. In Astro:

---
// Base.astro
interface Props {
  jsonLd: Record<string, unknown>[];
}
const { jsonLd } = Astro.props;
---
<head>
  {jsonLd.map((block) => (
    <script type="application/ld+json" set:html={JSON.stringify(block)} />
  ))}
</head>

Every page that uses the Base layout is required to pass jsonLd. The build fails if a key page tries to render without one. CI is enforcing what humans cannot reliably enforce.

Invalid graphs — fixed by CI validation

Run ajv against the JSON-LD blocks during the build. Use schema-dts types to get TypeScript-level validation before the JSON ever serialises. Either fails the build before merge.

import { validateJsonLd } from '@sitesforai/shared/validators';

const result = validateJsonLd(JSON.stringify(jsonLd));
if (!result.ok) {
  throw new Error(result.errors.join('\n'));
}

Invalid graphs don’t reach production because the CI gate doesn’t let them. Same outcome as the runtime API’s “the API refuses to emit invalid” guarantee — without the runtime API.

What the studio actually ships

A Setup engagement that includes the JSON-LD layer ships, in PRs into your repository:

  • Typed data sources for each schema type the site uses (Organization, Person, Article, BreadcrumbList, Offer, FAQPage, WebSite).
  • Layout slot in the base template that requires JSON-LD on every key page.
  • ajv + schema-dts CI validation that fails the build on invalid graphs.
  • A failing-test fixture for any specific JSON-LD shape that the team can extend when the schema needs to change.
  • The day-30 re-scan documenting that the JSON-LD coverage, validity, and alignment with visible content all moved measurably.

The customer ends up with structured data that lives in their repo, validates in their CI, and doesn’t disappear if anyone goes quiet.

When runtime is genuinely OK

There’s one case where the runtime argument has some weight: sites with a hundred thousand products and daily price changes that cannot realistically go through a build-and-deploy cycle for every update. There, on-the-fly schema generation has a cost case.

Even then, the answer isn’t necessarily a third-party API. Most modern frameworks support incremental static regeneration (ISR) or on-demand revalidation: change a product, the page regenerates, the JSON-LD updates with it. Build-time semantics, runtime freshness, no vendor in the request path. Same outcome, code still in your repo.

The Setup-scope flavour of “runtime is OK” is rare and architecturally distinctive. For the typical B2B SaaS marketing site, the catalog page, the blog, the pricing page, the docs — build-time JSON-LD covers everything.

What the Checker tells you

Run the Free Checker on your site. The relevant signals:

  • Signal #9 — JSON-LD parses cleanly via ajv. Fails if any block in your HTML is invalid.
  • Signal #10 — Organization complete. Fails if name, url, logo, or sameAs are missing.
  • Signal #11 — Person (founder) linked from Organization.founder. Fails if the identity chain isn’t reciprocal.
  • Signal #12 — Reciprocal sameAs verification across domains. Fails if LinkedIn, GitHub, or other linked profiles don’t link back.
  • Signal #13 — WebSite schema present.
  • Signal #14 — BreadcrumbList on inner pages.
  • Signal #35 — FAQPage schema if the page has Q&A blocks.
  • Signal #36 — Article with dateline on blog posts.

That’s seven signals across the Identity and Citation groups testing exactly the “hand-rolled JSON-LD has problems” claim. If your site fails them, that’s a diagnostic. The fix isn’t a subscription; the fix is the build-time pipeline above, shipped as code in your repo.

The studio’s internal headless engine adds one more check the light-path Free Checker can’t do: a comparison of JSON-LD field values to visible page content. Does Article.headline match the <h1>? Does Organization.name match the visible brand? Does Offer.price match the price the customer would actually see? That drift check needs a rendered DOM, so it runs as part of a Setup audit and the retainer’s monthly scan, alongside the AI-bot UA diff — not in the free light-path scan.

The studio recommendation

Build-time first. Code in your repo. Validated in CI. Audited annually.

Whichever approach you pick, run the Free Checker first. Then decide whether the fixes look like work you want to do yourselves or work you want shipped as PRs.

Run free check →

Talk to the studio →