Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions docs/SITE-IMPROVEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# polyemesis website — measured findings and recommended changes

Assembled 2026-08-13 from four independent reviews (information architecture,
copy, layout, and a comparable-product pattern survey), plus direct measurement
of the live site and the built output.

**Every claim here carries a number that was measured.** Where a review asserted
something that measurement contradicted, the measurement wins and the discrepancy
is written down — there are three such cases below, and they are the most useful
entries in the document.

---

## 0. Already fixed — do not re-report

These were found during the review and are merged or in review. Listed so the
next pass does not spend effort on them again.

| Fix | Was | Now |
|---|---|---|
| Whole page scrolled sideways on a phone | `/download` 630px, `/comparison` 570px against a 375px viewport | 375px, both |
| Comparison row label scrolled away from its Yes/No cells | first column `position: static` | pinned, opaque `--color-ink` |
| Nav marked no page as current, on every page | `Astro.url.pathname` is `/features.html`, href is `/features` | normalised; both navs mark, home marks nothing |
| Footer column labels polluted every heading outline | `<h2>` | `<p>` |
| No version anywhere on the site | — | footer, read from `git describe` at build time |
| Site never said who it is *not* for | — | "Is this for you?" with a not-for column |
| Screenshots a week stale, 18 UI commits behind | 2026-08-06 | re-shot; `docs/media` 4013K→1587K, site 1819K→707K |
| `og:image` had no dimensions or alt | — | added, stating the real 2560×1280 |

Three of these are guarded in `web/scripts/check-build.mjs` and each guard was
mutation-tested. The nav guard asserts **two** marked anchors per page rather
than one, because one would have passed the half-fix that left the mobile menu
blank.

---

## 1. Confirmed defects, not yet fixed

### 1.1 `/features` duplicates the entire comparison table — High, S
**Measured:** 9 of 9 rows are *character-identical* to the primary table on
`/comparison`. Not a summary, not an excerpt — the same table.

The cost is not bytes, it is authority. A visitor who reads it on `/features`
and meets it again on `/comparison` learns the second page has nothing new,
which is the wrong lesson about the page whose whole job is the comparison.

**Fix:** on `/features`, replace it with a three-line callout — the single
strongest row plus a link to `/comparison`. Keep the full table on the page
named for it.

### 1.2 `/docs` is a link farm off the site — Medium, M
**Measured:** 48 links, 25 to github.com, **17 of them straight to raw `.md`
files** in the repo.

Every one of those is an exit. The visitor lands on GitHub's markdown renderer —
no nav, no search, no way back, and none of the site's typography. For a project
whose adoption depends on someone successfully installing it, the documentation
funnel leaks at the exact moment of highest intent.

**Fix:** render the quickstart and configuration pages on-site from the same
markdown, via an Astro content collection. Reserve off-site links for reference
material that genuinely lives in the repo (`CHANGELOG`, `LICENSE`, issues).

### 1.3 Claims on `/` and `/comparison` carry no source — Medium, M
Flagged by the copy review as its top category. The specific strings:

- *"CPU cost is nearly independent of resolution"* — a performance claim with no
benchmark, workload, or hardware named.
- *"it is the path that is operated daily"* — a credibility claim with no
release cadence or test record behind it.
- *"What only polyemesis does"* — an exclusivity claim against named competitors
with no dated source beside any cell.

This audience checks. An unsourced comparison against named products is the
fastest way to lose a reader who would otherwise have installed the thing.

**Fix:** put a dated primary-source link in each competitor cell, and either
benchmark the CPU claim or soften it to what the architecture actually
guarantees (video is stream-copied, so it is not re-encoded per destination —
which is a *structural* statement and needs no benchmark).

---

## 2. Where a review was wrong, and what measuring showed instead

Recorded because the pattern repeats: a review reports a real symptom and
prescribes a fix for a cause that is not the actual one.

| Reported | Prescribed | What was actually true |
|---|---|---|
| "Active nav links render in identical `rgb(155,169,186)`" | "Apply `aria-current` and high-contrast styling" | `aria-current` and `text-fg` were **already there**. The input was wrong: flat-file output makes `pathname` `/features.html`, so the comparison was false on every page. |
| "Downscale screenshots to 1440px, 73.5% saving" | resize + quantise | Resizing makes PNG compression **worse** here. At 2304px the saving fell to 42.6% and `02-routing.png` came out **19% larger** than full-res. Quantise, do not resize. |
| (my own first fix) sticky column with `var(--color-bg)` | — | That token does not exist in this theme. It resolved to transparent while `position: sticky` and `stayedPinned: true` both read as correct. The mechanism was right and the thing it exists to do was broken. |

