Nakodo publishes 76 pages. 63 of them come out of one React component, ContentPage, which is 208 lines including three exported helpers. The routes that use it look like this, and seven of the eight are this short:
export default async function Page({ params }: { params: Promise<{ slug: string }> }) { return <ContentPage entry={requireEntry("guide", (await params).slug)} />; } Guides, YouTube niches, Instagram niches, TikTok niches, audience pages, competitor comparisons and blog posts: one line each. The eighth, /tools, passes children and one extra structured-data node, which I will come back to because it is the only place the template is extended at all.
This post is the anatomy of that component and then the audit, because a template is a promise about every page it renders and the only way to know it kept the promise is to go and look at all 63.
The shape
Fixed order, no options:
- Breadcrumbs
h1- The date it was updated (blog posts also show when they were published)
- A bordered panel headed "The short version"
-
children, if the route passed any - A contents list headed "On this page"
- The sections, each one an
h2and its blocks - A section called "Questions"
- One call to action
- "Read next" cards
You can see the whole shape on any of them: how much to pay YouTubers, creators in the coffee niche, Nakodo for SaaS. Three different content types, same skeleton, and that is the point: a reader who has read one knows where the answer lives on the next one.
Two parts of that list are generated rather than written.
The contents list is built from the sections, with the FAQ appended as the last item:
{page.sections.map((s, i) => ( <li key={s.id}> <span className="catalogue w-5">{i + 1}.</span> <a href={`#${s.id}`}>{s.heading}</a> </li> ))} <li> <span className="catalogue w-5">{page.sections.length + 1}.</span> <a href="#questions">Questions</a> </li> Numbering the FAQ as section n plus one is a small decision I keep being glad about. The questions are part of the page's outline, not an afterthought hanging off the bottom, and because the number comes from sections.length it cannot drift when a section is added.
The structured data is built from the same fields the page renders, not from a separate description of the page:
<JsonLd data={[ breadcrumbLd(crumbs), articleLd({ path, headline: page.h1, description: page.metaDescription, ... }), faqLd(page.faqs.map((f) => ({ q: f.q, a: plain(f.a) }))), ...(howTo && steps?.type === "steps" ? [howToLd({ ... })] : []), ...extraLd, ]} /> plain() is the interesting call. Our content is written in a small rich-text form with two inline tokens, which renders to real elements on the page, and plain() strips it back to text for the JSON-LD, so the answer Google is given and the answer the reader sees are the same sentence with different markup. There is no second copy of the FAQ anywhere.
The HowTo node is conditional in a way that is worth copying. A page can nominate one of its sections as the how-to, and the node is only emitted if that section exists and contains a steps block:
const howTo = page.howTo ? page.sections.find((s) => s.id === page.howTo) : undefined; const steps = howTo?.blocks.find((b) => b.type === "steps"); A HowTo with no steps is a structured-data error. Expressing "only if the content is actually a list of steps" as a type-narrowed lookup means the page cannot lie about itself. 21 of the 63 pages carry one.
The audit
A template is a theory. I fetched all 76 live pages and counted.
All 63 content pages have:
- exactly one
nav aria-label="Contents" - exactly one
nav aria-label="Breadcrumb" - a
#questionssection - a "Read next" section
- a "The short version" panel
- the call-to-action panel
No exceptions, no partial renders, nothing that lost a part through a conditional. Across the 63 there are 511 sections (6 to 10 per page, 8 in the middle) and 306 <details> elements for the FAQ answers (4 to 6 per page). Every content page carries exactly two application/ld+json scripts: one from the root layout with the Organization and WebSite nodes, one from the template with the BreadcrumbList, the Article, the FAQPage and, on those 21 pages, the HowTo.
Four pages on the site have no call to action at all: the home page and pricing page, which are one giant call to action each, and privacy and terms, which are deliberately dead ends. A legal page that funnels you into a signup is a legal page nobody trusts.
The sweep did produce one false positive, and it is a good lesson in auditing rendered HTML rather than components. 64 pages contain the string "The short version", not 63. The extra one is the terms page, which is not rendered by this template at all. It uses LegalPage, a separate 77-line component with no summary panel in it, and the phrase appears there because somebody, which is to say me, wrote the sentence "We've kept it as plain as we can. The short version: ..." into the terms copy. Grep a heading and you also find the prose. The fix for the audit was to count the 63 registry pages by path rather than by the words on them.
The one extension point, used once
ContentPage takes children and extraLd. In the whole repository, one route passes either:
<ContentPage entry={entry} extraLd={[{ "@type": "WebApplication", name: entry.page.label, url: absoluteUrl(entry.path) }]} > <Widget /> </ContentPage> That is the four calculator pages. The widget lands between the summary panel and the contents list, which is where a reader who came to use a calculator wants it, and the extra node says the page is a WebApplication as well as an Article, which is true of a calculator and of nothing else on the site.
There is one more guard worth mentioning, in the tools route:
// The calculator each tool page shows under its summary. A slug in // src/content/tools without one here fails the build. const WIDGETS: Record<string, () => React.ReactNode> = { ... }; Adding a tool page without a calculator is a build error, not a page with a gap in it.
What a single template is and is not good for
The upside is boring and large. 63 pages that cannot be structurally wrong. Changing the shape of every content page is one file. A new page is a data file and a one-line route, and the parts a writer can forget are the parts the template generates.
The costs are real too, and I would rather name them than pretend.
Everything is the same shape, including when it should not be. A comparison page and a niche page do not have the same job, and both get a contents list and a Questions section because the template has them. The comparison pages carry it well. Some of the niche pages read like a form that was filled in.
It makes pattern pages easy to produce. 63 structurally identical pages is exactly the shape that a search engine treats with suspicion if the writing underneath is thin, and a template cannot tell you whether the writing is thin. The editorial checks that guard against that live somewhere else entirely, in a test over the registry, because the template will happily render 600 words of nothing.
The uniformity hides per-page work. The sweep above says every page has the same parts. It says nothing about whether the eight sections on a given page are the right eight, and no amount of counting will.
If you want to see how far the sameness goes, open a guide, a niche page and a comparison side by side, and scroll. Then view source on any of them and look at the two JSON-LD blocks, which are the same shape too, built from the words you just read.
