feat: rebuild the landing page around the product

The home page led with a text hero, a boxed stats strip, a numbered
how-it-works list, a trust card and three Bootstrap featurettes with
postage-stamp screenshots. It now shows the product first and stays
flat:

- Hero: copy on the left, the anonymize form in a browser frame on the
  right, one primary CTA plus a link to a real anonymized repository,
  and three reassurance points (free, read-only, expires).
- Proof and live stats collapse into hairline strips with no cards;
  the stat bars keep their daily-delta charts and the "+N today" line.
- Before/after example gets a section head and shows the actual XXXX-n
  masks, each labelled for screen readers.
- "What you get" is one large screenshot with three vertical tabs
  (Anonymize, Review, Manage) instead of three columns. Tabs are real
  buttons with tablist/tab/tabpanel roles, roving tabindex and arrow-key
  navigation; the description and link sit outside the button. Signed-out
  visitors are sent to sign in rather than to a dashboard that redirects.
- FAQ teaser with the three questions that come up most, linking into
  the FAQ anchors.
- Phone layout: tabs become a strip above the screenshot with the
  selected description below it, stats go two-up, panes stack, the CTA
  fills the width, and horizontal overflow is clipped so the hero bleed
  can never cause sideways scrolling.

Removes the dead how-it-works, trust, featurette and metrics styles and
the legacy mobile hero font overrides that fought the new sizes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
tdurieux
2026-09-09 12:21:02 +02:00
co-authored by Claude Fable 5.1
parent b9189953d3
commit 88d8225f00
7 changed files with 887 additions and 561 deletions
+45
View File
@@ -140,6 +140,51 @@ describe("frontend production regressions", function () {
h.scope.saveDefault(); h.requests.at(-1).reject({ data: { error: "not_connected" } }); await h.flush();
expect(h.scope.error).to.equal("ERRORS.not_connected");
});
describe("landing page features", function () {
const home = fs.readFileSync(path.join(__dirname, "..", "public", "partials", "home.htm"), "utf8");
function landing(user) {
const h = harness(); h.scope.user = user;
const focused = []; h.focused = focused;
const win = { document: { getElementById: id => ({ focus: () => focused.push(id) }) } };
const timeout = fn => fn();
h.defs.homeController.at(-1)(h.scope, h.http, { url() {} }, win, timeout);
return h;
}
it("selects the first feature and switches on click", function () {
const h = landing(null);
expect(h.scope.feature).to.equal("anonymize");
h.scope.selectFeature("manage"); expect(h.scope.feature).to.equal("manage");
});
it("moves between tabs with the arrow keys, wrapping and moving focus", function () {
const h = landing(null); let prevented = 0;
const key = k => ({ key: k, preventDefault: () => prevented++ });
h.scope.featureKeydown(key("ArrowDown"), 0); expect(h.scope.feature).to.equal("review");
h.scope.featureKeydown(key("ArrowUp"), 1); expect(h.scope.feature).to.equal("anonymize");
h.scope.featureKeydown(key("ArrowUp"), 0); expect(h.scope.feature).to.equal("manage");
h.scope.featureKeydown(key("End"), 0); expect(h.scope.feature).to.equal("manage");
h.scope.featureKeydown(key("Home"), 2); expect(h.scope.feature).to.equal("anonymize");
h.scope.featureKeydown(key("Tab"), 0); expect(h.scope.feature).to.equal("anonymize");
expect(prevented).to.equal(5);
expect(h.focused).to.deep.equal(["review", "anonymize", "manage", "manage", "anonymize"].map(k => "feature-tab-" + k));
});
it("sends signed-out visitors to sign in instead of the dashboard", function () {
const manage = f => f.key === "manage";
const out = landing(null);
expect(out.scope.featureHref(out.scope.features.find(manage))).to.equal("/github/login");
expect(out.scope.featureTarget(out.scope.features.find(manage))).to.equal("_self");
const signedIn = landing({ username: "jane" });
expect(signedIn.scope.featureHref(signedIn.scope.features.find(manage))).to.equal("/dashboard");
expect(signedIn.scope.featureTarget(signedIn.scope.features.find(manage))).to.equal(undefined);
});
it("marks the tabs up as tabs and keeps links out of the buttons", function () {
expect(home).to.match(/<button[^>]*class="paper-feature-tab"[^>]*role="tab"/);
expect(home).to.match(/role="tablist"/);
expect(home).to.match(/role="tabpanel"[^>]*aria-labelledby="feature-tab-\{\{f\.key\}\}"/);
const button = home.slice(home.indexOf('class="paper-feature-tab"'), home.indexOf("</button>"));
expect(button).to.not.include("<a ");
expect(home).to.not.match(/href="#"/);
});
});
it("keeps the conference end date after the start across December", function () {
class December extends Date { constructor(...args) { super(...(args.length ? args : ["2026-12-15T12:00:00Z"])); } }
const h = harness(December); h.scope.user = {};