Skip to content

feat(feed): sponsored strip docked on the main feed [mock-up] - #6490

Draft
tsahimatsliah wants to merge 22 commits into
mainfrom
feat/feed-sponsored-strip
Draft

feat(feed): sponsored strip docked on the main feed [mock-up]#6490
tsahimatsliah wants to merge 22 commits into
mainfrom
feat/feed-sponsored-strip

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Aug 18, 2026

Copy link
Copy Markdown
Member

⚠️ Mock-up — please do not merge

A sponsored strip docked to the bottom of the main feed. Sponsors and every rail's data are hard-coded fixtures. Nothing here has been sold.

What it is

A 72px dock, flush and sticky, spanning the feed column:

  • Sponsor row (40px) — one lead sponsor plus a twelve-mark partner wall.
  • Value rail (32px) — an ambient ticker whose label is a dropdown switching between eleven channels.

The lead mark keeps brand colour, sits ~20% larger than the wall, and is the only link and the only hover in the strip. NVIDIA's lockup renders inline rather than as a flat file so its wordmark can take currentColor — black on the light feed, white on the dark one — while the symbol holds #76B900. A single flat asset cannot do that.

The partner wall is silhouetted through a CSS mask so one asset works in both themes, sized by area rather than cap height so a 2:1 mark and a 6:1 lockup carry the same weight, reshuffled on every page load so no advertiser is permanently first, and trimmed to the marks that fit whole — never a half logo under a fade.

The value rail defaults to Hot right now. Eleven channels, all backed by data already in the app: trendingTags, userStreak, the leaderboard queries, live rooms, opportunities, poll posts. The choice is kept in localStorage.

Why there are two rows

The browser's link tooltip paints over the bottom-left corner, cannot be read or suppressed from a page, and is exactly as wide as the URL inside it. Measured on the live feed, 28 links on one screen:

link type median chars tooltip width share of a 1440 screen
nav / tags 33 189px 13%
post slug 68 393px 27%
ad click-through 742 clipped to 720px 50%

Horizontal clearance therefore cannot be a guarantee — it would need half the bar. Vertical clearance is 26px whatever the URL says. The rail gives the paid row 32px of it, and each rail opens with its label on the left, where the tooltip lands first, so what it covers is a word rather than a number.

Worth fixing separately: promoted cards link through a signed token, so that URL carries a whole JWT.

Storybook

  • Extension / Sponsor Dock — the shipped design, its eleven rails, and the two-row argument
  • Extension / Sponsored Strip — five placements evaluated over a real feed
  • Extension / Bubble-Safe Strips — ten technical answers to the tooltip, with a live bench
  • Extension / Bottom Strip Variants — ten ways to hold a bar at the bottom

Rejected explorations live in the storybook package, not in shared — they are decision-making artifacts, not product code.

Before this could ship

  • Sponsors come from a fixture, not the ad service
  • No feature flag — it renders for everyone on the feed
  • No impression logging; click is an unused optional callback
  • Rails need wiring to their real queries
  • Partner logos are hot-linked from business.daily.dev
  • Advertiser assets need a spec: 31 of the 66 wordmarks in the current library are raster images wrapped in an .svg, which mask to a solid block

🤖 Generated with Claude Code

Preview domain

https://feat-feed-sponsored-strip.preview.app.daily.dev

@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Aug 25, 2026 6:10pm
storybook Building Building Preview Aug 25, 2026 6:10pm

Request Review

A bottom rail carrying one lead sponsor and a wall of partner marks.

The lead mark keeps brand colour, sits ~20% larger than the wall, and
is the only link and the only hover in the strip. NVIDIA's lockup
renders inline rather than as a flat file so its wordmark can take
currentColor — black on the light feed, white on the dark one — while
the symbol holds #76B900. A single flat asset cannot do that.

The wall is silhouetted through a CSS mask so one asset works in both
themes, sized by area rather than cap height so a 2:1 mark and a 6:1
lockup carry the same weight, reshuffled per page load so no
advertiser is permanently first, and trimmed to the marks that fit
whole rather than clipped behind a fade.

Sponsors are fixtures. Real inventory needs an ad-service source.
The dock stacks a value rail beneath the sponsor row, which does two
jobs at once.

It gives the strip a reason to exist for the reader: a permanent bar
carrying only advertising is rent, one carrying something the reader
came for is a feature that happens to be sponsored.

It also answers the browser's link tooltip. That tooltip paints over
the bottom-left corner, cannot be read or suppressed from a page, and
is as wide as the URL inside it — measured on the live feed, post
slugs run 393px and promoted cards' signed click-throughs run to
Chrome's 50%-of-viewport cap. Horizontal clearance therefore cannot
be a guarantee; vertical clearance is 26px whatever the URL says. The
rail gives the paid row 32px of it, and each rail opens with its
label on the left, where the tooltip lands first, so what it covers
is a word rather than a number.

