Jul 6, 2026
v7.41.39 · Two more guides + every tool scores 100
The guide shelf reaches 43.
What file conversion actually does (and what you lose) — conversion is re-encoding, not translation, and four kinds of loss ride along — and anatomy of a URL, a part-by-part tour of the seven-piece contract in every link, including the piece your browser never sends to the server.
Every tool now scores a hundred.
The internal health audit flagged two classes of gaps and both are closed: screen readers now hear tool feedback (status roles on toast messages across 27 tools, polite live regions on result areas), and preference inputs remember your choices — the QR maker’s colors and logo settings, quality sliders on the compressors, the contrast checker’s swatches. That took teaching the persistence engine about checkboxes and sliders first; like everything here, preferences live in your browser’s localStorage and nowhere else.
Totals after v7.41.39
60 tools — all scoring 100/100 on the internal health audit. 43 guides. SW cache → v7.41.39. Linter clean at 35/35 checks. All test suites green.
Jul 6, 2026
v7.41.38 · Three new guides + a sturdier safety net
Three more guides from the workshop.
Why your JSON won’t parse (the eight ways hand-written JSON breaks, and the two failures that aren’t errors at all), why your PDF is so much bigger than it needs to be (scans are photographs in a trench coat), and sort and dedupe: why “10” comes before “9” (character codes, not meaning). That’s 41 guides.
A sturdier safety net.
The five most-used tools that had no automated tests — JSON Fixer, Base64, JWT Decoder, Hash Generator, Regex Rambler — now have real regression suites pinned to published vectors (RFC 1321 MD5, the canonical CRC-32 check value, RFC 4648 Base64, the jwt.io example token). Two linter checks that only warned before now block a broken deploy outright, and a new check makes sure every guide links its neighbors. Plus small fixes: the keyboard-shortcuts overlay no longer overflows narrow phones, going offline on an uncached page shows a proper branded message, and the JSON Fixer’s Format button finally says something when it succeeds.
Totals after v7.41.38
60 tools. 41 guides. SW cache → v7.41.38. Linter clean at 35/35 checks. All test suites green, including five new core-tool suites (42 checks).
Jul 6, 2026
v7.41.37 · Twenty new tools (40 → 60) + For Teams, Book & Sponsor pages
The toolshed hits sixty.
Twenty new client-side tools land at once: HMAC Generator, Slug Generator, Line Ending Converter, Punycode / IDN, IPv4 Subnet Calculator, Levenshtein Distance, ROT13 & Caesar, Java Properties, JSON Path, CSS Formatter, Random String, Unit Converter, Timezone Converter, PEM Decoder, JWT Builder, Morse Code, Unicode Lookup, Luhn Validator, Semver Parser, and HTML Formatter. Every one runs entirely in your browser, is searchable via Cmd+K, and carries its own logic tests. Existing tools got deeper too: the JWT Decoder now verifies RS256/384/512 and ES256 signatures via Web Crypto, and the JSON Fixer guards its tree view against multi-megabyte inputs.
Three honest pages about money.
Three new pages describe how this site might sustain itself without betraying the support page's promises: Tooly for Teams (a one-time license to self-host the whole toolshed behind your firewall), Tools and Trust (the essays, collected for offline reading), and a sponsorship policy (disclosed essays only, capped at four a year, never ad scripts). None of them has a live payment link yet — they describe the offer honestly and take emails. Two new essays round it out: Shadow IT at the browser and what word count actually measures.
Totals after v7.41.37
60 tools. 38 guides. SW cache → v7.41.37. Linter clean at 34/34 checks. All test suites green, including four new suites covering the new tools, the monetization pages, and the depth improvements.
Jul 5, 2026
v7.41.34–35 · Five new guides, a sitemap fix, and the mascot returns
Five new guides from the workshop.
Each one is a companion to a tool that didn’t have one yet: Base64 explained (why encoded files are exactly a third bigger), URL encoding (why spaces become %20 — and sometimes +), CRC32 vs MD5 vs SHA-256 (match the fingerprint to the threat), Open Graph tags (how link previews are actually built), and KB vs KiB (why a 1TB drive shows 931 “GB”). That’s 31 guides on the shelf.
A search-engine fix and an invisible-mascot fix.
Two things that were quietly wrong: four guides shipped in June (WCAG contrast, reading chmod, number bases, magic numbers) had never been added to sitemap.xml, so search engines weren’t being told they exist — backfilled, with a parity check confirming all 31 posts now appear in every registry the site keeps. And the author’s note at the bottom of 24 posts referenced an SVG mascot symbol that was never defined anywhere, rendering a 56-pixel void where Tooly’s face should be. A new idempotent patcher swaps in the real mascot image, and six regression tests make sure it stays fixed.
Totals after v7.41.35
40 tools. 31 guides. 82 HTML pages. SW cache → v7.41.35. Linter clean at 34/34 checks. Test suite: 82 tests across 3 suites plus 16 per-tool logic suites, all green.
Jun 21–22, 2026
v7.41.27–33 · Sixteen new tools (24 → 40) + homepage filter
The toolshed grew up.
Seven releases in two days took the catalog from 24 tools to 40: Contrast Checker, Word Counter, Color Converter, URL Parser, Text Sort & Dedupe, Roman Numerals, Data Size Converter, Aspect Ratio Calculator, CRC32 Checksum, Meta Tag Generator, ASCII Table, and friends — every one client-side, every one with its own logic test. Six new guides landed alongside them (password entropy, lorem ipsum, WCAG contrast, chmod, number bases, magic numbers).
Finding things in a 40-tool grid
With the flat grid getting long, the homepage gained a category filter bar (All / Dev / Image / Design / Generate / Doc / Text) — progressive enhancement, so with JavaScript off every card simply stays visible — and the hero got its first proper call-to-action buttons.
Totals after v7.41.33
40 tools. 26 guides. SW cache → v7.41.33. All tools registered in the Cmd+K palette, homepage ItemList schema, and sitemap.
Jun 18, 2026
v7.41.25–26 · Reliability & hardening sweep
Production hardening.
A sweep focused on the unglamorous stuff: security headers, third-party CDN libraries vendored into the tree (so tools keep working if a CDN doesn’t), dev files hidden from the deployed site, and three new linter checks to keep all of it true on every future release. Plus the 20th guide: UUIDv4 vs UUIDv7.
Totals after v7.41.26
24 tools. 20 guides. SW cache → v7.41.26. Linter at 34 checks.
May 1, 2026
v7.41.24 · Google Fonts removed site-wide (privacy + perf)
The site no longer pings Google Fonts.
Every page used to declare a quartet (or triplet, or singleton, depending on how old the page was) of <link> tags pointing at fonts.googleapis.com and fonts.gstatic.com — preconnect warmups, a CSS fetch, then the actual font files served from Google's CDN. Since v7.41.15 the same three families (Fraunces, Inter, JetBrains Mono) have also been served self-hosted from /fonts/*.woff2 via shared.css. Both sources were loading in parallel; the self-hosted one was already winning the cascade because shared.css loads later in source order. The Google Fonts request was wasted bandwidth, a pair of unnecessary cross-origin DNS lookups, and a privacy footprint for every cold visit.
v7.41.24 removes one hundred and eighty-seven Google Fonts <link> tags across fifty pages via a new patcher (patches/remove-google-fonts.py) that handles every head-shape variant the site ships: noscript-wrapped, preload-with-onload-trick, single-line stylesheet, reversed-attribute, and multi-line stylesheet. Idempotent and tested against seven regression cases. Now in the patches/ glob so release.py auto-runs it on every release going forward — a future page accidentally pasted in with Google Fonts links gets auto-cleaned at release time.
What this saves on every cold load.
Two cross-origin DNS lookups, two cross-origin TCP+TLS handshakes, one cross-origin CSS fetch, and the three-to-six cross-origin WOFF2 fetches a typical page would make. The privacy footprint of telling Google "this user is on this page right now" goes from four-plus requests per page to zero. The site is now fully self-contained for fonts — no third-party font CDN is contacted on any page load.
Prose mentions of "Google Fonts" in the blog are intact.
The "What 'client-side' really means" essay literally uses Google Fonts as the canonical example of a CDN that sees your IP every time you load a page that uses it. That argument depends on the prose being accurate. The patcher's regexes only target <link> tags, not text content; the essay reads exactly as before, just with the meta-irony that the site it's hosted on no longer ships the dependency it warns about.
Totals after v7.41.24
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages. SW cache → v7.41.24. Linter clean at 31/31 checks (1 warning — this changelog page is 50.5 KB gzipped, 0.5 KB over the 50 KB budget; not blocking). Test suite: 61 tests across 3 suites, all green (was 54; +7 for the new patcher). Zero references to fonts.googleapis.com or fonts.gstatic.com in the deploy tree.
May 1, 2026
v7.41.23 · shared.css migration complete — 51/51 pages
Every page is on consolidated CSS.
The seven pages that v7.41.22's plan documented as needing browser-based visual verification turned out to fall into two clean risk tiers, neither of which actually needed a browser. Five (/jwt/, /cron/, /compress-pdf/, /json-repair/, /regex-ai/) have no <nav> element at all, so shared.css's elevated nav { ... } rule simply doesn't match anything — pure single-step migration. Two (/benchmarks/, /convert/) have <nav> inside a custom <header class="..."> with no inline override; they got the <link> plus a small inline rule scoped to their specific header class that nullifies shared.css's nav styling for that page only.
Coverage: 51 of 51 user-facing pages. The only HTML file in the tree that doesn't link shared.css is the Google Search Console verification file, which has to ship byte-exact and isn't a real page.
What this unblocks.
Roadmap item v7.43-E (remove the Google Fonts <link> site-wide) is no longer gated. Every page has the self-hosted variable fonts shipped in v7.41.15 available via shared.css's @font-face declarations pointing at /fonts/*.woff2. The Google Fonts CDN dependency on each page now becomes pure overpayment — redundant with the self-hosted bundle that's already precached by the service worker. Removing it (the <link rel="preconnect"> + <link rel="preload"> + <link rel="stylesheet"> triplet on each page) is a follow-up release.
Class-collision concerns from the original plan turned out to be non-issues.
The v7.41.22 migration plan flagged /json-repair/, /regex-ai/, and /compress-pdf/ as risky because they use .btn, .btn.secondary, or .container classes that shared.css also touches. Closer audit found that in practice the inline <style> block on each page wins the cascade for every property both define, and shared.css doesn't define .container at all (only .foot-links, .tool-card, .nav-brand, .nav-right). So the inline rules keep winning and the pages render identically.
Totals after v7.41.23
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages (52, including the Google Search Console verification stub). SW cache → v7.41.23. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 51/51 user-facing pages — migration complete.
May 1, 2026
v7.41.22 · All 19 blog posts migrated to shared.css — 88% milestone
The entire blog tree is on consolidated CSS.
All nineteen blog posts — from "Why free isn't free" through "Why your regex can hang the server" — now link shared.css and use the self-hosted variable fonts shipped in v7.41.15. The migration was mechanical: seventeen posts share a byte-identical inline nav rule from the unified blog template; two older posts (image-compression-guide, iphone-heic-problem) use a slightly older multi-line variant. Both shapes got the same recipe applied via a one-shot inline script.
Family C tools deferred, deliberately.
Family C (/convert/, /jwt/, /cron/, /json-repair/, /regex-ai/, /compress-pdf/) use <header> elements instead of <nav> at body root, AND share class names (.btn, .container, .primary, .secondary) that collide with shared.css's selectors. Batch-migrating any of them risks a visible button or container styling regression. Each tool needs individual review of how its existing classes interact with shared.css's bare-element rules. Slated for per-page audited releases.
88% migrated. 6 pages left.
Coverage jumped from twenty-five out of fifty to forty-four out of fifty in this release. Remaining: /benchmarks/ (different layout, deferred) and the six Family-C tools. Once those are reviewed, v7.43-E removes the Google Fonts <link> site-wide and the migration is complete.
Totals after v7.41.22
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages. SW cache → v7.41.22. Linter clean at 31/31. Test suite: 54 tests, all green. Pages linking shared.css: 44/50 (was 25). Net new: 19.
May 1, 2026
v7.41.21 · Family B tools migrated — 50% milestone
All seven Family-B tool pages now link shared.css.
Family B — the seven tools with a different head pattern than Family A — turned out to be simpler to migrate, not harder. UUID Generator, Hash, Base64, Markdown editor, Encode, HTTP Status, and ICO Inspector all share an inline nav { ... padding: 16px 24px; ... } rule that already has horizontal padding, AND their <nav> sits at body root rather than inside <div class="container">. So shared.css's elevated --bg-elev background spans the full viewport naturally with no nav-bg workaround needed. Single-step migration: add the <link rel="stylesheet" href="/shared.css">; done.
50% of the site is on consolidated CSS.
Coverage hit twenty-five of fifty pages with this release — exactly half. Pages migrated: the homepage, the offline fallback, five hubs (/about/, /support/, /changelog/, /blog/, /privacy/), eleven Family-A tools, seven Family-B tools. Remaining: /benchmarks/ (deferred — different <header class="site"> layout), six Family-C tools (minimal head, per-page audit needed), and nineteen blog posts (uniform template — will batch in one release once a representative post is verified).
Totals after v7.41.21
24 tools (unchanged). 19 articles. 52 HTML pages. SW cache → v7.41.21. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 25/50 (was 18). Net new in this release: 7.
May 1, 2026
v7.41.20 · last 3 Family-A tool pages migrated
Family A is done.
The last three Family-A tool pages — HEIC Unheicer, Favicon Foundry, SVG Shrinker — now link shared.css. All eleven Family-A tools have migrated cleanly with the same two-step recipe. Coverage: eighteen of fifty pages (homepage + offline fallback + five hubs + eleven Family-A tools).
What’s left
Forty-two percent migrated; fifty-eight percent on the per-page Google Fonts model. Remaining: /benchmarks/ (different layout, deferred for audit), seven Family-B tools (direct <link> + body-root <nav>; needs different recipe), six Family-C tools (minimal head; needs per-page investigation), and nineteen blog posts (uniform template, will batch in a single release once one representative post is verified). Full removal of the Google Fonts <link> site-wide is roadmap v7.43-E, gated on every page having migrated first.
Totals after v7.41.20
24 tools (unchanged). 19 articles. 52 HTML pages. SW cache → v7.41.20. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 18/50 (was 15).
May 1, 2026
v7.41.19 · 4 more tool pages migrated to shared.css
Phase 4-C continues: /regex/, /time/, /color/, /compress/.
Same recipe as the previous releases — <link> to shared.css before the inline <style>, plus the proven nav-bg margin/padding workaround. No structural surprises in any of the four. shared.css coverage is up to fifteen of fifty pages; three Family-A tools remain (/heic/, /favicon/, /svg/), then we move on to the auditing-required Family-B and -C tools.
Totals after v7.41.19
24 tools (unchanged). 19 articles. 52 HTML pages. SW cache → v7.41.19. Linter clean at 31/31. Test suite: 54 tests, all green. Pages linking shared.css: 15/50 (was 11).
May 1, 2026
v7.41.18 · 4 tool pages migrated to shared.css
The first tool pages are on the consolidated stylesheet.
v7.41.17 finished the hub pages; v7.41.18 opens the tool-page batch with the four most-trafficked Family-A tools: JSON Fixer, Format Flipper, Diff Checker, and QR Maker. Same two-step recipe as /privacy/ and the hubs — <link> to shared.css immediately before the inline <style>, plus the proven margin: 0 -24px; padding: 20px 24px nav workaround. These four were chosen because they were already touched by recent releases (input auto-save + tool-specific keyboard shortcuts), so any visual regression would be highest-impact-fastest-noticed.
Three families of tool pages, only one matches the recipe
An audit before this release surfaced that the twenty-four tool pages aren’t one shape but three: eleven Family-A tools (standard <noscript> + .container; the recipe applies cleanly), seven Family-B (direct <link> + <nav> at body root; needs different recipe), and six Family-C (minimal head, no detectable Google-Fonts loader; needs per-page investigation). Family A is being migrated in batches of four; Families B and C are deferred to dedicated releases that audit each page’s head and nav structure first. One-size-fits-all batching there would risk a v7.41.13-style visual regression per page.
Totals after v7.41.18
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages (unchanged). SW cache → v7.41.18. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 11/50 (was 7).
April 30, 2026
v7.41.17 · 4 more hub pages migrated to shared.css
Four more hub pages now use the consolidated stylesheet.
v7.41.16 ran a single-page experiment on /privacy/ to validate that the migration recipe produces byte-identical visual output. The recipe held, so v7.41.17 applies it in batch to About, Support, Changelog, and Blog. Each got the same two changes: a <link rel="stylesheet" href="/shared.css"> immediately before the inline <style> block, and the same nav-bg workaround as the homepage and /privacy/ — margin: 0 -24px; padding: 20px 24px on the inline nav rule so shared.css's elevated background spans the full viewport instead of being clipped by the container's twenty-four-pixel side padding.
/benchmarks/ deferred.
The Benchmarks page uses a different layout — <header class="site"> with a <div class="wrap-wide row"> wrapper, rather than the <nav>-inside-.container pattern every other hub uses. The standard recipe doesn't apply cleanly; a per-page review of how its custom header CSS interacts with shared.css's nav rules will happen in a separate release.
Totals after v7.41.17
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages (unchanged). SW cache → v7.41.17. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 7/50 (was 3). Remaining: /benchmarks/ (deferred), 24 tool pages, 19 blog posts — roadmap continues in batches of four per v7.43-C and v7.43-D.
April 30, 2026
v7.41.16 · /privacy/ migrated to shared.css (Phase 4-B start)
One more page now uses the shared stylesheet.
The privacy page is now linked to /shared.css alongside the homepage and the offline fallback. Three of fifty pages migrated; forty-seven to go. Each per-page migration is a small, independently-revertable step on the long tail of removing per-page CSS duplication — a long-running consolidation roadmap that ends with the Google Fonts <link> tag being removable from every page (planned for v7.43-E once full coverage is in).
Same nav fix as the homepage applied here.
The privacy page's <nav> sits inside a <div class="container"> just like the homepage's, so it shares the same gotcha: shared.css gives nav an elevated --bg-elev background, but the container's twenty-four-pixel side padding clipped the background to the content area on mobile. The fix is identical to the v7.41.13 homepage fix — margin: 0 -24px; padding: 20px 24px on the inline nav rule pulls the element out past the container's padding so the background spans full viewport width.
Migration recipe for the next hub pages.
The remaining six hub pages (About, Support, Changelog, Benchmarks, Blog, plus any future hubs) follow the same three-step recipe documented in CHANGELOG.md: add the <link rel="stylesheet"> immediately before the inline <style> block; apply the same nav-bg workaround if the page's <nav> sits inside .container; visually diff the deploy preview before merging. A future release can do three or four of those in one pass once we're confident the pattern produces byte-identical visual output.
Totals after v7.41.16
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.16. Linter clean at 31/31 checks. Test suite: 54 tests, all green. Pages linking shared.css: 3 of 50 (was 2).
April 30, 2026
v7.41.15 · self-hosted variable fonts
The site no longer asks Google Fonts for Fraunces, Inter, or JetBrains Mono on every load.
Four variable WOFF2 fonts now ship under /fonts/, sourced from the upstream OFL repos (rsms/inter, JetBrains/JetBrainsMono, undercasetype/Fraunces) and subsetted to a Latin glyph range via pyftsubset. Total bundle: 357 KB — smaller than the megabyte-class ceiling we'd budgeted, because the upstream variable fonts subset more efficiently than feared. All four are added to sw.js's PRECACHE so first-visit users on the pages that link shared.css see the real type immediately rather than a flash of system fallback.
The motivation isn't purely abstract privacy: shared.css has been linked from the homepage and /offline.html since the v7.43-A pilot, and its @font-face declarations were pointing at /fonts/*.woff2 URLs that didn't exist on disk — meaning every cold load on those two pages either fell through to Google Fonts (correct but cross-origin) or 404'd silently in the network panel. Either way, the site was claiming “self-hosted fonts” while not actually self-hosting any. Closed by shipping the actual files.
Lint check #24 is alive again.
Lint check #24 (font precache parity) was added in v7.41.5 but ran as a no-op until /fonts/ existed. With the directory now populated, the check enforces a bidirectional contract: every font URL referenced in shared.css's @font-face rules must appear in sw.js's PRECACHE list, and vice versa. So a future maintainer can't add a new font face without precaching it (or the inverse). Both lists agree at all four URLs as of this release.
What this release deliberately does NOT do
Fonts are shipped, but shared.css is still only linked from two pages (the homepage and the offline fallback). The remaining forty-eight pages still get fonts via the Google Fonts <link> tag in their inline <head>. Migrating them is a per-page exercise — cf. v7.41.13's nav-background fix, which was a one-page-at-a-time consequence of shared.css's opinions about nav styling. The roadmap continues to do this in batches of four with a visual diff each, not as a flag-day rewrite.
Totals after v7.41.15
24 tools (unchanged). 19 articles (unchanged). 52 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.15. Linter clean at 31/31 checks (lint #24 now active for the first time). Test suite: 54 tests, all green. Total bundle gained ~360 KB of variable woff2 binaries.
April 30, 2026
v7.41.14 · 19th blog post + feed.xml full-text migration
Why your regex can hang the server.
The 19th blog post is live: "Why your regex can hang the server" — a ~1500-word essay on catastrophic backtracking. The Stack Overflow 2016 incident, the 2n explosion in a twelve-character regex, the three patterns to recognize (nested quantifiers, alternation overlap, optional-then-required), and four ways to actually defend (rewrite, atomic groups, RE2-class engines, timeouts). Pairs with the Regex Rambler tool and complements the existing practical regex cheatsheet — reference + cautionary essay together.
RSS readers now get the full essay, not just a summary.
Every entry in feed.xml previously had only a hand-curated <summary type="text"> with a one-or-two-sentence excerpt. Aggregators in full-text mode (Reeder, NetNewsWire's article pane, Feedbin's full-content view) had nothing else to render — they either showed the summary twice or sent users out to the site for the actual content. Now every entry carries both the existing <summary> AND a <content type="html"> block with the full article body, CDATA-wrapped, with root-relative URLs absolutized so aggregators on third-party origins resolve them correctly. The feed went from thirteen kilobytes to two hundred thirty-five kilobytes — comfortably under the half-megabyte budget set in the migration plan, generous to RSS readers, no detectable cost on anything else.
Implementation is a new patcher, patches/feed-add-fulltext.py: idempotent, dry-run-able, handles both layouts (older posts use <section class="article-body">; newer ones use <div class="prose">) by walking the matching tag's depth. Eight regression tests pin the behaviour. release.py picks up the patcher automatically going forward, so any future blog post gets a <content> block at release time without manual steps.
Totals after v7.41.14
24 tools (unchanged). 19 articles (was 18). 52 HTML pages (was 51). 42 OG-PNGs. SW cache → v7.41.14. Linter clean at 31/31 checks. Test suite: 54 tests across 3 suites, all green (was 46).
April 30, 2026
v7.41.13 · homepage mobile nav background fix + Lighthouse-policy rewrite
The homepage nav background finally fills the viewport.
On every screen narrow enough to render the page in mobile-ish layout (anything roughly under 1250 pixels), the homepage’s nav header had its elevated dark background inset twenty-four pixels from each edge of the viewport instead of spanning edge-to-edge. The deeper black of the page body showed through those two strips, making the header look like a floating ledge rather than site chrome. Subtle on a quick glance, obvious once you noticed it, and visible on production since at least the initial public-repo upload.
The cause was a partial-migration artifact. index.html links shared.css (one of two pages that does, as the v7.43-A pilot); shared.css gives <nav> an elevated --bg-elev background. But the page’s inline <style> overrode the nav’s padding to 20px 0 — top and bottom only, no horizontal — without resetting the background. And because <nav> sits inside <div class="container">, which has its own twenty-four-pixel side padding, the nav element’s elevated background was clipped to a narrower rectangle than the viewport.
Fix: margin: 0 -24px; padding: 20px 24px on the homepage’s inline nav rule. The negative horizontal margin pulls the nav out past .container’s padding so the background spans the full viewport width; the inner padding restores the content position so nothing visually shifts inside the nav. Same end-state as tool pages, which already get edge-to-edge nav backgrounds because their <nav> sits at the body root rather than inside the container.
Lighthouse-baseline policy rewritten.
The deploy playbook used to read “capture a Lighthouse baseline post-deploy” — implying every release. That’s noise: most releases (this one included) don’t change anything that affects load performance, so re-baselining produces a stack of nearly-identical JSONs that obscure the meaningful comparisons. Reframed as “capture only at perf-relevant checkpoints” — specifically before and after v7.42-E (self-hosted fonts), v7.43-E (remove duplicated inline CSS), and v7.45 (resource preloads + fetchpriority). Correctness fixes, content additions, accessibility work, and infra changes don’t need a baseline capture. Keeps the benchmarks/history/ directory honest about what a captured measurement is for.
Totals after v7.41.13
24 tools (unchanged). 18 articles (unchanged). 51 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.13. Linter clean at 31/31 checks. Test suite: 46 tests across 3 suites, all green.
April 30, 2026
v7.41.12 · operational maturity — CI, continuous-deploy config, expanded shortcut + auto-save coverage
The site now has CI.
Every push to a branch and every pull request now runs the full pre-deploy gate inside GitHub Actions: python3 lint.py . for the thirty-one site invariants, bash run-tests.sh for the forty-six tests across catalog parity, lint regressions, and patcher idempotency, plus node --check on the JavaScript and python3 -m py_compile on the Python. There’s also a belt-and-suspenders check that the top ## v heading in CHANGELOG.md agrees with sw.js ’s CACHE_VERSION — the same invariant lint #22 already enforces, surfaced earlier in the workflow output so a forgotten release.py run shows up before the lint section runs.
Continuous deploy configured.
Shipped a netlify.toml declaring the static-first publish config, per-resource cache headers (long cache for content-addressed images / fonts / icons; short cache plus must-revalidate for HTML, sw.js, and the web manifest), and the security headers a 2026-grade dev tools site should have: X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and a Permissions-Policy that opts out of geolocation, camera, microphone, and FLoC. Replaces the drag-and-drop deploy workflow that’s been live since v1 with a real continuous-deploy pipeline once the maintainer wires up Netlify’s GitHub integration once. The deploy + rollback runbook lives at docs/DEPLOY-CHECKS.md.
More tools remember what you typed.
The localStorage auto-save module shipped in v7.41.11 with three pages opted in. v7.41.12 opts in four more — Regex Rambler’s test string, Markdown editor’s document, SVG Shrinker’s source, and Encode’s URL and HTML inputs. Nine textareas across seven tool pages now persist their value across sessions. As always: nothing leaves your browser, the size cap is 200 KB per field, and the credential-name deny-list (password, secret, token, credential, …) silently refuses to opt in any field whose id, name, or aria-label suggests it shouldn’t persist.
And more keyboard shortcuts in the help modal.
v7.41.11 shipped per-tool shortcuts on three pages. v7.41.12 adds three more: JSON Fixer (F Format, M Minify), Format Flipper (C Copy, D Download, S Swap), and Base64 (U URL-safe, W Wrap 76). Eleven bindings across six tools now. Hash and Regex were considered and skipped: their UIs are live-update with no single primary action, so a tool-specific binding would either duplicate the global / focus shortcut or pick arbitrarily among multiple equivalent results.
Totals after v7.41.12
24 tools (unchanged). 18 articles (unchanged). 51 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.12. Linter clean at 31/31 checks. Test suite: 46 tests across 3 suites, all green. CI workflow runs them on every push.
April 28, 2026
v7.41.11 · main landmark on 34 pages, per-tool shortcuts, input auto-save, 18th post
Thirty-four pages got a real <main> landmark.
v7.41.6 shipped a skip-to-content link on every non-exempt page, but on thirty-four of forty-eight pages the link's anchor target was a placeholder <a id="main-content" tabindex="-1"></a> rather than a real <main> element. The placeholder works fine for the keyboard-skip behaviour itself but doesn't give the page an HTML5 main landmark — a real Lighthouse-accessibility audit point and the kind of thing reader-mode rendering looks for to identify "the article." Promoted all thirty-four to a real <main id="main-content"> via the new patches/add-main-landmark.py. The fourteen pages that already had <main> are unchanged.
Per-tool keyboard shortcuts in the help modal.
Pressing ? on any page now shows a section labelled "On this page" if the current tool has bound any tool-specific shortcuts. The tools opt in by setting window.toolShortcuts = [{ keys: 'G', label: 'Generate' }, …]; the commandk.js help modal reads it on every open. Three tools have shortcuts so far: UUID/Nanoid generator (G generate, C copy), QR Maker (C copy PNG, D download SVG), and Diff Checker (S swap sides). All three skip when you're typing into an input or textarea, so the keys don't fight the actual content of the field.
Tool inputs save themselves.
If you've been formatting JSON for ten minutes and accidentally close the tab, the JSON Fixer, Format Flipper, and Diff Checker now restore your input on the next visit. The save is debounced 250 ms, capped at 200 KB per field, and stored under tooly.persist.<path>.<field> in localStorage. Cleared like any other browser state — nothing leaves your machine. Failure modes (private browsing, full quota) are silent.
Hard refusal: any field whose id, name, aria-label, or placeholder matches the regex /password|secret|token|api[\s_-]?key|private[\s_-]?key|credential|jwt[\s_-]?secret/i is excluded from auto-save. The deny-list is intentionally broad — if a tool legitimately needs to persist a "key" field it has to rename, not opt in. Aligned with the "tools that respect you" principle: don't store credentials anywhere the next user of this device could read them.
The 18th blog post: CSS color-scheme.
"What CSS color-scheme actually controls" — the small meta tag we shipped on every page in v7.41.4. Why it fixes the white-scrollbar-flash bug, what it does and doesn't change, how it differs from prefers-color-scheme, and the three traps that catch people. ~6 min read.
Totals after v7.41.11
24 tools (unchanged). 18 articles (was 17). 51 HTML pages (was 50). 42 OG-PNGs. SW cache → v7.41.11. Linter clean at 31/31 checks. Test suite: 46 tests across 3 suites, all green.
April 28, 2026
v7.41.10 · audit gap closure — license, repo URL, dev tooling
The site claimed to be MIT-licensed. Now it actually is.
The About, Support, Format Flipper, and Changelog pages have all stated for several releases that the source is “on GitHub under an MIT license.” The repo, until today, had no LICENSE file — which under GitHub’s defaults means the code was technically All Rights Reserved. So a reader who clicked through to verify the claim, and looked for the licence, would have found the claim wasn’t quite true. Shipped a real LICENSE file containing the MIT text, removed the LICENSE-NEEDED.md placeholder, and the public claim is now accurate.
Five “Star the GitHub repo” links pointed at a 404.
An older repo slug (J1E2M3/tooly-mctoolface) was still hard-coded in five places across About, Support, Format Flipper, and Changelog. The actual repository is J1E2M3/tooly-mctoolface — the format-flipper slug never worked. Every “star the repo” CTA on the support page sent readers to a missing GitHub page. Rewrote all seven occurrences in one pass via a new patcher, patches/fix-repo-url.py (idempotent, dry-run-able). The Format Flipper tool is still open source; the link now resolves.
Restored the dev test suite and six missing patcher scripts.
The repo’s tests/ and most of patches/ had drifted out of git between v7.41.4 and now — run-tests.sh exited non-zero on the first command because tests/test_commandk.js didn’t exist, and six patchers documented in CHANGELOG.md (add-og-type, add-twitter-image, add-resource-hints, add-color-scheme, add-breadcrumbs, revert-plausible-preconnect) weren’t in the working tree. Restored all of them from the v7.41.4 deploy snapshot. bash run-tests.sh now passes 33 tests across the three suites against the current v7.41.10 tree without modification.
Refreshed sitemap dates for the four pages touched in this release.
sitemap.xml’s <lastmod> values for /about/, /support/, /format/, and /changelog/ now reflect 2026-04-28 (the date of the repo-URL fix). Other URLs are unchanged because their content didn’t change in this release; bumping every <lastmod> wholesale would be a freshness-signal lie. Lint check #20 (sitemap lastmod accuracy) still passes cleanly.
Totals after v7.41.10
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.10. Linter clean at 31/31 checks (unchanged from v7.41.9). 33 tests pass across 3 suites (commandk + lint + patches).
April 26, 2026
v7.41.9 · audit-driven polish — form labels, theme-color, cleanup
Forty form controls finally have accessible names.
A fresh audit pass turned up forty <input> / <textarea> / <select> controls across fifteen tool pages that relied on placeholder text alone as their label. WCAG 1.3.1 / 4.1.2 are explicit that placeholder is not a label: it disappears once the user types, and screen readers don’t reliably announce it as the field’s name. So a screen-reader user opening the JSON tool would have heard “edit text” with no indication of what to type.
Each got a hand-written aria-label that reads as a noun phrase — JSON to format, Text to hash, Source format, Markdown source, JWT signing secret, etc. — rather than the original instructional placeholder text (“Paste JSON here, or load a sample from the toolbar…” is helpful as visible hint text but isn’t the right thing for a screen reader to announce as the name of the field). The original audit flagged sixty-eight controls; twenty-eight of those turned out to have implicit labeling (the input is wrapped in a <label> element with text content) which the first audit script had missed. The remaining forty are the real gap, all closed in this release.
The manifest and the meta tag now agree on the theme color.
Subtle drift caught during the audit: manifest.webmanifest declared theme_color: "#F4A940" (the brand amber) while every HTML page declared <meta name="theme-color" content="#0E0D0B"> (the dark background). The visible consequence: in a regular browser tab the mobile address bar tinted dark, but once a user installed the site as a PWA the system bars tinted amber. Two different identities depending on context. Synced the manifest to #0E0D0B so both contexts match.
Lint check #28 (theme-color parity) extended to include the manifest in the parity check, so the two can’t drift again. Negative test verified: introducing a mismatch between manifest and HTML pages now fails the check.
One new lint check pinning the form-label work.
#31 Form-control accessible name. Every <input> / <textarea> / <select> (excluding hidden / submit / button / reset) must have one of three valid labeling patterns: aria-label / aria-labelledby on the control, an explicit <label for="id"> referencing it, or implicit labeling (the control is inside a <label> element with text content). Only fully-unlabeled controls fail the check. All thirty-one checks now pass on the merged tree.
Totals after v7.41.9
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.9. Linter clean at 31/31 checks (was 30 in v7.41.8).
April 25, 2026
v7.41.8 · two mobile rendering fixes
Pre-existing: the homepage eyebrow clipped on mobile.
The hero’s eyebrow line — “Free tools · No signup · Works in your browser” — was overflowing the right edge of the viewport on phones. Caused by a .eyebrow rule that uses display: flex with a 24-pixel decorative line as a ::before pseudo-element. On a 360-pixel viewport that came out to roughly 437 pixels of uppercase JetBrains Mono text plus 32 pixels of line and gap, against an available content width of about 312 pixels. The text wrapped, but the first wrapped line still extended past the right edge and got clipped (“WORKS I” visible, the rest off-screen). The mobile @media block was only adding justify-content: center to the eyebrow, which doesn’t help when the content itself is wider than the container.
This bug pre-dates any of the recent v7.41 audit work — it’d been live since at least v7.40, surfaced now from a user-reported screenshot. Fix is small: in the @media (max-width: 800px) block, switch .hero .eyebrow from flex to block, hide the ::before decorative line on mobile, center-align the text. The line wraps naturally onto two short lines and no longer overflows. Desktop layout unchanged.
Regression: the skip-link CSS was breaking mobile viewport sizing.
The skip-to-content link added in v7.41.6 used the position: absolute; left: -9999px offscreen technique to hide itself until focused. That’s a known older accessibility pattern, and known to misbehave on mobile: some mobile browsers compute the document’s scrollable width including elements at negative-x coordinates. A 1×40-pixel link at left: -9999px made the layout viewport effectively 9999 + the page’s width, which the browser then zoomed-out to fit on a small screen. Every page rendered at desktop width on phones despite a correct <meta name="viewport">.
This was a regression I introduced and could have avoided. I’d flagged the concern in the v7.41.6 audit (“the -9999px approach has known issues”) but went with it anyway because it works for the desktop keyboard case. Mobile users paid the price between v7.41.6 deploy and this fix.
Fixed with the modern visually-hidden pattern.
The link is now positioned at left: 0; top: 0 — no negative offsets that can affect viewport calculations — and rendered invisible via a 1×1 box with overflow: hidden, clip: rect(0 0 0 0), clip-path: inset(50%), and white-space: nowrap. When focused, it switches to position: fixed; width: auto; height: auto; clip: auto; clip-path: none, becoming the same visible amber pill at the top-left as before. Keyboard tab order, screen-reader behaviour, and visual appearance under focus are unchanged. The 9999-pixel layout-viewport ghost is gone.
Applied in-place across all forty-eight patched pages (every page had byte-identical CSS, so a global find-replace was safe). patches/add-skip-link.py was updated to emit the new pattern on any future re-run, with a source comment naming the specific regression so the next maintainer doesn’t reach for -9999px again. Service worker bumped to v7.41.8 per the hard invariant.
Why the linter didn’t catch it
Lint check #19 (page weight) and check #27 (skip-link presence + anchor target) both passed before and after. The bug is a runtime / mobile-specific layout issue, not a static-HTML invariant: there’s no obvious cheap-to-write static check that distinguishes “valid visually-hidden CSS” from “valid offscreen-by-negative-x CSS” without a real browser. The defensive answer is the patcher’s updated source comment, which now warns specifically against the -9999px technique. The better defensive answer would be Lighthouse / actual mobile testing in CI, which is the v7.42-D roadmap item.
Totals after v7.41.8
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.8. Linter clean at 30/30 checks (unchanged from v7.41.7). Both mobile fixes shipped together because they’re both “the page renders wrong on a phone” bugs and the deploy gate is the same.
April 25, 2026
v7.41.7 · benchmarks og:type fix + three lint checks
The benchmarks page told Slack it was an article.
Subtle: benchmarks/index.html declared <meta property="og:type" content="article"> but it’s a hub page, not a blog post. Link-preview crawlers (Slack, iMessage, LinkedIn, Discord) read og:type when they decide how to render a shared link. For article they show date, byline, sometimes a different layout; for website they show a generic preview. The benchmarks page has no real article date or byline, so the article-style preview rendered weirdly — with falsely-implied freshness on a static reference page. Changed to og:type="website".
The fix is one line; the more interesting work is the lint check that pins the invariant. #29 og:type per page-type now requires every page under blog/<slug>/ to declare og:type="article", and every other non-exempt page to declare og:type="website". The benchmarks/ drift would have failed this check; reverting it during testing reproduces the error and reverting it back returns to clean.
Two more defensive lint checks closing real coverage gaps.
#28 theme-color parity. Every non-verification page must declare a <meta name="theme-color">, and the value must agree across all pages that have one. Browsers use theme-color for things like the address-bar tint on mobile and the title-bar colour in installed PWAs. Drift across pages produces a flickering experience as the user navigates. The check doesn’t hardcode today’s value (#0E0D0B) — it just enforces cross-page agreement, so a future redesign doesn’t require touching lint.py.
#30 Blog post structure. Every page under blog/<slug>/ must have BOTH an <article> HTML element AND article-like JSON-LD (Article, BlogPosting, or NewsArticle). The HTML side gives screen readers a landmark; the JSON-LD side gives search engines and link previews the structured metadata. Both halves matter and have historically drifted independently when blog templates were updated. All seventeen posts pass today.
Existing feed-validity check got stricter.
Check #26 already validated that every Atom <entry> had a proper <author>/<name> child and that blog/<slug>/ directories matched feed entry ids. It now also requires every entry to have a non-empty <id>, <title>, <updated>, and at least one <link rel="alternate" href="…"> child. Atom RFC 4287 requires these and strict feed readers (NetNewsWire, FreshRSS, Feedbin) drop entries that are missing any of them. None of the seventeen current entries was missing anything — the check is purely defensive against future drift.
Totals after v7.41.7
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.7. Linter clean at 30/30 checks (was 27 in v7.41.6).
April 25, 2026
v7.41.6 · skip-to-content link on every page
Keyboard users can finally skip the navigation.
WCAG 2.4.1 says the first focusable element on every page should be a way to bypass the site nav and jump straight to the page’s primary content. Forty-seven of forty-nine pages had nothing — press Tab on a tool page and the first stop was the “All tools” link in the nav, then every other nav link, then the Cmd+K trigger, then the input field you actually came to use. Eight Tabs to start working. Now one Tab → Enter, and focus jumps to the start of the actual content.
Implemented as a small visually-hidden link (position: absolute; left: -9999px) that becomes a focused amber pill pinned to the top-left when it receives focus. Visible only to keyboard / screen-reader users; invisible during normal browsing. The CSS lives inline in each page’s <head> so it works regardless of whether shared.css is loaded yet. Added to every site page except /offline.html (self-contained fallback) and the Google Search Console verification file (which has no UI).
Two anchor strategies, depending on what the page already had.
Fourteen pages already had a <main> landmark element (the dev-tooling pages added in v7.30+). Those got id="main-content" tabindex="-1" appended to their existing <main> tag — no structural change, just a new anchor target.
The other thirty-four pages (including the homepage and all seventeen blog posts) wrap their content in <div class="container"> rather than <main>. For those, an empty <a id="main-content" tabindex="-1"></a> got inserted immediately after the first </nav> closing tag — reliably the end of the site nav (any breadcrumb or article-internal nav appears later in the document). Adding a real <main> wrapper around thirty-four pages was tempting but a much bigger structural change for marginal additional benefit; the inline anchor satisfies WCAG 2.4.1 today and the pages can be migrated to <main> when the v7.43 CSS consolidation pass touches them anyway.
The patcher and the lint check go together.
The transformation lives in patches/add-skip-link.py: idempotent (re-running is a no-op), attribute-order-agnostic on its “is this already patched?” regex (so it survives Pattern H6 drift), with a --dry-run flag and per-file tally output. Same shape as the v7.41.4 patches that landed color-scheme and BreadcrumbList, just for a different target.
And linter check #27 now enforces what the patcher created: every non-exempt page must have an <a class="skip-link" href="#X"> whose target id X resolves to an element on the same page. So if anyone accidentally drops the skip link — or breaks the anchor target during an unrelated edit — CI catches it before deploy. Verified with both a positive case (47 of 48 pages have the right shape; build is green) and a negative one (deleted the skip link from one page, watched lint go red, restored, watched it go green again).
Totals after v7.41.6
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.6. Linter clean at 27/27 checks (was 26 in v7.41.5).
April 25, 2026
v7.41.5 · feed.xml fix + audit-driven linter hardening
An Atom feed bug that’d been live for three weeks.
Seven entries in feed.xml — the seven blog posts added in v7.41.2 (markdown-reference, favicon-ico-format, http-status-codes-meaningful, color-palettes-from-photos, cron-expressions-guide, llm-json-problem, jwt-decoded) had <author><n>Tooly McToolface</n></author> instead of the Atom-spec <name>. Valid XML, invalid Atom. Strict feed readers may have been silently dropping the author element on those entries since they shipped. The other ten entries plus the feed-level author element were already correct. Fixed all seven.
Surfaced during a careful end-to-end audit pass, not by automation — which is the kind of bug a structural lint check would have caught the first time it shipped. So one of the new lint checks below (#26) was written specifically to make that regression impossible. It validates that feed.xml parses as well-formed XML, that every <entry> has an Atom-namespace <author>/<name> child, and that blog/<slug>/ on disk and feed entry ids are 1:1.
Six new lint checks closing roadmap gaps and audit findings.
Three were already enumerated in PROJECT.md’s v7.42 / v7.44 sections and have now landed (#21 no-duplicate og:type — defends the add-og-type.py patcher against the same attribute-order regex pitfall it was originally written to avoid; #22 version consistency — compares the top heading in CHANGELOG.md against sw.js’s CACHE_VERSION and errors on a hard mismatch, warns on a prefix mismatch; #23 Cmd+K blog coverage — the commandk.js ARTICLES array and blog/ directory must agree, and emits an explicit skip warning when blog/ is missing rather than silently passing).
One was a deferred 7.42-B item, suppressing the long-standing TODO/FIXME warnings the linter raised against this very page (the changelog naturally contains the literal words TODO and FIXME in prose about past linter behaviour, between code spans rather than inside them). Now skipped via an explicit allowlist instead of pretending the warnings were OK.
Two surfaced during this audit cycle: #25 Cmd+K tool coverage (the analog of #23 for tools — every directory at the tree root with an index.html, that isn’t a hub or blog/, must be in commandk.js’s TOOLS array, and vice-versa); and #26 feed.xml validity, described above. The font-precache parity check (#24) is also live but a no-op until the long-deferred self-hosted .woff2 binaries actually ship.
Service worker bumped to v7.41.5.
Per the project’s hard invariant: every release bumps CACHE_VERSION so the old cache invalidates on next visit, even when nothing user-visible has changed. Caught by the new check #22 mid-edit, in fact — bumping CHANGELOG.md to v7.41.5 without bumping sw.js immediately tripped the consistency error. The check did its job.
One small precache fix landed alongside: /shared.css is now actually in the PRECACHE array. offline.html’s comment had been claiming “shared.css is precached by the SW” for a while, but the array didn’t list it. The comment is now true.
Totals after v7.41.5
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.5. Linter clean at 26/26 checks (was 17 at v7.41.2; +3 in v7.41.3, +6 in v7.41.5).
April 25, 2026
v7.41.4 · accessibility + structured-data hardening
Dark color-scheme on forty-three more pages.
Browsers render UI chrome — scrollbars, native form controls, autofill backdrops, datepickers — using a colour scheme they pick from the page. With no explicit signal, they default to the light scheme even on a black-background site, which is why you sometimes saw a white scrollbar flash on first paint, or a jarringly bright autofill dropdown over the dark UI. The fix is one line: <meta name="color-scheme" content="dark">. Forty-three pages were missing it; now they have it. Six pages already had it (or are explicitly excluded). Patch script: patches/add-color-scheme.py, idempotent and re-runnable.
BreadcrumbList on three more tool pages.
The other twenty-one tool pages already had a BreadcrumbList JSON-LD block, which Google’s rich-result rendering uses to show the navigation hierarchy in search snippets (Home › Tools › Tool name rather than just the URL). Three were missing it: /compress-pdf/, /jwt/, /cron/. Added now. The patch script handles both the @graph-based JSON-LD layout used by two of the three pages and the separate-block layout used by the third.
One thing from v7.41.3 that came back out.
v7.41.3 added a <link rel="preconnect" href="https://plausible.io" crossorigin> on every analytics-loading page, on the theory that warming the TCP+TLS handshake before the deferred analytics script fires would shave a few hundred milliseconds on slow networks. On closer look, the benefit was marginal (Plausible loads deferd from the bottom of <head>; the browser’s preload scanner usually opens the connection in time anyway), and it announced “I’m about to talk to plausible.io” earlier than the actual analytics request — a small but real conflict with the “tools that respect you” ethos. Removed from all forty-eight pages.
The CDN preconnects on /heic/, /markdown/, and /compress-pdf/ are not affected. Those scripts are on the critical rendering path for the tool to function, the connection-warming has clear measurable benefit, and the URLs they preconnect to (cdnjs.cloudflare.com, cdn.jsdelivr.net) are the same CDNs the tools were going to talk to anyway.
Skip-to-content link deferred to next release.
Forty-seven of the forty-nine site pages have no <a href="#main">Skip to main content</a> for keyboard / screen-reader users. WCAG 2.4.1. Audit caught it; the fix is small but the right insertion point depends on whether each page has a <main> landmark element (some don’t), and the visual rendering needs design QA so the link doesn’t flash awkwardly during paint. Scheduled for a follow-up release once a <main> landmark is verified or added on every page.
Totals after v7.41.4
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.4. Linter clean at 20/20 checks.
April 25, 2026
v7.41.3 · Cmd+K catalog fix + recents + new lint checks
Six tools were silently unreachable via Cmd+K.
An audit of the commandk.js catalog turned up a real bug. Six tools — uuid, hash, http-status, ico, markdown, and encode — were declared with the wrong field shape. The search matcher reads item.label and item.kw; those six entries were declared as {name, slug, desc, kw}. Result: the matcher saw their label as undefined and silently filtered them out of every query. They’d been unsearchable since at least v7.30 — for an unknown number of users an unknown number of times.
The catalog is now uniformly {href, label, cat, kw} across all forty-eight entries (twenty-four tools, seventeen articles, seven pages). A regression test (tests/test_commandk.js’s “no leftover {name,slug,desc} catalog entries” check) pins the fix — if anyone re-introduces the old shape, CI fails before it ships.
Cmd+K remembers the last five places you went.
Open the palette with an empty query and you now see your five most-recent destinations at the top, marked with a small ↺ glyph in their category column, deduped from the rest of the index. Click any one to jump back. The history is stored in localStorage under tooly.cmdk.recents.v1 and capped at five entries. Storage failures (Safari private mode, quota errors) are silent — the palette degrades to its default surface order rather than refusing to open.
The recents list is purely local: nothing is sent anywhere, including Plausible. If you clear browser storage or use a different browser, the list resets. The v1 in the key name is a deliberate version pin, in case the schema ever needs to change without breaking older clients on first load.
Press / to focus the tool you came for.
On every tool page, pressing / outside an input field now focuses the tool’s primary input and selects its current contents (so a paste replaces rather than appends). Pages opt in by tagging their main <textarea> or <input> with data-primary-input, which most of the dev-text tools (JSON, Regex, Diff, Format, Base64, JWT, Markdown, Encode) already do. Documented in the ? help modal so you can find it.
Three new lint checks brought the total to twenty.
#18 Twitter ↔ og:image parity. When a page declares og:image but no matching twitter:image, Twitter’s own previews fall back differently from Slack/Discord/iMessage’s. The check warns on missing, errors on mismatch. Closed thirty-six pre-existing warnings by mirroring og:image to twitter:image via patches/add-twitter-image.py — idempotent, attribute-order-agnostic.
#19 HTML page weight budget. Warns when any HTML file gzips to more than 50 KB. Most pages are well under; a sudden jump means inline-CSS bloat or unbounded JSON-LD generation slipped past review.
#20 Sitemap <lastmod> accuracy. Warns when a URL’s <lastmod> drifts more than thirty days from the actual file mtime. Stale lastmods misrepresent freshness to crawlers.
Totals after v7.41.3
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (unchanged). 42 OG-PNGs. SW cache → v7.41.3. Linter clean at 20/20 checks (was 17).
April 22, 2026
v7.41.2 · Cmd+K blog coverage + og:type cleanup + branded offline page
The Cmd+K palette now covers all seventeen blog posts.
Previously ten of the seventeen posts were registered in commandk.js. The other seven were live on disk (and correctly linked from the blog index and sitemap) but wouldn’t appear when you searched for them in the palette. Now all seventeen are registered, with labels taken directly from each post’s real <h1> — not invented from the URL slug.
og:type on the seven newer tool pages that were missing it.
The tools that shipped from v7.30 onwards — /base64/, /encode/, /hash/, /http-status/, /ico/, /markdown/, /uuid/ — were missing <meta property="og:type" content="website">. Social-media previews were falling back to the default, which made them render differently on some platforms. All seven are now patched. Blog posts already carried og:type="article" from an earlier release — no change there.
The patch was applied by a small idempotent script at patches/add-og-type.py that’s attribute-order-agnostic and safe to re-run. That code now ships in the repo so the next person writing a similar patcher has a template.
Branded /offline.html replaces the plain-text 503.
When the service worker couldn’t reach network and the page wasn’t already cached, the previous response was a plain-text Offline and this page is not cached yet. body. Functional, but it looked like a crash to anyone who didn’t read it carefully. The new /offline.html uses the site’s visual language with the mascot, a friendly message, and a retry hint. It’s self-contained (all CSS variables inlined) so it works even if no other stylesheet has loaded yet.
sw.js picked up three small upgrades alongside the offline page: CACHE_VERSION bumped to v7.41.2, /offline.html added to the precache list, and navigation failures now serve the branded page instead of the 503. Precache also switched from atomic cache.addAll() to per-URL cache.add().catch() so a single 404 no longer fails the entire install.
One real bug in v7.40 caught during the audit.
Line 104 of commandk.js ended with a spurious double comma that created an empty slot in the TOOLS array at index 19. JavaScript’s forEach skips array holes, so the palette worked correctly anyway, but TOOLS.length reported 25 when there were actually 24 tools. Caught by a source-parsing test that counts entries and compares against disk reality. Removed.
Developer tooling nobody will see, but future-me will thank present-me for.
Three test suites now live under tests/, runnable via ./run-tests.sh: catalog-invariant checks against commandk.js (14 tests), black-box lint runs on synthetic broken trees (5 tests), and idempotency checks for the og:type patcher (13 tests). Plus release.py, which reads the version from the top of CHANGELOG.md and stamps sw.js automatically so the two can’t drift.
Totals after v7.41.2
24 tools (unchanged). 17 articles (unchanged). 50 HTML pages (was 49). 42 OG-PNGs. SW cache → v7.41.2. Linter clean at 17/17 checks.
April 20, 2026
v7.28.3 · UX uplift
Paste buttons everywhere and a keyboard-shortcut pop-up.
Nine of the text-input tools now have a dedicated Paste button next to their main input: JSON Repair, JWT Decoder, Cron Explainer, SVG Shrinker, Regex Rambler, Diff Checker (both panes), Timestamp Converter, and AI Regex Generator. On mobile especially, long-press-paste is fiddly; a one-tap navigator.clipboard.readText() affordance is miles better. Image Smusher, Format Flipper, and JSON Fixer already had equivalent wiring.
Timestamp Converter also picked up a row of format chips — Unix seconds, Unix ms, ISO 8601, RFC 2822 — click any one to see it auto-detected.
And there’s now a site-wide keyboard-shortcut help modal. Press ? anywhere (when not typing in a field) to see what’s bound: ⌘K to open the tool switcher, ⌘Enter for primary actions in most dev tools, and the arrow keys / Enter / Esc in the palette. The Cmd+K palette also gained a small ? shortcuts hint in its footer so you can find the modal in the first place.
April 22, 2026
v7.40 · URL/Encode — first new tool in six releases
URL encoding, HTML entities, and case conversion — one page.
At /encode/. Three tabbed panels covering the daily dev-encoding tasks that eat time whenever you have to remember which function handles which case.
URL panel. encodeURIComponent vs encodeURI distinguished explicitly in the UI — most tools lump them together and produce subtly wrong output for URLs with reserved characters. A "space as +" toggle handles the application/x-www-form-urlencoded case where spaces need to be literal + rather than %20. Decode path catches URIError and surfaces the real issue ("invalid percent-encoded sequence") rather than blowing up silently.
HTML entity panel. Three encoding styles: minimal (the 5 reserved entities — safe for text content), non-ASCII (reserved + everything above 127 — safe for attributes and mixed text), and all (every character except the URL-safe set — paranoid mode for CSV/email/template contexts). Three numeric conventions: named, decimal, hex. Decoder handles ~60 common named entities plus all numeric forms via String.fromCodePoint, including hex (—) and decimal (—) forms. Emoji and astral-plane Unicode decode correctly because we use codePointAt, not charCodeAt.
Case panel. A proper tokenizer — splits on casing transitions (fooBar → foo, Bar), consecutive-caps boundaries (XMLHttpRequest → XML, Http, Request), explicit separators (-, _, ., /, \, whitespace). Then rebuilds into 12 conventions: camelCase, PascalCase, snake_case, CONSTANT_CASE, kebab-case, dot.case, path/case, Title Case, Sentence case, UPPERCASE, lowercase, alternating. Each result has a per-card copy button.
32 unit tests pass on the pure functions: tokenizer edge cases (empty, single word, consecutive caps, mixed separators, numbers, paths), all 12 conversions producing expected output for a realistic input (userEmail_address), HTML entity encoding in all three style × numeric-form combinations including astral Unicode (the fox emoji 🦊 → 🦊), and decode-map spot checks on 5 common named entities. One test case I initially asserted (foo2bar → ["foo2","bar"]) turned out to be wrong — there's no casing signal between 2 and b, so keeping it as one token is correct behavior.
Linter CANONICAL_TOOLS now includes encode.
Added /encode/ to the linter's 14-tool canonical set so the tool's footer-coverage consistency gets automatically verified on every ship. All 48 pages had their foot-links augmented with the encode link next to the existing markdown link, keeping the dev-text-processing cluster together thematically.
Totals after v7.40
24 tools (was 23). 17 articles (unchanged). 49 HTML pages (was 48). 42 OG-PNGs. SW cache → v7.40. Linter clean at 17/17 checks.
April 22, 2026
v7.39 · dedicated OG images + offline indicator
25 pages now have their own OG images.
The v7.37 audit flagged that 25 pages were all using the same og-preview.png as their social-media preview. Paste a link to any of them in Slack, LinkedIn, or Twitter and you'd see the same generic graphic whether you were sharing the hub, the changelog, the JWT tool, or a blog article. Unhelpful for the reader, and a missed differentiation opportunity for the site.
Generated 25 dedicated 1200×630 OG images via PIL, using Lora (a transitional serif close to Fraunces) for the title typography, Liberation Sans for the subtitle, and DejaVu Sans Mono for the eyebrow category tag. The template carries the brand DNA: dark #0E0D0B background with a subtle amber warmth gradient at the top-left, amber #F4A940 accent on the italic trailing word, the mascot in the bottom-right corner, and the full brand lockup plus domain in the bottom-left.
Each image has its own category (DEV · ENCODE, WEB · REF, ESSAY · PRIVACY, GUIDE · TIME, etc.) and a subtitle written specifically for that page — the JWT tool's OG says "Decode header and payload. See exp, iat, nbf. Verify HMAC signatures," the Markdown Editor's says "Live preview, GFM, tables, task lists, syntax-highlighted code blocks." Social previews finally tell the reader something specific about the destination.
The old og-preview.png stays on disk — it's still referenced as the Schema.org publisher logo in BlogPosting JSON-LD entries (that's a brand-logo role, not a per-page OG). 25 pages updated, 25 new PNGs totaling 1,395 KB. Per-image sizes range from 45 KB to 71 KB — Twitter/Facebook/LinkedIn/Slack all handle PNG reliably so I didn't convert to WebP despite the theoretical savings.
An offline ribbon now confirms tools still work.
Every tool on the site is client-side — no upload, no server dependency — which means none of them actually need the network after first page load. But users don't know that. When WiFi drops mid-task, the assumption is usually "oh, this broke." That's the wrong assumption and the site has never done anything to correct it.
Added a small offline-status ribbon to the bottom-center of every page. When the browser fires the offline event, a dark ribbon with an amber dot appears and reads "Offline — tools still work." When the browser comes back online, the ribbon fades to a green dot that reads "Back online" for two seconds before dismissing itself.
Implementation: ~80 lines appended to commandk.js (already loaded on every page). Zero new network requests. Zero new dependencies. Self-positioning fixed-bottom-center, semantic role="status" aria-live="polite", non-blocking (pointer-events: none) so it never intercepts clicks. Colors pulled directly from the site's CSS variable palette — #1A1815 background, #2A2720 border, #E89050 for the offline state, #6BAF6B for the reconnect confirmation.
Service Worker strategy was already fine.
The v7.38 plan listed "SW precache expansion + stale-while-revalidate" as a v7.39 item. Reading the existing sw.js carefully, stale-while-revalidate was already the strategy — every same-origin GET returns the cached version immediately and fetches a fresh copy in the background, with automatic fallback to cache on network failure. Expanding the precache list would trade install-time cost and deploy-time invalidation pain for no measurable benefit (runtime caching already covers every page the user actually visits).
No change to sw.js beyond the cache-version bump to v7.39. The time saved went into the offline indicator, which has a clearer user-facing value.
Totals after v7.39
23 tools (unchanged). 17 articles (unchanged). 48 HTML pages. 41 PNG files (was 32). 16 original OG images + 25 new dedicated ones + 9 mascot variants. SW cache → v7.39. Linter clean at 17/17 checks.
April 22, 2026
v7.38 · reliability triage + perf quick wins
The regex tool can no longer freeze your tab.
A performance and reliability audit in the previous turn surfaced three things worth fixing right now: a regex ReDoS vulnerability in Regex Rambler, 158 KB of cross-page Google Fonts URL drift, and 14 silent-catch error-swallowers spread across 8 tools. All three are fixed in this release.
Regex Rambler: Web Worker + terminate() timeout.
The regex tool previously did all match execution on the main thread. For most patterns that was fine, but for catastrophic-backtracking cases like (a+)+b against aaaaaaaaaaaaaaaaaaaaX, a single RegExp.exec() call can consume several seconds of CPU and lock up the tab. The existing 250ms wall-clock guard inside the match loop didn't help because control never returned to JavaScript during the backtrack.
The fix: execute every regex match in a Worker constructed inline via a Blob URL (so no separate .js file ships). The main thread arms a 2-second timer; if the worker hasn't responded, the timer calls Worker.terminate() and a fresh worker spins up on the next call. The UI shows a specific "Pattern took longer than 2 s — likely catastrophic backtracking. Try simplifying alternations like (a+)+ or (a|a)*" error so the user knows exactly what happened and has a concrete hint.
A runId state counter handles the race where a later typed character arrives before an earlier (now-obsolete) match finishes — stale results are silently dropped. The structured-clone message format carries pattern, flags, text, match indices, capture groups (positional and named), and for replace operations the replacement template.
One caveat worth naming: I couldn't run a full end-to-end browser test in the sandbox (no DOM, no Worker API in Node). The Worker source itself was verified to parse as valid JS and to reference all the required message-passing keys (pattern, flags, text, matchAll, replace, postMessage). The runRegexSafely wrapper was verified to use Promise, call terminate(), recreate the worker on timeout, and reference REGEX_TIMEOUT_MS. Worth a manual smoke test after deploy — paste (a+)+b into the pattern field, paste 20+ a's followed by X into the test string, and confirm the timeout fires within 2 s rather than freezing the page.
Google Fonts: one URL for all pages.
The perf audit flagged 6 distinct Google Fonts URLs across 82 <link> references site-wide. Each URL requested a slightly different weight matrix — Fraunces ital 400/500/700 here, 400/500/600/700 there; JetBrains Mono 400/500 on some pages, 400/500/600 on others. Browsers cache these separately by exact URL, so navigating from the hub to a tool page and then to a blog article hit 3 separate font-CSS requests even though most of the weights are shared.
Replaced all 82 occurrences with one canonical URL covering every ital/weight combination any page currently uses: Fraunces ital 0+1 × weights 400/500/600/700, Inter 400/500/600/700, JetBrains Mono 400/500/600. 47 pages updated. One font CSS fetch now serves the entire site.
14 silent catches now log to console.
The reliability audit found 14 catch blocks across 8 tools (/benchmarks/, /compress/, /compress-pdf/, /convert/, /json-repair/, /markdown/, /regex-ai/, /time/) that swallowed exceptions without surfacing them anywhere. When users hit a bug in those paths, the symptom was "the thing just didn't happen" with no visible error and nothing in my Plausible data to distinguish "user tried it and it silently failed" from "user never tried it."
Each one got a minimum-viable upgrade: console.debug('[tool] ignored exception:', e). The catches are genuinely non-fatal (they exist to keep UI responsive in edge cases), so surfacing with console.debug rather than console.error keeps the devtools console clean for normal use while making the failure mode visible when specifically debugging. Phase 2 would be converting the 4 most user-facing of these to toast-style UI errors; deferred to v7.39.
Totals after v7.38
23 tools (unchanged). 17 articles (unchanged). 48 HTML pages. SW cache → v7.38. Linter clean at 17/17 checks.
April 22, 2026
v7.37 · mascot reconciliation + footer refresh + audit
The mascot looks crisp and consistent now.
You noticed the nav mascot looked blurry on mobile. The root cause was two issues stacked: the raster mascot PNG was being downscaled ~26× from 720px to a 28px slot without a right-sized variant, AND the site had two different mascot designs running in parallel. 36 pages (the older tools) used a hand-drawn SVG cartoon mascot (rect body, rect antenna, two-dot eyes, curve smile). 6 pages (the newer tools from v7.30 onward) used the photographic mascot as a raster. Completely inconsistent brand identity, which you'd notice the moment you opened two tools side-by-side.
Fixed by standardizing on the photographic mascot across every page. Generated dedicated 32/64/96 pixel PNGs from the 1024×1024 master source (via PIL + LANCZOS), wired them into a srcset with 1x/2x/3x breakpoints so high-DPI phone screens get the 64px or 96px variant. The 32px variant for 1x displays is 1.4 KB. Unified the .tooly-mini CSS to 32×32 across all pages (was 28/32/36 mix).
The hub's big hero mascot (240×240 display) got the same treatment with 240/480/720 variants, including an updated <link rel="preload"> that carries the same srcset via imagesrcset. Browsers on 3x DPI displays (high-end phones) will now pull the 720px version, which is the exact native pixel match for 240 CSS-px displayed at 3x density.
The 36 obsolete <symbol id="tooly-mini-sym"> SVG sprite definitions were removed from each page along with their 144-byte cartoon mascot data. The /convert/ page's bespoke mascot variant got swapped for the standard one too.
Footer navigation finally includes the newer tools.
A site-wide audit found 38 pages had foot-links that predated the v7.30+ tools (hash, uuid, http-status, ico, markdown, base64, paperwork, jwt, cron) and still listed only the original 10 tools. Users on older pages couldn't reach the newer ones through the footer — they'd hit "all tools" or Cmd+K and navigate that way, but discoverability through the footer was broken for almost the whole site.
Rebuilt every foot-links block with a unified canonical set of 14 tools plus "All tools" and "Blog" (blog link is suppressed on blog pages so it's not self-linking). The linter's CANONICAL_TOOLS list was updated to match, so future footer drift gets caught automatically. 41 pages touched in a single pass.
Two img-tag alt attributes were empty.
compress/ and convert/ had dynamic thumbnail <img> tags with alt="" in JS template literals. Not strictly wrong per the a11y spec (empty alt is correct for purely-decorative images) but the linter flagged them. Swapped to dynamic alts using the uploaded file name via escapeHtml(f.file.name).
Linter expanded too.
Three smaller linter fixes: the nav-mascot check now looks for the raster <img class="tooly-mini"> instead of the removed SVG symbol. The tool-root-type check accepts WebPage or WebApplication in addition to SoftwareApplication, which /http-status/ legitimately uses since it's a reference page, not a tool. And the CANONICAL_TOOLS list went from 10 to 14 to include the v7.30+ additions.
Audit findings still open
Not everything got fixed this ship. 25 pages still share og-preview.png as their OG image — link previews on social will look identical across them. 7 tool/article pairs share an OG image between the tool and its companion article. These are v7.38 candidates (~3 hours to generate dedicated OG images). The HTML-weight audit flagged format/, qr/, convert/, time/, and changelog/ as the heaviest pages (each 57-96 KB) — worth revisiting the CSS-extraction question once there's real traffic data from Plausible to weigh the trade-off.
Totals after v7.37
23 tools (unchanged). 17 articles (unchanged). 48 HTML pages. Mascot variants now include 28/32/56/64/84/96/240/480/720 px PNGs at 1.2-168 KB each. SW cache → v7.37. Full linter: 0 errors across 17 checks.
April 21, 2026
v7.35 · four small-but-important items
The cobbler's children finally have shoes.
Four low-ceremony items in one release: favicon regen, CRC-32/Adler-32 added to Hash Generator, two linter improvements, and a practical Markdown reference article.
The favicon.ico is now multi-resolution.
Turns out the site's own favicon.ico was a single 16×16 PNG (170 bytes) while the favicon article and Favicon Foundry were arguing for multi-size. Regenerated from the 720×720 mascot source through PIL's ICO writer — now 16, 32, 48, 64, 128, and 256 px entries, all PNG-embedded, 30.9 KB total. The site's own favicon now matches its own prescription, which is a more credible look.
Hash Generator: CRC-32 and Adler-32 added.
Non-cryptographic 4-byte integrity checksums. CRC-32 is what ZIP files, PNG chunks, Ethernet frames, and rsync use; Adler-32 is what zlib and rsync use for file-level integrity. Neither offers collision resistance — they're fast, not secure. A visible note in the education section says so.
Both implementations are pure JS with incremental update() so they work for files up to 2 GB alongside the existing MD5/SHA-family streaming path. 19 unit tests pass end-to-end: the canonical CRC-32 check value (cbf43926 on "123456789"), six RFC/common Adler-32 vectors, streaming-vs-one-shot consistency (including across Adler-32's 5552-byte chunk boundary), and algorithm inference — 8-hex and 4-byte-Base64 are ambiguous between CRC-32 and Adler-32, so Compare mode tries both before declaring a mismatch.
The linter has two new checks and is in the tree.
The existing 472-line lint.py was sitting in project docs but never committed to the deploy tree. It's now at /home/claude/tooly-final/lint.py, runnable as python3 lint.py ., and produces 17 pass/fail checks across the full site. The two new checks close specific failure modes that caught us in recent releases:
Check 16: blog schema drift. Cross-references blog/<slug>/index.html files on disk against the BlogPosting entries in blog/index.html's JSON-LD. Flags missing entries AND entries that point to articles that don't exist. This is what would have caught the v7.29-v7.31 drift where 4 articles shipped without corresponding Schema entries (caught and repaired in v7.32's audit).
Check 17: ItemList count. The hub's Schema.org ItemList has a numberOfItems field that should match the length of itemListElement. Now verified automatically. A new tool ship will fail the lint if the card is added but the count isn't bumped, or vice versa.
Three older bugs in the linter also got fixed while I was in there: the link check stripped its ${template-interpolation} false positive (now skips URLs containing ${ and skips hrefs that appear inside <code>, <pre>, or <script> blocks — the promised improvement from the v7.34 retrospective); the placeholder check no longer flags TODO/FIXME markers inside demo JSON or sample code blocks; and the article-date check now accepts the "Published Month D, YYYY" format that my newer articles actually use. Baseline after the fixes: 0 errors, 0 warnings across 17 checks.
A new Markdown reference in the blog.
A practical Markdown reference. Covers every GFM construct, what it renders to, the traps (hard line breaks, tight vs loose lists, underscores-in-words, smart punctuation substitution, the table-cell-can-not-contain-a-list trap), and the bits different editors disagree on (footnotes, math blocks, emoji shortcodes, YAML frontmatter, GitHub callouts). Pairs with the v7.34 Markdown Editor.
Totals after v7.35
23 tools (unchanged). 17 articles (was 16). 48 HTML pages (was 47). SW cache → v7.35. The 10-check regression script that's been running all session is now superseded by the 17-check lint.py in the tree — next release will run that as the pre-ship gate.
April 21, 2026
v7.34 · Markdown Editor
A Markdown editor with live preview.
At /markdown/. Write on the left, see the rendered HTML on the right. GitHub-flavored — tables, task lists, strikethrough, autolinks — with syntax-highlighted code blocks that auto-detect the language when you don't specify one.
Three view modes: split (default), editor-only (for focused writing), preview-only (for reading). Toolbar actions: copy rendered HTML to clipboard, download as a standalone .html file with minimal styling, download the raw .md, or print to PDF via your browser's print dialog. The file name for exports is derived from the first # heading in the document, so "# Meeting Notes" saves as meeting-notes.html.
Keyboard shortcuts: Cmd/Ctrl+B wraps the selection in **, Cmd/Ctrl+I in *, Cmd/Ctrl+K in a link template. Tab inserts two spaces instead of moving focus so you can actually indent code inside fenced blocks.
Load order matches the convert/ precedent: marked.js and highlight.js from the same CDNs Paperwork uses for pdf-lib and pdf.js. The tool works offline once the SW has cached those libraries. If the CDNs fail on first load, the preview panel shows a friendly error instead of blacking out.
23 unit tests pass on the custom helpers — HTML escape handling across the five problem characters, markdown title extraction (respecting the first h1, ignoring h2s), filename slugification (unicode-stripped, length-capped at 60 chars, empty input falls back to "document"), and word-count accuracy across empty, whitespace-only, multi-space, and multi-line inputs. The marked.js + highlight.js integration itself is trusted as battle-tested.
This closes the "honest gap in Format Flipper" item that's been on the scope list since v7.30. Format Flipper does md↔html at a basic level; the Markdown Editor does everything else — the writing experience, the live preview, the export surface.
Totals after v7.34
23 tools. 16 articles. 47 HTML pages. SW cache → v7.34.
April 21, 2026
v7.33 · ICO Inspector
A tool to look inside favicon.ico files.
At ICO Inspector. Drop any .ico file (or .cur cursor file) and see the container structure: the 6-byte ICONDIR header, each 16-byte ICONDIRENTRY, the embedded image blobs (BMP or PNG), and a byte-level hex dump with the directory fields highlighted in color.
Every image gets its own panel showing width × height (with the "0 means 256" rule applied and flagged), bit depth, palette count, color planes, data offset + size, and for BMP blobs the full DIB header fields — size, width, DIB height (flagged when it's correctly 2× the entry height for the AND mask), compression type. Each image is rendered to a visible preview with a checkered transparency background and can be extracted as a standalone PNG.
Warnings surface when things don't match the spec. Reserved bytes that aren't zero. DIB heights that aren't doubled (the most common bug when hand-rolling ICOs). Overlapping data ranges. CUR files get their two reinterpreted bytes (hotspot X and Y) shown correctly instead of as bogus planes/bpp values.
The BMP renderer handles 1, 4, 8, 24, and 32 bpp pixel formats. Rows are read bottom-to-top as BMP requires, BGRA byte order is swapped to RGBA, row padding to 4-byte boundaries is applied, palettes are decoded for indexed modes, and the AND mask applies 1-bit transparency for the non-32bpp cases (32bpp has real alpha already). 29 unit tests pass end-to-end against synthetic ICO fixtures — 0→256 rule, CUR detection, truncation, unknown DIB sizes, and full pixel-level BMP-in-ICO roundtrip.
The companion article, What's actually in a favicon.ico file from v7.32, has been updated to cross-link to the Inspector in the appropriate spots. If you want to test-read your way through the format, the article and the tool are meant to be used side by side.
Totals after v7.33
22 tools. 16 articles. 46 HTML pages. SW cache → v7.33.
April 21, 2026
v7.32 · three new tools, two new articles
The workshop just got three more tools.
ID Generator, Hash Generator, and an HTTP Status Code Reference. Everything browser-only, everything free, no accounts.
ID Generator covers four formats: UUID v4 (plain random), UUID v7 (RFC 9562, time-sortable, what you actually want for new database keys), ULID (26-char Crockford Base32, the human-readable time-sortable option), and Nanoid (configurable-length URL-safe). Batch up to 1000 per click. The decode panel extracts the embedded Unix-ms timestamp out of a UUID v7 or ULID and reports the age in human time. Sourced from crypto.getRandomValues throughout — the same CSPRNG your browser uses for TLS key material. 19 unit tests pass end-to-end.
Hash Generator supports MD5 (pure-JS, 180 lines, RFC 1321 with streaming), SHA-1, SHA-256, SHA-384, SHA-512. Three modes: text (updates live as you type), file (drag-and-drop, up to 2 GB, streamed in 2 MB chunks with a progress bar), and compare (paste an expected hash + source; auto-infers the algorithm from hash length). Hex-lower, HEX-upper, or Base64 output. 30 tests pass including all seven MD5 vectors from RFC 1321 Appendix A.5, NIST SHA vectors for "abc", padding boundary cases at 55/56/63/64/65/127/128 bytes, and streaming-vs-one-shot consistency.
HTTP Status Code Reference is 63 status codes in one searchable page. Every row expands to show what it means, when to use it, and the practical pitfalls. Deep-linkable — /http-status/#404 jumps straight to 404 with the row expanded. Filter by class (1xx/2xx/3xx/4xx/5xx) or search by code, name, or content. Press / to focus the search box.
Two more articles in the blog.
HTTP status codes: the fifteen that actually matter — a ranked shortlist for REST API design, plus the mistakes I see most often (200-with-error-body, 401 vs 403 confusion, missing Retry-After, 400 for everything). Pairs with the reference page.
What's actually in a favicon.ico file — a byte-level walkthrough of the ICO container: the ICONDIR + ICONDIRENTRY records, the "0 means 256" dimension trap, the BMP-with-doubled-height quirk, the AND mask, PNG-embedded-in-ICO, and the eight gotchas worth knowing. Drawn from what I actually learned building Favicon Foundry's in-browser ICO writer. Pairs with Favicon Foundry.
And a Schema.org catch-up.
The blog Schema BlogPosting list had drifted behind reality across the last four releases — it was missing the LLM JSON, JWT decoded, cron, and color-palettes articles despite them being published and live. That's now corrected; the Schema has all 16 articles listed in publish order. Low-impact bug, but the kind of quiet drift that compounds.
Totals after v7.32
21 tools, 16 articles, 43 HTML pages. Cmd+K palette now carries all 21 tools. SW cache has bumped to v7.32 so you may see a "new version available" prompt on first visit.
April 20, 2026
v7.31 · paperwork upgrades + two more articles
Paperwork got seven knobs.
The file converter (Paperwork at /convert/) had three options before: page size, orientation, and margin in Images→PDF mode; resolution in PDF→Images mode. Real users immediately ran into the gaps. This release fills them.
For Images→PDF, you can now: rotate any image 90° at a time before stitching (the most common gap — landscape phone photos in portrait sets), set a custom PDF title in the document metadata, set a custom output filename, and pick between “Best quality” (PNGs embedded as-is, lossless) and “Smaller file” (PNGs re-encoded as JPEG 85, ~3-4× smaller for photo-heavy documents). For PDF→Images, you can now pick between PNG (lossless, default) and JPG output, and constrain to a specific page range with the standard syntax — 1-3, 5, 7-9. Eleven unit tests on the page-range parser confirm it handles ranges, lists, mixed input, over-total clamping, and reverse-range rejection sanely.
Two more pieces in the blog.
Cron expressions: a practical guide walks through the five fields, the five special characters, the patterns that cover 90% of real-world cron, and the four mistakes — timezone drift, the day-of-month/day-of-week OR trap, daylight savings transitions, and overlapping runs — that quietly cause real outages. Pairs with the Cron Explainer.
Color palettes from photographs: three algorithms, three trade-offs covers frequency sampling, k-means clustering, and median cut. Same photo, three valid but different palettes; the differences come from which question each algorithm is actually answering. Plus the role of color space (RGB vs HSL vs Lab), the two practical traps (over-sampling and over-quantizing), and where modified median cut wins. Pairs with Color Grabber.
April 20, 2026
v7.30 · base64 + two new articles
Eighteen tools, and a tour through tokens.
The workshop crossed eighteen with the addition of Base64 Everything — encode text or files to Base64, decode the other direction with live preview for images, PDFs, and text, plus URL-safe and MIME-wrap variants. The decode side does magic-byte sniffing on the recovered bytes, so a Base64-encoded PNG renders as the actual image, a Base64-encoded PDF renders in an iframe, and binary data offers a download with the right file extension. Roundtrip-tested across ASCII, Unicode (including emoji and CJK), XSS payload characters, and 10,000-character payloads.
The cron-tab, JWT, and JSON-Repair pages had a small adjacent improvement: AI Regex gained Cmd+Enter as a primary-action shortcut, finally matching the six other tools that had it. Six tools still don’t — they’re live-processing or upload-driven, where there’s no “run” button to bind to.
Two new pieces in the blog.
JWT decoded: what’s actually inside your auth token walks through the three Base64 chunks, names every registered claim (iss, sub, aud, exp, nbf, iat, jti), explains why the signature exists and what verification proves, and catalogs the four mistakes worth knowing about — alg: none, trusting unverified payloads, putting secrets in claims, and trying to retroactively shorten exp. It also names the case where JWTs are the wrong tool and a regular session cookie is better. Pairs with the JWT Decoder.
The LLM JSON problem: why your model’s output won’t parse covers the four categories of failure when consuming structured output from GPT, Claude, or Gemini: wrapper bugs (markdown fences, preamble prose, reasoning tags), syntax bugs (trailing commas, single quotes, Python literals), schema bugs (right syntax, wrong shape), and truncation bugs (token-budget cutoffs). Each gets its own fix and the right pipeline order. Pairs with JSON Repair, which already implements the wrapper-stripping pass.
Two orphan OG images found their homes.
og-file-converter-limits.png now anchors the JWT essay (its “honest about limits” framing maps cleanly onto JWT’s decoder-vs-verifier distinction). og-json-repair-taxonomy.png now anchors the LLM JSON essay. Both images were generated months ago for unwritten companion essays; finishing those essays closed the loop. Cache key bumped to v7.30 so service-worker users pick up the changes on next visit.
April–May 2026
v1.0 – v7.29 · the archive
Everything before v7.41 lives in the archive.
To keep this page fast, the older half of the timeline — from the v1.0 launch through v7.29, including the first tools, the early blog posts, and the mascot saga — now lives on its own page, kept verbatim. Read the changelog archive →