Our related-guides block gave every link to the A names, so 61 of 211 pages received none
Last week I crawled all 530 of our own pages and reported that there were no orphans, but 19 pages had exactly one way in, and all 19 were employer guides reachable only from the index that lists them. This week the site
Last week I crawled all 530 of our own pages and reported that there were no orphans, but 19 pages had exactly one way in, and all 19 were employer guides reachable only from the index that lists them. This week the site is 991 URLs and 211 of them are employer guides, so the problem got bigger before it got fixed. Here is the cause, which is embarrassingly ordinary.
Each guide ends with a "Continue Preparing" block holding up to four sibling guides, picked in three tiers:
const candidates = EMPLOYER_GUIDES.filter((other) => other.slug !== slug);
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),
];
Same sector and provider first, then same provider, then same sector. Take the first four. The sort is there for a good reason, recorded in the old doc comment: a list that reshuffles on every deploy makes the internal link graph look like it is constantly changing.
The trouble is that .sort(byName).slice(0, 4) is not an ordering decision, it is a selection decision. Every guide that shares a provider with Arconic takes Arconic. Nothing takes the guides at the other end of the alphabet.
What that cost, counted
I reimplemented the old function beside the new one and ran both over all 211 guides:
OLD 211 guides, 762 sibling links
in-degree 0 for 61 guides, max 21, median 3
top receivers: acg-world 21, aep 21, amtico 20, arconic 17, canonical 15
61 of 211 received no sibling link at all, and the five biggest receivers are exactly where you would expect them to be in a dictionary. The doc comment I wrote with the fix says "a quarter of the guides"; the count says 29 percent, so the comment is the generous version.
The fix is four lines
const fromAfterSelf = (tier: EmployerGuide[]) => {
const sorted = tier.sort(byName);
const start = sorted.findIndex((o) => byName(o, guide) > 0);
return start <= 0 ? sorted : [...sorted.slice(start), ...sorted.slice(0, start)];
};
Sort alphabetically, then rotate the list so it starts at the first name after this page's own. Barclays picks the names after Barclays, Rio Tinto picks the names after Rio Tinto, and the page at the end of the alphabet wraps around to the start.
It keeps the property the sort was there for. The output is a pure function of the data, identical between builds, with no hashing, no randomness and no stored state. What it drops is the assumption that "first by name" and "best four" are the same thing, which they never were: within a tier, every candidate is equally relevant by construction, because the tier is the relevance.
start <= 0 covers both the not-found case and the already-first case with one comparison, which is the kind of line worth reading twice when you copy it.
One slot had to be reserved
Rotating the tiers was not enough on its own, because 13 of the 211 guides have no evidenced provider. Those pages describe an employer whose real vendor is one we do not simulate, or one nobody has published, and we will not put a provider hub on a page that cannot honestly claim it. They therefore appear only in the third tier, and the first two tiers were filling every list before anything reached it:
// The last slot goes to a same-sector guide from the third tier when there is
// one. Guides with no evidenced provider only ever appear in that tier, so
// without the reserved slot the first two tiers filled every list and those
// guides were never linked from a sibling.
const [sameBoth, sameProvider, sameSector] = tiers;
const shared = [...sameBoth, ...sameProvider];
const sharedSlots = sameSector.length > 0 ? limit - 1 : limit;
const ordered = [
...shared.slice(0, sharedSlots),
...sameSector,
...shared.slice(sharedSlots),
];
The ordering is built once and the dedupe loop walks it, rather than three nested loops with an early return. sharedSlots is the whole policy: give the stronger tiers one fewer slot, but only when the weak tier has somebody in it.
After
NEW 211 guides, 762 sibling links
in-degree 0 for 7 guides, max 15, median 3
79 guides sit at exactly 3, 44 at 4
The same 762 links. Nothing was added, nothing crawls further, no page got longer. 54 pages that previously received nothing now receive something, and the most any page receives fell from 21 to 15.
The seven that still get nothing
This is the part I would want to read in somebody else's post, so: six of the seven are pages with nothing to trade. Albert Heijn, Monuta, NZ Police, Panera Bread, Saxo Bank and Teach First are each alone in their sector and either the only guide for their provider or carrying no provider at all. They give no sibling links either, and their "Continue Preparing" block falls back to later-stage practice links instead. We have 97 sectors across 211 guides and 68 of those sectors hold exactly one guide, which is what a long tail of real employers looks like.
The seventh is Rio Tinto, and it is a genuine limit of the rotation rather than a data problem. It shares SHL with ten other guides and its sector is its own, so it sits in all ten of their second tiers. A rotation only distributes links inside a tier that is actually reached, and the guide immediately before it alphabetically is HSBC, which fills three slots from the narrower first tier (Barclays, Commonwealth Bank, Standard Chartered, all banks on SHL) and hands the fourth to the reserved same-sector slot. HSBC never reaches its second tier, so nothing reaches Rio Tinto, which links out to four pages and receives none.
So the reserved slot that rescued 13 pages is also what closes the door on this one. That is the trade, and it is the right way round: a page with no provider at all had nothing else coming.
Fixing that properly means tracking what has already been given out across the whole set, which is a different function: stateful, order-dependent and much harder to keep stable between builds. Seven out of 211, with six of them structural, is where this one stops.
See it
Open Barclays and look at the bottom block: Commonwealth Bank, HSBC, Standard Chartered, Caisse d'Epargne Normandie. Two of those four are pages we added this month, and under the old rule they would have waited their turn behind the same alphabetically early names for ever.
Then open ACG World, which is one of the pages that used to collect everything. It now links Amtico, Arconic, Canonical and Crown Holdings, its own alphabetical neighbours, which is what a rotation looks like when you are at the front of the list.
Rio Tinto shows the limit: four sibling links out, none in. Teach First shows the structural case, with the block rendering later-stage practice and no siblings at all.
One honest note on counting in-degree from the outside: every page on the site carries four hand-picked employer links in the footer, so Amazon, Barclays, HSBC and PwC each receive around 991 inbound links no matter what this function does. The sibling block is what decides the other 207, which is why one sort call in one file was worth this much arithmetic.
The full index, which is the one link all 211 guides were relying on, is /employers.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.