Eleven channels, all backed by data already in the app — trendingTags,
userStreak, the leaderboard queries, live rooms, opportunities, poll
posts. The label doubles as the switcher, so the control costs no
space the row was not already spending. The choice lives in
localStorage and is read after mount, so server and client agree on
the first paint.
MOCK-UP — not for merge. Wired unconditionally so the design can be
reviewed on a preview deployment rather than only in Storybook.

The dock is sticky rather than fixed: fixed positions against the
viewport, so it spans the whole window and slides under the left
sidebar. Sticky keeps it in flow inside the layout's padded main,
which is where the sidebar offset already lives, so it spans the feed
and follows that offset across both layout variants without knowing
what either is.

Missing before this could ship: an ad-service source instead of the
fixture, a feature flag, impression logging, and the rail wired to
its real queries.
Stories for the shipped design and for the exploration behind it: five
placements evaluated over a real feed, the dock and its eleven rails,
ten ways to hold a bar at the bottom, and ten technical answers to the
link tooltip.

The rejected explorations live in the storybook package rather than in
shared. They are decision-making artifacts, not product code, and
nothing outside their own stories imports them.

The tooltip bench is a testbed rather than a gallery: hovering a card
draws a stand-in at the real size and corner, and the feed's link
length is switchable, because the card href comes from the post slug
and benching against a short one hides the problem entirely.

Storybook's tailwind config now scans packages/extension. Utilities
used only there generated no CSS, so ShortcutLinks and any story
importing an extension component rendered a layout the app would not.

Mockup-to-eng-pass: 1
The breadcrumbs started flush against the site header — 0px above
them — and then sat 16px clear of the tab strip, so the two read as
one loose block instead of a heading with its tabs under it.

16px above the breadcrumbs, 8px between them and the tabs. The 16px
from the tabs down to the cards is unchanged.

Cherry-picked from #6516. Byte-identical, so that PR merging is a
no-op here.

Mockup-to-eng-pass: 1
The previous commits added a gutter on FeedContainer while FeedPage
was already applying `laptop:p-10` in the legacy layout. Both were in
the same ancestry, so they stacked — and because `isV2` resolves after
mount, which of them applied changed as the flag settled. The feed
visibly jumped between a 64px inset and a 24px one depending on
whether you loaded the page, refreshed it, or navigated to it.

FeedPage now sets the inset for both variants and is the only place
that does: `feedGutterV2` under v2, the existing `pageMainClassNames`
otherwise. v2 previously set nothing at all, which is what left the
cards flush against the frame.

The breadcrumbs and tab strip sit outside FeedPage, so they take the
same value through `useFeedGutter` rather than a hardcoded copy, and
it is in the memo's deps so it cannot go stale.

Cherry-picked from #6516. Byte-identical, so that PR merging is a
no-op here.

Mockup-to-eng-pass: 1
`isV2` is not stable across client-side navigation: it reads true on
load and false after a route change. Any inset keyed to it therefore
changed as you moved around the app — the feed sat 24px in on arrival
and 40px in after clicking a tab, and refreshing "fixed" it by landing
on the other state.

The feed now keeps one inset, `pageMainClassNames`, whatever the
layout experiment is doing. v2's special case is gone: it set nothing
at all, which is what left the cards flush against the frame.

The breadcrumbs and tab strip take the horizontal half of the same
value through the `feedGutter` constant.

Cherry-picked from #6516. Byte-identical, so that PR merging is a
no-op here.

Mockup-to-eng-pass: 1
…share

The feed renders through two different page containers depending on
layout and route: FeedPage, which carries `pageMainClassNames`, and
FeedPageLayoutList, which forces `!px-0`. Which one you get flips with
`isV2` — itself unstable across client-side navigation — so an inset
placed on either covered some routes and not others, and stacked with
the other where both applied. That is the whole bug: 64px on one
arrival, 24px on another, 0px after a route change.

FeedContainer is the only element common to both, so the inset lives
there now and nowhere else. FeedPage keeps its vertical padding and
gives up its horizontal, which it could not apply everywhere anyway.

One value, `px-4 tablet:px-6 laptop:px-10`, for every feed route and
both layout variants — 40px at laptop, matching what the legacy path
already gave. The breadcrumbs and tab strip share the constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit a366dd8)

Cherry-picked from #6516. Byte-identical, so that PR merging is a
no-op here.

