Skip to content

feat(monetization): sponsor strip experiment (footer_logo) - #6605

Open
rebelchris wants to merge 16 commits into
mainfrom
feat/sponsor-strip
Open

feat(monetization): sponsor strip experiment (footer_logo)#6605
rebelchris wants to merge 16 commits into
mainfrom
feat/sponsor-strip

Conversation

@rebelchris

@rebelchris rebelchris commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

A sponsored logo wall docked under the main feeds, with the breaking-news ticker stacked under it. Design comes from the mock-up in #6490 — this is the "breaking news" concept of it, wired to a flag and to real tracking.

Behind sponsor_strip, a boolean defaulting to isDevelopment — on locally, off in production, the way cores, quest_offers and post_boost_ads are. GrowthBook owns the ramp.

Slots

  • 1 gold — leftmost, keeps its brand colour, the only mark with a hover. A second gold creative is dropped rather than demoted.
  • 4 premium — left of the wall, a hair more ink, and deliberately the scarcer tier.
  • Community — takes whatever the measured row has left, so it always has more slots than premium.

Turnover is per page load: the decks are dealt once on mount, so the row holds for as long as the page does and the next load deals a different set — no advertiser is permanently first, or permanently the one a narrow window drops. Nothing rotates on a timer.

Tracking

Four signals per logo, per slot, all through one builder so the placement can be cut by tier and slot:

Signal Mechanism
impression log event + ad.pixel[] (viewport-gated, as elsewhere)
viewable impression useViewability (MRC/IAB 50% for 1s) + viewed=true pixel re-fire
click useAdClickUrl + rel="sponsored"
air time new duration eventlogEventStart/logEventEnd, so it lands with event_duration = how long one logo held one slot

Every event carries extra: { placement: 'footer_logo', tier, slot_index, gen_id }.

@idoair time is a new event name and needs to be known downstream; it's the same logEventStart/logEventEnd duration mechanism feed-ad impressions already use.

Happening Now

The strip is where breaking news goes while it's up, so the feed drops its Happening Now card — but only when the strip is actually carrying headlines. Strip up with no fresh headline, Plus, ads disabled, or below tablet: the card stays. The user's own highlightsPlacement setting is never written, so the card returns when the experiment ends.

Not final

  • Ad source is a fixture (mockSponsorStripAds.ts), in the exact wire shape the schema parses, so swapping it is one call: GET /v1/a/footer_logo?count=<n> plus the consent params in lib/ads.ts. Note AdPlacement.SponsorStrip has no case in resolveAdFetchOptions/fetchAdByPlacement yet — today it would fall through to the feed endpoint, so that switch needs a branch when the placement lands.
  • Logo assets need a spec. The wall silhouettes its marks through a mask, which needs real vector assets: gitlab.svg on business.daily.dev is a base64 WebP wrapped in an <svg> and masks to a solid block, so it's left out of the fixture. Real creatives are expected to carry a themed light/dark pair.
  • mockFeedHighlights.ts injects a Happening Now card and the ticker's headlines locally (development only). Delete both mocks when the experiment is decided.

Testing

pnpm --filter shared test (2827 pass, 59 in the strip), pnpm --filter webapp test, pnpm --filter extension test, strict changed-file typecheck, lint. Rotation rules, tier partitioning, optical sizing, creative parsing, the gate, the suppression rule, and all four tracking signals have specs.

To see it locally: force sponsor_strip on and load a feed with ?mockHighlights=1 to get the card for comparison with the flag off.

🤖 Generated with Claude Code

Preview domain

https://feat-sponsor-strip.preview.app.daily.dev

A logo wall docked under the main feeds — one static gold slot, four
rotating premium slots and a community row that fills whatever the row
has left — with the breaking-news ticker stacked underneath it. While the
strip is carrying those headlines, the feed drops its Happening Now card.

Impressions, IAB viewable impressions, clicks and a new `air time`
duration event are logged per logo, keyed by tier and slot. The ad source
is a fixture until the `footer_logo` placement exists on the ad server.