---

## 3. Layout and rhythm

### 3.1 `/features` is a conveyor belt — Medium, M
**Measured:** sections `#routing`, `#matrix`, `#renditions`, `#monitoring`,
`#recording`, `#chat` are each **exactly 593px tall** on desktop. Six identical
blocks in a row give the eye no focal point and no sense of which capability
matters most.

**Fix:** give the differentiator (`#routing`, `#matrix`) more vertical room and a
distinct treatment; compress the four supporting ones. Uniformity here reads as
"six equally minor features", which undersells the one that is not.

### 3.2 The landing page is 8918px tall at 390px — Low, M
**Measured:** 22 phone screens. `WHAT ELSE IT DOES` alone is 1454px and sits
between the proof section and the architecture section, so a technical reader
scrolls a long way past secondary material to reach the part that would convince
them.

**Fix:** compress the six secondary features into a 3×2 grid and move
`HOW IT IS BUILT` up, directly after the proof.

### 3.3 Nothing that scrolls or expands says so on touch — Medium, S
Two findings with one cause, and it is the class of bug that desktop review
never catches: **every affordance on this site is expressed as a hover or a
cursor**, and a phone has neither.

- **The lightbox is undiscoverable.** `.shot-open` communicates itself with
`cursor: zoom-in`. On touch that is nothing at all. Measured at 390px, each
2880×1800 capture renders at **348×218** — an 8.28× reduction per dimension,
which is precisely where a reader most needs to expand it and has no way to
learn they can.
- **The comparison tables give no sign they scroll.** The visible frame shows
**348 of 736px — 47%** of the table. macOS and iOS both hide overlay
scrollbars until a scroll is already in progress, so the reader has to guess
that the other 53% exists.

**Fix:** a persistent expand glyph in the corner of each screenshot rather than
a cursor change, and a fade or edge shadow on the table wrapper that says
content continues past the right edge. Both are visual, both work without
pointer events.

### 3.4 The 768px layout is longer than desktop — Low, M
**Measured:** `/features` is **7527px** at 768px against **5576px** at 1440px.
All six text/image pairs stack into single columns at tablet width, so each
becomes 853–943px tall. The tablet reader scrolls 35% further than the desktop
reader for identical content.

### 3.5 Screenshot resolution is correct — do not "optimise" it
**Measured:** inline shots render at 688 CSS px at a 1440 viewport; the lightbox
expands them to **1152 CSS px**, which at dpr 2 needs 2304. They are 2880 wide.

A future performance audit will recommend downscaling these. It will be wrong
for the reason in §2 — and it will also blur the lightbox, which is the view the
screenshots exist to provide.

---

## 4. Patterns worth adopting, from comparable products

From a survey of adjacent categories on Mobbin. Honest gap up front: **the
streaming/broadcast category returned no domain peers** — no Restream, Castr,
OBS or Owncast in the library. The applicable patterns come from developer
tools, open-source infrastructure and self-hosted products instead.

| # | Change | Why | Effort |
|---|---|---|---|
| 1 | **Say the matrix is interactive.** A visible affordance next to it, not prose. | The copy currently says "Light a crosspoint" and "Hover a column" — buried in a paragraph, in jargon. It is the only interactive thing on the site and the strongest asset it has. | S |
| 2 | **License eyebrow + copyable install line in the hero.** `MIT · SINGLE BINARY · SELF-HOSTED` above the H1, the install command beside the CTA. | The two facts this audience decides on are licence and how hard it is to run. Both are currently below the fold. | S |
| 3 | **Annotate the spectrograms.** Leader lines marking the band that actually differs between the two mixes. | The proof section shows two images and asks the reader to spot the difference. Naming it converts a picture into evidence. | S |
| 4 | **Add a boxed "Why can't OBS just do this?"** — three sentences, beside the differentiator. | It is the first objection every reader in this audience raises, and the site never answers it directly. | S |
| 5 | **A fan-out diagram in the hero** (one source → hub → many destinations, each with a different mix). | Topologically identical to Customer.io's, and it states the product's whole thesis without prose. | M |

### Deliberately rejected
Common in SaaS marketing, actively harmful for a self-hosted MIT project with
this audience: logo walls, review-site award badges, pricing or plan tables,
"no credit card required", email-gated demos, stock photography, mascots, and
unattributed testimonials. A quote reading "— Streaming Engineer" costs more
credibility than it buys. Supabase's quote carousel works *only* because every
quote carries a real linked handle.

