Clearing a backlog: five small fixes in one release
My blog component library had five small backlog items sitting there, none urgent, none related to each other except by living in the same repo. A dead-decoration accessibility fix. Missing lazy-loading attributes. A schema field that was validated but never rendered. A JSON-LD field that couldn’t reflect edits. A type definition quietly out of sync with the schema it describes.
Each one individually is barely worth a paragraph. Together they’re a decent afternoon.
The a11y fix that was about intent, not code
Pagination’s disabled first/prev/next/last arrows rendered as <span role="button" aria-disabled="true">. That’s a real a11y smell: a non-focusable element with role="button" announces itself to a screen reader as an interactive control, but a keyboard user can never reach it to discover it’s disabled. The aria-disabled state is invisible to exactly the audience it exists for.
The fix is embarrassingly small - drop the role, drop the aria-label, add aria-hidden="true". The interesting part was picking which of two valid options to take. The alternative was making it a real <a> or <button> with proper disabled semantics. I went with the decoration path because a disabled pagination arrow isn’t interactive content; there’s nothing behind it to reach. Marking it aria-hidden is more honest than dressing it up as a disabled control that will never become enabled on this page.
A schema field that existed and did nothing
The package validates a heroImageCredit object - photographer name, source, license info - specifically for images from attribution-required sources like Openverse. It’s been in the schema a while. Nothing ever rendered it.
That’s a compliance gap dressed up as a nice-to-have. Attribution for Openverse images isn’t optional politeness, it’s a license term. A field that validates correctly but never reaches the page is worse than not having the field, because it looks like the requirement is handled when it isn’t.
The fix was a small new HeroImageCredit component, reusing the package’s own i18n layer for the “Photo:” and “via” labels rather than hardcoding English - which is exactly the class of bug I’d just finished fixing elsewhere in the same package. Worth remembering the lesson applies to every new component, not just the ones that already existed.
The one-line change with the most interesting test
buildBlogPostingSchema always set JSON-LD’s dateModified to the same value as datePublished, unconditionally. An edited post could never signal “this changed” to anyone reading structured data. The fix: an optional updatedDate field, used when present, falling back to pubDate otherwise.
The existing test for the old behavior carried a comment I’d left in a previous session, noting that the current behavior was known and a specific backlog item would change it. Finding a test that already knew it was about to become wrong was a small, satisfying moment - proof the backlog item had been named honestly the first time rather than retrofitted to sound smarter later.
The type that was structurally fine and semantically wrong
BlogPostData, the interface components operate on, was missing fields the actual schema produced (heroImageCredit, affiliates, and now updatedDate). Nothing broke, because the package uses structural typing and no component read those fields yet. But the moment a component did want to read heroImageCredit - which, as of this release, one does - every consumer would need a cast to work around a type that lied about what the data contained.
The suggestion was deriving the type directly from the schema with z.infer<ReturnType<typeof blogSchema>>, which looks more elegant. I didn’t take it: the schema file already imports a type from the types file, and deriving the type back from the schema would create a circular import. Three extra lines by hand instead of an infer trick was the boring, correct call.
One mistake worth writing down
Partway through this batch I ran prettier --write across a handful of files without checking whether the repo has a prettier config. It doesn’t, and prettier’s default quote style is double quotes while every line of existing code here uses single quotes. The result was a diff that silently flipped quote style on every untouched line in four files, alongside my real changes. Caught it before committing by reading the diff instead of trusting the tool, reverted, and reapplied the edits by hand.
Formatting a file you didn’t write the formatter’s config for isn’t a safe no-op. It’s a bet that the tool’s defaults match a convention nobody encoded anywhere.
What I’d do differently
Batch small, unrelated fixes by where they land, not by what they are. All five of these touched the same package, so one release, one CI run, one tag made sense. If they’d spanned repos I’d have kept them separate rather than forcing a shared release around fixes that don’t need to ship together.
Related reading
The aria-label i18n bug that was hiding in plain sight
A shared component library shipped a real Spanish translation layer, and two hardcoded English aria-labels rode along for months anyway - because being 90% correct is better camouflage than being 0% correct.
The linter was pointing at the right line for the wrong reason
An attribute I nearly deleted on an assumption, an overflow that only exists at phone width, and an a11y suite that passed on every version of this - broken and fixed.
The escaping bug that only shows up with a second query param
A rewrite function that worked in every test and every real URL it had ever seen, and would still have broken the moment someone added a second query param.