Skip to content

1.35.0 — the app arrives on Windows and Linux

Choose a tag to compare

@github-actions github-actions released this 29 Aug 09:46
· 30 commits to main since this release

Added

  • A desktop app for Windows and Linux. desktop/ is a Tauri shell that
    starts bin/seo-audit.mjs --serve 0, reads the port off stdout and points a
    webview at it — exactly what mac/SeoAudit/Engine.swift has always done. It
    draws nothing: the report is the HTML the server already produces, so both
    platforms get the same sidebar, score ring and library, from the same folder
    on a machine that has both. The Swift app stays the macOS release.

    It carries the lessons of the window next door. Port zero, because guessing is
    how two copies fight over one. Node found by path rather than by PATH,
    because a window launched from a dock inherits almost none. And a failure that
    opens a window saying why, because an app that opens and does nothing is the
    worst version of that.

    A menu of File, Edit and Help — Edit predefined, because on Linux and Windows
    a webview without one is a text field where Ctrl-C does nothing. Every other
    item is navigation or a link. External links leave the app: a report is
    full of the audited site's own URLs, and following one used to replace the
    report with somebody else's website, in a window with no address bar to get
    out of.

    Packaged with a Node inside it: a 23 MB setup.exe, a 45 MB .deb, a 115 MB
    AppImage. Each platform builds on its own runner, then installs what it
    built, runs it, and waits for the engine to appear
    — because building a
    bundle only proves it bundles, and four separate bugs survived to that step.

  • The desktop shell knows when there is a new version, and offers the one
    thing that is safe.
    The macOS app settled the principle — the thing that
    installed it is the thing that replaces it — and it generalises unevenly,
    because the platforms differ in what a program may do without a password it
    cannot ask for. A winget install runs winget upgrade in place and offers to
    relaunch; an apt install is shown the command, because apt needs root and a
    GUI app that shells out to sudo hangs on a prompt nobody can see; an
    AppImage, an installer copy or a hand-placed one gets the release page.

    It does not download a binary and put it in place itself. That needs a
    signature to check or a checksum to compare, and an updater that overwrites an
    application on the strength of a plain HTTPS response is a supply-chain hole
    with a progress bar on it.

    Once a day, on a thread, after the window is up, with Check for Updates…
    in the menu for asking anyway. It uses GitHub's API and falls back to the Atom
    feed, because the anonymous quota is sixty an hour per address and being
    refused is ordinary — and a version learned from the feed is announced with
    "whether it is a full release could not be checked" rather than asserted,
    which is the wording the macOS app already uses.

    Native dialogs, so the shell still owns no interface of its own. And no HTTP
    client crate: the Node it already ships has fetch, and adding a TLS stack so
    a 110 MB bundle can make one request a year is the larger thing.

  • Every report can be saved, from any browser.
    /reports/<id>/export?as=… returns the file with a name that sorts, in all
    seven formats. Links rather than a native dialog: a browser downloads them and
    the desktop shell inherits the lot, so the feature landed once and every
    platform got it. The macOS app has had an Export menu since it shipped and the
    browser had none — somebody on Linux could read a report and not save one.

    The format list moved to src/exports.mjs. It was written out twice already,
    and a third copy would have been where they started disagreeing about whether
    "Structured data" is called that.

  • A tagged release carries the Windows and Linux bundles too. Pushing a
    version tag already ran three workflows; this is the fourth, and it attaches
    the setup.exe, the .deb and the AppImage beside the macOS zip. It builds
    them the same way a branch push does — including installing what it built and
    waiting for the engine — so the file on a release page is one that was proven
    to start, not one that was proven to compile.

    It never creates the release, only waits for it. The macOS job writes the
    title from the tag's annotation and the notes from the CHANGELOG section, and
    a job racing it to create an empty one would win about half the time and take
    both away. Waiting is the whole of the coordination between them.

    A winget manifest goes with it, which is what makes the Windows half of
    the updater reachable at all: winget upgrade can only move a copy winget
    installed. Submitting one opens a pull request against microsoft/winget-pkgs
    and so needs a token this repository does not have — so the job says out loud
    that it was skipped, because a release that quietly did not publish looks
    exactly like one that did.

  • Two things that were written down twice are now checked. The version
    lives in four files, and the shell refuses to start when its version and the
    engine's disagree — so a half-finished bump would have produced bundles that
    cannot run, discovered after the tag was pushed. And the winget identifier
    lives in the workflow that publishes it and the Rust that queries it; winget
    answers a name it has never heard of with silence, so that drift would have
    shipped as a Windows build that simply never finds an update. Neither had a
    symptom worth debugging. Both now fail on the machine of whoever caused them.

  • npm run test:all runs three suites, adding cargo test over desktop/.
    Same rule as the others: a toolchain that is not there, or an engine not yet
    staged beside the shell, is reported as not run and never silently skipped.

  • --serve is the desktop UI for Linux and Windows. It always was, in the
    sense that the macOS window is a thin client over exactly this server — but
    it printed a URL and offered a form with two inputs, so nobody read it that
    way and nobody could have.

    It now opens a browser when a person ran the command, and never when
    something else did. The same distinction the pipe check already made: the
    macOS window spawns this and draws its own report, and would otherwise get a
    browser it never asked for on every launch. --no-open is for the person who
    wants neither. Three platforms, nine lines, no dependency — and failing to
    open one is not a reason to refuse to serve, so the URL is printed either way.

  • Every setting the command line takes is in the form. It offered url and
    limit while the engine took a dozen parameters, so somebody at a browser
    reached a sixth of what somebody at a terminal did — and /run forwarded only
    those two, which meant a control somebody set could quietly do nothing.

    The controls are drawn from src/options.mjs, the one table that already
    knows every flag and whether a window can reach it. Adding a flag with a
    field now adds the control; a flag without one is simply not offered, which
    is a decision written down beside the flag it is about. PageSpeed and Search
    Console are drawn only where the deployment has said those credentials are
    the visitor's own to spend, so a public Worker still offers neither.

  • A Preview button beside Audit. /preview has answered clients since it
    shipped and no page ever reached it. A preview nobody can reach is a preview
    nobody uses, and the whole point of one is being reachable before the minutes
    are spent.

  • Finished runs are kept, and the browser can compare two. The macOS window
    has kept every run since 1.23.0 and --serve kept none, so somebody on Linux
    or Windows got one report and lost it the moment they audited something else.
    A seven-minute crawl should only ever happen once, and that is not a
    macOS-only claim.

    /reports lists them, /reports/<id> reopens one as the same report every
    other front end draws, and ticking two runs compares them through the same
    diff() the CLI's --baseline uses — including the cross-site path matching,
    so a rebuild can be compared with the site it replaces.

    On macOS it writes the folder the app already uses, so a crawl started in
    the window is in the browser's list a second later and the other way round:
    nothing is synchronised, exported or copied, because there is one folder.
    Elsewhere it is %APPDATA% and $XDG_DATA_HOME, which is where those systems
    keep documents an application manages.

    A deployed Worker has no library at all. worker/index.mjs must stay
    web-standard — Cloudflare has no filesystem — so the local server hands it a
    store and a Worker is handed nothing, which is also the right answer for a
    shared host: keeping strangers' crawls is a thing nobody asked for.

  • The app updates itself, rather than explaining how to. Pressing Update
    opened Terminal and pasted brew upgrade --cask seo-audit into it, which is a
    tool telling somebody how to update it. It now runs Homebrew in the window,
    streams what Homebrew says line by line, and offers Relaunch when it is
    done.

    Homebrew still does the part that matters — it verifies the download against
    the checksum the build wrote, and it keeps its own records straight, which is
    exactly why this app must not replace its own bundle behind its back. What
    changed is where it runs. No sudo: the cask installs into /Applications
    and clears the quarantine flag without one, so nothing can sit waiting for a
    password nobody can type. Terminal and Copy are still there for anyone who
    would rather watch it happen.

    Verified by downgrading a real install to 1.33.1 and pressing the button.

