feat(web): redesign landing, pricing, and FAQ, and add a download page - #602
Merged
Merged
Conversation
titanism
added a commit
that referenced
this pull request
Sep 23, 2026
1) Cache stampede (DoS) across all three read-through caches
helpers/single-flight-cache.js (new), get-faq-index.js, get-faq-schema.js,
get-mail-app-releases.js. Each did a plain "read redis, on a miss compute +
write redis", so a cold key let every concurrent request across every worker
run the expensive compute at once (N markdown parses; N GitHub API calls).
The new helper wraps the compute in a redis SET NX PX lock released by a
compare-and-delete Lua script (the get-database.js idiom): one caller
computes, the rest wait or fall back. Fails open, never memoises a null/empty
result, validates cached values on read (an entry failing the caller's
shouldCache is re-computed, not served — matching the old getFaqSchema), and
accepts either a raw string or an already-parsed object from client.get (a
get reply transformer, which the tests use, hands back the latter). Bounds
the GitHub fetch with AbortSignal.timeout(10s).
2) isSANB is not defined — the FAQ was always empty
routes/web/index.js used isSANB(ctx.query.q) but never required it; the throw
was swallowed and the page fell back to an empty index ("0 answers"). Added
the require. FAQ now renders all 147 answers.
3) Guide pages rendered blank
controllers/web/guides.js renders faq/index.pug to scrape an answer out of it
(#send-mail-as-content, #smtp-instructions, #legacy-free-guide) but didn't
pass `faq`, so the mixin threw and the scrape came back empty. Fixes:
- guides.js populates `faq` (via #helpers/get-faq-index) before rendering.
- get-faq-index.js sanitizer keeps `id` on every element (it named only the
heading tags, stripping the <div id>/<li id>/<table id> anchors the scrape
and in-page links need).
- get-faq-index.js cache key bumped to `faq_index:v2:` — the parsed HTML
shape changed and the cache has a 12h TTL, so without a new key a deploy
would keep serving the old id-stripped HTML and the guides would stay blank
for up to 12h. The new key sidesteps the stale cache with no manual flush.
- _fe-faq.pug defaults `faq` to an empty index so the mixin can't 500.
4) Footer dropdowns jumped the page (mobile/tablet)
_footer.pug: collapse toggles used href="#footer-col-*", which the global
a[href^='#'] handler scrolls to. Switched to href="#" + data-target, the
framework's non-jumping idiom. Collapse still toggles; the page does not move.
5) Dark mode — consistent across the whole site
_fe-tokens.scss, app-dark.scss.
- .fe-surface-light was locked light in both schemes, so the FAQ body and
other sections stayed near-white in dark mode. Re-pointed it to the dark
ramp, and to the same Ink as every other surface, so a lighter band no
longer sets the redesigned pages apart from the legacy ones — every page is
one cohesive Ink in dark mode.
- The marketing/blog comparison tables (.bg-themed) kept light
.table-success/-primary row tints; re-pointed to dark tints with light
text, scoped to .bg-themed so the app's status tables keep theirs.
- The article pages (about, guides, blog, docs, terms, privacy) render
through .markdown-body, which github-markdown-dark paints in GitHub's own
palette (brighter #f0f6fc text, a different #4493f8 blue link). Re-pointed
the markdown text, headings and links to the shared app tokens so those
pages match the redesign's colors too.
6) Test fixes
- config/mail-app-release-fallback.json: added the release's SHA256SUMS.txt
asset, which the checked-in snapshot omitted — get-app-downloads asserts
the checksum link points at that file, so the test failed on a clean PR.
- get-faq-schema cache tests pass again (see the read-side handling in 1).
- ava.config.js: exclude test/visual/**. test/visual/sweep.js is a standalone
puppeteer CLI (reads process.argv, calls process.exit), not an AVA test;
AVA was running it and failing on the process.exit. Excluded the same way
test/utils.js already is.
- test/web page snapshots made build-independent. The local-only
(`if (!isCI)`) index.js/otp.js snapshots embedded rev-hashed asset
filenames and SRI hashes, which differ between build environments, so a
fresh checkout failed them with no source change (and the footer markup
change also needed them updated). Added utils.normalizeBuildHashes to strip
those hashes before snapshotting — the same intent as otp.js already
stripping the <head> — so the snapshot captures page structure, not build
noise, and passes on any build. Snapshots regenerated in that normalized
form (0 build-specific hashes remain; the footer fix is captured).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Landing and pricing
FAQ
Download page
Build