DEV Community

Daniel Pertu
Daniel Pertu

Posted on

132 pages that ended at their own FAQ, and the three tiers that un-stranded them

CogniPrep has a page per employer whose hiring assessment we can describe: 132 of them, at /employers/<slug>. Each one explains which test that company runs and what its stages look like.

Until recently every single one of those pages ended at its FAQ, and the only links leaving it were a breadcrumb and a signup button.

That is a strange thing to discover about your own site. The provider hub covering the very test the page spends its whole body describing was not linked from it. Two guides for employers running the same assessment could not reach each other without going back out through the index. In link-graph terms, 132 pages were leaves, and the only thing they fed was an auth-gated route that a crawler cannot follow.

The data was already there

What made this fixable without writing anything new is that "employer X runs assessment Y" is not a fact the pages hold. It lives in one registry, which is the only place in the codebase allowed to claim it:

export interface ProviderEvidence {
  provider: AssessmentProvider;
  /** A URL that was actually opened and checked, not a plausible-looking one. */
  url: string;
  type: EvidenceType;
  date: string;
  confidence: Confidence;
  endedOn?: string;
  /** What the source actually says. Shown to editors, not rendered. */
  note: string;
}
Enter fullscreen mode Exit fullscreen mode

There are 239 employer profiles in there and 263 associations between an employer and a provider: 115 at high confidence, 125 at medium, 23 at low. 132 of the profiles have a page; the other 107 are data that other surfaces read.

So the interlinking module derives rather than declares. It adds nothing factual:

export const EMPLOYER_GUIDES: EmployerGuide[] = EMPLOYER_ASSESSMENTS
  .filter((profile) => profile.hasGuide)
  .map((profile) => ({
    slug: profile.slug,
    name: profile.name,
    providers: providersForEmployer(profile.slug),   // medium confidence or better
    sector: profile.sector,
  }));
Enter fullscreen mode Exit fullscreen mode

providersForEmployer applies a confidence floor of medium, because low means one prep-site claim, and one prep-site claim is not something to route a paying candidate on.

Relatedness in three tiers

"Similar employers" is where this kind of block usually goes wrong, because the obvious implementation is a sector match and the obvious implementation is not what a reader wants. Someone reading about a retail graduate scheme does not primarily want other retailers. They want other employers who run the same test, because that is the thing they are about to sit.

So the ranking is tiered, not scored:

const tiers = [
  candidates.filter((o) => shares(o) && o.sector === guide.sector).sort(byName),
  candidates.filter((o) => shares(o) && o.sector !== guide.sector).sort(byName),
  candidates.filter((o) => !shares(o) && o.sector === guide.sector).sort(byName),
];
Enter fullscreen mode Exit fullscreen mode

Same test and same sector first. Same test, different sector second: that tier is the one that earns its place, because it is how a reader learns the test they were invited to is not industry-specific. Same sector but a different test last, as a weak fallback.

Note what is missing: there is no fourth tier. An employer that shares neither the test nor the sector is not related, and the block would rather render fewer cards than fill a grid with filler.

Within a tier the sort is alphabetical by name, which is deliberately boring. The alternative is some notion of popularity, and a popularity sort means the block's output changes when the data changes for reasons unrelated to the page. Alphabetical means a given page's links are stable until somebody edits the registry, and a diff of the rendered HTML is reviewable.

101 of the 132 guides fill all four slots. The rest get fewer, and that is the interesting half.

The pages that honestly have less to offer

Seven guides render no "practice the test" card at all, because providers is empty for them. Two reasons for that: the employer's real vendor is one we do not simulate, or no vendor is evidenced to the standard above.

The temptation is to link the nearest provider hub anyway. The whole block is one href away from being a lie, and nobody would notice. The rule is that a link out of a guide is a claim that the destination matches the test the page just described, so an empty providers array renders nothing.

Six guides go further and have no sibling either, usually because they are the only employer in their sector with a page. Allstate is one: its block is just the four fixed cards, two later-stage links and two browse links. Compare that with Deloitte, which opens with a Cappfinity practice card and then three sibling guides and a fourth that shares its sector. Same component, four cards versus nine, no special-casing in between.

One more case worth naming. An association can be marked as over:

  /**
   * Set when the association is real but over: the employer used this provider
   * for a stage it has since replaced.
   */
  endedOn?: string;
Enter fullscreen mode Exit fullscreen mode

Exactly one association currently carries that flag. The evidence stays in the registry, because a candidate holding an old invitation still deserves an explanation. But the resolver never returns it, because a guide's links and its signup routing are a promise about what someone is about to sit, not a record of what the employer used to do.

Two small things that stopped this being fragile

The component returns null for a slug with no registry entry. Pages and registry entries are added by different hands at different times, so a page added without an entry degrades to its old behaviour instead of crashing a route that was fine yesterday.

And the block reuses the card grid the blog already uses for related articles. That was not a styling shortcut. Before this, a blog post ended with a grid of cards and an employer guide ended with a bordered panel of bullet lists, for no reason other than the order they were built in.

What it was actually for

Two things, and only one of them is SEO.

The crawl side is simple: every destination in that block is a public, indexable page, so the guides now pass authority to the provider hubs and to each other rather than terminating at a signup wall.

The reader side matters more. An employer guide is, for most of its readers, the first CogniPrep page they ever see, because they searched their employer's name and not a vendor's. Before this block existed, the page told them which test they were about to sit and then offered them no way to go and practise it. The same resolved association now also decides which provider a new account is dropped into after signup, at the same confidence floor, so the test somebody is sent to practise is one the guide was willing to claim their employer runs.

The index for all of it is at cogniprep.app/employers.

Top comments (0)