Fixed

  • The first desktop bundle opened, started nothing, and waited. It staged
    bin and src and not worker, which --serve imports, so it spawned its
    engine and waited fifteen seconds for an address that was never coming. The
    staging script now starts what it staged and waits for it to announce a
    port
    — a list of directories is exactly the kind of thing that looks right
    and is not. Verified by removing worker again and watching it fail.

  • The Windows build installed, started, and refused its own engine.
    resource_dir() returns a Windows verbatim path — the \\?\ form that
    lifts the 260-character limit. Rust is happy with it and Node is not: the
    script runs, but import.meta.url comes out malformed and anything resolving
    a sibling file fails. --version reads ../package.json, so it answered
    nothing, and the shell concluded its engine was broken. On that evidence it
    was right to.

  • The engine outlived the app when the app was killed rather than closed.
    Closing fires Destroyed and quitting fires Exit; nothing fires when a
    process is terminated outright, and a 110 MB engine then sat holding its port.
    A job object with KILL_ON_JOB_CLOSE moves that promise from this code to the
    kernel. Linux passed the same test only because SIGTERM lets Tauri run Exit
    — the guarantee was missing on both and visible on one.

  • A windowed app had nowhere to say what went wrong. Built with
    windows_subsystem = "windows" it has no console and no stdout, so a failure
    is a sentence in a window and nothing else — fine for a person, useless for
    the job that just installed it. SEO_AUDIT_SHELL_LOG now names a file the
    startup path narrates itself into, and both Windows bugs above were found by
    reading it.

  • A run started from the browser could not save three of its seven formats.
    The sitemap, the llms.txt and the structured data are built during the crawl
    from data that is gone by the time the report arrives, and the served form
    never asked for them — so those links were permanently refused for every Linux
    and Windows user, while the macOS window has always asked. Found by clicking
    them.

  • The category guard read the export list as seven checks needing categories,
    because it matched every id: in src/. It now asks
    scripts/check-levels.mjs, which only counts an id carrying a level — a
    scanner that can tell a finding from a file format.

  • The update banner never appeared on a machine whose GitHub quota was
    spent.
    The anonymous API allows sixty calls an hour per address, shared
    with every other tool on the machine, so exhausting it is ordinary — this one
    was at 0 of 60. The app falls back to the Atom feed there, and a feed release
    is never allowed to announce an update, because the feed cannot tell a
    prerelease from a release.

    So it said nothing at all. No banner reads exactly like "you are up to date",
    which is the failure this project's entire report format exists to avoid,
    applied to itself. A newer version found only in the feed is now announced as
    what it is: "Version 1.34.0 appeared — GitHub's API was out of quota, so
    whether this is a full release could not be checked."
    The person decides.

  • Downgrade offered a command that has never worked. There is one cask and
    it tracks the latest version, so brew install --cask seo-audit@1.33.1 was
    offered for older releases and answered "Error: No casks found". Found by
    pressing it. Homebrew can move forward and not back, so an older release now
    offers its zip, which works for any version.

  • A perfect score read as "0 points across 0 checks", which is arithmetic
    rather than a sentence. It now says nothing took points off.