Behind `sponsor_strip`, default off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Sep 8, 2026 6:32am UTC
1 Skipped Deployment
Project Deployment Actions Updated
storybook Ignored Ignored Sep 8, 2026 6:32am UTC

Request Review

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

A large, well-tested feature, but as pushed it does not ship default-off — the committed flag default turns the strip on for every eligible reader on merge, and the ad source it renders from is an unguarded fixture of real third-party logos. Those two together are the blocker; the rest is non-blocking.

Blocking

  1. lib/featureManagement.ts:273 — flag default is enabled: true.
  2. sponsorStrip/mockSponsorStripAds.ts — fixture is not environment-gated, unlike mockFeedHighlights.
  3. CI — the daily-webapp Vercel deployment is red on this commit; the preview URL in the description can't be verified until it's green.

Non-blocking

  • Comment volume against the repo's code-style rule (AGENTS.md: comments explain why, and are rare — no multi-line preambles above a component, hook or constant). A sizeable share of the added lines is prose that will drift as the code moves; the same reasoning is already in this PR description, where it stays accurate.
  • AdPlacement.SponsorStrip has no branch in resolveAdFetchOptions/fetchAdByPlacement (noted in the description). Since the enum value is now live, a future caller falls through to the feed endpoint silently — either add the branch or keep the value out until the placement exists.
  • Verification I could not do from the diff: the sticky bottom-0 dock inside the extension new-tab shell, and the two out-of-tree consumers of --sponsor-strip-height (ScrollToTopButton, FeedbackWidget) with the property unset on every other surface. Worth a screenshot each on webapp + extension at tablet and laptop widths.

Reviewed by AI.

export const featureSponsorStrip = new Feature<SponsorStripConfig>(
'sponsor_strip',
{
enabled: true,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the default is enabled: true, three lines under a comment that says it must stay false, and against the root AGENTS.md rule ("NEVER default an experiment flag to true").

useSponsorStrip returns isEligible && config.enabled, and useConditionalFeature falls back to this committed default whenever GrowthBook has no value for sponsor_strip — so on merge every non-Plus, tablet-and-up reader on a hero-eligible feed gets the strip, and loses the Happening Now card via disableHighlightItems. There is no rollback short of a deploy. The PR description says "default off", so I read this as an unintended leftover from local testing.

The specs don't catch it: useSponsorStrip.spec.tsx mocks useConditionalFeature with enabled: true, so the real default is never exercised. Set it to false and add a case asserting the committed default is off.

Reviewed by AI.

wallSponsor('Sentry', 'sentry', 512 / 113, SponsorTier.Community),
];

export const fetchSponsorStripAds = async (): Promise<unknown> =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this fixture has no environment guard. mockFeedHighlights puts isDevelopment inside the module precisely so no query param can put a fixture in front of a real reader — the ad source needs the same treatment, and needs it more.

Combined with the flag default above, merging renders real third-party marks (Datadog, PostHog, ClickHouse, Sentry, JetBrains, …), hotlinked from business.daily.dev, in a row labelled "Made possible by", to production readers — companies presented as paying sponsors of daily.dev who have not bought the placement, with outbound ?utm_source=sponsor_strip links. That's a brand and legal exposure, not just a data-quality one, and it also feeds real impression/viewable impression/air time/click events for invented gen_ids into the warehouse.

Even with the flag correctly off, the fixture and its links ship in the bundle. Suggest fetchSponsorStripAds returns [] unless isDevelopment, and that the wall marks become neutral placeholder shapes rather than identifiable brands until the footer_logo placement is live.

Reviewed by AI.

globalThis.removeEventListener('statechange', onLifecycle);
logEventEnd(key);
};
}, [genId, slotIndex, logEvent, logEventStart, logEventEnd]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: logEvent/logEventStart/logEventEnd are in the dep array, so any re-memoization of the log context value re-runs this effect — which ends the open air-time event and logs a second impression for a logo that never left its slot. sponsor is already held in a ref for exactly this reason; the three callbacks are the same kind of dependency. Worth putting them behind a ref too so the effect keys only on genId/slotIndex.

Reviewed by AI.

