feat(preferred-sources): ask readers to make daily.dev preferred on Google - #6667
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…oogle Ports the post page and settings surfaces from the design branch (#6619), leaving the feed ad fallback and the Storybook mock-ups behind. Google's JS API always adds the domain hosting the script, so on a post page the ask is for daily.dev and never for the article's publisher. It runs in manual mode because the documented auto-scan happens once at script load and every post page here is reached by a client-side route change long after that. The settings row now goes through the same gate as the post widget rather than calling the script hook directly. Without that it rendered — and pulled in Google's script — for everyone on Appearance regardless of the flag. It passes isPermanent so it stays visible after the reader has answered, which is the whole reason it exists: there is no read API, so "added" is our own optimistic state and a reader who changed their mind needs a way back in. Dropped on the way over: the ?preferredSource=1 review backdoor (GrowthBook dev mode already overrides flags outside production), the dismissed state and its event along with the only surface that could fire them, and normalizePreferredSourceDomain, which had no caller once the per-publisher deeplink went.
The ask ships to everyone, so the GrowthBook gate goes with it. That also removes the only reason this hook read auth state: flag evaluation waited on isAuthReady, while nothing else here does. Events fired before boot are held by useBackfillPendingLogs and sent once shared props land, so the impression is still attributed. Measurement has to carry the whole feature: Google has no read API and no Search Console dimension, so nothing downstream of the click is observable and the events are all we get. Two gaps closed: - The click now records which route it took. Google's dialog and the deeplink are a same-tab overlay and a new tab, so they are not the same ask, and the split is the only thing that says whether the fallback earns its place. - A blocked script gets its own event. The impression cannot carry it — the impression fires on sight, the block resolves on a timeout after it — so without one those readers look like an audience that saw the ask and refused, when they were shown a button that could never be clicked.
idoshamun
force-pushed
the
preferred-source-google
branch
from
September 16, 2026 08:40
007aff6 to
06984eb
Compare
…ost type Placement follows the intent rather than the page order: a reader weighing "would you recommend this post?" has already decided the source was worth their time, which is a better moment to ask than the top of a rail they have not read yet. The ask now renders from all three widget columns instead of only the article one, so squad, social, poll and collection posts get it too. Brief and digest posts still do not: they render no rail at all, only PostContainer, and giving them one is a layout change this does not want to make. On the squad rail it sits outside the canShare gate. A private squad cannot share a post, but that says nothing about whether the reader wants daily.dev in their Google results. Each rail passes its own placement, since the component cannot tell which one mounted it and the post types are worth separating in the events.
Google tells the backend nothing, so without this call the API never learns the reader clicked and the achievement could never fire. The mutation records the attempt, not the outcome. Fire and forget, and skipped when signed out. The ask deliberately renders for signed-out readers, but the mutation is `@auth` and there is no user to credit, so calling it would only produce a guaranteed error. A failure must not stop Google's dialog opening or the prompt going quiet either: the achievement is the least important thing happening on this click.
Member
Author
|
Backend half is up: dailydotdev/daily-api#4275 — adds |
The article page view test asserted the total number of log events, so the preferred source impression firing from the widget rail made it two and broke the test. It only passed locally because the ask is gated on an idb read that usually settles after the assertion, which made the failure a race rather than a certainty. Counting the page view itself keeps what the test is actually guarding, that the view is not logged twice, and stops the next widget with an impression from breaking it again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the post page and settings surfaces from the design branch (#6619) into production shape. The feed ad fallback and the Storybook mock-ups from that branch are not here.
Changes
Google lets a reader mark a site as preferred, after which it surfaces that site more often in Top Stories, AI Overviews and AI Mode. Two surfaces ask for it:
PostWidgets(article,/read),SquadPostWidgets(squad, social, poll) andCollectionPostWidgets— so every post type that has a rail gets it. Brief and digest posts do not: they render no rail at all, onlyPostContainer. On the squad rail it sits outside thecanSharegate, since a private squad can't share a post but its readers can still want daily.dev in their Google results. Goes quiet once the reader answers.Worth knowing about the mechanism: Google's JS API takes no arguments and always adds the domain hosting the script. On a post page that is daily.dev, never the article's publisher, so this can only ever ask on our own behalf. It runs in
manualmode because the documented auto-scan happens once at script load, and every post page here is reached by a client-side route change long after that — the auto-scan would find nothing.The script is fetched on demand rather than from
<head>, so a page that never shows the ask never pays for it. If it never arrives (thenews.google.com/swg/...host is exactly what a content blocker eats, and a blocked script fires no error in every browser), a 4s timeout drops the button to Google's deeplink, which needs no script.The "already added" state lives in idb-keyval, so it is per browser rather than per account, and it is written optimistically — abandoning Google's dialog still counts as done. Neither is fixable: Google exposes no read API.
Differences from #6619
?preferredSource=1review backdoor is gone — it was marked must-remove-before-merge on that branch.normalizePreferredSourceDomain(no caller once the per-publisher deeplink went), the dismissed state and its event (no surface left to fire them), and thewidgetsLeadingprop chain throughPostContent/common.tsx(only ever fed a Storybook story).Events
impression preferred sourcetarget_type: preferred source,target_id: <placement>click preferred sourcetarget_type: preferred source,target_id: <placement>,extra: { deeplink: boolean }preferred source blockedtarget_type: preferred source,target_id: <placement><placement>is one ofpost widgets,squad post widgets,collection post widgetsorsettings— each rail passes its own, since the component cannot tell which one mounted it.Google reports nothing back — no read API, no Search Console dimension — so nothing downstream of the click is observable and impression → click is the only conversion rate this feature can ever have.
Two things the events deliberately separate:
deeplinkon the click. Google's dialog is a same-tab overlay and the deeplink is a new tab, so they are not the same ask. The split is the only evidence for whether the fallback earns its place.preferred source blockedas its own event. The impression cannot carry it: the impression fires on sight, the block only resolves on the timeout after it. Without it, readers whose blocker ate the script are indistinguishable from readers who saw the ask and refused — they were shown a button that could never be clicked.Analytics Taxonomy sheet still needs updating.
Note
Impressions fire on mount, not on viewport entry. The rail is
laptop:flex-row, so below 1020px it stacks after the main column and its comments — the button is far below the fold on mobile and tablet, but still logs an impression. Mobile conversion will read as artificially low. Moving toreact-intersection-observer(asuseLogImpressionalready does for feed items) would fix it; not done here.Experiment
No.
Manual Testing
Not verified in a browser. The visual design is unchanged from #6619, which has its own preview deploy; what changed here is placement, gating, structure and analytics.
usePreferredSource.spec.tsx— 9 tests: the cap silences the rail after an add and leaves the settings row alone, and the event payloads are asserted directly (impression once and only for a shown ask, click carries the rightdeeplinkboth ways, blocked fires only on failure).tsc(no errors in touched files), ESLint and Prettier clean.Important
Google's script appends an undocumented 1×1 iframe to
<body>. #6619 wanted privacy sign-off on that before the flag went above 0%; with no flag here, that sign-off has to happen before merge.Preview domain
https://preferred-source-google.preview.app.daily.dev