DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Twelve pages in one commit, and four of the new ones are linked from nowhere but each other

One commit this week added twelve pages to CogniPrep: six under a new /application-tracker cluster, three more interview guides, and three more assessment centre guides. The live sitemap went from 386 URLs to 398, which I can check rather than assume:

curl -s https://cogniprep.app/sitemap.xml | grep -c '<loc>'
Enter fullscreen mode Exit fullscreen mode

398, broken down by first path segment: 133 employer pages, 102 blog posts, 57 provider pages, 57 cheating guides, 18 under /interview, 11 under /assessment-centre-practice, 6 under /application-tracker, 6 test formats, and nine one-off pages.

The interesting part is not the page count. It is that the third cluster cost almost no new code, and that the thing it still lacks is the thing the first two also lack.

A registry of metadata, and not a word of content

The pattern, now in its third copy:

/**
 * Registry of metadata only, for the public application tracker cluster:
 * /application-tracker (the product page) and /application-tracker/<slug>.
 *
 * Every word a visitor reads is written in the page.tsx under the guide's own
 * route folder. What lives here is what other pages need without rendering the
 * guide: the hub cards, the sitemap, the sibling links at the foot of each
 * guide, and the Next metadata export.
 *
 * Adding a guide means one entry here plus one folder. Forgetting the entry is
 * the loud failure: requireTrackerGuide() throws at build time.
 */
Enter fullscreen mode Exit fullscreen mode

The division is the whole trick. Prose in a TypeScript array is miserable to write and worse to review, and it tempts you into a CMS you do not need. Metadata in the page file means the sitemap has to import 400 pages' worth of components to find out their slugs. So: slug, label, h1, metaTitle, metaDescription, ogTitle, keywords and a card blurb in the registry, and every sentence a reader sees in app/application-tracker/<slug>/page.tsx.

The loud failure matters more than it sounds. A page whose slug has no registry entry would otherwise render, answer 200, and be absent from the sitemap, the hub and the sibling strips: a page nobody can reach and nobody notices. requireTrackerGuide throws, so that mistake is a failed build rather than an orphan:

export function requireTrackerGuide(slug: string): TrackerGuide {
  const guide = getTrackerGuide(slug);
  if (!guide) {
    throw new Error(
      `No entry in TRACKER_GUIDES for slug "${slug}". Add one in lib/applications/guides.ts.`
    );
  }
  return guide;
}
Enter fullscreen mode Exit fullscreen mode

The shared GuideMeta type carries its constraints as comments, and both of them are load-bearing:

/**
 * The <title>, before the root layout appends ' | CogniPrep' (12 characters).
 * Keep this at 48 characters or fewer so the rendered title stays under 60.
 */
metaTitle: string;
/** Under 160 characters. */
metaDescription: string;
Enter fullscreen mode Exit fullscreen mode

Measured on the five new registry entries: metaTitle runs 30 to 46 characters against the documented 48 ceiling, and metaDescription 149 to 155 against 160. Rendered, with the suffix, the live titles are 42 to 58 characters. Nothing truncates, and nothing came that close by accident, because the ceiling is written where you type the string rather than in a style guide nobody opens.

One comment is now wrong, and it is the type's own:

Shared metadata shape for the two guide clusters: /interview/<slug>
(lib/interview/guides.ts) and /assessment-centre-practice/<slug>
(lib/exercises/guides.ts).
Enter fullscreen mode Exit fullscreen mode

There are three. The builder next to it got updated honestly ("every page in the guide clusters" replaced "the two clusters") and gained a third one-liner:

export function trackerGuideMetadata(slug: string): Metadata {
  return buildGuideMetadata(requireTrackerGuide(slug), `/application-tracker/${slug}`);
}
Enter fullscreen mode Exit fullscreen mode

Three functions, one body, zero per-page metadata exports restating 40 lines of canonical, OpenGraph and Twitter tags.

53 keywords, all distinct, and one that is also a page in another cluster

Within the cluster, the five guides carry 53 keyword strings and all 53 are distinct, which is the minimum bar for not writing five pages about the same search.

Across clusters, exactly one phrase repeats: apprenticeship assessment day, which appears in the apprenticeship tracker guide's keywords and in the assessment centre cluster. That is not a near miss. The same commit shipped /assessment-centre-practice/apprenticeship-assessment-day, a page whose slug is the phrase. Two of my own pages are now aimed at it, and the one named after it should win, so the keyword on the other should go.

I would not have found that by reading either file. I found it by extracting the keyword arrays from all three registries and intersecting them, which takes about fifteen lines of Python and is the only reliable way to audit a set of strings nobody is allowed to duplicate.

What the live pages actually weigh

Fetched from production, text only, scripts and styles stripped:

Page Words Internal links
/application-tracker 1,495 38
/application-tracker/how-to-keep-track-of-job-applications 3,383 39
/application-tracker/job-application-process 3,356 45
/application-tracker/apprenticeship-applications 2,846 40
/application-tracker/graduate-scheme-applications 2,785 46
/application-tracker/internship-applications 2,570 38

Each page carries exactly one h1 and exactly four JSON-LD blocks. On the hub those are Organization, BreadcrumbList, FAQPage and WebApplication. On all five guides they are Organization, BreadcrumbList, HowTo and FAQPage, with four Course offers nested in the organisation's offer catalogue rather than emitted as blocks of their own.

The part that is not finished

Here is the link graph of the new cluster, counted from the source rather than guessed:

  • the hub, /application-tracker, is linked from the public footer and from behind the header's "More" menu, so from every public page on the site;
  • each guide links to the hub and to its four siblings;
  • and from outside the cluster, four of the five guides are linked from nothing at all.

The exception is apprenticeship-applications, which the new apprenticeship assessment day page and the new apprenticeship interview questions page both link to, because they are about the same reader. That is what the other four should look like.

This is a mistake I have now made three times on the same site, and the reason it keeps happening is that adding a cluster feels complete when the cluster is internally consistent. The hub lists the guides, the guides list each other, the sitemap lists all six, the breadcrumbs resolve. Everything is reachable. But a sitemap is a discovery hint, not internal linking, and a page whose only inbound links come from its own four siblings and a hub is a page with no evidence that anyone else on the site thinks it is important.

The fix is editorial, not structural: 102 blog posts and 133 employer pages mention application deadlines, test invitations and scheduling constantly, and each of those is a sentence that could carry a link. No new abstraction required, which is exactly why it keeps not getting done.

See it

The cluster is live: cogniprep.app/application-tracker. The hub is a free tool page, the five guides beneath it are long reads, and you can check every claim above from outside.

Count the sitemap, and the cluster inside it:

curl -s https://cogniprep.app/sitemap.xml | grep -c '<loc>'
curl -s https://cogniprep.app/sitemap.xml | grep -c 'application-tracker'
Enter fullscreen mode Exit fullscreen mode

398 and 6. Then count the inbound links on a guide, which is the number that is too low:

curl -s https://cogniprep.app/application-tracker/internship-applications \
  | grep -o 'href="/application-tracker[^"]*"' | sort -u | wc -l
Enter fullscreen mode Exit fullscreen mode

Five: the hub and four siblings. Now try to find that page linked from anywhere else on the site and you will be looking for a while, which is the honest state of it.

Top comments (0)