Contract the output — keep changing the inside, even after you ship
Part 2 of 5 — series: Building a publishing tool, and shipping it. Last time I shared the overall picture and premises. Each part stands alone; see the series index. This time: keeping the inside changeable even after you ship.
A tool only you use, you can rebuild however you like. The trouble starts after you publish it and other people begin using it.
Rename a single config key, say, and anyone who wrote settings with the old key suddenly finds their setup broken. Once it’s out, you can’t casually change the parts people lean on. “Whatever is visible from the outside, someone will eventually rely on” — this is the everyday face of what’s known as Hyrum’s Law.
So before shipping, you decide one thing: what to fix as a promise, and what to leave free to change on your own terms. crofty puts that promise on the output.
Input or output?
Here “input” and “output” mean what the tool (crofty) takes in (the posts and settings you write) and what it produces (dist). The choice is which side to put the promise on.
| Contract the input | Contract the output | |
|---|---|---|
| What’s fixed | config keys, file layout, theme internals | what must appear in the generated HTML |
| Changing internals | tends to break users’ setups | free, as long as the contract holds |
| What hurts | renaming a key, reworking a part | only breaking the contract itself |
Contract the input and your internals become the promise; contract the output and the way you build is free. crofty chose the latter.
What the “contract” really is
Words alone stay abstract, so here’s the real thing. Every post page crofty generates has this in its <head>:
<!-- generated HTML (excerpt) -->
<html lang="en">
<head>
<title>Post title · Site name</title>
<link rel="canonical" href="https://example.com/posts/hello/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- plus: a feed (/feed.xml) exists, and nothing phones home -->
</head>
These “always present” items are the contract. crofty won’t emit a page missing its lang or canonical. Conversely, what’s not here — how the theme is built, how the HTML is assembled — is free to change.
Enforced by a machine, not a document
A promise written only in prose drifts from the implementation and rots. So crofty doctor checks, every time, that the built dist meets the contract.
$ crofty doctor
✓ output contract: all good
Checks: canonical link, feed, <html lang>/<title>/viewport, no phone-home.
Now the contract is a line a machine holds, not a best effort. However much you rework the internals, as long as doctor passes, users’ sites keep their promise.
You still can’t get rid of input entirely
Even with the output under contract, the input side — a post’s front matter, some settings — doesn’t vanish. What matters is drawing the line clearly.
| Place | How it’s handled |
|---|---|
| files crofty owns | crofty writes them |
your files (hugo.yaml, etc.) |
left alone — crofty only guides |
| visual customization | a later-winning layer (custom.css from crofty theme eject) overrides |
Even after you override, doctor still checks the contract, so “what you’re free to change” and “what’s held” never get mixed up.
It works anywhere
This isn’t special to crofty. It’s the general practice of defining a public API by its visible behavior, not its internals.
- a library — version-promise (semver) the behavior, not the internal structure
- a CLI — keep the internals free, but stabilize the output format
- a theme — promise the produced result, not part names or class names
Choose the promised surface — the visible surface — deliberately small. What to freeze, and what to keep movable: the design of a tool you give away starts from drawing that line.
← Previous: Keeping what you write in your own hands | Next: Designing a multilingual site →