### Already right — do not break
Measured and confirmed consistent across all pages and all three viewports:

- **The grid.** Every page uses a 1280px wrap starting at x=80, content aligned
to x=100, and the side inset stays consistent down to 390px.
- **The type scale.** H1 48px desktop and tablet, 36px mobile (home 54.4px);
body 16px/24px; lead 18px. One system, applied everywhere.
- **The screenshot treatment.** Feature captures are 688×430 at 1440px — 1.60:1,
matching the source aspect ratio exactly, in a single aligned column.
- **The docs split.** 208px side nav, 64px gutter, 968px reading column, folding
to one column at 390px with no overflow.

And, from the pattern survey: the interactive matrix; the spectrogram proof
section; the problem-before-solution order; the for-you/not-for-you list; dark
developer-tool styling; the absence of a pricing page; the 404 page written in
the product's own vocabulary; and a footer that deep-links to sections rather
than cloning the header.

---

## 5. Suggested order

Ranked by (severity × reach) ÷ effort.

1. **§1.1** de-duplicate the comparison table on `/features` — S, and it is a deletion
2. **§3.3** touch affordances for the lightbox and the scrolling tables — S, and it is the only category here that makes a working feature *reachable* rather than making an existing one nicer
3. **§4.1–4.4** the four S-effort marketing wins — each is one small block
4. **§1.3** source the competitive claims — the credibility risk with this audience
5. **§3.1** break the 593px conveyor belt on `/features`
6. **§1.2** bring the docs on-site — largest effort, largest funnel payoff
7. **§3.2, §3.4** compress and reorder the landing page; fix the tablet stack

Items 1–3 are roughly an afternoon together and touch nothing structural.

---

## 6. Method

Four reviewers, deliberately given different briefs so they would not converge:
information architecture, copy, layout, and a comparable-product pattern survey.
Findings were then re-measured directly before being written down — which is how
the three corrections in §2 were caught.

One reviewer confirmed the mobile fixes independently by measuring the live
Worker against the local build in the same run: live computed
`.card { min-width: auto }` with `.cmp` cells `static`, local computed `0px` and
`sticky`. That is the deploy gap, not a disagreement.

A fifth review (layout, second opinion) did not return in time; a sixth was
refused for quota. Neither gap affects the findings above, all of which are
measured rather than reported.
Binary file modified docs/media/01-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/02-routing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/03-routing-filtergraph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/04-meters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/05-sources.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/06-renditions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/07-monitoring.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/08-mix-matrix.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/09-chat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/10-playout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/11-library.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/12-jobs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/13-automation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/14-clips.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/15-recordings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/16-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/17-settings-full.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/media/tour.mp4
Binary file not shown.
Binary file modified docs/media/tour.webm
Binary file not shown.
Binary file added index-1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions scripts/capture-media.sh
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,47 @@ if [ -n "$VID" ]; then
fi
rm -rf "$OUT/.playwright"

# Quantise to a 256-colour palette. HERE rather than as a hand-run afterthought,
# because the shots are committed and shipped: a manual step gets skipped once
# and every later capture is silently twice the size it needs to be.
#
# WHY IT WORKS SO WELL. A UI screenshot is mostly flat fills, one-pixel borders
# and antialiased text over those fills, so it uses far fewer than 256 distinct
# colours to begin with. Measured across the 17 shots: 61% smaller, RMSE 0.06%
# to 0.15% of range.
#
# AND WHY NOT ALSO RESIZE. Downscaling makes the compression WORSE here, which
# is the opposite of the intuition. Resampling invents intermediate colours
# along every edge, which defeats both the palette and PNG's row filters: at
# 2304px the same quantisation saved only 42.6%, and 02-routing.png came out
# 19% LARGER than its full-resolution self. The site's lightbox also expands
# these to 1152 CSS px, so at dpr 2 it wants 2304 anyway -- there is no width to
# give back. Quantise, do not resize.
#
# Skipped rather than fatal if Pillow is absent: this runs on laptops, and a
# missing optional dependency should cost bytes, not the whole capture.
echo "==> compressing"
if python3 -c "import PIL" 2>/dev/null; then
python3 - "$OUT" <<'PY'
import glob, os, sys
from PIL import Image

