Skip to content

feat(voice): offer Windows downloads - #155

Merged
wavekat-eason merged 15 commits into
mainfrom
feat/windows-downloads
Aug 16, 2026
Merged

feat(voice): offer Windows downloads#155
wavekat-eason merged 15 commits into
mainfrom
feat/windows-downloads

Conversation

@wavekat-eason

@wavekat-eason wavekat-eason commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

wavekat.com has never offered a Windows visitor the installer we build on
every release — it hands them a .dmg. wavekat-voice docs/54 calls that
the one item on its list that is pure loss rather than an investment call.
This is its Phase 1a and Phase 4 together: offer the build, and say we
offer it.

Spec: docs/05-windows-downloads.md.

Merge last. The buttons point at the platform's resolver and the build
reads /releases/latest, so this needs wavekat-platform#256 deployed
first — otherwise the build falls back to constants and renders Windows
buttons that 404. wavekat-voice#318 (the synced docs) goes in between.

One button per platform, x64 for everyone

Windows is two files, but they are not symmetric: an x64 installer runs on
every Windows PC we support — natively on Intel and AMD, emulated on
Windows 11 for ARM — while an arm64 installer runs on almost none of them.
Only one of the two mistakes is fatal, and it isn't the one x64 makes. So
Windows gets the same plain pill as Mac and Linux, wired to windows-x64,
and the words "Intel or AMD" appear nowhere on the page.

The ARM build lives in "other platforms", as the one row that stays listed
when Windows is the active platform — a Windows visitor has already been
handed x64, so it's the only choice their primary can't make for them.
Detection promotes the platform, never the chip: navigator.userAgentData
is the only thing that knows the chip, and it's Chromium-only and
asynchronous.

Windows rows carry an Early tag, and the line saying the builds aren't
code-signed appears twice on purpose — in the menu for someone choosing
Windows from a Mac, and under the button for the visitor the promotion
script just handed a Windows download.

The install walkthrough is ours now

That line used to point at /docs/voice/installation/, which is synced
from the private repo and English-only — a Chinese reader clicking
"如何继续安装" landed in English prose. /voice/download/ is already
translated into nine locales, so it gains an #windows section instead:
the SmartScreen box in one self-contained paragraph, three steps past it,
and which build to pick if your PC is ARM. Step 1 of "Getting set up" now
names the .exe next to the .dmg and the .deb.

A clipping bug the taller menu exposed

The disclosure panel overflows whatever contains it, and two containers
stopped it: the homepage hero and the /voice/ download card both used
overflow-hidden to keep a blurred glow in bounds, cutting the open menu
in half. The clipping moves onto the decoration itself. On the homepage
there was a second half — the band below reveals on scroll, and that
transform makes it a stacking context painting after the hero, so it
covered the menu even unclipped; the hero takes z-10. Linux was clipped
the same way. Windows just made the menu tall enough to notice.

Two details worth review

A null from /releases/latest is a real answer, not a gap to fill from
the fallback table: no release has published an arm64 installer yet, so
that row ships hidden and the browser-side refresh reveals it the day one
does, with no rebuild. Falling back to a constant there would render a
choice that 404s.

Each toggleable element names its display in data-dl-show, and every
"other platforms" row is wrapped — so promoting a platform toggles the
wrapper while a row's own hidden state stays about whether that file
exists. Two independent reasons to hide on one element was the bug waiting
to happen.

Promotion is a named list, not a sweep

The site holds ~325 "Mac & Linux" strings, most of them SEO titles tuned
to "… for Mac" demand queries where a third platform name spends
characters against a 50-char budget and dilutes an exact-match query. Only
the statements that deny Windows change: the platforms FAQ and
description on /voice/, the comparison table's platform row, the
alternatives intro's "with Windows on the way", About, Privacy, and the
five posts closing with "Windows is coming when there's demand".

"Will you support Windows?" becomes "Does WaveKat Voice run on Windows?" —
what someone actually types — and feeds FAQPage schema from the same
array the page renders.

What the SEO review caught

public/llms.txt is the site's own AI-answer file, separate from the
automation contract synced from wavekat-voice, and still described a
softphone "for Mac and Linux" with a download line listing only .dmg and
.deb. /voice/download/'s description read "for Mac or Linux" in nine
languages — a snippet telling a Windows searcher to leave the page that
now offers them the build.

Not done here