Mockup-to-eng-pass: 1
Explore showed a large gap above the breadcrumbs on arrival that a
reload "fixed" — the same shape of bug as the horizontal inset, in the
other axis.

`disableTopPadding` was keyed to `shouldUseListFeedLayout`, and that
value is not stable: `enableSsrSafeLayout` forces the list layout
before mount and hands over to the real one after, so the page's 40px
top padding was present in one state and zeroed in the other. The
header sat 104px down or 64px down depending on how you arrived.

Explore now keeps that padding in both layouts. It is the one feed
that renders a breadcrumb and tab header above the cards, and with the
padding zeroed that header is jammed against the site header.

The `pt-4` added earlier to the Explore header goes away with it. It
was compensating for this missing padding, which is why the gap read
as too large in the state where both applied. Production has no such
compensation, and Explore now matches it: 104px to the breadcrumbs,
64px of header offset plus the page's 40px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cherry-picked from #6516. Byte-identical, so that PR merging is a
no-op here.

Mockup-to-eng-pass: 1
CodeRabbit takes the paid slot and NVIDIA becomes a regular partner
mark, silhouetted with the rest of the wall.

CodeRabbit has the same problem NVIDIA did: an orange mark with a
near-black (#171717) wordmark, which is the only version the
advertiser library ships and which disappears on the dark feed. So it
renders inline as well — the mark keeps #FF570A, the wordmark takes
`currentColor` and flips black to white with the theme. The split is
by fill colour rather than by eye: ten #171717 paths are the wordmark,
#FF570A is the mark, #FEFEFE is the detail inside it.

NVIDIA is not in the advertiser library, so its silhouette source is a
data URI rather than a hosted file — the app and Storybook would
otherwise each need their own copy, and only one of them had it last
time.

Verified in both themes: wordmark computes to white on dark and
#0f1218 on light, mark holds #FF570A in both.

Mockup-to-eng-pass: 1
Leaves twelve marks: eleven from the advertiser library plus NVIDIA.

Mockup-to-eng-pass: 1
"Hot right now" becomes "Breaking news", in the rail, the switcher
menu and the feed wiring's default.

The label also sat 4px right of "Made possible by" above it: the
dropdown trigger carries `px-1` so its hover chip has something to sit
on, and that padding pushed the text in. `-ml-1` cancels it, so the
two rows start on the same left edge and the chip keeps its padding.

Mockup-to-eng-pass: 1
…the wall

NVIDIA returns to the paid slot with its two-tone lockup: symbol on
#76B900, wordmark inheriting `currentColor` so it flips with the
theme. CodeRabbit becomes a regular silhouetted mark, taken from the
advertiser library like the rest of the wall.

The data URI that let NVIDIA be masked in the wall goes with it — it
was only needed because NVIDIA has no library asset, and the lead slot
renders inline instead.

Both lockups stay: the one not currently leading appears in
LogoTreatment's candidate list, which is where the two-tone argument
is made, so neither is dead code.

Wall stays at twelve.

Mockup-to-eng-pass: 1
CodeRabbit's mark carries a white detail painted inside the orange
shape. The wall silhouettes through a CSS mask, which reads paint as
opaque, so the flat asset came through as a featureless disc rather
than the rabbit.

The lockup now renders inline in the wall as well, and in that
treatment the mark and its detail become one path under `evenodd`, so
the detail is a hole. Colour treatment is unchanged.

`Sponsor.Artwork` is therefore used in both treatments and is told
which one it is drawing, via a shared `LockupProps`. NVIDIA's lockup
takes the same prop: its symbol keeps #76B900 in colour and joins the
silhouette in the wall, so either sponsor can hold either slot without
a second asset.

This is the general fix for the knockout problem the LogoTreatment
story documents — it just needs an inline lockup rather than a flat
file, which is what the two sponsors that hit it already have.

Mockup-to-eng-pass: 1
…ped hole

The faithful monochrome transcription of the mark keeps the brand disc
and knocks the rabbit out of it. That is correct and unreadable: at the
16-20px the partner wall gives a logo, the disc is the ink and the
rabbit is a smudge of background, so the mark reads as a plain circle
next to eleven other light-on-dark glyphs.

Drop the disc in the silhouette treatment and draw the rabbit itself.
The colour treatment, which has the orange to carry the disc, is
unchanged.

Mockup-to-eng-pass: 1
…bbit-shaped hole"

This reverts commit fc8a5cd. The wall shows the knockout — disc plus
rabbit-shaped hole — which is CodeRabbit's actual mark. Rabbit-only
reads better at 16-20px but is artwork we invented for them, and this
is a paid slot.

Noted in the file so it does not get "fixed" again.

Mockup-to-eng-pass: 1
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