Commit 48fe9c1
add Launch Week Vol.01 campaign page (#6647)
* feat(landing-page): add Launch Week Vol.01 event page
Five-day campaign page at /community/events/launch-week/ for Aug 10–14.
Structure: full-screen campaign hero (stateless — nothing here needs a daily
edit), a pinned lineup panel beside a sticky stack of five drop cards, the
All-Week Pass / Community Open Call tickets, and a short closing CTA into
Discord and Feishu.
Drop state is derived, not authored. Each day flips at 08:00 UTC+8 (00:00 UTC,
no DST to resolve), computed client-side so a cached page still shows the right
day without a redeploy. Sealed days render as full cards with their contents
redacted, so the pile reads on day one when traffic peaks rather than as four
flat bars. `?preview=all` and `?preview=1`–`5` simulate any day for review.
All CSS is scoped under `.lw` and the two campaign faces (Anton, IBM Plex Mono)
are self-hosted latin subsets, 36 KB total — no third-party font request, and
nothing leaks into the rest of the site.
Motion is limited to one entrance per element behind a
`prefers-reduced-motion` guard; with motion reduced every element renders in
its final state.
* fix(landing-page): gate Launch Week preview and settle the page chrome
Preview modes now need a shared key (`?preview=<day>&key=…`); without it the
parameter is ignored and the real schedule applies, so the running order can't
be read off a URL guess before each drop opens.
The page also stops borrowing `.sub-main`. That class assumes a normal article
column — a 140px top gap and a 48px bottom gap — which left dead space under
the nav and a beige seam between the closing CTA and the site footer. The page
now declares only the offset it needs (clearance for the sticky nav) and butts
straight against the footer, matching how the story pages sit.
Dropped our own copyright baseline: the site footer already carries it.
* fix(landing-page): restore the Launch Week hero wash and size the closing CTA
The hero's paper wash and drawing grid were never painting. Two causes stacked:
the rule hung off `.hero::before`, which globals.css already owns as a section
divider it sets to `display: none`, and even without that collision an absolute
pseudo-element inside the flex hero collapsed to a 1px sliver. The wash is now
its own `.lw-wash` element with explicit dimensions, so the paint reads at the
intended strength instead of full saturation.
The closing CTA was also shorter than the section CTAs on neighbouring
/community pages. It now carries a comparable height and headline scale.
* fix(landing-page): drop the Launch Week masthead strip, rebuild the closing CTA
The strip under the site header repeated the logo beside it and read as a
second navigation bar. Removed; the date it carried moves into the hero as a
normal eyebrow, where a campaign page usually puts it.
The closing CTA now follows the house `.v4final` pattern used by the story and
community pages: a single left-aligned column stepping from eyebrow to headline
to copy to one row of buttons, over a darkened image rather than flat black.
Previously it split 60/40 with the buttons stranded in their own column, which
is why it read as under-designed next to its neighbours. The darkening image is
our own plate, so the campaign's material carries to the last screen.
* fix(landing-page): restore Launch Week's screen-by-screen scroll rhythm
Scroll snapping was silently lost in the port. In the mockup it lived on `html`;
scoping every rule under `.lw` moved it onto `<main>`, which is not a scroll
port, so the property had no effect and the ticket section spilled past the fold.
It now sits on `html:has(main.lw)` — this stylesheet ships only with this page,
so the scope still holds — with `scroll-padding-top` so a snapped section clears
the sticky nav.
The hero and the ticket section also disagreed on what "one screen" meant. The
hero opens below the nav and takes `100svh - header`; every later full-screen
section is reached with the nav scrolled away and takes the whole viewport.
Also rewrote the closing paragraph as one sentence — the previous four clipped
sentences read as a list rather than an invitation.
* copy(landing-page): sharpen the Launch Week closing paragraph
Leads with what the community actually gives you — how to claim the perks, what
each drop really does, how to enter the Open Call — then the people, the team,
and what comes next. Drops the perk list that the two tickets already cover in
full, and stops naming Discord and Feishu in prose since the buttons below
already do.
* feat(landing-page): lead the events page with Launch Week
Launch Week was buried in the "Coming soon" grid below a July recap, which read
as if the flagship event were an afterthought. It now opens the page as its own
band directly under the hero, and comes out of the grid so it appears once.
The band shows the approved key visual whole rather than a page screenshot —
the poster already carries the name, the dates, the five days, the challenge and
the Pass, so the only things laid over it are what it cannot say: that the week
is live, and where to go next. Those sit in a bar beneath the art instead of on
top of it.
The share card now uses the same key visual for the same reason.
* fix(landing-page): scale the Launch Week band to a feature card
Full-bleed 16:9 made the poster over 600px tall and swallowed the page. It is
now a horizontal card the same size as the recap feature below it (352px), with
the art in a 16:9 column so the key visual still shows whole, and the copy the
poster cannot carry beside it.
* fix(landing-page): let the Launch Week poster be the only loud thing
The copy column repeated the poster's own palette — ink, acid, high contrast —
so the band read as two competing halves. It is now the same neutral white card
as every other event on the page, using the shared pill. The artwork carries the
campaign; the card carries the facts.
* fix(landing-page): give every Launch Week day a sealed card
Only days 3–5 had a sealed twin, carried over from a mockup that assumed the
first two days were always already out. Before the week opens — and on day one,
when traffic peaks — the page was therefore short two and one cards.
Every day now has both halves, and the no-JS fallback shows all five sealed,
which is honest on any date; the script then opens whichever days have happened.
* fix(landing-page): keep the TBA stamp clear of the stacked card above it
The stamp sat low enough in a sealed card that the next card in the sticky
stack sliced it in half. It now sits centred against the redacted lines, inside
the area the peek leaves visible.
* seo(landing-page): tighten Launch Week metadata and image loading
Audit of the built page found the basics sound — canonical, 12 hreflang
alternates, full OG/Twitter set, one H1, no missing alt, in the sitemap — with
five gaps:
- Title was 30 characters and carried no brand, wasting half the SERP line. Now
states what the week is and ends in "Open Design".
- Description opened on a sentence fragment and buried the premise; rewritten to
156 characters that lead with the dates and the promise.
- Event schema lacked `image`, `performer`, `offers` and `isAccessibleForFree`,
which Google wants before it will render an event rich result.
- Seventeen images all loaded eagerly on a page read a screen at a time; the ten
below the fold are now lazy with async decoding.
- Target logos had no intrinsic size, so each one could shift layout on load.
* i18n(landing-page): localise Launch Week into the ten non-English locales
Adds a body partial per active locale plus the meta table, wired through
`localeFromPath` like the story pages. The client script that writes drop states
receives this locale's words too, so a translated page does not flip back to
English once it runs.
What stays English everywhere, deliberately: the campaign lockup
("Launch Week Vol.01"), the kill questions — each reads against a competitor's
brand name — the ticket furniture, and the rubber stamps, which are artwork with
the word cut into them.
CJK sets far wider than Anton at the same size, so zh/ja/ko get their own step
for the headline scales; without it the drop titles and the lineup title spilled
past two lines.
Translations are machine-drafted and want a native read per locale before this
goes to production.
* i18n(landing-page): translate the closing headline as a whole line
"Get inside." is split across two elements so the second half can take the acid
colour, and the word-level pass never matched the first half — every locale
shipped a literal "Get" followed by a translated fragment, and French read
"GET À L'INTÉRIEUR." Each locale now supplies the whole headline, with its own
choice of which words carry the accent.
* i18n(landing-page): drop the CJK word space, even out the join buttons
Splitting the closing headline so the second half can take the accent colour
left a space between the two halves — correct in Latin scripts, wrong in
zh/ja/ko, where "进来 一起做。" should read without the gap.
The two community buttons also sit at very different widths once translated
("加入 DISCORD" beside "加入飞书群"); a shared minimum keeps the pair level in
every locale.
* fix(landing-page): stop shipping unopened Launch Week drops in the HTML
Self-audit found the campaign's central mechanism was not actually holding: a
static page sends its whole DOM to every visitor, so each unopened day's
headline and target sat in view-source days early. The preview key gated the
display, never the markup — anyone could read the running order straight out of
the page.
Unopened drops are now dropped at build time. The sealed twin always ships; its
revealed counterpart is emitted only once its date has passed, so the secret is
never in the response. The cost is that a day opens on the next deploy rather
than on the hour, which needs a scheduled build during the week.
Also from the audit:
- Mobile had no breakpoint at all. The kill list collapsed into an unreadable
row, the lineup kept its two-column desktop shape, and the pass ticket
overflowed because its grid track held the desktop intrinsic width. All three
now have a narrow-screen layout.
- `white-space: nowrap` on the hero lockup keeps it on one line on desktop, but
every translated slogan is longer than the English and ran off a phone screen;
it wraps below 900px.
- Six assets were left behind by earlier revisions and no longer referenced.
* fix(landing-page): release Launch Week drops at request time, not build time
504a8e5 kept unopened drops out of the HTML by stripping them at build, which
bought secrecy at the cost of needing a deploy every morning of the week — five
scheduled production builds, each one able to fail on the one day it matters.
Serve them from a Pages Function instead. The page ships only the sealed cards;
`/launch-week/drops` returns the markup for days that have actually opened and
the browser slots it in. A day now flips on its own, and an unopened one is
never in a response for anyone to read early.
- Revealed markup moves to `_partials/launch-week-drops.ts`, per locale.
- Edge cache expires at the next 00:00 UTC boundary, so a stale day-1 response
cannot survive into day 2.
- `?preview=N&key=…` still rehearses any day, and is served `no-store`.
- A failed fetch leaves the sealed cards up: it fails toward the pre-launch
page, never toward a leak.
Tests cover the boundary, the key gate, cache lifetime, locale fallback, and
that no locale partial carries revealed markup. Verified in the browser across
11 locales x 3 viewports, with scroll reveal and reduced motion.
* fix(landing-page): drop the Watch button until its post exists
The X post for a day is not written when the page ships, so the button on the
live card pointed at `href="#"` — the most prominent control on the card, and
clicking it threw the visitor back to the top of the page for nothing.
Serve the button only once `DROP_LINKS` has that day's URL. The card reads fine
without it: the kill strip becomes the closing line and the composition still
balances.
Filling a link in costs a production deploy and a `landing-deployers` approval,
so the comment says to batch the days rather than edit it each morning.
* fix(landing-page): stop shipping the Launch Week preview key to visitors
The page carried `PREVIEW_KEY` in its inline script, so every visitor was served
the key that unlocks the unreleased days. Read it off the page, request
`?preview=all&key=…`, and the endpoint handed back all five days — the schedule
protected nothing. Caught in review on the PR preview deployment.
The key now comes from `LAUNCH_WEEK_PREVIEW_KEY`, a Pages secret, and appears
neither in the page nor in this repository — both are public, so a key in either
is a key everyone has. Unset means preview is off rather than open: a missing
secret must not open the week up.
The page no longer needs to know the key. It forwards whatever `?preview=` and
`?key=` are in the URL and the endpoint decides; the response reports which
preview it honoured, and the day labels are rebuilt from that.
Two tests now guard the leak directly: one asserts the page source carries no
key, the other that no literal key sits in the function.
Requires `LAUNCH_WEEK_PREVIEW_KEY` on both the staging and production Pages
projects. Until it is set, preview is simply unavailable — the live schedule is
unaffected.
---------
Co-authored-by: Scarlett Huang <scarletthuang@Scarletts-MacBook-Air.local>1 parent f580271 commit 48fe9c1
42 files changed
Lines changed: 4751 additions & 0 deletions
File tree
- apps/landing-page
- app
- _partials
- pages
- [locale]/community/events/launch-week
- community/events
- launch-week
- functions/launch-week
- public/launch-week
- fonts
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
Lines changed: 189 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
0 commit comments