/voice/'s title still spends its budget on the brand and carries no
demand term, and "for Windows" is a modifier we rank for nowhere. Retitling
the flagship page is a deliberate decision, not a side effect of this PR.

Testing

npm run build && npm run check:links && npm run check:meta, built
against the upstream docs branch via SYNC_DOCS_REF_VOICE. check:meta
took two rounds: a third platform name pushed the two Chinese /voice/
descriptions to 90 and 92 against the 85-char CJK budget, and the Korean
alternatives one to 87 — all rewritten rather than extended. The whole
control was also built against a mock of the new API shape to confirm both
Windows rows render with distinct sizes before the platform ships.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi

wavekat-eason and others added 5 commits August 15, 2026 21:14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
wavekat.com has never offered a Windows visitor the installer we build on
every release. It hands them a .dmg. wavekat-voice docs/54 names that the
one item on its list that is pure loss rather than an investment call, and
this is its Phase 1a.

Windows is two files — x64 and arm64 — and which chip a visitor has is not
in a user-agent string. `navigator.userAgentData` knows, but only on
Chromium and only asynchronously, so it would buy an informed guess for
some machines and a blind default for the rest. So the Mac and Linux
buttons still download in one click, and the Windows primary opens a
two-row menu instead. That asymmetry is the design: the alternative is
handing an x64 desktop a file Windows refuses to run.

The rows carry an Early tag and the panel says plainly that the Windows
builds aren't code-signed yet, linking to installation.md's SmartScreen
walkthrough. Signing, PR CI on Windows, and the clean-VM smoke test are
docs/54's later phases; this ships distribution, not a claim that Windows
is as proven as macOS.

Two details worth keeping:

A null from /releases/latest is a real answer, not a gap to fill from the
fallback table. No release has published an arm64 installer yet, so that
row ships hidden and the browser-side refresh reveals it the day one does
— without a rebuild. Falling back to a constant there would render a
choice that 404s.

Each toggleable element names its display in `data-dl-show`, and the alt
menu wraps each platform GROUP, so promoting a platform toggles the
wrapper while a row's own hidden state stays about whether that file
exists. Two independent reasons to hide, on one element, was the bug
waiting to happen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
The site told a Windows visitor, in nine languages, that the product does
not run on their computer — while the release pipeline built them an
installer. #251 stripped Windows from the platform claims deliberately;
this puts it back now that the download page offers the build.

Scope is a named list, not a sweep. The site holds ~325 "Mac & Linux"
strings and most are SEO titles tuned to "… for Mac" demand queries, where
a third platform name spends characters against a 50-char budget and
dilutes an exact-match query. So only the statements that DENY Windows
change: the platforms FAQ and meta description on /voice/, the comparison
table's platform row, the alternatives intro's "with Windows on the way",
About, Privacy, and the five posts that close with "Windows is coming when
there's demand". The last of those matter most — a "which platforms?"
answer is the passage an AI answer lifts verbatim.

"Will you support Windows?" becomes "Does WaveKat Voice run on Windows?",
which is what someone actually types, and it feeds FAQPage schema from the
same array the page renders. It answers yes, names both architectures, and
says the build is unsigned — a fair answer beats a flattering one on a
page whose job is to be quoted.

The two Chinese descriptions were rewritten rather than extended: a third
platform pushed them to 90 and 92 against the 85-char CJK budget that
check:meta enforces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
The SEO/GEO review CLAUDE.md requires for edited pages found three
surfaces the copy pass missed, all of them the ones a searcher or an
answer engine actually reads:

/voice/download/'s description read "Download WaveKat Voice for Mac or
Linux" in nine languages — a SERP snippet naming the platforms and
omitting the one the page now offers. It also had room: at 107 characters
it was under the 110 floor.

/llms.txt is the site's own file in public/, not the automation contract
synced from wavekat-voice, and it still described a softphone "for Mac and
Linux" with a download line listing only .dmg and .deb. That is the file
ChatGPT and Perplexity read.

The alternatives descriptions and the docs index carried the same claim.

Every description was re-measured rather than extended: the Korean one
landed at 87 against the 85-char CJK budget and was shortened instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
Three surfaces the copy pass missed and the review caught (the download
and alternatives descriptions, public/llms.txt, the blog posts' platform
closer), and the no_arch/no_installer split, which was a decision made
while implementing rather than one the spec anticipated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
@github-actions

Copy link
Copy Markdown
Contributor

