Skip to content

1.32.0 — five things a real site found

Choose a tag to compare

@github-actions github-actions released this 24 Aug 16:14
· 67 commits to main since this release

Added

  • twitter-image-broken — a declared twitter:image that does not load.
    The absence of a Twitter card stays unreported and always will: X falls back
    to Open Graph correctly, so reporting it would invent a defect. This is the
    opposite case, and the roadmap's rejection of the first did not cover it —
    nothing falls back to anything when the tag is present and 404s, and the one
    platform handed its own tag previews blank.

    Only when it differs from og:image, so one picture never produces two
    findings, and only on 404/410/no-answer, because a 403 is hotlink protection
    working. All four silent cases are tested: same picture, no tag at all, a
    403, and a redirect to the real file.

  • body-not-html — a URL the server calls HTML that is not HTML. The body
    is already read, so it costs no request, and it replaces the checks it
    silences rather than joining them. A real site serves an XML document at
    /locations.kml with Content-Type: text/html; the crawl believed the header
    and reported thirteen things — no title, no h1, no viewport, no charset, thin
    content, three Open Graph tags — every one of them true about a document that
    was never a page, and not one of them the thing to fix. Now it is one finding
    saying what is actually wrong, and the reason it matters: Google indexes
    whatever comes back under text/html.

    It answers on positive evidence only. XHTML opens with an XML prologue and is
    HTML; a fragment with no <html> wrapper is HTML; a byte-order mark before
    the doctype is HTML; something merely starting with a brace is not JSON. All
    four are tested, because guessing wrong here would silence every check on a
    real page — a far worse failure than the noise it removes.

Fixed

  • A dead site with an expired certificate is told what is actually wrong.
    expired.badssl.com came back as "The site did not answer a single request …
    The TLS connection succeeds but no response arrives … Cloudflare Bot Fight
    Mode does exactly this."
    Both halves were wrong: the TLS connection does not
    succeed, and the cause is a certificate that ran out in 2015. The site checks
    that would have named it never run, because the crawl gives up first — and a
    browser refuses an expired certificate exactly as fetch does, so from the
    crawl's side "nothing answered" and "the certificate lapsed" are the same
    silence.

    The certificate is now read before bot protection is blamed, over the same
    non-validating socket the check already used, and the report says
    "The TLS certificate expired 4151 day(s) ago … which is why nothing here
    could be fetched."
    Found by asking whether the Raycast extension really gets
    the certificate checks: it does, and this hole was underneath the question.

    Where the certificate cannot be read at all — the hosted Worker has no socket
    — the old wording stands unchanged, minus the sentence claiming the
    certificate is fine. A runtime that cannot run a check says so rather than
    implying a result.

  • One row named one Open Graph tag and counted three. A run against a real
    site printed Missing og:description ×6 over four pages — three of which were
    missing only the description, one missing all three tags — and listed that one
    page three times. Every finding under it was true. The sentence above them was
    not, and a summary that misreports is the thing grouping exists to prevent.

    The cause: og:title, og:description and og:image were pushed under one
    id, and a group takes its title from the finding it saw first. They are now
    og-title-missing, og-description-missing and og-image-missing — three
    ids, because they are three different repairs. A missing og:image is a
    picture somebody has to make; a missing og:title is one line of template.
    Splitting them also makes each separately ignorable, so a site that ships no
    og:image on purpose can silence that without silencing the other two.

    --ignore og-missing keeps working and still silences all three. Splitting a
    check is our decision, not the decision of whoever wrote that config, and an
    upgrade that promises to be compatible should not start failing their build.

    Not generalised, and the reason is worth writing down: four other ids also
    carry more than one title — img-alt-long, img-dimensions, heading-skip,
    anchor-ambiguous — and all four are correct. They are the same problem with
    the page's own numbers in the title. A test asserting one id, one title would
    have flagged the four right ones and this was the only wrong one.

  • A Raycast preference could do nothing and nobody would know. The
    extension's hand-written Preferences type listed three of the thirteen
    preferences crawlOptions() actually reads; the other ten were invisible to
    every caller. It is now ExtensionPreferences, which the Raycast build
    generates from the manifest, so it cannot disagree with what the manifest
    says — and the two components that read preferences had been calling
    getPreferenceValues() untyped, which is what the change surfaced
    immediately. Arguments.Audit replaces a second hand-copied shape.

    Types only cover half of it: present.mjs is plain ESM so node --test can
    run it, so a test now checks in both directions that every preference the
    manifest declares is read and that nothing reads a preference the manifest
    does not declare. A setting that quietly does nothing is the same failure
    src/options.mjs exists to prevent for the macOS window.

    Found by Greptile on the Store submission, which is worth recording: it
    called the type duplication a maintainability nit, and the count made it a
    defect.

  • A release no longer starts by failing. The macOS job builds on a tag and
    attaches the app to that tag's release — a release the documented procedure
    created afterwards, by hand. So every release began the same way: a red
    release not found, a gh release create, a re-run. A failure that is part
    of the normal procedure is a failure nobody reads, and the next one, which is
    real, reads exactly the same.

    It now creates the release when it is missing, titled from the tag's own
    annotation with that version's CHANGELOG section as the notes — both already
    written by the time a tag is pushed, so neither becomes a second copy to keep
    in step. A tag with no CHANGELOG section gets a warning and generated notes
    rather than a lost build, because by then the app is already sitting there
    signed.

    The section is found with index() rather than a regex, which is the bug that
    was there to be written: ## [1.31.0] interpolated into a pattern makes
    [1.31.0] a character class matching the digits 0, 1 and 3. It would
    not have errored. It would have published the wrong section, or none.

Changed

  • npx @nurkamol/seo-audit is the install the site and README lead with.
    The repository form still works and is still documented, pinned — but it
    clones 16 MB of app sources, screenshots and tests to reach a 115 kB crawler,
    measured rather than guessed. npm i -g is written down too, for anyone who
    would rather type seo-audit.