DEV Community

Daniel Pertu
Daniel Pertu

Posted on

One string is the page title, the h1 and the JSON-LD headline, which makes shortening it a content edit

Open cogniprep.app/cheating/shl and run this in the console:

const ld = [...document.querySelectorAll('script[type="application/ld+json"]')]
  .map(s => JSON.parse(s.textContent)).find(j => j['@type'] === 'Article');
const h1 = document.querySelector('h1').textContent.trim();
console.log({
  title: document.title,
  h1,
  headline: ld.headline,
  headlineMatchesH1: ld.headline === h1,
  descMatches: ld.description === document.querySelector('meta[name=description]').content,
});
Enter fullscreen mode Exit fullscreen mode

You get this:

title:              "Can You Cheat SHL Tests? The Verification Test | CogniPrep"
h1:                 "Can You Cheat SHL Tests? The Verification Test"
headline:           "Can You Cheat SHL Tests? The Verification Test"
headlineMatchesH1:  true
descMatches:        true
Enter fullscreen mode Exit fullscreen mode

Same for every page in the cluster. I checked five of them:

arctic-shores  title 57  h1 45  desc 152  headline==h1 true  desc==meta true
shl            title 58  h1 46  desc 146  headline==h1 true  desc==meta true
aon            title 42  h1 30  desc 141  headline==h1 true  desc==meta true
hogan          title 56  h1 44  desc 150  headline==h1 true  desc==meta true
korn-ferry     title 52  h1 40  desc 136  headline==h1 true  desc==meta true
Enter fullscreen mode Exit fullscreen mode

Two strings per page, rendered in five places. That is one line in a registry:

export interface CheatingGuide {
  provider: AssessmentProvider;
  /** Page h1 and meta title, kept identical so the SERP snippet matches the page. */
  metaTitle: string;
  metaDescription: string;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Why identical rather than tuned separately

The conventional advice is to write the title for the search result and the h1 for the reader, because they have different jobs. In practice, for a cluster of pages that answer one question each, the two jobs are the same job. Somebody searches "can you cheat SHL tests", the result says "Can You Cheat SHL Tests? The Verification Test", they click, and the first thing on the page is that exact sentence. Nothing about the click needs re-justifying.

The Article node reuses it as headline, and description reuses the meta description, for the same reason: structured data that paraphrases the page is structured data that can drift from the page. Half our FAQ markup once quoted questions that appeared nowhere in the DOM, which is a genuinely bad look, and reuse is the cheapest defence against a second instance of that.

The 12 character tax

The root layout sets a title template:

title: { template: '%s | CogniPrep' }
Enter fullscreen mode Exit fullscreen mode

So the rendered <title> is always the page's string plus | CogniPrep. Twelve characters, on every page, appended after the string you actually wrote.

The usual guidance is to keep a title under about 60 characters before it gets cut in a result. That means the budget for the string in the page's metadata export is 48 characters, not 60. Every table of title lengths above is under the limit only because they were written against 48.

This is the bug that keeps coming back in a codebase with a template, because the number you check in the editor is not the number Google sees. The only escape is title: { absolute: '...' }, which drops the brand suffix, and it is worth using deliberately rather than by accident.

The trap

Here is the failure mode that makes all of this worth writing down.

A title runs long. The obvious fix is to trim four words off metaTitle in the registry, ship it, and move on. What you have actually done is rewrite the page's <h1>, and the headline in its structured data, in the same commit. There is no code review signal that this happened, because the diff is one string in a data file and nothing in the page component changed at all.

The fix is not to stop sharing the string. It is to know which fields are shared, and to write them down where the person editing will see it:

/** Page h1 and meta title, kept identical so the SERP snippet matches the page. */
metaTitle: string;
Enter fullscreen mode Exit fullscreen mode

One comment, at the point of use. The same applies to a few of our employer pages, which pass their metadata title straight into a shell component's visible title prop.

I would rather have the comment than a second field. A separate h1 field is the version of this that drifts: within a year one of the twenty four pages has an h1 that disagrees with its title and nobody knows which one is right.

What else the registry decides

The other reason the cluster works is what it is keyed on. One page per assessment provider, not per employer:

Whether an assessment can be cheated is a property of the instrument, not of the company that bought it. Arctic Shores defeats a shortcut in exactly the same way at Thales as it does at Capita, so a page per employer would be dozens of restatements of one argument.

That decision is upstream of everything above. Key the cluster wrongly and no amount of metadata hygiene saves you, because you have committed to writing the same page forty times with the company name swapped, and the shared string trick then propagates one sentence across forty near-duplicates. Key it on the thing the page is genuinely about and each page has its own argument to make.

Each one also carries a one-line defence field, used as the chip on the hub at cogniprep.app/cheating, so the index reads as twenty four different reasons rather than twenty four copies of one. That field exists purely to make duplication visible while the content is being written, which turns out to be the most useful kind of constraint in a cluster like this.

Check any of the pages with the snippet at the top. If your own site has a title template, run the length check against your real <title> rather than the string in your metadata export, and see how many pages are over once the brand suffix is counted.

Top comments (0)