wavekat-eason and others added 7 commits August 15, 2026 22:55
The disclosure panel is taller than the gap below the button, so it
overflows whatever contains it. Two containers stopped it: the homepage
hero and the /voice/ download card both used `overflow-hidden` to keep a
blurred glow in bounds, cutting the open menu in half. The clipping moves
onto the decoration itself, leaving the section free to overflow.

The homepage needed a second half: the band below it reveals on scroll,
and that transform makes it a stacking context painting after the hero, so
it covered the menu even unclipped. The hero takes `z-10` — the header is
`z-40`, so nothing changes there.

Neither is new. Linux was clipped the same way; Windows just made the menu
tall enough to notice.
The unsigned-build note under the Windows rows pointed at
/docs/voice/installation/, which is synced from the private repo and
English-only — so a Chinese reader clicking "如何继续安装" landed in English
prose.

/voice/download/ is already translated into all nine locales, so it gains
an #windows section instead: the SmartScreen box explained in one
self-contained paragraph, three steps past it, and which of the two
installers to pick. Step 1 of "Getting set up" now names the .exe next to
the .dmg and the .deb. The href is built with localizedPath(), so each
locale's menu points at its own page.

Also two menu details the taller Windows panel exposed: the panel forces
`text-left` (the CTA card centres its text) and the Early tag no longer
wraps to two lines beside a wrapped label.
Asking "Intel or AMD?" treated the two installers as symmetric. They
aren't: an x64 build runs on every Windows PC we support — natively on
Intel and AMD, emulated on Windows 11 for ARM — while an arm64 build runs
on almost none of them. Only one of the two mistakes is fatal, and it
isn't the one x64 makes.

So Windows gets the same plain pill as Mac and Linux, wired to
windows-x64, and the words "Intel or AMD" leave the page. The ARM build
moves into "other platforms" as the one row that stays listed when
Windows is the active platform — it's the only choice the primary can't
make for a visitor. `dlWindowsX64` is gone; the row uses `dlWindows`.

The unsigned-build note now also sits under the button, because the
visitor being handed a Windows download is the one about to meet
SmartScreen. /voice/download/#windows says the same thing about arch that
the menu now implies, in all nine locales.
0.0.46 is the first release to publish a native Windows-on-ARM
installer, so the menu row that shipped hidden is now live. Verified
against the real feed rather than a mock: all four targets 302 to
distinct files that HEAD 200 at the advertised size, and every control
on the page — primary and each menu row, in three locales — resolves to
the target it names.

The build-time fallback can hold windows-arm64 now that one exists; it
was held out because a constant for a never-published target renders a
button that 404s.
Three things in the "other platforms" menu contradicted each other, and
each was visible in the same screenshot.

The unsigned-build note covers BOTH Windows installers but hung off the
x64 row, so it rendered between the two Windows entries — reading as a
caveat about x64 alone and cutting the ARM row loose from the group
above it. It caps the group at the foot of the panel now, on the same
show/hide rule as before: hidden exactly when a Windows visitor is
already reading the same line under their button.

The ARM row said "Windows on ARM" while the two rows above it said
"Download for …". Every row in the menu is a download, so every row is
worded like one; the requirement line under each label is what says
which machine it is for. Nine locales take their own download verb, and
each /voice/download/ page quotes the label verbatim in the Windows
walkthrough, so those nine sentences move with it.

Those requirement lines named Surface as the ARM cue, but most Surface
models are Intel or AMD — the one mistake that hands someone an
installer their PC refuses to run. Both Windows rows now name the chip
family instead, in parallel: Intel & AMD (x64), Snapdragon (ARM64).

The panel needed `w-max` to hold any of it. It is absolutely positioned
inside the `relative` <details>, so its containing block is only as wide
as the summary; shrink-to-fit capped it there and long rows wrapped,
which is what the fixed `min-w` was really propping open. A fixed
minimum can't hold nine locales. The note carries its own `max-w` in
return — a paragraph's max-content width is the whole sentence on one
line, and it would otherwise set the width of the entire menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
The unsigned-build note lives inside the download control and only
appears once the promotion script picks Windows, so the control is one
pill tall for a Mac or Linux visitor and about 46px taller for a Windows
one. Every row holding it centred its items, which meant that on Windows
the buttons beside it dropped by half the note's height — the homepage's
"View on GitHub" sat visibly below the download button it is supposed to
sit next to.

