feat(explore): run the new tab's hijacking strip on the Explore hub for anonymous visitors - #6613
Conversation
…or anonymous visitors Search engines land logged-out visitors on the Explore hub — the explore feed and its sort tabs, the tag and source pages, the directories, the leaderboard and the Squad directory — and none of those pages made the new tab's signup pitch. This runs the new tab's hijacking strip there, on the same `hijacking_variants3` arm, so the surfaces share one experiment and one design. The strip moves from the extension into shared as `auth/HijackingLoginStrip`, with the new-tab-only concerns turned into props: the extension keeps its layout-v2 gate (the slot it renders in disappears under v2) and its handoff to the onboarding page (its origin cannot run social OAuth); the webapp opens auth inline. The headline is a prop too, because "Own your new tab" is wrong on daily.dev/tags. `ExploreSignupStrip` is the webapp wrapper: anonymous visitors only, since members already have the feed it offers. Two things the move surfaced: the webapp's `onboardingUrl` is a bare path, so the href is now string-built instead of `new URL()` — which threw at module load and took every page importing MainFeedLayout down with it; and the in-feed placement needs `feedGutter` on the strip itself, because the horizontal inset lives on `<Feed>` rather than on the page layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The Explore hub's strip reaches organic and search traffic, which is not the new-tab experiment's population, so enrolling those visitors in `hijacking_variants3` measures nothing. The strip now renders the arm that experiment settled on (the cat-stage hero) directly, without evaluating the flag; `HijackingHeroStrip` is exported for that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#6611 took the phone gutter off `feedGutter` because feed cards run full-bleed there and carry their own inset. The Explore strip rode that gutter, so on phones it ended up flush against the screen edges — a rounded, bordered card cannot do what a full-bleed card does. It keeps its own `px-4` below tablet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of #6613 found the Explore strip was borrowing the new tab's whole state machine, and paying for it four ways: - The extension new tab renders MainFeedLayout too, and its in-place Explore tab resolves to OtherFeedPage.Explore, so the Explore strip rendered inside the extension — stacked on the extension's own strip on layout v1, and opening auth inline where the extension origin cannot run OAuth. It is now webapp-only. - HijackingHeroStrip short-circuits to the "Welcome back" panel when a signBack record exists, and that record survives logout. A visitor who once logged in saw their name and email on a public, indexed page. The Explore surface now renders the cover strip's visual directly, with its own copy and its own inline auth. - Every impression carried the experiment's target_id. The Explore strip logs under its own. - Importing the experiment module pulled the registration forms into the shared mainFeedLayout chunk and the Squad directory. Only the cover strip's visual is shared now (`HijackingCoverStrip`); the experiment moves back to the extension, where its single consumer is. Two smaller things the review caught: the strip rendered null until boot answered, so it was spliced in above the H1 on statically generated pages — it now holds its slot with a placeholder of the same height; and the Squad directory's section gained an unconditional gap-6 that spaced the loading skeleton for logged-in members who never see the strip — the strip now sits between the header and the section and carries its own margin. The Explore hub's feed list moves into useFeedName beside the other feed lists, and sources/[source] uses its own inset constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…b's copy Profiles are the other public page search engines land on, so the strip goes into ProfileLayout's main column — once, for every profile tab. It is inset with padding rather than margin: the section is full-width, and a margin on a full-width box overflows the phone viewport. The copy is the new tab's own, with only the headline pointed at the feed instead of the tab. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Phones already carry the header's Log in / Sign up pair, so a second signup surface there is noise. The strip and its boot-time placeholder are hidden below tablet, and the phone inset the in-feed placement carried for it goes with them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The phone gate was CSS only, so every anonymous phone visitor to the Explore hub, the tag and source directories, the leaderboard, the squad directory and public profiles fired an explore-strip impression for a card their viewport never paints. That is most of the logged-out search traffic these pages get, so the surface's CTR would have been unusable. Give the impression the matching JS gate with useViewSize(Tablet); the visual gate stays in CSS so the slot is still in the SSR HTML and hydration does not reflow the H1. Adds the component's first spec, covering anonymous, logged-in and boot-pending renders, the phone impression veto, and both click paths.
tsahimatsliah
left a comment
There was a problem hiding this comment.
Summary
The extraction of HijackingCoverStrip out of the extension is clean and the extension's cover arm keeps its sizer-based height parity, so the new-tab experiment is intact. The rollout of the strip itself is what needs a decision before this lands.
Blocking
1. This ships a new signup surface to 100% of anonymous traffic with no flag and no rollback short of a deploy.
The PR adds the strip to fourteen route families — the Explore sort tabs, /explore/[tag], /discussed, the tags and sources directories and their detail pages, the leaderboard, every squad-directory tab and every public-profile tab — for every anonymous visitor at once. AGENTS.md and house convention are that a behaviour change of this shape arrives as a GrowthBook experiment with a false default, and this one has neither a flag nor an enrollment. Not enrolling in hijacking_variants3 is right (different population), but the conclusion should be a new flag for this surface, not no flag.
It matters more than usual here because the PR's own "Known trade-off" section is the problem: /tags, /tags/[tag], /sources, /users and profiles already render PublicPageSignupBanner behind featurePublicSignupBanner for exactly this audience. On merge, a visitor in that arm gets two competing signup surfaces on the same page, and neither surface's numbers are attributable afterwards — the existing banner's experiment is contaminated by a change that is not part of it. "The flag can turn the bottom one off without a code change" is only true if someone turns it off before this ships; as written, the default is double-serving.
Suggested direction: put the strip behind its own flag defaulting to off, and decide the interaction with featurePublicSignupBanner explicitly (mutually exclusive arms, or the strip suppressing the banner on the pages that carry both) rather than leaving it to flag config.
2. Impressions were logged on phones where the strip is CSS-hidden. — fixed in 4ac3e6a, see below.
The phone gate was hidden tablet:block, but the impression effect ran for every anonymous visitor regardless of viewport. Phones are most of the logged-out search traffic these pages get, so impression / signup button / explore strip would have been dominated by visitors who never saw the card and the surface's CTR would have been unusable — and it would have failed silently, since the number still looks plausible. I've pushed the JS gate (useViewSize(ViewSize.Tablet)) on the impression only, keeping the visual gate in CSS so the slot stays in the SSR HTML, plus the component's first spec covering anonymous / logged-in / boot-pending renders, the phone impression veto and both click paths. This mirrors GetAppButton's CSS-gate-plus-JS-veto pattern.
Non-blocking
- The boot placeholder moves the layout shift onto signed-in users.
!isAuthReadyis true during SSR for everyone, so the 14rem placeholder is in the served HTML on every one of these routes. Anonymous visitors get the intended no-jump swap; signed-in visitors get a 14rem block above the H1 that collapses once boot answers, i.e. an upward shift on every page load at tablet and up. That may well be the right trade given these are search-landing pages, but it is a CLS cost paid by the majority cohort and it isn't mentioned in the PR. Worth confirming against a Lighthouse/CLS reading on/tags/[tag]signed in before this goes to 100%. ProfileLayoutplaces the strip abovechildren, so an anonymous visitor arriving from search sees a full-width signup card before the profile they searched for. On the directory pages the strip sits above the hero too. Intentional? A placement below the hero would keep the page's own answer first and is likelier to convert on intent-driven traffic.- Comment density. Root
AGENTS.mdasks that the reasoning behind a change go in the commit message rather than above the code; the two new files carry six explanatory blocks. Not worth a round-trip on its own, but the copy-provenance and the phone-gate rationale in particular read as PR-description material. - Impression fires on mount, not on view. Fine where the strip is above the fold, which is everywhere it currently renders — flagging only so it is a known property if the placement moves down the page later.
Verification
- Root and package
AGENTS.mdread - Consumers enumerated:
SquadDirectoryLayout(4 webapp squad-discover pages, none in the extension),TagTopicPage(/tags/[tag]),TagsDirectoryPage(/tags),ProfileLayout; no route renders the strip twice -
isExploreHubtraced throughuseFeedName;!isExtensionguard confirmed inMainFeedLayout - SSR / boot-pending / anonymous / logged-in paths traced
-
packages/sharedlint,typecheck-strict-changedand the new spec run locally and pass - CLS on a signed-in load of these routes with the placeholder in the SSR HTML
- Product decision on the flag and on double-serving with
PublicPageSignupBanner
Verdict
Request changes — on the rollout, not the code. The extraction and the wiring look right; the flag and the PublicPageSignupBanner overlap need resolving before merge.
Reviewed by AI.
…nd Worlds, and one per source page Placement, from review of the preview: - Explore feeds: the strip now sits above the breadcrumbs and sort tabs rather than between the tabs and the cards. The feed-width wrapper it shares with the engagement banner becomes a constant so both use it. - Discussions: the comment column has no gutter, so the strip drops `feedGutter` there and matches the cards edge for edge. - Squad directory: above the category tabs. - Watercooler and the Worlds directory get the strip, at the top of their content column. - Source pages showed two signup surfaces — this strip at the top and `AuthenticationBanner` fixed at the bottom. The bottom one goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pages-dailydotdev # Conflicts: # packages/webapp/pages/sources/[source].tsx
…cover art Review of #6613, second pass: - The boot-time placeholder rendered for everyone and collapsed once boot named a member — a 224px reflow on every route, for the audience that never sees the strip. The cached boot user reaches AuthContext a commit after hydration, before boot answers, so the member check now comes first; the SSR HTML is unchanged because that first client render still has no user. - The placeholder put the min-height on the same box as the caller's padding, so padded placements (Worlds, profiles) still shifted 32px when the real strip arrived. It now renders the strip's own box, empty, from the same file. - Below tablet the strip was CSS-hidden, and a display:none <img> still downloads — every anonymous phone visit fetched the 2072px cover art. The strip is only ever client-rendered after boot, so it simply does not render below tablet; the placeholder keeps the CSS gate it needs for the server HTML. - The impression uses useLogEventOnce (hooks/AGENTS.md names it for fire-once events), the target id joins TargetId, the art asks for high fetch priority, the duplicate min-height on the card goes, and the Watercooler placement inherits its header block's gutter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| ButtonVariant, | ||
| } from '@dailydotdev/shared/src/components/buttons/Button'; | ||
| import { ProgressBar } from '@dailydotdev/shared/src/components/fields/ProgressBar'; | ||
| import { ExploreSignupStrip } from '@dailydotdev/shared/src/components/auth/ExploreSignupStrip'; |
There was a problem hiding this comment.
Why have you added it to the world? We already have signup there
What
Search engines land logged-out visitors on the Explore hub and on public profiles, and none of those pages made the new tab's signup pitch. This renders the new tab's Cover strip there — the cover art with centered copy and Sign up / Log in — for anonymous visitors only, on the webapp only, from tablet up — phones already carry the header's Log in / Sign up pair.
It is not enrolled in
hijacking_variants3: this is organic and search traffic, not the new-tab experiment's population, so evaluating that flag there would measure nothing.Pages
/posts,/posts/upvoted,/posts/discussed,/posts/latest/posts/best-of/explore/[tag]/discussed/tags·/tags/[tag]/sources·/sources/[source]/users/squads/discover/*/[userId],/[userId]/posts,/replies,/upvoted, …/watercooler/worldCopy
The new tab's own copy, with only the headline pointed at the feed instead of the tab:
How
packages/shared/src/components/auth/HijackingCoverStrip.tsx(copy, click handlers, optionalsizer,className). The extension's cover arm renders it with its two invisible sizers, so the height-parity rule of the new-tab experiment is intact — its spec still passes. The experiment's state machine stays in the extension, where its only consumer is.ExploreSignupStripis the webapp wrapper: gates onisAuthReady && !user, opens auth inline viashowLogin, logs impressions and clicks under its owntarget_id(explore strip), and holds its slot with a same-height placeholder until boot answers so the H1 on statically generated pages does not jump.isExploreHub) lives inuseFeedNamebeside the other feed lists;MainFeedLayoutrenders the strip for those feeds on the webapp only (the extension new tab renders the same layout and has its own strip).ProfileLayout's main column, the Squad directory throughSquadDirectoryLayout— one insertion each, every tab covered.AuthenticationBanner); the bottom one is removed there.Verification
HijackingLoginStrip.spec22/22), shared 379 / 2708, webapp 86 / 683. Lint andtypecheck-strict-changedclean; full webapptschas no non-test errors.Known trade-off, not changed here
/tags,/tags/[tag],/sources,/usersand profiles still renderPublicPageSignupBanner(fixed bottom, behindfeaturePublicSignupBanner) for the same anonymous audience, so a visitor in that arm sees two signup surfaces. Which one wins is a product call; the flag can turn the bottom one off without a code change.🤖 Generated with Claude Code
Preview domain
https://hijack-strip-explore-pages-daily.preview.app.daily.dev