DEV Community

Cover image for Building MachineConvert: a static site where every number has to show its source
MachineConvert
MachineConvert

Posted on

Building MachineConvert: a static site where every number has to show its source

If you train in more than one gym, you know the problem: 60 kg on one brand's leg press is not 60 kg on another's. Levers, cams and pulleys change the force you actually feel, so the number on the stack doesn't carry over. I built MachineConvert to catalog strength machines across brands and to answer one question: "I lift X on this machine, what is it on that one?"

Here is how it's built, and the one rule that shaped almost every decision.

The rule: no number without a source

The rule is that every figure on the site carries a source (URL plus an exact quote), a date and a confidence level. If any of those is missing, the site says "no data" instead.

There are three confidence levels:

  • measured: a force gauge or dynamometer reading with photo or video proof
  • manufacturer: copied from an official spec page or sheet
  • estimated: calculated from lever, cam or pulley ratios, with a written method and a low/high range

One design decision followed from that. Manufacturer stack weights live on the machine record, never in the measurement data, and the comparator only uses measured or estimated force curves. A stack weight tells you what's on the pin, not what you feel in your hands. When the comparator does return a number, it is labeled with the lowest confidence of the two sides, and both machines show their own badge underneath.

Today the catalog has 24 brands, 50 product lines and 283 machines, with 353 sourced manufacturer values and 81 estimated points. There are 0 measured points so far, and the site says so. A comparator that says "No data yet" is better than one that makes up an answer.

The badges are filled pills with dark text and a distinct icon per level (check, document, ruler), so they don't rely on color alone.

A plain static site

There's no framework and no backend for the catalog. It's a static site: JSON data, a small vanilla JS app, and one prerendered entry page per route. That's 289 pages (283 machine pages plus a few others), each with its own title, description, canonical URL and JSON-LD. Structured data never contains load figures, so search snippets can't show a number without its source. The site is hosted on Vercel.

A synthwave grid in pure CSS

I wanted a Miami-at-night look: sun, mountains and a perspective grid scrolling toward you, animated on phones too. My first attempt drew the grid on a <canvas>, and Lighthouse made clear that was too slow. The second version is pure CSS, about 3 KB. The grid is a transformed, repeating gradient whose animation runs on the GPU, and one line of JS pauses it when the tab is hidden. With prefers-reduced-motion you get a static version. The technique was inspired by Jane Ori's "to the future" CodePen. I rewrote it from scratch rather than copying it, and the credit is in the repo.

The catch with a busy background is legibility. A navy layer always sits over the wallpaper, and an extra dimming layer turns on everywhere except the hero areas. A script (check-wallpaper-contrast.js) measures actual pixels behind the text. The worst case is 5.58:1, comfortably above WCAG AA's 4.5:1.

build.sh is the real product

Most of the work went into checks rather than features. ./build.sh has to be green before anything ships, and each check gets negative tests: I break things on purpose and confirm the check fails. Some of the checks:

  • Rendered figures: headless Chrome visits 341 pages and routes and makes sure every number shown on screen links back to a data record with source, date and level. It found 8,747 numbers and 0 unmarked ones.
  • Comparator engine: runs 5,518 machine pairs × 60 loads and verifies that no number appears unless both sides have a force curve.
  • Accessibility: axe-core at 1280 and 390 px across 368 page states, plus a full keyboard Tab walk. 0 violations.
  • Rendering: 30 page types at 390 and 1280 px, checking for no horizontal scroll, no clipped or overlapping text, and touch targets of at least 24 px on mobile.
  • Internal links: a crawler follows every route, asset, CSS url() and anchor. 0 broken out of 43,527 links.
  • Source links: all 360 unique source URLs in the data are fetched and the reports are dated.
  • Performance budget: guards against regressions, like a removed preload or a non-deferred script.

That pays off in Lighthouse. Mobile scores on the live site sit at 96–100 for performance and 100 for accessibility. Before I added font/JSON preloading and defer, performance was in the low 80s.

One lesson: checks need to know where they run. A coverage check read a local proofs/ folder that isn't part of the deploy bundle, and it broke the Vercel build. Now checks that need Chrome or local proofs print SKIPPED in the cloud and run fully on my machine.

Community measurements, moderated

The only way to get measured data is from people with a force gauge in a gym. There's a "Prepare a measurement" form that builds an email instead of posting anywhere. The form and the review tool share one rules file:

  • a measured entry requires an https link to proof
  • an estimated entry requires a method (at least 20 characters) and low < value < high
  • aggregated "it feels heavier" opinions are refused for now

Submissions go into a local queue (pending → accepted or rejected, final), handled with a small CLI. The queue is never deployed. Writing the moderation check found a real crash in the form, which is the best argument I know for writing checks.

i18n, tested with a fake language

The site is in English with French on the way. Every UI string (360 keys) goes through translation files, and a check enforces that English and French have the same keys, that no key goes unused and that placeholders match. To find hard-coded text, the whole site is rendered in a pseudo-locale (en-XA): 28 states and 17,733 text nodes. After fixes, no English remained outside catalog data such as brand and machine names. That pass also caught a page that declared itself French while serving English. The hreflang for French is only emitted once the translation is at least 95% complete, so search engines never get a half-empty locale.

What's next

The next step is more data, especially real measurements, plus a French version and comparison guides for common questions. If you lift in more than one gym, try the comparator on machineconvert.com and tell me where it's confusing. And if you own a force gauge, I'd love your help.

Top comments (0)