The two shapes need different alignment. In the homepage hero the
sibling is a matching pill, so `items-start` is equivalent to centring
when the note is hidden and correct when it is not. On the alternatives
and provider pages the sibling is a small text link, which `items-start`
would slam to the top of the pill; `items-baseline` puts it on the
button's text line instead, landing within half a pixel of where
centring puts it today and staying there whether or not the note shows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
A `git add -A` swept `.claude/worktrees/` into the previous commit as a
gitlink. It is a local session worktree — its own git repository, empty
to anyone who clones this one — so it is untracked and ignored now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
@wavekat-eason
wavekat-eason marked this pull request as ready for review August 16, 2026 00:35
wavekat-eason and others added 3 commits August 16, 2026 12:56
Windows on ARM64 reports "Windows NT 10.0; Win64; x64" in its
user-agent string on purpose, so x64 software keeps working. The
promotion script read only the UA, which meant an ARM machine could
never be told apart from an Intel one and always landed on the
emulated x64 installer, with the native build two clicks away under
"other platforms".

The chip is only exposed through
navigator.userAgentData.getHighEntropyValues(), which is Chromium-only
and async — the two reasons docs/05 originally ruled it out. So it runs
as a second pass rather than in place of the first: the platform swap
stays synchronous and x64 still goes on the button for everything that
cannot answer, and a machine that answers arm/64 is upgraded a few
milliseconds later. Being wrong in that direction costs nothing, since
Windows 11 for ARM emulates x64.

Supporting changes:

  * altEntries collapses into one `entries` list keyed on (platform,
    archKey). The primary and the menu are exact opposites by
    construction, so they cannot drift into offering a file twice or
    not at all — an ARM visitor now sees x64 in the menu for the same
    reason an Intel one sees ARM.
  * Primaries carry data-dl-avail, and resolve() reverts to the arch
    every machine can run when the one it wants is unpublished. The
    release refresh updates that flag and re-runs the promotion, so a
    target appearing or vanishing between deploys moves the button and
    not just the menu row.
  * resolve() is now idempotent and re-entrant, which is what makes the
    second pass a small addition instead of a second code path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
Two statements survived the Windows promotion sweep and now contradict
the ones next to them.

`click-to-call-phone-links` had its platforms FAQ rewritten to "Mac,
Windows and Linux, the three platforms" but kept the following clause —
"and it works on both" — in all nine locales. One sentence, two counts.
The other four posts in that sweep say "all three"; this one was missed.

`place-calls-from-the-command-line` opens with "It's built into the app
today on Mac and Linux" while its own FAQ, three screens down, answers
"Mac, Windows and Linux". The lead is the passage an answer engine
lifts, so it was the wrong half to leave stale. Upstream
`docs/voice/installation.md` calls Windows a supported platform and the
automation doc limits the CLI to nothing, so "all three" is the true
claim.

Also drops a JSDoc line describing a design that didn't ship: `dlWindows`
labels a button that downloads x64, not one that opens a choice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
Three gaps the review found, all of them the same shape: a rule this
branch established and then applied incompletely.

`678d7b2` re-aligned every row holding <VoiceDownload /> because the
control grows ~46px taller once the unsigned-build note appears on
Windows — but it missed `VoiceOverview.astro`, which is the `/voice/`
hero in all nine locales. The sibling there is a small text link, the
same shape as the alternatives and provider pages, so it takes
`items-baseline` for the same reason those did.

The promotion script only consulted `data-dl-avail` for a non-default
arch, so the rule the file's own comment states — "a row for a file the
platform says is missing would 404, so the same rule runs in reverse" —
held for menu rows and not for primary buttons. A release that published
`windows-arm64` but not `windows-x64` left an Intel visitor a visible
"Download for Windows" button resolving to a 404, and `mac` was worse
still: it is the SSR default, so it rendered shown regardless. The arch
is now the first published candidate of `[activeArch, 'default']`, or
none — no button beats a broken one, and the menu is still there.

Exercised by running the shipped script over the built markup: Mac,
Linux, Windows, Windows-on-ARM, Android and iPhone are unchanged in the
live all-four-published state, and each degraded combination now hides
the primary instead of pointing it at a missing file.

CLAUDE.md still told the next author that Voice is "Mac and Linux today;
Windows when there's demand", which would reintroduce the copy this
branch just removed, and `.gitignore` swallowed all of `.claude/` when
only the session worktrees and settings.local.json are local state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi
@wavekat-eason
wavekat-eason merged commit c0ff5a7 into main Aug 16, 2026
2 checks passed
@wavekat-eason
wavekat-eason deleted the feat/windows-downloads branch August 16, 2026 01:47
wavekat-eason pushed a commit that referenced this pull request Aug 16, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.0.56](wavekat-com-v0.0.55...wavekat-com-v0.0.56)
(2026-08-16)


