Nakodo has 63 public content pages: guides, niche pages, pages for particular kinds of
business, four calculators, a blog. None of them is markdown. Each one is a TypeScript file exporting an object
with a title, a summary, and an array of typed blocks, and one renderer turns that array into a page. You can read
the output at nakodo.app/guides and the structure is the same on every one of them.
There are eight block types and 853 blocks across the 63 pages:
| Block | Count |
|---|---|
p | 524 |
list | 139 |
terms | 53 |
table | 50 |
tags | 26 |
steps | 22 |
template | 20 |
callout | 19 |
A block holds text, and text needs links and emphasis, so the text had to carry some markup. This is the part I
want to write about, because it is 15 lines and it has been load bearing for a month.
The type is a string
/** * Text with a small inline markup, rendered to HTML and copied verbatim into * markdown: [label](/path) or [label](https://...) for links, **bold**. */ export type Rich = string; Rich is string. It buys no type safety at all, and it is the most useful line in the file, because it names
which strings are allowed to carry markup. A table's head is string[] and its rows are Rich[][]: column
headers cannot contain a link, cells can. A template's body is string, a callout's text is Rich. The
distinction is enforced by nothing but it is written down everywhere, and when I add a field I have to decide
which it is.
The two forms are the whole grammar:
// The inline markup content is written in: [label](href) and **bold**. Kept to // two forms so the same string is valid markdown for llms-full.txt. export const RICH_TOKEN = /\[([^\]]+)\]\(([^)\s]+)\)|\*\*([^*]+)\*\*/g; /** The text without markup, for structured data and meta tags. */ export function plain(text: Rich): string { return text.replace(RICH_TOKEN, (_, label: string | undefined, _href, bold: string | undefined) => label ?? bold ?? ""); } /** Every link target in the text. */ export function linksIn(text: Rich): string[] { return [...text.matchAll(RICH_TOKEN)].map((m) => m[2]).filter(Boolean); } Two forms, not twelve, because the same string has a second reader. The site publishes
llms-full.txt, which is every page as one markdown file, and that generator
copies Rich strings through untouched. A link and bold text mean the same thing in both targets. Had I allowed
`code` or _italic_ or a heading, either the HTML renderer would need them too or the markdown file would
carry markup that the page does not render. Two forms is the largest grammar where "the string is valid markdown"
is true by inspection rather than by test.
Here is the whole census, counted over every Rich field on the 63 pages, the how-it-works page and the glossary:
2,932 strings, 493,042 characters 692 links: 392 internal, 300 external, 0 anchors 537 bolds Three consumers, and the third one is a build step
plain() is for anywhere the text must not be markup: the acceptedAnswer of an FAQPage node, a HowTo
description, a glossary DefinedTerm, the <description> in the blog's RSS feed. linksIn() is for the content
test, which resolves every link target against the page registry, so a typo in an internal path fails the build
rather than shipping a 404 into a guide.
The renderer is 37 lines and does one pass:
export function RichText({ text }: { text: Rich }) { const out: React.ReactNode[] = []; let last = 0; for (const m of text.matchAll(RICH_TOKEN)) { if (m.index > last) out.push(text.slice(last, m.index)); const [, label, href, bold] = m; if (bold) out.push(<strong key={m.index} className="font-semibold text-foreground">{bold}</strong>); else if (href.startsWith("/") || href.startsWith("#")) out.push(<Link key={m.index} href={href} className={linkClass}>{label}</Link>); else out.push(<a key={m.index} href={href} target="_blank" rel="noopener noreferrer" className={linkClass}>{label}</a>); last = m.index + m[0].length; } if (last < text.length) out.push(text.slice(last)); return <>{out}</>; } Internal links go through next/link so they prefetch and navigate client side. External links get
target="_blank" and rel="noopener noreferrer", which is a decision made once in one file rather than 300 times
in content. You can see both on one live page:
$ curl -s https://nakodo.app/guides/instagram-and-tiktok-disclosure-rules \ | grep -o 'rel="noopener noreferrer"' | wc -l 12 The branch that has never run
href.startsWith("#") sends an anchor link through next/link. The census above says 0 anchors. In a month of
writing, 692 links, not one of them is a #.
That branch is not wrong, it is unexercised, which is worse in a small way: it reads like a feature and it is a
guess. It also happens to be the wrong guess. next/link to a bare #section on the current page is routing
machinery for something the browser does natively, and until a string actually uses it I have no idea whether the
scroll behaviour would be the one I want. Every page already has an id on every section heading, and
the contents list at the top links to them with a plain <a href="#...">, which is the markup I would want here
too; nothing in the prose points at a section at all.
Two honest options: delete the branch and let # fall into the external arm, where it would render a plain <a>
that works, or write one sentence in a guide that links to a later section and find out what it does. I prefer the
first, because the regex gives me no way to express "same page" versus "other page, then anchor" anyway.
What the tight regex is protecting
\[([^\]]+)\]\(([^)\s]+)\) demands the ]( with nothing between, and forbids whitespace in the href. That looks
like laziness until you count how much of the content contains square brackets that are not links. 26 of the 2,932
strings do.
Some of them are reader instructions about brackets:
$ curl -s https://nakodo.app/guides/how-to-find-a-youtubers-email \ | grep -o 'name \[at\] domain \[dot\] com' name [at] domain [dot] com Some are the fill-in slots in the message templates the guides publish, 220 of them across 20 template blocks:
$ curl -s https://nakodo.app/guides/youtube-outreach-email-templates \ | grep -o 'Paid integration on \[Channel name\] from \[Brand\]' Paid integration on [Channel name] from [Brand] Both render as typed, because [at] is not followed by (. If I had reached for a markdown parser instead, every
one of those lines would be a bug report waiting for a reader to notice, and the template body is not even
Rich: it is a plain string rendered with whitespace-pre-line, so the brackets in a copyable message cannot be
eaten by markup rules at all.
There is one hole in the grammar, and it is the star:
plain("a ** b ** c") // "a b c" CommonMark would leave that alone, because ** followed by a space does not open emphasis. My regex does not
care, so a sentence containing two unrelated double stars loses the text between them. Today no string has an odd
or stray ** (I checked: zero), and the content test already greps every string for banned punctuation, so the
cheap fix is to add "no ** that does not pair into bold" to that test rather than to grow the regex.
One regex object, two safe uses and one unsafe one
RICH_TOKEN is a module-level /g regex shared by every renderer on every request, which is the kind of thing
that bites people:
RICH_TOKEN.test("[a](/b)") // true RICH_TOKEN.test("[a](/b)") // false A /g regex carries lastIndex, and test() and exec() advance it. Both uses in the codebase are immune:
String.prototype.matchAll works on an internal clone and never touches the original, and replace with /g
resets lastIndex to 0 when it finishes. So the shared object is fine, and it is fine for reasons that are not
obvious from reading the call sites, which is why plain and linksIn are the only two functions allowed to
touch it and nothing else imports the regex.
The renderer the blocks get
The block renderer is 148 lines, one switch over the eight types, no default branch. Adding a ninth member to
the union turns into a type error at the switch rather than a page with a hole in it. The parts worth copying:
- A
table's first cell per row is a<th scope="row">and the headers are<th scope="col">, so a screen reader announces "Structure, Flat fee" instead of two unrelated cells. 50 tables, 296 rows, 900 cells, all of them wrapped in one horizontally scrollable div with a negative margin so a wide table can bleed to the edges of a phone screen. -
termsrenders a<dl>with a two-column grid, which is the one block type whose HTML I would not have guessed from the name six weeks later. 53 of them. -
stepsnumbers itself withString(i + 1).padStart(2, "0")in a condensed mono face, and 21 of the 63 pages name onestepssection as the page's real procedure so it is emitted asHowTostructured data. The numbers come from the renderer, so a step cannot be numbered wrong in content. -
templateis the only block with a button in it. Six of them on this page, and the copied text is assembled for the clipboard rather than scraped from the DOM, soSubject: ...arrives with the body.
The whole content layer is 63 TypeScript files, 853 blocks, 2,932 strings, one 15-line markup module and two
renderers. The thing I would tell anyone building the same: pick the smallest grammar that two output formats both
agree on, then write down which strings are allowed to use it. The type that is string is doing more work than
the regex.
Go and look if you like: nakodo.app/guides for the HTML, and
nakodo.app/llms-full.txt for the same content as markdown, where those
[label](/path) strings are the output rather than the input.
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)

