Changelog UI
A static, editorial changelog interface for developer audiences: MDX release notes, URL-synced tag filters, an RSS feed, and keyboard nav.
The changelog feedThis changelog is a self-contained, reading-first surface for developers scanning for the one change that affects them: MDX-authored release notes, shareable tag-filtered views, an RSS feed, and keyboard navigation. Most changelogs are a plain markdown file or a CMS bolt-on; this one treats a changelog as editorial content that deserves real writing. Built to show design-to-code execution as a portfolio piece, not a real product.
Entries are typeset, not listed. A real type scale, generous measure, and rhythm make a release note read like a short article rather than a commit message. The system is three fonts on a warm near-black palette with an amber accent, and a highlighted release gets an amber left border and a larger timeline dot to mark a major version.
Filter state lives in the URL, not just in component state. A filtered view like ?tags=feature,fix is a shareable link, back and forward work, and the server re-renders from the URL so a direct load is always correct.
One tag config is the single source of truth. A tag's color, border, and label are defined once and reused by both the inline badges and the filter chips, so the visual language cannot drift between the two surfaces.
A tag-filtered viewNext.js 14 App Router with TypeScript and Tailwind v4. The content source is a typed in-memory array of entries, each carrying a version, date, tags, a highlight flag, and a markdown body; there is no database. The root page is an async server component that reads the tags param, filters the array server-side, and hands the result to a client feed. Each body is rendered from markdown to HTML at request time through next-mdx-remote, with custom overrides for code blocks and links, and a single route handler emits an RSS 2.0 feed from the same array.
Public and live at changelog.javiertpadilla.com. Eight sample entries across five filterable tags, URL-synced shareable filters, an RSS 2.0 feed at /rss, MDX rendering with copy-enabled code blocks, and j/k keyboard navigation, all statically served with no backend.
Filter state lives in the URL
- Chose
- The tag filter serialised into the URL as ?tags=feature,fix, with the server re-reading that param and filtering before the feed renders.
- Over
- Holding the selected tags in component state, the default for a filter UI of this size.
- Because
- A filtered view is then a shareable link, back and forward behave, and a cold load of that URL is always correct.
- Cost
- Filtering stops being a local interaction. The root page has to be an async server component that reads the param and filters server-side before handing anything to the client feed, so the simplest possible state change now goes through the server.
Entries are a typed array, not a database
- Chose
- A typed in-memory array of entries carrying version, date, tags, highlight flag and markdown body, read by both the feed and the RSS route handler.
- Over
- A database or the CMS bolt-on most changelogs reach for.
- Because
- It keeps the whole surface statically served with no backend and the runtime dependency list at five.
- Cost
- Publishing is a code change. Nobody who cannot edit the source and redeploy can add an entry, which is a large part of why the eight entries that ship are sample content and why this is a portfolio piece rather than a real product.
The subscribe form is honest fiction right now: it fakes a success toast and never sends the email, so I'd wire it to a real endpoint or relabel it as decorative before calling the demo done. The RSS feed also hardcodes the wrong base host, so its links point off-domain; that base URL wants to be one config value. The decision I'd keep is making the URL the single source of truth for filters, so every view is shareable and the back button just works.