### Features

* **voice:** offer Windows downloads
([#155](#155))
([c0ff5a7](c0ff5a7))


### Bug Fixes

* **deps:** clear Dependabot alerts, Astro 7
([#156](#156))
([fb51a03](fb51a03))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
wavekat-eason added a commit that referenced this pull request Aug 19, 2026
## What was wrong

Two page families claimed WaveKat Voice runs on two platforms while the
comparison table on the same page said three. Windows shipped in #155;
`/voice/alternatives/linphone/` and `/voice/providers/2talk/` were never
updated, in any of the nine locales.

The 2talk page was worse than stale — it told the reader Windows was
hypothetical, then sent Windows visitors elsewhere:

```
'Mac and Linux today (Windows when there is demand).'
'Mac 和 Linux(有需求时支持 Windows)。'
"You're on Windows and want a free, lightweight client today — that's MicroSIP"
```

Separately, **call flows** and the **HubSpot integration** — two of the
clearest differences against a plain SIP client — appeared on neither
page.

Five of the nine alternatives titles also led with the brand (`WaveKat
Voice — a Linphone alternative`, `WaveKat Voice — alternativa a Linphone
(Mac/Linux)`), spending budget on a term that already ranks #1.

## What changed

Both pages, all nine locales:

| | Before | After |
|---|---|---|
| Platform claim | title, description, H1, intro all "Mac & Linux" | all
three, everywhere, with the honest caveat that the Windows build is
newer and unsigned |
| Titles | brand-first, or 2 platforms | demand-first, 3 platforms
(`Linphone alternative for Mac, Windows & Linux`, `2talk
软电话(Mac/Win/Linux)`) |
| Comparison rows | 8 / 7 | 10 / 9 — added answering & voicemail, and
CRM logging |
| FAQs | 3 / 6 | 7 / 9 |
| Breadcrumb node 1 | `Voice` | `WaveKat Voice` + `inLanguage` |

- **Call flows / voicemail** are stated with the mechanism, not as a
bullet: greeting, opening hours, menu, voicemail or transfer; the
message is recorded *and transcribed*; you can pick up mid-message. The
FAQ names the real limits — menus, hours and transfers are Pro (free for
a year in early access), and the flow runs on your own computer, so if
the app isn't running the provider's voicemail takes the call.
- **HubSpot**: connect once, every call files itself on the
number-matched contact with time, direction, outcome, duration,
transcript, and a recording playable inside HubSpot. Pro, free during
early access, webhooks for other CRMs.
- **Fair to the competition**: Linphone, Bria and MicroSIP each get "no
answering of its own — voicemail is your provider's mailbox, which it
can dial", and MicroSIP keeps its genuine strength ("the smallest
possible Windows client") without implying we don't run there.
- **Every localized title and description re-measured** against its own
budget (CJK ≤ 28/85, Latin ≤ 50/160) instead of translated from English.
`check:meta` caught four CJK descriptions over budget mid-review —
they're now 80–85.
- Localized "where to go next" blocks (the pages previously linked out
only to the hub) and each language's conventional FAQ heading.

## Verification

```
npm run build && npm run check:links && npm run check:meta
✓ links OK — 241 pages scanned, no broken or leaking internal links.
✓ meta OK — titles and descriptions within budget
```

Checked against `dist/`: all 18 pages carry 7/9 FAQs, 10/9 comparison
rows, the corrected breadcrumb entity, and both new topics. The only
surviving `Mac and Linux` strings on these pages are inside the new
Windows FAQ answers, where they're correct.

## Deliberately not in this PR

- **The Bria price** — `NZ$5.95 + GST / month (as of July 2026)` appears
in the 2talk table and two FAQs in all nine locales. The dataset comment
says re-check before quoting; I couldn't verify it, so I left the figure
and its "as of" stamp rather than restamping it and manufacturing false
freshness. Worth a two-minute check.
- **Seven blog posts** still open with "WaveKat Voice — the SIP
softphone **for Mac and Linux** …" in all nine locales, two of them in
the meta description. Same defect class, ~60 strings; these are dated
announcements, so how to phrase it is a separate call.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01P1W6SyAGqXSQUbDN4fHfUi

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant