Skip to content

feat(explore): run the new tab's hijacking strip on the Explore hub for anonymous visitors - #6613

Merged
tsahimatsliah merged 13 commits into
mainfrom
hijack-strip-explore-pages-dailydotdev
Sep 7, 2026
Merged

feat(explore): run the new tab's hijacking strip on the Explore hub for anonymous visitors#6613
tsahimatsliah merged 13 commits into
mainfrom
hijack-strip-explore-pages-dailydotdev

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Sep 6, 2026

Copy link
Copy Markdown
Member

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

Page Route
Explore — Popular / By upvotes / By comments / By date /posts, /posts/upvoted, /posts/discussed, /posts/latest
Explore — Best of /posts/best-of
Explore tag feed /explore/[tag]
Discussions /discussed
Tags directory · Tag page /tags · /tags/[tag]
Sources directory · Source page /sources · /sources/[source]
Leaderboard /users
Squad directory (all tabs) /squads/discover/*
Public profile (all tabs) /[userId], /[userId]/posts, /replies, /upvoted, …
Watercooler /watercooler
Worlds directory /world

Copy

The new tab's own copy, with only the headline pointed at the feed instead of the tab:

Own your feed. Make it your dev briefing.
Sign in and daily.dev remembers the topics, saves, and discussions that matter to you.
[Sign up →] [Log in]

How

  • Only the cover strip's visual is shared: packages/shared/src/components/auth/HijackingCoverStrip.tsx (copy, click handlers, optional sizer, 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.
  • ExploreSignupStrip is the webapp wrapper: gates on isAuthReady && !user, opens auth inline via showLogin, logs impressions and clicks under its own target_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.
  • The Explore hub's feed list (isExploreHub) lives in useFeedName beside the other feed lists; MainFeedLayout renders the strip for those feeds on the webapp only (the extension new tab renders the same layout and has its own strip).
  • Placement: on Explore feeds the strip sits above the breadcrumbs and sort tabs; on the Squad directory above the category tabs; on Discussions it matches the comment column (no feed gutter). Profiles go through ProfileLayout's main column, the Squad directory through SquadDirectoryLayout — one insertion each, every tab covered.
  • Source pages used to show two signup surfaces (this strip plus the fixed-bottom AuthenticationBanner); the bottom one is removed there.

Verification

  • All 16 routes checked logged-out in the browser at desktop and tablet (768); at mobile (375) the strip is confirmed hidden. The boot-time placeholder holds the slot wherever the strip renders.
  • Tests: extension 6 suites / 52 (HijackingLoginStrip.spec 22/22), shared 379 / 2708, webapp 86 / 683. Lint and typecheck-strict-changed clean; full webapp tsc has no non-test errors.

Known trade-off, not changed here

/tags, /tags/[tag], /sources, /users and profiles still render PublicPageSignupBanner (fixed bottom, behind featurePublicSignupBanner) 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

…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>
@vercel

vercel Bot commented Sep 6, 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 7, 2026 5:55am UTC

Request Review

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 tsahimatsliah left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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. !isAuthReady is 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%.
  • ProfileLayout places the strip above children, 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.md asks 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.md read
  • Consumers enumerated: SquadDirectoryLayout (4 webapp squad-discover pages, none in the extension), TagTopicPage (/tags/[tag]), TagsDirectoryPage (/tags), ProfileLayout; no route renders the strip twice
  • isExploreHub traced through useFeedName; !isExtension guard confirmed in MainFeedLayout
  • SSR / boot-pending / anonymous / logged-in paths traced
  • packages/shared lint, typecheck-strict-changed and 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';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why have you added it to the world? We already have signup there

@tsahimatsliah
tsahimatsliah merged commit f40c9bc into main Sep 7, 2026
14 checks passed
@tsahimatsliah
tsahimatsliah deleted the hijack-strip-explore-pages-dailydotdev branch September 7, 2026 06:25
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