const { data } = useQuery({
...majorHeadlinesQueryOptions({ first: HEADLINE_LIMIT }),
enabled,
refetchInterval: ONE_MINUTE,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: refetchInterval: ONE_MINUTE means every eligible reader polls majorHeadlines once a minute for the whole time a main feed is open, on top of the highlight items the feed request already carries. Against a 24-hour freshness window a minute buys very little — what's the freshness requirement here? A few minutes reads identically and cuts the request volume by the same factor.

Reviewed by AI.

}: SponsorStripProps): ReactElement | null => {
const { gold, premium, community, wallRef } = useSponsorStripAds({
enabled: true,
config,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: enabled: true is hardcoded at the only call site, so the enabled prop on useSponsorStripAds (and the enabled it forwards into useRotatingSlots) can never be false — the strip is already gated by sponsorStrip.isEnabled in MainFeedLayout before this mounts. Dead parameter; dropping it removes a branch from the rotation hook too.

Reviewed by AI.

The default was committed as `enabled: true`, which would have shipped the
experiment to everyone on merge with no rollback short of a deploy. On in
development and off in production instead, the way `cores`, `quest_offers`
and `post_boost_ads` are — so a local session still gets the strip while
GrowthBook owns the ramp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Turnover is per page load rather than on a timer: the decks are dealt once
on mount, so the row a reader sees holds for as long as the page does and
the next load deals a different set. That drops the rotation intervals,
and with them the whole config payload — the flag is a boolean like
`cores`, which also removes the chance of a mistyped interval reaching
`setInterval`.

Slot scarcity is unchanged and now stated where it lives: one gold, four
premium, community takes whatever the row has left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`feedGutter` collapses to `px-0` inside the v2 floating card, because the
feed's inset there comes from the grid being narrower than the frame rather
than from padding. The dock spans the frame edge to edge, so it inherited
nothing and its first logo sat against the sidebar. It carries the same
values `feedGutter` uses outside the frame instead, which is where the
cards start in both layouts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rebelchris and others added 2 commits September 7, 2026 10:19
Replaces the fictional Quantile placeholder in the paid slot, per the
design mock-up.

The mark is the official four-colour Google Cloud logo, reusing the exact
asset the Explore ad-card mock-up already established in this repo rather
than sourcing a second copy of it. Its white backing plate is dropped:
that plate exists to round-crop the mark into a favicon, and on the strip
it would sit as a white tile against the dark ground.

The slot keeps its two-ink design — the mark holds its brand colour in
both themes while the wordmark flips — which is the case a single flat
file cannot serve and the reason the wire contract takes a themed pair.
The wordmark is set in the fixture's own stack rather than Google Sans; a
licensed face cannot be inlined, and it is the part a real creative
replaces anyway.

Still a fixture behind the experiment flag. Nothing here says the slot
has been sold, and "Made possible by" claims more than a display
campaign does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ticker filtered `majorHeadlines` to a 24 hour window client-side, so
on any day the feed went quiet the row emptied and the dock silently
dropped from two rows to one. That is not rare: checked against the live
API, all twelve headlines returned were 36 to 92 hours old and every one
was filtered out, leaving the row gone with nothing to tell the reader
why. The development-only fixture underneath papered over exactly this
on one environment while production had the same hole.

The window was never right for this surface. The row stands in for the
feed's Happening Now card, which applies no age filter at all — it
renders whatever the backend serves — and neither does /highlights. The
gate was copied from the post-page sidebar widget, the one surface of
the three that has it. Deciding what still counts as a major headline is
the backend's call; a second opinion held only by this row just emptied
it.

So the filter goes, and the fixture with it — the API already returns
newest first and every row renders its own relative timestamp. Stays on
the light `majorHeadlines` query the widget uses (4.4KB) rather than the
/highlights document, which carries post bodies at 147KB for the four
fields this row needs.

Also refreshes the `DOCK_GUTTER` note: main dropped `px-4` from
`feedGutter`, so the two constants no longer match below tablet, where
the dock does not render anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…roll

`disableHighlightItems` was read off the headlines having arrived, and the
headlines are their own round trip that lands after the feed has painted.
So the feed rendered its Happening Now card, the query came back, the flag
flipped, and the card was pulled out of the middle of the feed with
everything below it jumping up.

The old 24 hour window hid this: headlines were usually filtered to
nothing, so the flip mostly never happened. Serving them unfiltered made
it fire on every load.

The strip stands in for the card whenever it is up, so that is what the
decision reads now. `useStripHeadlines` reports whether its query has
answered, and the card is given back only when it settles with nothing at
all — breaking news must never fall out of the product entirely just
because the experiment is on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sponsor row and the ticker are two independent round trips into a dock
pinned to the bottom of the viewport. The ads resolve first, so the paid row
painted alone against the viewport floor, and when the headlines landed a
second row appeared underneath it and shoved the logos upward — a visible
jump on the row somebody paid for, on every load.

Each row now holds its own height open until its own query answers, and
collapses only once that query has answered with nothing to show. The empty
states are unchanged: no fill still means no sponsor row, no headlines still
means no ticker, and neither still means no dock.

`useSponsorStripAds` reports whether the ad query has settled, and
`useSponsorStripFeed` the same for the headlines. The ticker's impression log
is now conditional, because the row is mounted empty while it waits and an
impression logged then reported a ticker of nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sticky bottom-0` reaches the viewport bottom only while its containing
block extends past that line. Inside the v2 floating card the block stops
14px short — `laptop:my-3` on the wrapper plus `laptop:p-0.5` on the frame
— so the dock had two resting places: 14px high whenever the card's own
bottom was what was on screen, and flush when it was not. On first paint
the card is `min-h-[calc(100vh-1.5rem)]` and the page is not yet
scrollable, so the dock loaded high and dropped once the feed filled. The
same jump was visible scrolling to the end of the feed.

Pinning to the frame's own inset makes both positions one position, so the
dock rests on the card's bottom edge and stays there. Keyed to the frame's
class rather than to `isV2`, the way `feedGutter` is, because the flag
resolves after mount; outside the frame there is no inset and the offset
stays zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinning the dock to the frame's inset stopped it jumping but parked it
14px above the window, which is not where a dock belongs. It now sits
flush and stays there.

`sticky bottom-0` reaches the viewport bottom only while its containing
block extends past that line, and `laptop:overflow-clip` on the v2 frame
means a sticky child can never paint into the frame's gutter. So the frame
gives the gutter up instead: where it holds a dock it drops its bottom
margin, padding, border and corner rounding, and the dock becomes its
bottom edge. Everywhere else the frame is unchanged.

Matched with a `has-[.feed-dock]` variant rather than a prop, because the
frame is two components above the dock's mount point, and written as a
literal because Tailwind scans source text and generates nothing for an
interpolated class name. Verified the four rules land inside the laptop
media query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…a dock

The frame's `min-h` subtracts the chrome above it plus its own margins, so
it finishes exactly at the bottom of the window. Dropping its bottom margin
for the dock left that sum 0.75rem too large: the frame stopped 12px short,
and a `sticky bottom-0` dock rests at its container's end rather than the
window's, so the dock sat 12px high for as long as the feed was too short
to make the page scrollable, then dropped when it wasn't.

Both values lose the margin that is no longer there, which puts the frame's
bottom edge back on the window and the dock flush in either state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rebelchris and others added 2 commits September 7, 2026 14:24
Four things from review.

Alignment: `DOCK_GUTTER` was `feedGutter`'s values copied by hand, and had
already drifted — main dropped `px-4` from `feedGutter`, and more importantly
`feedGutter` collapses to nothing inside the v2 frame while the copy kept
`px-10`, so the dock's contents sat 40px in from the cards above them. The
copy is gone; the dock uses `feedGutter` itself, plus the same width clamp
the grid uses on wide screens, now shared as `feedWidth` rather than spelled
out again. Only the contents are held to those edges — the border and ground
still span the dock, so it reads as one bar under a column of cards.

Hover: the gold mark's scale-up on hover is gone.

Gold sizing: the mark was being optically normalised along with the wall.
That normalising trades height for width to stop a dozen unrelated marks
fighting, which punishes exactly the wide mark-and-wordmark lockup a paid
slot supplies — it came out at 18px, shorter than the tallest silhouette
beside it. It is drawn at a flat 28px now and the width follows. The
wordmark also moves off Verdana, which is far wider than the real one.

Ticker: scrolls horizontally instead of only clipping, so the headlines
under the fade can be reached. Scrollbar hidden; the fade already says the
row continues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gold slot carried the cloud-symbol lockup, drawn from the devicon
mark the Explore ad-card mockup uses, with "Google Cloud" set beside it
in a font stack standing in for Google Sans. Google no longer leads with
the symbol: cloud.google.com serves
gstatic.com/cgc/google-cloud-logo-fullcolor.svg — the four-ink Google
wordmark followed by "Cloud", no symbol, in their refreshed palette
(#3186FF, #FC413D, #FFBE00, #00AF57) rather than the classic four. That
file is the artwork now, so the paid slot shows the mark the advertiser
actually publishes instead of an approximation of it.

"Cloud" ships as #212226 — near-black, and it all but disappears against
the dark feed — so it stays the one ink taken off their file and flipped
with the ground, which is what the light/dark pair in the wire contract
is for. Nothing else about the artwork changes.

GOLD_HEIGHT is the slot's box height, and the swap changes what fills it:
a symbol lockup spends most of that box on the symbol, an all-wordmark
lockup spends it on letterforms. At 28 the type came out roughly twice
the size of the old one, so the box drops to the wall's own ceiling — in
colour and half again as wide as a wall slot, it still leads the row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`feedGutter` is where the feed's column starts in the legacy layout, and
the dock has used it since the hand-copied `DOCK_GUTTER` went away. Inside
the v2 floating card it is zero — the card is already the frame — but the
grid then takes a `laptop:p-6` inset of its own so the cards sit off the
frame's rounded corners. So in v2 the cards start 24px further in than the
gutter does, and the dock's "Made possible by" and "Breaking news" sat a
full inset to the left of the column they belong to.

Measured on the preview by adding the frame class and then the grid's own
inset to a live feed: card left 44 → 68 while the dock's label stayed at
44. That 24px is the whole bug, and it is why this looked correct in v1 —
there is no inset there for the gutter to miss.

`feedFrameInsetX` carries it, next to `feedGutter` and `feedWidth`, so the
three things that decide where the feed's edges are sit together rather
than one of them being rediscovered per consumer. Horizontal only: the
grid's vertical half is the frame's breathing room, and chrome outside the
grid sets its own height.

It rides its own element in both rows rather than joining the gutter and
the width clamp on one. All three set horizontal padding, and stacked on a
single element which one wins is left to the order Tailwind emits them in.
Nested, they compose: the gutter finds the frame, the inset finds the
cards inside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rebelchris rebelchris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of the last 15 commits. The flag default is fixed and CI is green, so the first blocker from the previous round is closed. The second one is not — the ad fixture still has no environment guard, and the gold slot now carries a real company's brand lockup, which makes shipping it a larger problem than it was a week ago. Two blocking comments inline; the rest are non-blocking repeats.

Closed since the last review

  • featureSponsorStrip now defaults to isDevelopment, matching cores / quest_offers / post_boost_ads. isDevelopment is NODE_ENV === 'development', so a production build cannot enrol anyone from the committed default. Good.
  • CI and the daily-webapp preview deployment are green on a2e1b0b.
  • The air-time effect's dependency array is fine after all — useLogContext returns a useMemo(..., []), so logEvent/logEventStart/logEventEnd are stable for the component's lifetime and the effect cannot re-run and double-log. Withdrawing that comment.

Blocking

  1. mockSponsorStripAds.ts:151 — the fixture is still ungated, so ramping the flag ships fabricated sponsorships.
  2. mockSponsorStripAds.ts:62 — a third party's logo reproduced as inline path data with one ink recoloured.

Non-blocking (all previously raised, still open)

  • useStripHeadlines.ts:38refetchInterval: ONE_MINUTE against a multi-hour freshness window.
  • SponsorStrip.tsx:172enabled: true is a dead parameter on useSponsorStripAds.
  • AdPlacement.SponsorStrip still has no branch in resolveAdFetchOptions / fetchAdByPlacement, so a future caller falls through to the feed endpoint silently.
  • Comment density against root AGENTS.md ("match its comment density … the reasoning behind a fix goes in the commit message, not a comment above the code"). This has grown rather than shrunk: mockSponsorStripAds.ts opens with a 26-line preamble and the new lockup constants carry three more prose blocks. The same reasoning is in the PR description, where it stays accurate as the code moves.

Still not verifiable from the diff

The has-[.feed-dock] compensations in MainLayout are a four-value contract between a class name in sponsorStripOffset.ts and literal Tailwind variants two components up, with min-h-[calc(...)] values that have to stay in step with the chrome above the frame. They are correctly scoped — nothing changes with the strip absent — but nothing can verify them either. Worth a screenshot each of: the dock at laptop and tablet width in the v2 frame, the dock in the extension new-tab shell, and ScrollToTopButton + FeedbackWidget lifting and settling back.

Reviewed by AI.

wallSponsor('Sentry', 'sentry', 512 / 113, SponsorTier.Community),
];

export const fetchSponsorStripAds = async (): Promise<unknown> =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (repeat). fetchSponsorStripAds still returns MOCK_SPONSOR_STRIP_ADS unconditionally. mockFeedHighlights in this same PR puts isDevelopment inside the module so no query param can put a fixture in front of a real reader; the ad source needs the same guard, and needs it more.

The flag default being isDevelopment closes the merge risk, not the risk this comment is about. The whole point of sponsor_strip being a GrowthBook flag is that someone ramps it — and the moment they do, on any percentage, production readers get a row labelled "Made possible by" holding a dozen named third-party marks that have not bought the placement, with rel="sponsored" outbound links carrying ?utm_source=sponsor_strip, plus real impression / viewable impression / air time / click events keyed to invented gen_ids landing in the warehouse next to real ad reporting.

The module comment says "this stays behind the experiment flag until the slot is actually sold", but nothing in the code enforces that — it relies on whoever ramps the flag having read this file. Make it structural: return [] unless isDevelopment. Then the flag can be ramped safely, the dock's empty-row path gets exercised by the ramp, and the fixture becomes what the ad server replaces rather than something that has to be remembered.

Reviewed by AI.

* symbol lockup is an older mark they no longer lead with, and the inks here
* are their refreshed palette, brighter than the classic four.
*/
const GOOGLE_WORDMARK = [

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking. This is new since the last review and it is the part I would not merge in any form.

The gold slot — the one the row presents as paid, at full height, in its own inks, behind a "Made possible by" label — now names a specific real company, in a public repository, with its wordmark hand-transcribed into eight <path> constants and one ink deliberately changed (CLOUD_WORD flipped from #212226 to the theme ground). Three separate problems, any one of which is enough:

  1. Commercial. Naming a real, identifiable company in the paid slot of a sponsorship strip, in a public repo, reads as a statement about a commercial relationship. Design mock-ups are internal; this file is not. Whether or not any conversation exists with that company, the repo should not be where it is implied.
  2. Trademark. Most brand-permission terms — theirs included — allow the mark only unaltered, from the files they publish. Re-drawing the lockup and recolouring one word to suit our dark theme is exactly the alteration those terms exclude. The same applies more weakly to the wall marks, which are hotlinked off business.daily.dev into a context that labels them sponsors of this placement.
  3. Reviewability (per the repo's own bar for opaque generated constants). Nobody reviewing this diff can tell what those path strings draw. There is no regeneration step, no source file, and no check that would notice if one path were wrong or drifted from what the vendor now serves — and if it renders wrong, it renders wrong as somebody's logo.

For judging weight, spacing and optical sizing — which is what the comment says the artwork is for — neutral placeholder shapes work identically: a coloured block and a word at the measured logo_ratio exercise the same code paths, the same masking, and the same theme pair, without naming anyone. If a real lockup genuinely has to be there to review the design, it belongs in a screenshot on this PR, not in main.

Reviewed by AI.

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.

2 participants