DEV Community

Nooralto1
Nooralto1

Posted on

Structured data that survives a redesign

Structured data that survives a redesign

Structured data breaks in a specific, boring way. Someone hand-writes a <script type="application/ld+json"> block in the page template, ships it, and it works. Six months later the site gets a redesign: the breadcrumb component changes, a category gets renamed, the templating engine moves from server includes to a component framework. Nobody touches the JSON-LD block, because nobody remembers it exists. It keeps rendering, keeps validating as syntactically correct JSON, and keeps describing a page that no longer matches what a visitor sees.

Search Console reports the drift weeks later, if at all, as a drop in a rich result rather than an error. By then it is a mystery to debug instead of a diff to review.

The block is duplicated data, not markup

The underlying problem is that the JSON-LD is a second, hand-maintained copy of information the page already has. The breadcrumb trail exists once, to render the visible nav. It exists again, retyped, inside the schema block. Two copies drift the moment one of them changes and the other does not get the memo.

The fix is to stop writing the block by hand and generate it from the same data the page uses to render. One function, one input, two outputs: the visible breadcrumb nav and the JSON-LD, both built from the same array.

// schema/breadcrumb.js
function buildBreadcrumbSchema(breadcrumbs, baseUrl) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: breadcrumbs.map((crumb, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: crumb.label,
      item: new URL(crumb.path, baseUrl).toString(),
    })),
  };
}

module.exports = { buildBreadcrumbSchema };
Enter fullscreen mode Exit fullscreen mode

breadcrumbs here is the exact array the nav component already receives as a prop. Rename a category and both outputs update from the same edit. There is no second place to remember.

Validate the shape before it ships

Generating the block from real data removes typos, but it does not stop someone from passing an empty array, or a crumb with no label, or a path that resolves to a relative URL instead of an absolute one. BreadcrumbList expects itemListElement as a non-empty list of ListItem entries, each with position, name, and item. A build step that checks that shape catches a broken page before it deploys instead of after Search Console notices.

// schema/validate.js
function assertValidBreadcrumbSchema(schema) {
  if (schema['@type'] !== 'BreadcrumbList') {
    throw new Error('expected @type BreadcrumbList');
  }
  if (!Array.isArray(schema.itemListElement) || schema.itemListElement.length === 0) {
    throw new Error('itemListElement must be a non-empty array');
  }
  schema.itemListElement.forEach((item, i) => {
    if (item['@type'] !== 'ListItem') throw new Error(`item ${i}: wrong @type`);
    if (typeof item.position !== 'number') throw new Error(`item ${i}: position must be a number`);
    if (typeof item.name !== 'string' || item.name.length === 0) {
      throw new Error(`item ${i}: missing name`);
    }
    if (typeof item.item !== 'string' || !/^https?:\/\//.test(item.item)) {
      throw new Error(`item ${i}: item must be an absolute URL`);
    }
  });
}

module.exports = { assertValidBreadcrumbSchema };
Enter fullscreen mode Exit fullscreen mode

Call it right after buildBreadcrumbSchema, in the same build script that writes the page out. A page with a broken schema should fail the build, not reach staging.

Test the rendered output, not the generator

Unit-testing the generator function proves the function works. It does not prove the schema actually lands on the page, survives the templating layer, and matches what a crawler would parse out of the final HTML. That needs a test against the rendered output itself.

// test/breadcrumb-schema.test.js
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { renderPage } = require('../render');

test('category page ships a valid BreadcrumbList', () => {
  const html = renderPage({
    type: 'category',
    slug: 'hiking-boots',
    breadcrumbs: [
      { label: 'Home', path: '/' },
      { label: 'Footwear', path: '/footwear' },
      { label: 'Hiking boots', path: '/footwear/hiking-boots' },
    ],
  });

  const match = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
  assert.ok(match, 'no JSON-LD script tag found in rendered HTML');

  const schema = JSON.parse(match[1]);
  assert.equal(schema['@type'], 'BreadcrumbList');
  assert.equal(schema.itemListElement.length, 3);
  assert.equal(schema.itemListElement[2].name, 'Hiking boots');
  for (const item of schema.itemListElement) {
    assert.equal(typeof item.position, 'number');
    assert.ok(item.name.length > 0);
    assert.match(item.item, /^https:\/\//);
  }
});
Enter fullscreen mode Exit fullscreen mode

This is the test that actually catches a redesign breaking things. If a future template swap drops the <script> tag entirely, or renames the prop the generator reads from, this fails on the next commit instead of on the next Search Console crawl.

Run one of these per page type that carries a schema block: product, article, FAQ, organization. Each type has its own required fields, so each gets its own assertions rather than one generic check.

Watch it after deploy too

A build-time test only proves the schema was correct at build time. A CDN caching an old bundle, a canary rollout serving two versions, or a third-party script mutating the DOM after load can still put stale or missing structured data in front of real crawlers. A lightweight periodic check, fetching a handful of key pages in production and re-running the same validator against whatever JSON-LD comes back, closes that gap without needing a full monitoring platform.

We wire this kind of build-time validation into new builds at Nooralto, so the schema block stops being a thing someone remembers to update by hand.

Structured data earns its keep when it is generated, not authored. Treat it as a projection of the page's own data, test that projection like any other output, and a redesign stops being a threat to it.

Written by the team at Nooralto, a web and SEO studio working out of Agadir and Paris.


Built by Nooralto — Nooralto.

Top comments (0)