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 20, 2026
v7.29.1 · production bugs, fixed
The JSON Fixer was completely broken.
A full audit of the site turned up five production issues. The worst of them, the one that had the site’s most-used tool silently non-functional: JSON Fixer had a nested block comment in its inline script — /* ... /* block-comments */ */. JavaScript doesn’t support nested block comments. The inner */ closes the outer comment early, leaving syntactically-invalid code after it. The browser refused to parse the entire script. Every button was dead. Every event listener was never attached. Users landed on the page, pasted JSON, clicked “Format,” and nothing happened.
Rewrote the offending comments as line-comments (// instead of /* */) on lines 19 and 239. Full Node --check pass across every inline script on every page of the site confirmed this was the only instance. The tool works now.
JSON Repair had a half-built feature no one had finished wiring.
The “Show detailed diff” toggle button was there in the UI. Clicking it called a renderDiff() function that was never defined — ReferenceError. Dead code on a live page. Implemented the function properly: line-by-line LCS diff, context-truncated output (shows 2 lines of context around each change), fully HTML-escaped against injection, matching CSS for added/removed/skipped lines. Tested against five cases including an XSS injection attempt. The toggle works now.
Command Palette was missing six tools.
When we added JWT Decoder, Paperwork, PDF Compressor, Cron Explainer, AI Regex, and JSON Repair, they were added to the nav bar and the sitemap but never added to the Cmd+K command palette index. Six tools, invisible to the keyboard-navigation workflow. Added all six with proper search keywords — JSON Repair indexes on llm chatgpt claude thinking fence broken so users land on it from the symptoms, not just the tool name.
Service worker was caching a four-month-old release.
The CACHE_VERSION constant in sw.js was still v7.18.2, from the big merge release back in March. Users who’d visited once with a browser that registered the service worker had been quietly served stale HTML and assets ever since — they weren’t seeing the paste buttons, the new mascot, or any of the recent work. Bumped to v7.29. Next visit, their cache invalidates; the one after that, everything is current.
Five tools had bespoke Open Graph images they weren’t using.
Dedicated og-*.png social-preview images for Image Compressor, HEIC Converter, JSON Fixer, Color Converter, and SVG Cleaner existed on disk — correctly sized at 1200×630 — but the pages all pointed to the generic og-preview.png fallback. Wired each tool page to its own image. Did the same for the three blog articles that thematically matched (image-compression-guide, iphone-heic-problem, svg-bloat). Social shares now show tool-specific branding instead of the generic site preview.
April 20, 2026
v7.29 · LLM output, and a sharper mascot
JSON Repair now handles the way LLMs actually talk.
The JSON Repair tool is built for fixing malformed JSON. That’s what the underlying jsonrepair library does well: trailing commas, single quotes, Python True/False, unquoted keys, truncation. But when you paste the output of a real LLM — Claude, ChatGPT, Gemini, DeepSeek, o1 — the JSON is almost never the only thing in the blob. Claude wraps it in <thinking> reasoning tags. Everybody wraps it in ```json markdown fences. ChatGPT prefaces with “Here’s your JSON:”. Gemini appends “Let me know if you need any changes!”.
JSON Repair now peels all of those layers before the repair pass runs. The headline fix is a fence-block extractor that catches the most common real-world pattern in one step: prose + opening fence + content + closing fence + more prose. The content gets extracted; the wrapper and the prose on either side get stripped and reported in the fixes list with the exact text that was removed. <thinking>, <reasoning>, <scratchpad>, <thought>, <rationale>, and <analysis> tags all get detected. 14 unit tests cover the patterns we saw in the wild.
The mascot got its resolution back.
The mascot PNG was shipping at 240×240. On modern phones with 3× pixel density (including the Samsung Galaxy line) that means the browser was upscaling 240 source pixels to fill 660 device pixels — which looks pixelated if you catch it in bright light. The new delivery is 720×720, regenerated from the original 1024×1024 clean source with silhouette-aware artifact cleanup (caught 1,513 residual pixels from the original AI generation that the 240px version missed), auto-centered, palette-quantized to 128 colors for compression. Net result: 47 KB instead of 35 KB, for 9× the intrinsic resolution. No HTML changes needed — the <img> still declares 240×240 CSS pixels; browsers downsample the richer source automatically.
April 20, 2026
v7.28.3 · paste and shortcuts
Paste buttons, everywhere they should be.
Nine 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. On mobile — where long-press → paste is a two-step dance, and Samsung Internet in particular fights you on it — a single-tap Paste button is a real improvement. The implementation is shared: one delegated handler in commandk.js listens for clicks on any element with data-paste-to and routes the clipboard text into the targeted field, dispatching both input and change events so every tool's existing wiring picks it up. Graceful fallback if the browser blocks clipboard access (Firefox does this occasionally without recent user activation).
Also added sample chips to Timestamp Converter — Unix epoch, ISO 8601, Milliseconds, Natural phrase — so first-time visitors can see what the tool does before digging up a timestamp of their own to paste in. And documented the ⌘K palette shortcuts on the About page for people who like to know what keys do things.
April 20, 2026
v7.28.2 · consistency sweep
Six tool pages brought back into the fold.
Six of the newer tool pages — /compress-pdf/, /convert/, /cron/, /json-repair/, /jwt/, and /regex-ai/ — had been shipped without two site-wide scripts that every other page loads: Plausible (the cookie-free analytics) and commandk.js (the Cmd+K tool switcher). That meant a third of the workshop wasn’t measurable and wasn’t jumpable-from via the command palette. Both now load on all 18 tool pages.
Also wired up four cross-tool links that were missing: Image Smusher and HEIC Unheicer now both point at Paperwork for “now turn these into a PDF,” and JWT Decoder and Cron Explainer now both point at Timestamp Converter — because the one thing users reach for right after decoding a JWT is figuring out what its exp field actually means in local time.
April 20, 2026
v7.28.1 · mascot tidy
Cleaner hero mascot — sparkle removed, shadow follows silhouette.
Also in this release: an editorial tidy pass trimmed 18 page titles and 10 meta descriptions down to within Google's SERP display budget (≤60 chars for titles, ≤160 for descriptions), corrected a stale “workshop of 11 tools” claim on the Benchmarks page (the workshop holds 17 now), fixed a non-existent canonical URL on an orphan page, repointed three broken OG-image references to the site’s default social card, and rebuilt the Schema.org ItemList on the hub so the full workshop of 18 tools is now advertised to search engines (was stuck at 11 with two stale tool names). Plus an accessibility pass: fixed heading-hierarchy skips on 11 pages so screen readers can navigate the document outline without jumping past levels.
Three small quality fixes to Tooly on the homepage. First: painted out the four-pointed AI-generation watermark that had been baked into the bottom-right corner of the source PNG. Second: converted the PNG from RGB (solid black background) to RGBA so the amber drop-shadow now outlines Tooly's actual shape instead of the 240×240 rectangle around him — a proper Tooly-shaped glow. Third: tidied the caption layout (was floating absolutely outside the flex column), added mobile-specific sizing (200×200 on narrow screens instead of the desktop 240×240), and dropped the ← this is Tooly arrow since Tooly isn't to the left of the caption in any current layout. Also removed a dead <symbol id="tooly"> block on the hub that was left over from the old fallback SVG — no references to it anywhere, just ~700 bytes of extra markup on every homepage load.
April 20, 2026
v7.28 · removed the wardrobe
The outfit picker is gone.
After multiple iterations trying to make the 31-outfit mascot picker render cleanly, the system was creating more friction than joy. The /mascot/ page, the toggle button, the popover, the two-layer crossfade, and the per-variant position-normalization math — all removed. Tooly is back to a single plain expression on the homepage. Four of the thirty variant PNGs have been deleted; only the base is kept as the hero image. If the feature comes back later, it comes back simpler.
April 20, 2026
v7.27.8 · mascot shuffle eliminated
The mascot no longer shuffles between outfits.
Pixel analysis revealed the PNG assets had the mascot face positioned at different Y coordinates across variants (50px spread) and X coordinates (33px spread). When swapping outfits, the face literally jumped up/down and side-to-side — the "shuffle" effect. Fixed by computing a per-variant translate offset from the analysis and applying it via CSS transform. Every face now lands at the exact same screen position regardless of outfit. Also found and cleaned three PNGs that had stray blue/green pixels from generation artifacts (aviator-goggles had 1,012 rogue pixels, beanie had 812, chef-hat had 106). Rebuilt both pages around a two-layer crossfade system where the new variant preloads into a hidden layer, then opacity-swaps with the visible one — no flash, no gap, no composition shift. Removed all continuous animations in favor of subtle hover-only interaction (gentle lift + glow intensify on mouseover). Mascots now stay perfectly still unless you engage with them.
April 20, 2026
v7.27.5 · mascot page polish
Cleaner everything on /mascot/.
Removed the heavy polaroid frame around the mascot — now it floats on a soft amber glow with a subtle ground shadow. Variant grid cells are transparent by default with amber hover/active states instead of feeling caged. "Currently: X" is now an elegant pill chip. Fixed sticky header overlapping the page deck text. Added scroll-padding-top so anchor navigation lands correctly. Mascot image bumped to 280×280 with internal padding so variants with extending features (santa hat, scarf, antenna) no longer get cropped. Crossfade transition between outfits matches the homepage pattern. Added aria-live to the status label so screen readers announce outfit changes.
April 20, 2026
v7.27.4 · hero unification
Homepage mascot now matches /mascot/ quality.
The homepage was using a basic inline SVG (simple shapes, dot eyes) while /mascot/ used the polished PNG variants. Now both use the same high-quality original.png as the default. Wrapped the mascot in a fixed-size stage so the OUTFIT toggle anchors to the mascot's corner instead of floating off in the wide hero column. Added crossfade transition and dynamic caption (e.g. "← Tooly in sunglasses"). Touch target bumped to 36px.
April 20, 2026
v7.27.3 · bulletproof
Eliminated the broken-image render for good.
Turns out <img hidden> with author CSS display: block can still paint the alt text and broken-icon fallback in some Chromium variants — regardless of [hidden] selectors. The fix: don't have the <img> in the DOM at all until JavaScript needs it. The overlay image is now created via document.createElement only when an outfit is picked, and removed on reset. Structurally impossible to misrender.
April 20, 2026
v7.27.2 · touch fixes
Toggle now visible on mobile.
The OUTFIT button was hidden until :hover or :focus-within — but touch devices have no hover state, so it was effectively invisible. Added @media (hover: none), (pointer: coarse) to force-show on touch. Added explicit width/height to the hero SVG to prevent a layout collapse. Centered the mobile popover with viewport-bounded width.
April 20, 2026
v7.27 · Tooly gets a wardrobe
Thirty-one outfits. One toggle.
Fifth release. The mascot system shipped. Tooly now has 31 outfit variants — astronaut, wizard, pirate, hard hat, sunglasses, headphones, and more. Hover the homepage mascot, click the new menu pill, pick an outfit. Your choice persists across visits.
Shipped in this release:
- New
/mascot/ page — full picker with all 31 outfits, random-outfit button, copy-share-link with deep-link via URL hash (visit /mascot/#pirate to land on pirate Tooly).
- Homepage hero mascot toggle — discreet menu button on hover/focus opens an 8-outfit quick-pick popover. Click outside or press Esc to close. Selection saved in localStorage.
- 31 PNG variants in
/mascot-variants/, each ~40-65 KB. Cached aggressively via _headers (immutable, 1-year max-age).
- Inline SVG mascot preserved as default — first paint is unchanged. The image overlay only loads when an outfit is picked.
- Full keyboard accessibility — toggle is focusable, popover has
role="menu", items have role="menuitem" and aria-pressed states. Respects prefers-reduced-motion.
No tracking added. The outfit choice is a single localStorage key (tooly-outfit) on your device. Nothing leaves your browser.
April 20, 2026
v7.26 · Expansion II
Six more tools, same discipline.
Fourth release. The workshop now holds seventeen tools. Every one still runs in the browser. No uploads, no signup, no tracking. Three essays from the last cycle already went up — this release is the tools that pair with them.
Shipped in this release:
- Paperwork — stitch images into PDFs, slice PDFs back into images, reorder pages. 50 MB cap, pdf-lib + pdf.js loaded on demand. Declines to convert Word docs and says why.
- JSON Repair — paste broken JSON, get conservative fixes back. Trailing commas, unquoted keys, Python literals, truncated payloads. Every change is logged; risky guesses are refused.
- Regex Generator — describe a pattern in English, draft a regex. On-device AI via Chrome's built-in Prompt API with a WebGPU fallback. Silent fallback on unsupported hardware.
- Cron Explainer — paste any cron expression, get the next five trigger times. Twelve timezones, full crontab syntax (/, -, comma), copy-to-clipboard.
- JWT Decoder — paste a token, see header + payload + signature. Live countdown on exp claims. Flags alg=none and other security footguns. Token never leaves the tab.
- PDF Compressor — shrink PDFs by re-encoding embedded images. Quality slider, DPI target, settings persisted across visits via localStorage.
Still zero servers. The Regex Generator uses the browser's built-in Prompt API (Chrome 140+) or falls back to Transformers.js + WebGPU — no API calls, no inference servers. Paperwork and PDF Compressor use pdf-lib/pdf.js loaded from a CDN on first use; JWT decoding is fully inline.
April 19, 2026
Commitment
A page about money.
There's now a Support page. It does two things: it gives you a place to help if you want to, and it states — in writing, in advance — what monetization paths are on the table and what's off the table permanently.
On the table: EthicalAds when the site hits ~50,000 pageviews a month (text-only, no cookies, no tracking). Buy Me a Coffee and GitHub Sponsors once the accounts finish verification. A native desktop app, eventually, one-time purchase. Off the table, permanently: subscriptions to the existing tools, signup walls, Google Analytics, Facebook pixels, data sales, or selling to a surveillance-economy buyer.
It's easier to make these commitments now, before there's any money on the line. That's why the page exists before the money does.
April 19, 2026
Open source
Format Flipper is now open source.
The full source of Format Flipper is now published on GitHub at github.com/J1E2M3/tooly-mctoolface, under the MIT license. Includes the complete tool, a documented architecture (the star-topology design that makes 9 formats × 72 directions tractable), passing round-trip tests, and a contributor guide for adding a tenth format.
This is the first of what will be several tools extracted from the workshop over the coming months. The commitment: if a tool graduates to having its own repo, it stays open-licensed. If the site ever goes away, the tools survive.
April 19, 2026
Article
New article: why your SVGs are 3× too big.
A guide to the 50-80% bloat that comes with most SVG exports from Figma, Illustrator, and Inkscape — where it comes from, which parts are safe to strip, and which parts will quietly break things. Pairs with SVG Slimmer. The workshop now holds eleven tools and ten articles.
Also a small infrastructure improvement: the feed generator now runs automatically on every release, so the Atom feed is always current without manual regeneration.
April 19, 2026
Update
An Atom feed.
The blog now has a proper Atom feed, autodiscoverable from every page via <link rel="alternate">. Subscribe with any feed reader — NetNewsWire, Feedly, Miniflux, Reeder, whatever — and new articles will appear without you having to check back. We won't email you. We won't ask for your address. The feed is the whole mechanism.
Also a small cleanup: the JSON vs YAML vs TOML changelog entry said "eight articles" when it should have said "nine" (the client-side essay had already shipped). Fixed.
April 19, 2026
Article
New essay: what "client-side" actually means.
The word "client-side" appears on every "free online tool" landing page, and it can mean five different things — three of which still leak your data. This piece breaks them apart honestly, admits where Tooly McToolface still has one small leak (Google Fonts on page load), and ends with a 90-second DevTools procedure to verify any tool's claim for yourself — including ours.
Nine articles now. One of them assumes you won't take its word for anything.
April 19, 2026
Article
New article: JSON vs YAML vs TOML.
A practical guide to picking the right config format, written as a companion to Format Flipper. Covers origin stories, when each format wins, a 10-row decision matrix, and five gotchas that come up regularly enough to be worth knowing in advance — including the YAML Norway problem, JSON's silent 253 integer clamp, and why TOML datetimes are actually a first-class type.
The workshop now holds eleven tools and nine articles. Also fixed a stale FAQ on Format Flipper that still said TOML wasn't supported (it shipped in v1.2).
April 19, 2026
Update
A benchmarks page. Plus a site-wide quality pass.
Two things shipped today. First, a new benchmarks page that measures the workshop honestly — page weight, bytes uploaded (zero), task latency, and the three places where Tooly is measurably slower than the best paid alternative. Includes a live benchmark that runs in your browser when you load the page, so the numbers come from your actual CPU instead of a pitch deck.
Second, a site-wide polish pass based on a ten-agent audit: BreadcrumbList structured data on every tool page, fuller Schema.org on the hub (WebSite + ItemList of all 11 tools) and blog index (Blog with all 7 posts), async font loading on all 23 pages (meaningfully faster first paint), aria-label on previously-unlabeled buttons and inputs, unified image-error messages across three tools, and AVIF feature-detection in Image Smusher so the AVIF button auto-disables in browsers that can't encode it.
No new tools, no visual changes. Quieter release — the kind that doesn't make for exciting changelog copy but that matters if you land here via Google in six months.
April 18, 2026
v1.2 · Timestamp & TOML
One new tool, one format, a few quality-of-life wins.
Third release. Adds a new tool and extends two existing ones. The toolshed now holds eleven tools. Every tool still runs entirely in the browser — no uploads, no servers, no tracking beyond the privacy-friendly aggregate pageviews added in v1.1.1.
Shipped in this release:
- Timestamp Converter — paste any Unix epoch, ISO 8601, RFC 2822, or human-readable date; get every other representation at once. Timezone-aware, DST-correct, with math and diff panels.
- Format Flipper now speaks TOML, extending coverage from 8 formats to 9 and conversion directions from 56 to 72.
- JSON Fixer accepts JSONC — JSON with
// and /* */ comments plus trailing commas (tsconfig.json, VS Code settings).
- Image Smusher gains AVIF output alongside PNG, JPG, and WebP.
- Favicon Foundry accepts SVG input and rasterizes down to the full favicon bundle.
- New article: ISO 8601 vs Unix epoch vs human-readable: when to use which, paired with the Timestamp Converter.
Eleven tools, seven articles. Still one person, still no framework, still no build step.
April 18, 2026
Update
Privacy-friendly analytics added.
Installed Plausible to count aggregate pageviews. No cookies, no IP tracking, no cross-site tracking, no personal data collected — just "this page was viewed N times this week." The privacy page was updated first, as promised, before anything was deployed. Plausible's full data policy is at plausible.io/privacy-focused-web-analytics if you want the details.
April 18, 2026
v1.1 · Expansion
Five more tools, three more articles.
Second release, doubling the workshop. Five new tools shipped, three new articles published. The toolshed now holds ten tools across image, dev, design, and generation categories. Every tool runs entirely in the browser — no uploads, no servers, no tracking, same as v1.0.
Shipped in this release:
- Regex Rambler — live regex tester with plain-English explainer
- QR Maker — QR generator with 8 data types, color customization, and logo overlay
- Favicon Foundry — drop one image, get the full favicon bundle (6 sizes + multi-res ICO + SVG)
- Diff Checker — LCS-based text diff with word-level highlights and unified patch export
- Format Flipper — bidirectional conversion between 8 data formats (JSON, YAML, CSV, TSV, XML, Markdown, HTML, SQL)
- Three new articles — practical regex cheatsheet, favicon guide, and why QR codes aren't dead
- Hub page refreshed with all 10 tool cards, cross-linked footers across every page
56 bidirectional format directions in Format Flipper alone. Several of the tools ship with libraries bundled inline (js-yaml, a QR encoder, custom ZIP/ICO writers) so they work without any external network requests after the page loads.
April 18, 2026
Tool
Format Flipper shipped.
Convert between JSON, YAML, CSV, TSV, XML, Markdown tables, HTML tables, and SQL INSERT statements. Any of the 56 bidirectional directions, with proper escape handling for each format (CSV quoting, SQL apostrophe doubling, XML entity escapes). Type coercion, nested-data handling, one-click swap.
js-yaml 4.1.1 bundled inline (~39 KB) so YAML parsing works offline. Other formats are native JS. Per-format options appear contextually — table name for SQL, root tag for XML, indent depth for JSON/YAML. Open it.
April 18, 2026
Tool
Diff Checker shipped.
Paste two texts, see what changed. LCS-based line diff with a second pass for word-level highlighting within changed lines. Split or unified view, ignore-whitespace and ignore-case toggles, whitespace visualization, 2,000-line cap for performance.
Export as a standard unified-diff patch compatible with git apply and patch. 16 ms for 500×500 lines in testing. Four sample data sets (code, prose, config, JSON) for quick exploration. Open it.
April 18, 2026
Tool
Favicon Foundry shipped.
Drop one image, get the full favicon bundle: 16, 32, 48, 180, 192, 512 px PNGs, a multi-resolution .ico, and favicon.svg passthrough when the source is vector. Plus the ready-to-paste HTML <link> snippet. Download individually or as a ZIP bundle.
Self-contained — no external dependencies. Inline CRC-32, stored-mode ZIP writer, and ICO container writer were built from scratch and verified against unzip and file(1). Your logo never leaves your device. Open it.
April 18, 2026
Tool
QR Maker shipped.
Eight data types: URL, text, WiFi credentials, email, SMS, phone, vCard contact, and location coordinates. Full color customization, quiet-zone margin slider, all four error-correction levels (L/M/Q/H). Drop a logo in the center, with optional white padding for contrast. Download as PNG (1200 px) or crisp SVG, or copy as PNG to clipboard.
The QR encoder is Kazuhiko Arase's reference library (MIT, ~29 KB), bundled inline so the tool works fully offline. Your WiFi password never leaves your browser. Open it.
April 18, 2026
Tool
Regex Rambler shipped.
Live regex tester with a plain-English explainer. Type a pattern, see matches highlighted as you type, and get a token-by-token breakdown of what every piece actually does — anchors, escapes, character classes, quantifiers, named captures, lookaheads, lookbehinds. Also supports find-and-replace with live preview.
Eight sample patterns included (email, URL, phone, IPv4, hex color, ISO date, quoted string, capitalized word). 100 ms debounce plus a 250 ms catastrophic-backtracking guard, capped at 5,000 matches to keep the browser responsive. Open it.
April 18, 2026
Write
Article: QR codes aren't dead.
After years of mockery, QR codes quietly became useful again. What changed (native camera detection), what they actually solve well (WiFi sharing, event tickets, payments in China, context-specific print-to-web), and what they're still wrong for (anything you can type, moving objects, tracking without disclosure). Read it.
April 18, 2026
Write
Article: the favicon guide.
Why modern sites need six favicon sizes plus a multi-res .ico and an SVG. What each file does (16 for tabs, 180 for iOS, 192 for Android, 512 for PWAs), the HTML snippet that ties them together, and notes on designing a logo that survives being scaled to 16 pixels. Read it.
April 18, 2026
Write
Article: practical regex cheatsheet.
The regex tokens, quantifiers, anchors, groups, and lookarounds you actually reach for — in the order you reach for them, with notes on the ones that trip people up. Plus a small library of copy-paste patterns (email, URL, IPv4, ISO date, hex color). Read it.
April 18, 2026
v1.0 · Launch
Tooly McToolface is alive.
After a weekend of building, the site is live at toolymctoolface.com. Five tools, three articles, a mascot, and an opinion about how the web should work.
Built solo, shipped fast, on a budget of $11.18 (the price of the domain). No VC money, no co-founders, no roadmap deck. Just tools that work.
Shipped with:
- Image Smusher — batch image compression
- HEIC Unheicer — iPhone HEIC photo converter
- JSON Fixer — formatter, validator, tree viewer
- Color Grabber — extract palettes from images
- SVG Shrinker — in-browser SVG optimizer
- Three SEO articles — image compression, HEIC, and the "free isn't free" essay
- Full design system (Fraunces + Inter + JetBrains Mono, amber accent, warm dark theme)
- Open Graph preview images, favicons, sitemap, robots.txt
- Client-side processing for every tool — nothing leaves your browser
April 18, 2026
Tool
SVG Shrinker shipped.
A minimal, in-browser SVG optimizer. Strips Inkscape/Illustrator/Figma metadata, removes comments and default attributes, rounds numeric precision. Side-by-side visual preview confirms the optimized output looks identical to the original. Typically shaves 40–70% off file size for SVGs exported from design tools.
No libraries, no WebAssembly — just a few hundred lines of regex and DOM work. Open it.
April 18, 2026
Tool
Color Grabber shipped.
Extract color palettes from any image using median-cut quantization. Pick 5, 8, or 12 dominant colors, then export to CSS variables, SCSS, Tailwind config, Figma tokens, JSON, or plain hex. Download a branded palette PNG for sharing.
Entire algorithm runs client-side. Your image never leaves your browser. Open it.
April 18, 2026
Writing
Third blog post: why "free" isn't free.
A ~2,000-word essay on the five hidden costs of "free" online tools — data harvesting, attention arbitrage, upsell funnels, watermarking, and account lock-in — and how to spot each before you upload anything sensitive. Reinforces Tooly's trust positioning.
Read it.
April 18, 2026
Tool
JSON Fixer shipped.
A JSON formatter that's actually good. Format, minify, sort keys, validate. Tree view with collapsible nodes and item counts. Syntax-colored text view. Error messages that show the exact line and column of syntax errors (not just "Unexpected token at position 42").
Runs entirely in the browser. Samples built in for quick testing. Cmd+Enter to format. Open it.
April 18, 2026
Writing
Second blog post: the iPhone HEIC problem.
A ~1,800-word guide to Apple's HEIC photo format — what it is, why Apple uses it, the genuine compatibility problem, and three ways to stop caring about it. Targets the high-volume "heic to jpg" search query. Natural companion piece to the image compression guide.
Read it.
April 18, 2026
Tool
HEIC Unheicer shipped.
Apple's HEIC format is technically better than JPG but universally annoying when you need to share photos outside the Apple ecosystem. This tool converts HEIC to JPG, PNG, or WebP in your browser using a WebAssembly-compiled decoder.
Supports batch conversion, quality control, and format choice. Open it.
April 18, 2026
Tool
Image Smusher shipped.
The first tool. Batch-compresses PNG, JPG, and WebP images entirely in-browser using the Canvas API. Handles up to 50 files at once. Quality slider, optional resize, EXIF stripping, batch ZIP download.
Built because I was tired of TinyPNG's 20-file limit and Squoosh's one-at-a-time flow. Open it.
April 18, 2026
Writing
First blog post: image compression guide.
A ~2,100-word practical reference for developers and designers. Covers JPG vs PNG vs WebP vs AVIF, quality settings, batch workflows, EXIF stripping, and the 15-minute checklist that cuts most sites' image weight by 60–80%.
Read it.
April 18, 2026
Branding
The name, explained.
After briefly considering "Workbench" (too generic, SEO-saturated), "Smushr," "Blorp," and "ShrinkRay," we landed on Tooly McToolface — a Boaty McBoatface reference that refused to stop being funny. The name is a joke. The tools are not.