Return to notes
Systems engineering· 5 min read

Twenty-three calculators, one definition

HariTools could have been twenty-three hand-built pages. Making each tool a typed definition that routes, metadata, structured data, and the sitemap all derive from changed what it costs to add the twenty-fourth.

#Next.js#TypeScript#Content architecture#SEO

HariTools is a Sri Lanka-focused calculator platform: estimate an electricity bill, check APIT deductions, plan a loan, work out a university GPA, compare EPF and ETF outcomes. The repository currently holds 23 calculators across finance, student, health, business, and everyday categories, with seven long-form guides supporting them.

The engineering question was never how to write a GPA calculator. It was what happens on the twenty-fourth.

Count the concerns, not the pages

A calculator is not one artifact. Shipping one properly means touching:

  • a route at /calculator/[slug];
  • a discovery card in the right category directory;
  • page metadata, canonical URL, and Open Graph tags;
  • FAQ content, and the structured data that mirrors it;
  • related calculators and related articles;
  • a sitemap entry;
  • navigation placement.

That is seven or eight concerns. Built by hand, 23 calculators is not 23 units of work — it is 23 x 8 places that have to stay consistent with each other, forever, while a human maintains them.

The failure mode is not dramatic. Nothing crashes. You just end up with a calculator that works perfectly and is missing from the sitemap, or whose FAQ section and FAQ structured data have drifted apart, or that no other page links to. Quiet, individually trivial, and collectively the difference between a platform and a folder of pages.

One typed definition, everything derived

Each calculator is instead represented as a typed definition: its slug, category membership, component key, availability, headings, metadata, keywords, introduction, and FAQ content.

Typed calculator definition
        |
        +-> category directories and discovery cards
        +-> static /calculator/[slug] route
        +-> canonical and Open Graph metadata
        +-> related calculators and articles
        +-> FAQ structured data
        +-> sitemap entry
        +-> dynamically imported calculator component

Every arrow in that diagram is generated. The sitemap is not a file someone maintains; it is a projection of the registry. The FAQ structured data is not written twice; it is derived from the same field that renders the visible FAQ, so they cannot disagree.

Adding a calculator becomes a controlled extension of the platform: write the definition, write the component, and the eight downstream concerns resolve themselves.

The type system carries the enforcement. A definition missing its metadata is a compile error, not a page that quietly ships without a description. That is the real substitution — a code review checklist becomes a type, and types do not get tired on the twenty-fourth review.

A registry is worth it the moment consistency across instances matters more than flexibility within one.

SEO is a derived artifact

Worth separating out, because this is where hand-built platforms decay fastest.

Search metadata is unusual: it is invisible during development, invisible during QA, and only observable weeks later in aggregate. Nothing about your local workflow tells you a canonical tag is wrong. There is no error, no warning, no failing test — just a slow, unattributable underperformance.

Deriving it removes the category of mistake. If canonical URLs are computed from the slug that also generates the route, they cannot point somewhere the route does not exist. If the sitemap enumerates the registry, a published calculator cannot be missing from it. If FAQ structured data reads the same field as the rendered FAQ, they cannot describe different content.

This is the same reasoning as constraining data at the database rather than in application code. Push correctness to the layer where the invariant is structural, and violations stop being possible instead of merely being caught.

Dynamic imports keep the pattern honest

A registry that eagerly imports every component would mean each calculator page shipping the JavaScript for all 23. The registry stores a component key; the actual component is imported dynamically at the route.

So the shared structure is static and cheap — routing, metadata, sitemap generation, and discovery all work from plain data — while the expensive part stays per-route. The definition is metadata about a component, not a reference that drags the component into the bundle.

The distinction matters for any registry pattern. Registering data about a thing is cheap. Registering the thing is a bundle.

Where a registry costs you

Registries have a real failure mode, and it is worth naming rather than selling the pattern clean.

A registry encodes an assumption that its members are the same shape. That holds beautifully for 23 calculators that all take numeric inputs and produce a result with an explanation. It stops holding the moment one tool needs something structurally different — a multi-step wizard, a comparison across two scenarios, a result that is a chart rather than a number.

At that point you have three options, and only the first two are good:

OptionWhen it is right
Widen the definitionThe new shape is general — other tools will want it
Let the tool sit outside the registryIt is genuinely one of a kind
Bend the tool to fit the registryAlmost never

The third is how registries become the thing everyone complains about. The pattern is a description of what your tools have in common, and it stays useful exactly as long as that description stays true. When it stops being true, change the description — do not deform the tool to preserve it.

The takeaway

The registry did not make the first calculator faster to build. It made it slower, because I had to design a definition shape before writing anything concrete.

It paid back at around the fifth, and by the twenty-third the alternative had become unmaintainable — not because the work would be hard, but because the consistency would be impossible to hold in a human head.

The question worth asking early is not "how do I build this page?" It is "what will be true of all of these, and can I make that structural?"

Continue reading

Enterprise software is a problem of state and ownership