1.35.0 — the app arrives on Windows and Linux
Added
-
A desktop app for Windows and Linux.
desktop/is a Tauri shell that
startsbin/seo-audit.mjs --serve 0, reads the port off stdout and points a
webview at it — exactly whatmac/SeoAudit/Engine.swifthas 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 byPATH,
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 runswinget upgradein place and offers to
relaunch; an apt install is shown the command, becauseaptneeds root and a
GUI app that shells out tosudohangs 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 hasfetch, 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
thesetup.exe, the.deband 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 upgradecan only move a copy winget
installed. Submitting one opens a pull request againstmicrosoft/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:allruns three suites, addingcargo testoverdesktop/.
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. -
--serveis 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-openis 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
urland
limitwhile the engine took a dozen parameters, so somebody at a browser
reached a sixth of what somebody at a terminal did — and/runforwarded 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
fieldnow 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.
/previewhas 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--servekept 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./reportslists 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--baselineuses — 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.mjsmust 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 pastedbrew upgrade --cask seo-auditinto 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. Nosudo: 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
binandsrcand notworker, which--serveimports, 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 removingworkeragain 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, butimport.meta.urlcomes out malformed and anything resolving
a sibling file fails.--versionreads../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 firesDestroyedand quitting firesExit; nothing fires when a
process is terminated outright, and a 110 MB engine then sat holding its port.
A job object withKILL_ON_JOB_CLOSEmoves that promise from this code to the
kernel. Linux passed the same test only because SIGTERM lets Tauri runExit
— 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_LOGnow 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, thellms.txtand 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 everyid:insrc/. 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, sobrew install --cask seo-audit@1.33.1was
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-auditHomebrew 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
# c85496f75e6b887be496107086da4d3debe22fa67d5e8389d1fc8a7a21158645Then, 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.txt4ec8cb441ed6dc66a8c336e42e4fcb2ef09ad321d42f83044991bdec83c26a35 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.