One changelog file, three places it shows up
A single typed changelog file feeds an in-game badge, a Settings tab and a public website page, so release notes cannot drift between the product and its marketing.
Most products keep their release notes in three places. A CHANGELOG.md in the repo. A release note somewhere in the product, if the product has that. And a post on the store page or the marketing site.
Three copies of the same information, written at three different moments, by a person who is tired by the third one. They drift immediately. The store post gets the polished version, the in-app note gets a shortened version that loses a feature, and the repo file gets whatever the commit messages happened to say.
In Cozy Coast there is one file. shared/changelog.ts is a TypeScript module holding the entire release history, and it feeds every surface that shows release notes.
The shape
An entry has a version name, a date, and groups. A group has a title, an icon, and a list of one-line items.
export interface ChangelogEntry {
version: string; // "Hubs with friends", not "0.4.2"
date: string; // "20 August 2026"
groups: ChangelogGroup[];
}
export interface ChangelogGroup {
title: string;
icon: ChangelogIcon;
items: string[];
}
The version is a name rather than a number, because a player reading a badge does not care that they are on 0.4.2. They care that hubs arrived.
Icons are named, not pathed
This is the small decision that makes the file portable across surfaces. ChangelogIcon is a union of names: chest, heart, fish, star, palm, gear, and so on. The file never contains a path.
Each surface resolves the name through its own map. The game looks up palm and gets sprites/palm.png from its bundled assets. The website looks up palm and gets /img/palm.png from its public folder. Same art, different serving mechanics, one word in the source.
Had the changelog stored paths, it would have had to know which surface was reading it, and that is the exact coupling that stops a shared file from being shared.
The icons are always game sprites, never emoji. That is a taste rule, but it is enforced by the type: there is no way to write an emoji into an icon field.
The badge fires once
The save records changelogSeen, the version whose notes were last read. On launch the game compares that against CHANGELOG[0], which is the newest entry because the array is ordered newest first.
If they differ, a badge appears in the top right. Read the notes and the save updates. The badge never comes back for that release.
Everything past that is the Settings tab, which is permanent and holds the whole history. Two surfaces, one interrupting and one not, driven by one field on the save.
The rules live in the file
At the top of changelog.ts there is a comment block that is really a style guide:
Plain player-facing sentences. One line per item, never a paragraph. No version numbers inside items. No internal jargon. And an entry that has already shipped is never rewritten.
That last one matters more than it looks. A shipped changelog entry is a historical record of what a player was told at the time. Going back to tidy the wording of a release from two months ago quietly rewrites the past for anyone reading the archive. It also breaks the guarantee that the website and a copy of the game installed in June say the same thing.
Writing the rules where the entries are written means I actually follow them. A style guide in a wiki is a style guide nobody opens.
Why this generalises
The specifics here are a fishing game, but the shape applies anywhere the same words have to reach a product surface and a public page.
The conditions are worth naming, because it does not always work. The two surfaces need to want the same content, not overlapping subsets. They need to share a language or a build step, which for me is TypeScript across the Electron app and the Astro site. And somebody has to be disciplined enough to keep presentation out of the source, which is what the icon-name indirection is really enforcing.
When those hold, one file beats three every time. When they do not, you are better off with a generator than with a shared module pretending to be one.
You can see the output side of it at cozycoast.website.