Back to Ideas 7 min read

Telling the Machine What the Page Is

E
EkoHacks Team
·

Four posts into this series, every page has one address, a place on the map, and an image to travel with. What none of them had was a way to stay that way. Each fix so far was checked by a script we run against the live site after a deploy, which catches drift after it has shipped. This post moves the check earlier, to the one place drift cannot get past: the build.

Two jobs, one commit

The first job is to say, in the vocabulary crawlers read, what each page is. A post is an Article with a headline, a date, an author, a publisher and an image. The home page belongs to an Organization with a name, a logo and its public accounts. This is a block of JSON in the head of the page, and it is the difference between a search engine guessing what a page is from its text and being told.

We put it in the same composable that already carries the title, description, canonical and image, because it is made of the same facts:

export function articleData(post) {
  return {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.description,
    datePublished: post.date,
    author: { "@type": "Organization", name: post.author || "EkoHacks" },
    publisher: { "@type": "Organization", name: "EkoHacks", logo: { "@type": "ImageObject", url: `${SITE_URL}/logo.png` } },
    image: absoluteImageUrl(post.image),
    mainEntityOfPage: canonicalUrl(post.path),
  };
}

The post page hands its frontmatter to that function; the home page hands nothing and gets the Organization. No page describes itself twice. The headline in the structured data is the title in the tab is the title on the share card, because they are one value.

The second job is the test.

The test runs after the build, and it can fail it

The rules are the ones a careful person applies by reading the head of a page. We wrote them down once:

title          present, 10 to 70 characters
description    present, 50 to 160 characters
canonical      present, equal to the page's own address
og:image       present, absolute, on the production origin
h1             exactly one
JSON-LD        every post carries an Article, the home page an Organization

The script reads every HTML file the build wrote, applies the rules, prints each page that breaks one with the rule it broke, and exits non zero if there were any. It is wired as postbuild, so npm run build runs it without anyone remembering to, on a laptop and on the host. A non zero exit on the host means the deploy does not happen and the previous one stays up. A page that fails the rules cannot reach production.

This is the series reframe applied to metadata. In test driven work, the failing test is the review: it states what must be true before the code exists, and its first run is supposed to be red. We knew it would be. Day zero had counted forty seven descriptions over 160 characters.

Red

67 pages checked, 49 problems
  /blog/a-branch-is-inventory: description is 267 characters
  /blog/an-off-switch-not-a-disguise: description is 196 characters
  ...
  /blog/stage-then-transform-...: title is 87 characters
  /blog/stage-then-transform-...: description is 354 characters
  /blog/stage-then-transform-...: 2 h1 elements
  ...
  /curriculum: description is 168 characters
  /edtech-accelerator: description is 180 characters
  /: description is 238 characters

Forty six descriptions too long, the longest at 414 characters, which a search engine cuts at roughly 160. One title of 87 characters. One post that repeated its title as a heading inside the body, so the page had two. And the home page, whose description had grown to 238 characters by accretion, one good phrase at a time.

That run is committed as it happened, red, with the test. The commit after it is the fix.

Green

Forty five post descriptions were rewritten. Each one kept the claim of the original and lost the second clause that was usually restating it; the longest is now 160 characters and the average across all posts is 155. The title became "Stage, Then Transform: Crawlers Stay Out of Production", the duplicated heading came out of the body, and the three page descriptions were cut to say one thing each.

Rewriting forty five descriptions by hand is the kind of work a test makes you do once. Without the test it would have been done never, because nothing made it urgent; with the test it was done in an afternoon, because the build would not go green until it was. Then:

67 pages checked, 0 problems

Structured data is in the head of every post and the home page, and the test parses it, so a typo in that JSON is a failed build rather than a silently ignored block.

What the check does not know

It does not know whether a description is any good, only whether it fits. It does not know whether the structured data is what a search engine wants, only that it is well formed and of the right type; the richer checks live in Google's own testing tool, which is run by hand, and which we have not run yet. When we do, the result goes in the measurement post. And it runs on what the build wrote, which is the right place for these rules and the wrong place for the ones about the live host, which stay in the after deploy check.

The number for today: forty nine problems on the first run, none on the second, and a build that will stay that way or stop.

E

Written by

EkoHacks Team

More from Ideas

·6 min read

What the Nullable Gave Back

One file, seven behaviours held fixed, the database swapped for a Nullable: about 180 times less time inside the tests, and coverage flat to two decimals.

E
EkoHacks Team
·6 min read

Twenty Six More Tests, Four Fewer Behaviours

Removing the mocks grew the suite from 44 tests to 70 and quietly deleted four behaviours, every one of them a failure path. Test count is not coverage.

E
EkoHacks Team
·6 min read

The Best Coverage Number in the Room

Same commit, same spec, same test count. The mocked suite ran 5.6 times faster, covered 3.5 fewer points of real code, and posted the best branch coverage.

E
EkoHacks Team