Email me

Dev · · 13 min read · Matt Busel

Filed under App development Growth Guides

Landing page grader tested on 2,077 Show HN pages: correlation 0.00

Landing page grader tested on 2,077 Show HN pages: correlation 0.00
On this page · 9 sections
  1. The grader, and why there is no AI in it
  2. The dataset: every Show HN that linked to its own site
  3. The first result was a bug: 80% of pages "used Inter"
  4. Two more rules I audited while I was in there
  5. What actually fails on Show HN landing pages
  6. Did the score predict upvotes? Not even a little
  7. I ran it on my own sites. They failed too
  8. Do this in your project
  9. The numbers

TL;DR: On October 17, 2026 I ran my free, no-AI landing page grader over every Show HN post from the last month that linked to its own website: 2,146 URLs, 2,079 graded. Two things came out. First, one of my 26 rules was broken: it said 79.9% of pages used the Inter font, because it was matching the letters "inter" inside words like pointer and interactive. After the fix, 18.8%. Second, the grader's score had no relationship at all with Hacker News upvotes: Spearman correlation -0.001. The month's top post (728 points) scores a perfect 0. A landing page grader measures hygiene. It does not measure whether anyone wants the thing.

Short answer for the search. A landing page grader fetches your page's HTML and runs a list of checks on it: is there a title and a meta description, an Open Graph image for link previews, one clear headline, a mobile viewport tag, a price if you are selling, no "lorem ipsum", no wall of buzzwords. Each failed check costs points and comes with a fix. Good graders are deterministic: the same page gets the same score every time, and every point can be traced back to one line of code.

What a grader cannot do is tell you whether your product is interesting. This article is the measurement behind that sentence: the code, the dataset, the bug it exposed, and a checklist you can run on your own page today.

Animated histogram of grader scores for about 2,078 Show HN landing pages. It starts with the buggy font rule: 80 pages with zero findings and a median score of 31. The bars slide left as the fixed rules take over, ending at 412 clean pages and a median of 25.
Score histogram before and after fixing two rules. Same 2,146 URLs, graded Oct 17 2026 at 10:11 and 10:34 AM ET. Lower is cleaner.

The grader, and why there is no AI in it

The grader is the free Page Teardown on tensorust.com. It's a port of the rule engine from COOKED, a site roaster I built earlier with one hard rule: no model. Every check is a regular expression or a count over the raw HTML, and the whole engine is one JavaScript file of about 130 lines that runs as a Vercel function.

Why no LLM? Three reasons, and they all showed up in this experiment:

  • Same input, same output. I could grade 2,146 pages three times in one morning and diff the runs. With a model in the loop, a score change could be the page or the dice.
  • Cheap and fast. The final run took 96 seconds for 2,146 URLs with 12 requests in flight, a median of 355 ms per page including the fetch. Cost: zero.
  • Every point is explainable. When a result looks wrong, you can open the rule and see exactly why. That's how I found the bug below in about five minutes. Try that with a prompt.

Each rule is one row: an id, an area, a point cost, a check, the roast line, and the fix. Here are three of the 26, straight from teardown/api/teardown.js:

// [id, area, points, run(ctx) -> params|null, burn(params), fix]
['no-og-image','Identity',10, c => /property=["']og:image/i.test(c.html) ? null : {},
  () => 'No og:image. Every share of your page arrives as a grey rectangle.',
  'Add an og:image at 1200x630. The single highest leverage tag on the page.'],
['no-h1','Headline',11, c => tags(c.html,'h1') > 0 ? null : {},
  () => 'There is no h1 anywhere on this page. No headline, no promise, no point.',
  'Add one h1 that states the single thing you do.'],
['vague-cta','Design',6, c => /get started/i.test(c.text) ? {} : null,
  () => '"Get Started" is the button you write when you have not decided what happens next.',
  'Say what the click does. "Scan my repo" beats "Get Started" every time.'],

The score adds up the points of every failed rule and squashes the total onto 0 to 99, so a page with lots of problems reads as bad without ever pinning at 100. Lower is better.

const raw = found.reduce((s, f) => s + f.points, 0);
const score = Math.min(99, Math.round(100 * (1 - Math.exp(-raw / 45))));
// 0 SOMEHOW FINE, 1+ LIGHTLY SEARED, 20+ MEDIUM RARE, 40+ WELL DONE, 60+ CHARRED, 80+ INCINERATED

The dataset: every Show HN that linked to its own site

Show HN is where people post things they made on Hacker News. It's close to a perfect test set for a landing page grader: thousands of real launch pages, each with a public score (points) attached.

The HN Search API by Algolia caps a query at 1,000 hits, so I pulled one UTC day at a time from September 15 to October 14, 2026 (pulled at 10:11 AM ET on October 17, so even the newest posts had three days to collect votes):

t = start
while t < end:
    t2 = min(t + 86400, end)
    u = ("https://hn.algolia.com/api/v1/search_by_date?tags=show_hn"
         f"&numericFilters=created_at_i>={t},created_at_i<{t2}&hitsPerPage=1000")
    d = json.load(urllib.request.urlopen(u))
    assert d["nbHits"] <= 1000, d["nbHits"]   # never silently truncate a day
    for h in d["hits"]:
        out[h["objectID"]] = {k: h.get(k) for k in ("objectID", "title", "url", "points", "num_comments", "created_at")}
    t = t2

That gave 3,949 Show HN posts in 30 days. The first surprise had nothing to do with landing pages: 1,350 of them (34.2%) linked straight to github.com. Another 83 were text posts with no link, and a long tail pointed at the App Store, the Chrome Web Store, Hugging Face, npm and PyPI. I dropped all of those platform pages, because their HTML belongs to the platform, not the maker. That left 2,146 unique pages on the maker's own domain.

The grading loop imports the exact engine the live site runs (the file is copied, plus one export line) and writes one JSON line per page, so a crash halfway through loses nothing:

import { analyze, fetchPage } from './teardown_engine.mjs';
// ...filter out platform hosts, dedupe by host + path...
async function worker() {
  while (i < jobs.length) {
    const s = jobs[i++];
    let rec = { id: s.objectID, url: s.url, points: s.points, comments: s.num_comments };
    try {
      const r = analyze(await fetchPage(s.url));
      rec = { ...rec, ok: true, score: r.score, verdict: r.verdict, failed: r.failed.map(f => f.id) };
    } catch (e) { rec = { ...rec, ok: false, err: String(e.message || e) }; }
    out.write(JSON.stringify(rec) + '\n');
  }
}
await Promise.all(Array.from({ length: 12 }, worker));

The fetcher is the production one too: it refuses private IP addresses, follows at most 3 redirects, stops after 10 seconds and reads at most 2 MB. 67 pages failed to load in the final run: 12 unreachable, 10 answered HTTP 429 (rate limited, which I respected and did not retry), 10 answered 403, 7 had domains that no longer resolve, 7 timed out. A month after launch, those last 7 plus the dead DNS are their own small story.

The first result was a bug: 80% of pages "used Inter"

The first full run finished at 10:13 AM. The top failure was slop-font, the rule that teases you for using one of the five most common web fonts (Inter, Roboto, Open Sans, Montserrat, Poppins). It fired on 1,659 of 2,077 pages, 79.9%.

Inter is popular. It isn't that popular. Here's the original check:

const FONTS = ['inter','roboto','open sans','montserrat','poppins'];
c => { const h = FONTS.find(f => c.lhtml.includes(f)); ... }   // c.lhtml = the whole page, lowercased

A substring search over the entire HTML. So I re-fetched all 1,659 flagged pages and logged what the first match actually was:

Breakdown of the 1,657 re-fetched pages the old font check flagged as using Inter. 660 matched inside another word, mostly pointer, pointer-events-none and pointer-events. 609 matched a word starting with inter: interactive 107, interface 67, internet 53, internal 44. Only 388 really used the font.
What html.includes("inter") actually matched. Every flagged page re-fetched Oct 17 2026.

660 matches were inside another word. The top one by far: pointer. Every page with cursor: pointer in its CSS, or a Tailwind pointer-events-none class, "used Inter". So did pages with afterInteractive script loading, a printer, or a setInterval. Another 609 were words that start with "inter": interactive, interface, internet, internal, interest. Only 388 pages really chose Inter or one of the other four. Two pages didn't answer the re-fetch.

That's 1,269 pages paying a 7-point penalty for writing English. And the rule had been live on the free tool for days.

The fix: only count a font where a font is actually chosen. Four places cover nearly every site I looked at: a CSS font-family declaration, a Google Fonts CSS link, the hashed class or CSS variable that next/font writes (inter_7b064e0d, --font-inter), and a font file name like /assets/inter-latin-standard-normal.woff2. That last one I only added after the first fixed version missed a real Inter preload on a 251-point post.

const FONT_USE = [
  /font-family\s*:\s*[^;{}<]*?\b(inter|roboto|open sans|montserrat|poppins)\b/i,
  /fonts\.googleapis\.com\/css2?\?[^"'>]*family=(inter|roboto|open\+sans|montserrat|poppins)\b/i,
  /(?:^|[\s"'.])_{0,2}(inter|roboto|open_sans|montserrat|poppins)_[0-9a-f]{6,8}\b|--font-(inter|roboto|open-sans|montserrat|poppins)\b/i,
  /\/(inter|roboto|open-?sans|montserrat|poppins)[-_.][^"'\s>]*\.(?:woff2?|ttf)\b/i,
];

It still has a blind spot, and I'd rather say so: the grader reads only the HTML, not linked stylesheets, so a site that loads Inter purely from an external CSS file now passes. I'll take a rule that misses some real cases over one that accuses everybody. The fixed font rule went live on tensorust.com at about 10:24 AM ET the same morning.

A rule that fires on 80% of pages isn't a finding. It's a bug, or a rule about nothing.

Two more rules I audited while I was in there

Once one rule lied, I checked two more that fire a lot.

no-og-image only read the first 40,000 characters. The engine looks at a head slice of the HTML for identity tags. Modern pages ship big inline CSS and script blobs before their meta tags, and 31 of the 550 pages flagged for a missing og:image actually had one, just further down. One-word fix: search the whole HTML. It shipped with this article.

no-favicon ignores the default /favicon.ico. Browsers ask for /favicon.ico on their own even when no <link rel="icon"> tag exists. Of the 250 pages flagged, 81 (32%) served a real image there. That one needs an extra request inside the API, so it isn't fixed yet; until it is, read a "no favicon" result as "no favicon link tag". Google's favicon guidelines for search results do ask for the link tag, so the advice still stands, it's just less urgent than the roast makes it sound.

Lesson for anyone writing checks with regexes (or reviewing ones an AI wrote for you, which is the same job, see how I review AI-written code): run every rule over a big real sample and look at its hit rate before you trust it. Anything above about 50% deserves a hand check of twenty random hits.

What actually fails on Show HN landing pages

With both fixes in, the final run (10:34 to 10:36 AM ET, 2,079 pages graded) looks like this:

Bar chart of the share of 2,079 Show HN landing pages failing each check: no og:image 25.0 percent, 18 or more script tags 20.9, Inter or similar font 18.8 (the old buggy check said 79.9), no h1 16.0, Get started button 12.7, no favicon 12.0, no meta description 9.6, empty JS shell 9.0, thin page 8.8, purple gradient 7.2, selling without a price 6.5, title over 70 characters 6.2.
Failure rate per rule after both fixes. Final run, Oct 17 2026, 10:34 to 10:36 AM ET.
  • No og:image: 25.0%. One in four launch pages shows up as a blank grey box when someone shares it on X, Slack, Discord or iMessage. This is the most common miss and the cheapest fix on the list: one tag, one 1200x630 image.
  • 18 or more script tags: 20.9%. Mostly framework chunks. Not fatal, but a lot of pages are paying for analytics and widgets before their headline renders.
  • Inter, Roboto, Poppins, Open Sans or Montserrat: 18.8%. Now real. Honestly this is the softest rule of the 26: a common font is a taste call, not a mistake, and it's only worth 7 points.
  • No h1: 16.0%. Some of these are JavaScript apps that render the headline in the browser; a crawler and a slow phone both see nothing until the JS runs. 9.0% of pages were a near-empty shell with a <div id="root">.
  • "Get started" as the call to action: 12.7%. No meta description: 9.6%. Title over 70 characters, where Google's title links will likely cut it: 6.2%.

And the good news: 412 pages (19.8%) passed all 26 checks, up from 80 under the buggy font rule. The median score dropped from 31 to 25.

Did the score predict upvotes? Not even a little

This is the part I actually ran the experiment for. If a landing page grader measures quality, pages that score better should, on average, get more points on Hacker News. I used Spearman's rank correlation, because HN points are wildly skewed (the median Show HN in this set got 2 points) and Spearman only cares about order, not size.

Result: -0.001 with the fixed rules, 0.001 with the buggy ones. Zero, twice. Grouping by points tells the same story:

Paired bar chart of median grader score by Hacker News points. Old rules: 31, 31, 31, 31 and 33.5 for 0-1, 2-4, 5-19, 20-99 and 100-plus points. Fixed rules: 25, 25, 23, 20 and 26.5. Spearman rank correlation is 0.00.
Median score by HN points bucket. Points as of 10:11 AM ET Oct 17 2026; 798, 929, 244, 66 and 42 pages per bucket.

The median score is 25 for posts with 0 to 1 points, 25 for 2 to 4, 23 for 5 to 19, 20 for 20 to 99, and 26.5 for the 42 graded posts that cleared 100 points. If anything the 100+ group is a hair worse. Pages with zero findings got 20+ points 5.1% of the time; everyone else, 5.2%.

The month's top Show HN in this set (728 points) is a page that turns any screen into a big sign. Under the old rules it scored 14, and its only finding was the font bug: the word "interval" in a link. Under the fixed rules it scores 0. So the best-received launch of the month was also a perfectly clean page, but so were 411 others that mostly got 1 or 2 points.

My read, and it's an opinion, not something this data proves: Hacker News votes on the thing, not the page. People click through from a title, decide in ten seconds whether the idea is interesting, and vote. A missing meta description doesn't stop that. The places hygiene probably does matter are the ones this dataset can't see: the share preview when someone posts your link in a group chat, the snippet Google shows for your name, the phone visitor who arrives from an ad. Nobody upvotes an og:image. But a grey box in a busy Slack channel gets scrolled past, and you never find out.

I ran it on my own sites. They failed too

It would be cheap to publish this without grading myself, so I did. Seven of my other public sites, through the live tool after the fix: five of seven have no og:image, six of seven have no favicon link tag, and three are "thin" (under 1,200 characters of text). Scores from 20 to 44. tensorust.com itself scores 0, which mostly means it's the only one I've polished.

Screenshot of the tensorust.com Page Teardown run on beings-phi.vercel.app: score 44, WELL DONE, 3 of 26 checks failed. Findings: no og:image, minus 10; thin page, minus 10; no favicon, minus 6. Each has a one-line fix underneath.
The live Page Teardown grading my own BEINGS page, Oct 17 2026, after the font fix.

That's BEINGS, the chatroom where AI creatures run in your own browser (the WebGPU benchmark article is about its engine). 44, WELL DONE. Every finding is true and every fix is a few minutes. I hadn't done them because nothing forced me to look, which I suspect is the story for most of the 25% too.

Do this in your project

If you're shipping a landing page this week, here's the order I'd fix things in, based on how often they fail and how much they cost to fix:

  1. Add an og:image (1200x630) plus og:title and og:description. Paste your URL into a Slack or Discord message to yourself and check the preview.
  2. Put your headline in the HTML. View source (not DevTools, the actual source). If your h1 isn't in there, crawlers and slow phones don't see it.
  3. Write a meta description that says what the product does in 150 characters. Not a mood. A function.
  4. Rename the button. "Get started" tells nobody anything. Say what the click does.
  5. If you sell something, show a number. Even a range.
  6. Add the favicon link tag while you're in the <head>.

And if you're writing a checker of your own, for landing pages, code review, anything:

  • Match where the thing is declared, not anywhere in the text. includes() over a whole document is how you get 1,269 false positives.
  • Run every rule over a few thousand real inputs and look at hit rates. Under 1%: maybe dead. Over 50%: probably lying.
  • Keep it deterministic so you can diff runs. That's what made this whole article possible: three full runs of 2,146 pages, compared line by line, in under an hour. The same idea is behind the crates.io download count deep-dive: pull the raw numbers yourself before you believe any dashboard.
  • Then ask what the score is for. Mine turned out not to predict upvotes at all, which is fine, as long as nobody, me included, pretends it does.

The numbers

MeasureValueSource, time
Show HN posts, Sep 15 to Oct 14 20263,949HN Algolia API, pulled 10:11 AM ET Oct 17
Linked to github.com1,350 (34.2%)same
Own-domain pages / graded2,146 / 2,079final run 10:34-10:36 AM ET
Median time per page (fetch + 26 rules)355 msfinal run
Font rule hit rate, old / fixed79.9% / 18.8%runs 1 and 3
False font hits (re-fetched, hand-classified by script)1,269 of 1,657probe, ~10:20 AM ET
og:image hidden past 40,000 chars31 of 550probe, ~10:30 AM ET
Pages passing all 26 checks, old / fixed80 / 412runs 1 and 3
Spearman, score vs HN points-0.001run 3, n = 2,079
Terminal capture of the final run: 3,949 stories, 2,146 own-domain URLs, 2,079 graded, 67 failed fetches. Mean score 25.8, median 25, 412 pages with zero findings. Top rules: no-og-image 25.0 percent, script-spam 20.9, slop-font 18.8, no-h1 16.0. Spearman score vs points -0.001.
Real output of the grading and analysis scripts, Oct 17 2026, 10:34 to 10:36 AM ET.

Next up: the favicon fix (one extra request, only when the link tag is missing), then the same experiment on a different crowd. Product Hunt launches vote differently from Hacker News, and my bet is the og:image matters more there. If you have a better outcome to test scores against than upvotes (signups, bounce rate, anything), tell me what it is: I'd love a dataset where the score is supposed to predict something.

And if you want to know where your own page lands, run the free Page Teardown. Same engine as this article, with both fixes, and it answers in a second or two.

First published here on tensorust.com on Oct 17, 2026. Also on dev.to. Related: Free Page Teardown, Agent Distribution, How to review AI-written code.

Get the next one by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Read next

Free tool: iPhone app cost calculator → Pick an app type and features, get an honest budget range.

All articles →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