Installing on macOS

brew tap nurkamol/seo-audit https://github.com/nurkamol/seo-audit
brew trust nurkamol/seo-audit
brew install --cask seo-audit

Homebrew verifies the download against the checksum below and clears the
quarantine flag for you, so the app opens the first time.

Downloading the zip instead? macOS may say "SEO Audit is damaged and
can't be opened."
It is not damaged. The app is ad-hoc signed rather than
notarised — notarising needs a paid Apple Developer account this project
does not have — and macOS refuses quarantined apps without a notarisation
ticket. Check what you downloaded first:

shasum -a 256 seo-audit-1.35.0-macos.zip
# c85496f75e6b887be496107086da4d3debe22fa67d5e8389d1fc8a7a21158645

Then, once it matches, clear the flag — no sudo needed:

xattr -dr com.apple.quarantine "/Applications/SEO Audit.app"

That is what right-click → Open does in the Finder, without the dialog.
Apple Silicon, macOS Tahoe or newer.

Windows and Linux

The setup.exe, .deb and .AppImage below are attached by a separate
job and may appear a few minutes after this release does.

The Windows installer is unsigned, so SmartScreen shows "Windows protected
your PC"
More info → Run anyway. Same reason as macOS: a code-signing
certificate is a paid, per-year thing this project does not have. The
.AppImage needs chmod +x before it will run.

Checksums

Verify a download before you wave a warning away. With SHA256SUMS.txt
saved beside the file, in the same folder:

shasum -a 256 --ignore-missing -c SHA256SUMS.txt
4ec8cb441ed6dc66a8c336e42e4fcb2ef09ad321d42f83044991bdec83c26a35  SEO.Audit_1.35.0_amd64.AppImage
b20e134e54bea173a7490cd2360e331322c4456defaf22e46d8caa282884be6c  SEO.Audit_1.35.0_amd64.deb
2a28326e4cae06b87cbabad3e06b5d8b2f0dbe31ed7054d21965798cb40a96d9  SEO.Audit_1.35.0_x64-setup.exe
c85496f75e6b887be496107086da4d3debe22fa67d5e8389d1fc8a7a21158645  seo-audit-1.35.0-macos.zip

The macOS line appears twice in these notes on purpose: once above, computed
when the app was built, and once here, computed from what is actually attached.
They should agree.