before = after = 0
for path in sorted(glob.glob(os.path.join(sys.argv[1], "*.png"))):
was = os.path.getsize(path)
im = Image.open(path).convert("RGB")
im.quantize(colors=256, method=Image.MEDIANCUT,
dither=Image.FLOYDSTEINBERG).save(path, optimize=True)
Comment on lines +253 to +256
now = os.path.getsize(path)
before, after = before + was, after + now
if before:
print(f" {before // 1024}K -> {after // 1024}K "
f"({100 - 100 * after / before:.0f}% smaller, resolution unchanged)")
PY
else
echo " skipped: python3 with Pillow not found, shots left unquantised"
fi

echo
echo "==> done"
ls -la "$OUT"
Binary file modified web/public/shots/02-routing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/04-meters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/06-renditions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/07-monitoring.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/08-mix-matrix.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/09-chat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/shots/15-recordings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions web/scripts/check-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,38 @@ if (!/@media\s*\(prefers-reduced-motion/.test(css)) {
fail.push("reduced motion does not set `animation-name: none !important` — a scroll-driven reveal ignores animation-duration");
}

/* The two mobile-overflow fixes must still be IN the shipped CSS.
*
* These guard a fix, not a behaviour, and the distinction is worth stating: a
* static file cannot prove no page overflows on a phone -- that needs a browser
* and a real layout. What it can prove is that the two declarations that made
* the overflow go away are still present, and deleting one of them is the
* realistic way this regresses, because both look inert when you read them.
*
* Measured before the fix, at a 375px viewport: /download scrolled to 630px and
* /comparison to 570px. Both were the whole document moving sideways, not an
* element scrolling inside its own box.
*/
// `min-width: 0` lets a grid child shrink past its content's intrinsic minimum.
// Without it a card holding a long <pre> pushes the document wider than the
// screen, and the <pre>'s own overflow-x never engages.
if (!/\.card\{[^}]*min-width:0/.test(css.replace(/\s+/g, ""))) {
fail.push(
"`.card` has no `min-width: 0` — a card containing a <pre> will push the whole page wider than a phone screen",
);
}
// A pinned first column with a transparent background still reports as sticky
// and still looks broken: the scrolling cells slide visibly underneath it.
// --color-bg does not exist in this theme; the page paints --color-ink.
{
const flat = css.replace(/\s+/g, "");
if (!/\.cmp[^{]*first-child\)?\{[^}]*position:sticky/.test(flat)) {
fail.push("comparison tables have no sticky first column — the row label scrolls away from its Yes/No cells on mobile");
} else if (/\.cmp[^{]*first-child\)?\{[^}]*background:var\(--color-bg\)/.test(flat)) {
fail.push("sticky comparison column uses `--color-bg`, which this theme does not define — it resolves to transparent and cells scroll under the label");
}
}

// 3+4. Internal links and fragments must resolve to something that was built.
/* The motion tokens have to survive minification as VARIABLES.
*
Expand Down Expand Up @@ -84,6 +116,32 @@ if (!meterKf) {
}

const pages = readdirSync(DIST).filter((f) => f.endsWith(".html"));

/* Every nav page must mark ITSELF as the current one, in BOTH navs.
*
* This broke silently in the move to Cloudflare Workers. The build emits flat
* files, so Astro.url.pathname is "/features.html" while the link href is
* "/features" -- the comparison was false on every page and the nav marked
* nothing at all. No aria-current for a screen reader, and the link for the
* page you were already on painted the same muted grey as the others.
*
* It is checked here rather than trusted because of HOW it failed: a nav with
* no highlight looks like a design decision, not a bug, so nothing about the
* rendered page announces it. Two counts rather than one, because the desktop
* nav and the mobile menu are separate markup and the mobile one -- the only
* navigation a phone has -- was the half left behind when this was first fixed.
*/
for (const f of ["features", "comparison", "docs", "download"]) {
if (!pages.includes(`${f}.html`)) continue;
const html = readFileSync(join(DIST, `${f}.html`), "utf8");
const marked = [...html.matchAll(new RegExp(`<a href="/${f}"[^>]*aria-current="page"`, "g"))].length;
if (marked < 2) {
fail.push(
`${f}.html marks its own nav link as current ${marked} time(s), expected 2 ` +
`(desktop nav + mobile menu) — check Astro.url.pathname is normalised against the flat-file output`,
);
}
}
const built = new Set(pages.map((f) => "/" + f.replace(/\.html$/, "").replace(/^index$/, "")));
for (const f of pages) {
const html = readFileSync(join(DIST, f), "utf8");
Expand Down
Loading
Loading