Dev.to WebDev ๐Ÿ›  Dev ๐Ÿ‘ 0 ๐Ÿ“– 6 min read

Ten rules for writing a page, measured against the 76 pages: six a test holds, four only I do

Every public page on nakodo.app is a typed object in src/content, and before anyone adds one there is a 95-line file, docs/seo.md, that says how. It ends with ten numbered writing rules. Next to it there is a 133-line te

Every public page on nakodo.app is a typed object in src/content, and before anyone adds one there is a 95-line file, docs/seo.md, that says how. It ends with ten numbered writing rules. Next to it there is a 133-line test file, src/content/content.test.ts, that runs over all 63 content pages in the registry.

The interesting question about a rulebook is not what it says. It is which rules are enforced, which are enforced differently from how they are written, and what the unenforced ones actually cost. I measured all three, partly by reading the test and partly by fetching every URL in the sitemap and checking the rendered HTML.

The ten rules, scored

# Rule Enforced?
1 Answer first: summary is 40 to 80 words Yes, but the test allows 40 to 90
2 Write the title in the searcher's words No. Length, brand and uniqueness are
3 One FAQ is the exact query, at least three FAQs Partly: the count and answer length, not the exactness
4 Only claim what can be checked No
5 No real creators by name No
6 Don't quote prices or limits in content, link to /pricing No
7 "Influencer" only where the page meets the search Yes, field by field
8 House style: no em or en dashes, no exclamation marks, no banned words Yes. British spelling and sentence case, no
9 Link generously, 3 to 6 in-text links, only to real pages Partly: every link is resolved, the count is not
10 Not thin: at least 600 words Yes

Six of the ten have a real assertion behind them. The four that do not are the four that need a reader: whether a title is phrased the way people search, whether a claim is checkable, whether a name belongs to a person who never asked to be on a list.

Where the test is stricter than it looks

The enforcement is per field, not per page, which is what makes rule 7 testable at all. The page object is split in two for the purpose:

const bodyTexts = (p: ContentPage) => [
  ...p.sections.flatMap((s) => [s.heading, ...s.blocks.flatMap(blockTexts)]),
  ...p.faqs.map((f) => f.a),
];
const searchTexts = (p: ContentPage) => [p.metaTitle, p.ogTitle ?? "", p.metaDescription, p.h1, p.summary, p.label, p.cardBlurb, ...p.keywords, ...p.faqs.map((f) => f.q)];

Banned words are checked against both lists. "Influencer" is checked against the body list only, so a question can be phrased the way it is typed into Google and the answer under it says creator:

for (const t of bodyTexts(page).map(plain)) assert.ok(!INFLUENCER.test(t), `say "creator" in body copy: ${t.slice(0, 90)}`);

blockTexts is a switch over the eight block types with no default branch and an exhaustive return type, which is the part that keeps the rule honest over time: add a ninth kind of block and the build fails until the test knows how to read its text. A rule enforced over "the text of a page" is only as good as the function that decides what the text of a page is.

Here is the one place the doc and the test disagree. Rule 1 says 40 to 80 words. The test says:

const n = words(page.summary);
assert.ok(n >= 40 && n <= 90, `summary is ${n} words`);

Nobody loosened the rule on purpose; one of the two was edited and the other was not. The honest fix is to make the doc quote the constant, the way it already does for titles and descriptions.

What the live pages actually look like

I fetched all 76 URLs, which is about 36 MB of HTML, and measured the rendered result rather than the source:

Measure Result
Titles over 60 characters 0 of 76. The longest is 59, and six pages sit at 59
Descriptions over 160 0 of 76. The longest is 158
<details> elements, which is one FAQ each 337
Pages with no FAQ at all 10
Pages under 600 words 5
Em dashes in visible text 0
Exclamation marks in visible text 0
En dashes in visible text 1

The title and description numbers are the ones worth dwelling on, because the test only sees 63 of the 76 pages. The home page, the hubs, pricing, about, the privacy notice and the terms are hand-built React, and the doc says so plainly: "Hand-built pages follow the same limits but the test doesn't see them, so count them by hand." Hand counting has held for titles, with 59 as the worst case against a budget of 60. The 50-character budget on metaTitle exists because the layout appends " ยท Nakodo", and a hand-built page writes its own title, so nothing stops one from being 70 characters except remembering.

The ten pages with no FAQ are the same hand-built set. The five under 600 words are the five hub pages, which are link indexes by design and run from 411 to 586 words. Both of those are the thinness rule bumping into pages it was never written for, which is a different thing from a violation, and both are invisible to the test for the same reason.

The one en dash

Rule 8 bans en dashes as well as em dashes, and BANNED enforces it with /โ€“/ over every content page. There is exactly one en dash on the whole site, and it is on the privacy notice:

Where they do, the transfer is covered by the safeguards the law requires, such as the UKโ€“US data bridge or standard contractual clauses.

That is a hand-built page, so the test never looked at it. It is also the most defensible en dash on the internet, since the mechanism's own name is written that way in the UK government's documentation. It is still a rule with one exception nobody decided to make, which is the thing I actually want to know about a codebase.

Rerun it yourself

There is nothing private in any of this. The sitemap lists every page, and the checks are one pass over the HTML:

curl -s https://nakodo.app/sitemap.xml | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' > urls.txt
while read -r u; do
  t=$(curl -s "$u" | grep -o '<title>[^<]*</title>' | sed 's/<[^>]*>//g')
  printf '%3d  %s\n' "${#t}" "$t"
done < urls.txt | sort -rn | head

The first line of output should be a number below 60. If a Nakodo page ever shows 61 there, it will be one of the thirteen pages the test cannot see, and the fix is three lines: the registry already exports STATIC_PAGES, and the test already imports it for an unrelated check.

The rules that cannot be tested are the ones worth writing down

Rule 6 is the clearest example. It says never to quote a price or a limit in page copy, and to link to pricing instead, which reads the plan definitions from one file. A regex could probably catch a currency symbol, and it would not catch "about twenty-five a day", which is the version that goes stale and embarrasses you. The rule's value is that it is read before the copy is written, not that it fires afterwards.

Rule 10's second half is the same: at least 600 words is testable, and "a niche page with the noun swapped is a doorway page" is not. The test can tell you a page is thin. Only a person can tell you it is hollow. That is why the file exists alongside the test, and why the honest scoreboard for a rulebook has two columns rather than one.

The output of all this is public: the guides, the blog, the glossary and the rest, every one of them generated from the same typed object and checked by the same 133 lines.

๐Ÿ“ฐ Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.