Skip to content

feat(preferred-sources): ask readers to make daily.dev preferred on Google - #6619

Open
tsahimatsliah wants to merge 8 commits into
mainfrom
preferred-sources-scoped-dailydotdev
Open

feat(preferred-sources): ask readers to make daily.dev preferred on Google#6619
tsahimatsliah wants to merge 8 commits into
mainfrom
preferred-sources-scoped-dailydotdev

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Sep 7, 2026

Copy link
Copy Markdown
Member

Replaces #6616, scoped down to what survived review.

What ships here

Where Notes
Post page First item in the widget column, above the source card Article posts only. Desktop/tablet — the rail stacks below the comments on phones, so there is no mobile ask by design.
Feed An ad slot the ad server could not fill Replaces the grey placeholder. Never takes a slot real content would have used.
Settings Permanent row in Appearance The ask silences itself after one answer, so this is the only way back in.

What is in Storybook only

Both need no app code, so they are mock-ups built from the real components, with their image assets in packages/storybook/public/:

  • Marketing CTA — a Customer.io campaign. CTAButton renders <a href={ctaUrl} target="_blank"> and Google's deeplink is an ordinary URL, so this route needs none of the publisher script — which is also why it works on iOS and Android, where the script does not run.
  • AchievementAchievement rows are backend-owned (id, name, image, points, rarity); the client renders what it is handed.

The reframing worth reading

Google's addPreferredSource() takes no arguments — it always adds the domain hosting the script. On a post page that is daily.dev, never the article's publisher. Only the deeplink (google.com/preferences/source?q=<domain>) can name a different domain.

Constraints that shaped it

  • No read API. "Already added" is our own optimistic state, capped globally, and impression/click/dismiss are the only measurement this feature will ever have. There is no Search Console dimension either.
  • Manual mode is not optional. Google's auto-scan runs once at script load; the webapp client-routes to post pages long after that.
  • The script appends an undocumented 1×1 iframe to <body> — worth privacy sign-off before the flag goes above 0%.

Flag

preferred_source, default false. ?preferredSource=1 forces it on for review and is sticky per tab (the param does not survive client-side navigation into a post modal). Both are marked REVIEW AFFORDANCE and must come out before merge.

Open questions

  1. Achievement rewards an unverifiable action. No read API means the unlock can only fire on our own click event — a reader can trigger it and cancel Google's dialog.
  2. No fallback when the script fails to load. Every in-app surface gates on isReady; if news.google.com/swg/... is blocked, the button stays disabled. PreferGoogleButton has a useDeeplink prop built for this, not yet wired to a timeout.

🤖 Generated with Claude Code

Preview domain

https://preferred-sources-scoped-dailydo.preview.app.daily.dev

…oogle

Google's publisher button always adds the domain that hosts it, so on a post
page it adds daily.dev — never the article's publisher. That reframing is the
whole feature: this is an ask on our own behalf, so it rides moments of intent
or space that is already empty, and never becomes page furniture.

Three placements ship here:

- **Post page** — first item in the widget column, above the source card.
  Article posts only; the rail stacks below the comments on phones, so there is
  deliberately no mobile ask.
- **Feed** — an ad slot the ad server could not fill renders our card instead of
  a grey placeholder. Never a slot real content would have taken.
- **Settings** — a permanent row in Appearance. Because the ask silences itself
  after one answer, this is the only way back in.

Foundation: `usePreferredSource` is the single gate (flag on, not already
answered). It is deliberately not gated on sign-in — post pages and the feed are
public, and a reader arriving from Google is usually signed out.
`useGooglePreferredSource` loads Google's script in manual mode, which is not
optional: the auto-scan runs once at load and the webapp client-routes to post
pages long after that.

Google exposes no read API, so "already added" is our own optimistic state and
the impression/click/dismiss events are the only measurement this will ever
have. Flag `preferred_source` is off by default.

Storybook carries the two routes that need no app code — a Customer.io
marketing CTA and an achievement — as mock-ups built from the real components,
with their image assets, so the campaign and the definition can be written from
them.

Supersedes #6616, which carried five more placements that were cut in review.

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

vercel Bot commented Sep 7, 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 11:53am UTC

Request Review

Three changes to the empty-ad-slot card's preview, all pulling the same way —
it is a picture *of Google*, not a piece of our UI:

- **Light in both themes.** It followed the app theme, and rendered dark it
  stopped reading as a search result at all, which is its only job. The colours
  are now literals from Google's own result palette rather than our tokens,
  which would flip with the app.
- **The real favicon.** It drew the bare white logo mark; Google shows a site's
  actual favicon beside its result. Rebuilt in CSS — the shipped icon is a PNG
  in each app's `public/`, which a shared component cannot reach — as the
  dark-to-purple gradient tile sampled from `favicon-32x32.png`, mark in white.
  On the new light background the old mark would have been invisible anyway.
- The article is Cursor, and the title is now a prop rather than hardcoded.

Storybook's copy of `SearchPreview` is deleted in favour of importing the real
one. The copy had already drifted, so the review image showed something that
does not ship; only the ungated card wrapper stays duplicated.

The Customer.io cover asset is regenerated to match: same light palette, same
favicon, same Cursor article.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The search snippet sat small inside the 1200x675 frame, and the card crops it
to a narrow strip where the result title has to survive at feed-card size.
Scaling the whole block (not just its width) keeps the type proportional:
900 -> 990px wide, 355 -> 391 tall, still centred with 105px side margins.

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

Two defects from review, both in useGooglePreferredSource.

**A blocked script left a permanently disabled button.** Every surface gates on
`isReady`, which only flips once Google's publisher script initialises, and
`news.google.com/swg/...` is exactly the shape of host a content blocker eats.
A blocked script fires no error in every browser, so a 4s timeout is the real
detector and `onerror` only makes it faster. On failure the hook reports
`hasFailed`, the gate turns that into `useDeeplink`, and the button renders as
an anchor to `google.com/preferences/source?q=daily.dev` — which needs no
script at all. `PreferGoogleButton` already had the prop; nothing was setting it.

**Every theme toggle leaked a callback.** The effect listed `themeMode` in its
deps, and each run pushed another callback onto `PREFERRED_SOURCE` — a queue
with no way to remove one — re-running `init` each time. The theme is now read
from a ref at callback time, so the effect only re-runs when `enabled` or
`lang` actually changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The failure timer fired on a wall clock and nothing cancelled it, so four
seconds after mount every surface dropped to the deeplink even when the script
had loaded fine — verified against the preview with the script allowed and the
button still rendered as an anchor.

`onApi` now clears the timer before it does anything else, and `fail` checks
`apiRef` so a late arrival that beats the callback cannot be overruled either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`git add -A` in 967583d swept in the ad-hoc Playwright scripts I used to
check the deeplink fallback against the preview. They were scratch, not tests —
they hardcode a preview URL and a post slug, and belong in neither the repo nor
the playwright package.

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

# Conflicts:
#	packages/shared/src/lib/featureManagement.ts
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