Skip to content

feat(webapp): ad-monetised post template for paid and SEO traffic - #6500

Merged
rebelchris merged 57 commits into
mainfrom
claude/ad-arbitrage-assets-analytics-87b2db
Aug 25, 2026
Merged

feat(webapp): ad-monetised post template for paid and SEO traffic#6500
rebelchris merged 57 commits into
mainfrom
claude/ad-arbitrage-assets-analytics-87b2db

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Aug 20, 2026

Copy link
Copy Markdown
Member

Ad-monetised article template for paid-acquisition traffic, a reusable programmatic ad layer, and — called out explicitly because it affects every page and every visitor — a scoping change to the consent stack.

Route

Ships at /articles/:id (implementation at pages/articles/[id].tsx). The earlier /posts/:id/read path 307s to it. Noindexed twice over: noindex,nofollow meta (no canonical — mixed signals) and an X-Robots-Tag header on /articles/:path*. getStaticProps 404s anything that is not an article / video / collection carrying a summary or body — AdSense's low-value-content enforcement is account-level, so title-plus-ads shells are not generatable. The page forces light mode while mounted (display-only; the stored preference is untouched) and uses the post's own cover as og:image. No sidebar (showSidebar: false).

Placements (live at merge)

Slot Placement Behavior
2 Leaderboard above the article Sticky under the header; releases 10s after first scroll
3 Inline MPU beside tags/cover Eager
4, 5, 6, 11, 12 Rail MPUs between widget furniture Only the first is phone-visible (density cap)
7 Native between comments, every 5th Collapsed until its layoutKey is filled; preconditions in slots.ts (label ships in code; phone density re-measure required)
Bottom sticky leaderboard Google Auto-ads Anchor, account-side — see below

Retired since earlier drafts: slot 1 (sidebar), 8/9 (multiplex grids), 10 (half-page tower), 13 (custom floating leaderboard — a publisher sticky of that shape violates AdSense placement policy; Google's Anchor is the compliant equivalent).

Phone ad density measured at ~27% of page height against the Better Ads 30% cap (violation = Chrome filters ads domain-wide).

Gating

  • Both surfaces are anonymous-only (isAuthReady && !user) — logged-in members and Plus never see programmatic ads.
  • /articles: read_adsense boolean, default on (documented non-experiment kill switch — never ramp with it).
  • Organic post page: post_adsense boolean (default off) + 2 slots (15/16, ids still empty); enrollment additionally requires a live unit id so the experiment cannot fill with users who can't see it.
  • Leaving the ad route forces a full page load (routeChangeStart guard + beforePopState), so ad code cannot follow SPA navigation into the app.

⚠️ Site-wide consent change (needs privacy/legal sign-off)

Iubenda.tsx previously loaded the IAB TCF stub and the GPP stub for every visitor worldwide. Google's ad tags hold all ad requests on any page carrying __tcfapi/__gpp until the CMP answers — which iubenda only does once the banner is actioned — so ads were consent-gated in countries where no law and no Google policy requires it (reported from South Africa and Israel).

Now: the TCF stack loads only for visitors in the certified-CMP mandate's scope — an explicit EEA + UK + Switzerland country list (certifiedCmpRegions, including EU outermost regions whose ISO codes sit on other continents: GF/GP/MQ/RE/YT, plus AX/GI) — and the GPP/USPR stack only for US visitors. Unknown geo fails safe to the full stack. The banner itself still shows everywhere and preferences are still recorded; ads outside those scopes simply no longer wait for it. EEA/UK/CH behavior is unchanged: certified CMP, consent-gated requests, Limited ads on rejection. Covered by Iubenda.spec.tsx (mandate / outermost-region / US / outside / unknown-geo paths).

Sign-off requested: privacy/legal on the scoping list, analytics owner on the six new LogEvent values (request/fill/empty/view adsense slot, adsense slot error, adsense test mode) and their extra shape.

Auto ads account configuration (not expressible in this repo)

The bottom leaderboard requires, in the AdSense UI: Auto ads → Anchor format only (all in-page formats OFF), "wide screens" enabled for desktop anchors, URL group scoped to app.daily.dev/articles/* only — the script also loads on /posts/[id] when post_adsense is on, so the exclusion is what keeps anchors off the organic page. Before flipping the switch: verify the anchor against the fixed mobile footer nav on a real phone (they share the bottom edge; an obscured ad is a policy violation).

Reusable ad layer

features/monetization/ProgrammaticAd — neutral component with the full lifecycle in first-party telemetry (request, fill, unfilled, push error, test-mode alarm, IAB viewable impression via the existing useViewability), standardized extras, remount key on unit identity, mount-at-eligibility to keep adsbygoogle.push binding safe, CSS-only collapse on Google's unfilled verdict. ArbitrageAdSlot is a thin per-surface wrapper. inFeed units carry the policy-permitted "Advertisements" label in code.

Verification

  • Shared post/sidebar/monetization suites + webapp suite green (one pre-existing WorldGuideSheet failure, present before this branch's changes)
  • typecheck_strict_changed, lint green; CI green on head
  • Consent scoping verified live on the preview: served bundle carries the geo gate; boot from ZA returns {region: ZA, continent: AF} → no consent APIs load
  • Not yet verified: Auto-ads anchor vs footer nav on a phone (blocked on account switch), organic slots with real unit ids

🤖 Generated with Claude Code

Preview domain

https://claude-ad-arbitrage-assets-analy.preview.app.daily.dev

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Aug 25, 2026 9:35am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Aug 25, 2026 9:35am

Request Review

@tsahimatsliah

Copy link
Copy Markdown
Member Author

Preview links — verified live

Use the assigned preview domain, not the raw Vercel URL (the Vercel one can't complete boot, so the sidebar and header stay empty there):

Link
New template https://claude-ad-arbitrage-assets-analy.preview.app.daily.dev/posts/qojM1enSN/read
Same post, current template https://claude-ad-arbitrage-assets-analy.preview.app.daily.dev/posts/qojM1enSN

More posts to try: nSmlLGJoe, qHnVnVCHv — append /read.

What the side-by-side shows

Current template renders the geo-personalised PostAuthBanner covering roughly half the viewport ("daily.dev is the fastest growing developer platform in Israel!"), with the sidebar collapsed to its icon rail.

New template has neither: no banner, sidebar expanded to 240px with slot 1 pinned at its bottom, and the article starting immediately under the leaderboard.

Verified on the preview at 1440×900

  • All 13 in-page slots render in order (1–13)
  • Sidebar 240px expanded, slot 1 present, collapse toggle correctly hidden
  • Widget column 340px with slots 10, 11, 12
  • Anchor (slot 13) pinned to the viewport bottom, content clears it
  • Header shows "Log in" and "Sign up" as intended
  • No signup-banner text anywhere on the page
  • Standard /posts/[id] template unchanged

CI

All 12 checks green — lint, strict typecheck, and the shared/webapp/extension test suites.

Still open

  • Slot 1 only wires into SidebarDesktop. layout_v2 is off by default, so this is the shipping path, but SidebarDesktopV2 needs the same prop before that flag rolls out.
  • Ad slots are reserved placeholders — no ad tag exists in the codebase yet.
  • Routing selector is a plain route for now; a next.config.ts rewrite keyed on a UTM param can map paid traffic to it, mirroring the existing ?userid/share rewrite.

@rebelchris

Copy link
Copy Markdown
Contributor

Live AdSense layer (b5350c4)

The template can now serve real ads, strictly scoped to `/posts/[id]/read`:

  • New GrowthBook JSON feature `read_adsense_slots` (slot number → AdSense unit). Empty default = the placeholder draft you see on the preview, zero Google code loaded anywhere. Any entry flips the template live. Clearing it is the kill switch — no deploy needed.
  • `adsbygoogle.js` is rendered only by the /read page, and leaving the route cancels client-side navigation in favor of a full page load, so Auto ads overlays (anchor/vignette) can never follow a soft navigation into the app. Extension untouched (AdSense is prohibited there).
  • Units lazy-request via IntersectionObserver (200px rootMargin) for viewability; non-production builds send `data-adtest=on` so previews never create billable impressions.
  • In live mode, unconfigured slots collapse (slot 4/outstream stays empty until GAM) and the placeholder anchor unmounts so the Auto ads anchor owns the viewport bottom.

Go-live = paste this into GrowthBook `read_adsense_slots` with the real unit ids

{
  "1":  { "id": "", "type": "display", "width": 240, "height": 400 },
  "2":  { "id": "", "type": "display" },
  "3":  { "id": "", "type": "inArticle" },
  "5":  { "id": "", "type": "inArticle" },
  "6":  { "id": "", "type": "inArticle" },
  "7":  { "id": "", "type": "inFeed", "layoutKey": "…from the generated snippet…" },
  "8":  { "id": "", "type": "inArticle" },
  "9":  { "id": "", "type": "multiplex" },
  "10": { "id": "", "type": "display", "width": 300, "height": 600 },
  "11": { "id": "", "type": "display" },
  "12": { "id": "", "type": "display" }
}

Slots 13 (anchor) + 14 (vignette) come from AdSense Auto ads → overlay formats only, scoped in the AdSense UI to a URL group matching `/posts/*/read`.

Verified: 7 new ArbitrageAdSlot/Anchor tests, shared post (42) + sidebar (37) suites, strict typecheck guard, lint — all green.

🤖 Generated with Claude Code

@rebelchris

Copy link
Copy Markdown
Contributor

Update (e0c3245): the placeholder boxes are now dev-only. Production with an empty read_adsense_slots value renders a completely clean page — no yellow boxes, no ad markup, no Google script. Populating the flag is the only thing that ever puts ad code in front of a visitor. Test creatives are decided by hostname at request time (anything ≠ app.daily.dev, previews included, gets data-adtest=on), since preview deployments are production builds.

🤖 Generated with Claude Code

@rebelchris

Copy link
Copy Markdown
Contributor

Boot-light config + organic post page slots (e08939c)

Per the boot-payload concern: the slot maps now live in code (`post/arbitrage/slots.ts`) and remote config shrinks to two booleans — unit ids are public in any live page's source, so the JSON bought nothing for its per-boot cost on every surface.

GrowthBook migration (one-time)

  1. Delete the `read_adsense_slots` JSON feature (its values are now hardcoded).
  2. Create two boolean features, both defaulting off:
    • `read_adsense` — the /read arbitrage template
    • `post_adsense` — the organic post page (separate so organic can ramp/kill independently)

Organic post page (`/posts/[id]`), behind `post_adsense`

  • Leaderboard above the post content (slot 15)
  • Sticky 300×600 closing the widget column (slot 16)
  • Hidden from Plus members (same rule as the internal sidebar ad), wired at the webapp page level so post modals and the extension (AdSense-prohibited) can never render them, script loads only when active.

Still needed before these slots serve (all in slots.ts TODOs)

What Where to get it
Slot 7 `layoutKey` read_s07_comment_native → Get code → `data-ad-layout-key`
Slot 13 unit id was only in the retired remote config
Slot 15 + 16 unit ids create `post_s15_top_leaderboard`, `post_s16_rail_half_page` (Display)

Every slot with a missing id simply collapses — nothing breaks meanwhile.

🤖 Generated with Claude Code

tsahimatsliah and others added 25 commits August 25, 2026 11:32
The iframe-based emptiness check collapsed every below-the-fold slot:
grace ran from mount, and a lazy slot four screens down has no iframe
four seconds after mount because it has not been asked yet. Collapsed
means display:none, display:none never intersects, and a slot that
cannot intersect never requests — so every slot past the first viewport
died before its first request.

The request and the collapse now share one effect and one clock: the
grace timer starts when adsbygoogle.push actually runs, eager or on
intersection, so a slot is only judged empty after Google has had its
window to answer.
… unit

SidebarRail, Video and RichMedia had no call sites left. The organic
half page pinned at a hardcoded 5rem, wrong whenever a top banner adds
2rem above the header — it reads MainLayout's --sticky-header-offset
like every other pinned unit.
Review blockers, in order:

Internal data out of the public repo: plans/ad-arbitrage/** is removed
from every commit (history rewritten, not deleted on top — the branch
is public), .gitignore covers the whole plans subtree, and the shipped
FORMAT_SPEC loses its cpm figures along with the reach annotations —
review aids, not runtime data.

Flag hygiene: useOrganicAdsenseSlots evaluates post_adsense through
useConditionalFeature gated on the visitor being anonymous, and
ArbitrageAdSlot splits per surface so /read slots never touch the
organic hook — no more enrolling users who structurally cannot see a
unit.

Script gating: live-ness now means at least one unit id, not key
presence, via hasLiveAdsenseUnits — flipping post_adsense on while the
organic ids are still empty no longer loads adsbygoogle.js for
inventory that cannot fill. Same gate on /read and the anchor.

Push binding: adsbygoogle.push({}) initialises the first uninitialised
<ins> in document order, not the slot that pushed — so with every ins
mounted up front and lazy pushes firing in intersection order, a push
from low on the page could initialise an earlier slot instead. The ins
now mounts only when its slot becomes eligible, keeping the invariant
that every uninitialised ins is one that should be processed right now,
which makes the pushes interchangeable. The wrapper keeps the min-height
so the page reserves the same space.

And the non-blocking notes: the /read departure interceptor ignores
shallow navigations; the sidebar unit rides in from the /read page as
MainLayout's sidebarTrailing node instead of living in the shared nav
(which the extension also bundles); FurtherReading narrows its missing-
source guard at the boundary instead of optional-chaining into "More
posts from undefined"; and the slots get first-party analytics — a
per-placement fill event and dismiss events on the two closable units.
Found in final self-review: a fill arriving after the four second grace
found its slot already display:none, where everything measures zero, so
the height check re-judged the creative as empty and the slot stayed
collapsed forever — the mutation observer's whole reopen path was dead.
The height check now applies only while the ins is actually displayed
(getClientRects, empty exactly under display:none); a hidden slot
reopens on the creative alone and the resize observer re-judges it at
real geometry. Regression test simulates the late fill.
- Mobile density: only the first rail unit keeps its phone placement,
  bringing the phone run from ~40% of page height to ~27%, inside the
  Better Ads Standards 30% cap whose violation filters ads domain-wide.
- /read is generated only for articles, videos and collections that
  carry a summary or body; title-plus-ads shells 404, because AdSense's
  low-value-content enforcement is account-level.
- read_adsense boolean returns as an emergency kill switch, default on
  by design (documented as a non-experiment).
- Both surfaces now serve anonymous visitors only, gated on isAuthReady
  so a logged-in boot can never flash an ad before the user resolves.
- post_adsense enrollment additionally requires a live unit id, so the
  experiment population can't fill with users who could never see it.
- Back/forward from /read goes through beforePopState with an in-place
  load, keeping history intact instead of navigating forward.
- Non-blocking picks: plans/ ignored wholesale, one-shot telemetry
  event when data-adtest engages so silent test-mode in production is
  visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On top of the blocker fixes already on the branch: the fork's rationale
and fold-back debt recorded on ArbitragePostContent; the push-ordering
invariant documented at the push site so nobody adds `eager` and
quietly re-opens the mis-binding race; the two near-identical timer
hooks merged into useTimedRelease; and the anchor offset on the shared
footer made an explicit opt-in from /read instead of compensating CSS
in a component every page renders.
The box rendered whether or not a creative arrived, so the bottom of
the nav carried a bordered empty band with a dismiss button and nothing
to dismiss — measured live at 41px of border-plus-padding around a
collapsed slot, and taller while the format still reserved a
min-height. It now stays out of the layout until the slot reports a
creative, and reserves nothing while hidden; the slot itself stays
mounted so it can still request and report.

Fill is reported before the collapse grace, so the box reveals when the
ad lands rather than on the four second timer.

Both dismiss buttons go subtle and extra small — the anchor's solid
primary was the loudest thing on the page.
The /read container padded its bottom by 7rem plus the anchor's height.
The 7rem predates the anchor measuring itself and also double-counted
the mobile footer nav, which FooterNavBarLayout already spaces for — so
on desktop, where neither exists, the page simply ended in 112px of
nothing below the rail's last slot. Measured live: rail bottom 788,
container bottom 900.

Now the container's own pb-6 plus whatever the floating leaderboard
actually occupies, which is nothing while it is absent or unfilled.
Cancelling it with p-0 did not work — Tailwind emits the padding
shorthand before the per-side utilities, so px-2/pt-1/pb-3 won and the
hidden box still measured 16px under the nav, h-0 notwithstanding. The
padding now only exists in the filled state, so hidden is genuinely
zero.
It behaved like layout, not like a floating placement. The page padded
its bottom by the ad's measured height and the footer wrapper was
lifted by it at every width — so on desktop, where the ad is confined
to the article column and the wrapper's only occupant is the
scroll-to-top button over on the right, the button was parked in
mid-air above a gap it had no reason to clear. The offset now applies
only below laptop, where the ad does span the width and the nav bar
would otherwise sit on top of it, and the page reserves nothing.

Also stops slots collapsing mid-request. Emptiness was judged four
seconds after the push whether or not AdSense had touched the element,
so a slow auction looked exactly like a declined one — and once
collapsed the slot is display:none, which Google will not render into,
so it never came back. That is the "loads very slowly, or not at all"
behaviour. A slot now collapses on the tag's own data-adsbygoogle-status
being 'done', with a far longer window before an untouched slot (blocked
or still-loading script) is written off.
I added two heuristics that guessed a slot was empty — no iframe yet,
and a measured height below a threshold — and both mistake a slow
auction for a declined ad. Guessing wrong is not recoverable: a hidden
slot is display:none, Google will not render into one, and the ad then
never arrives at all. That is why ads stopped showing up.

Emptiness is back to the one signal that actually means no ad:
data-ad-status="unfilled", which the wrapper's CSS rule already
collapses on, with no timers and nothing to get wrong. A creative now
appears the moment AdSense delivers it.

The sidebar box had the same fault in a worse place — it hid the slot
behind visibility:hidden and h-0 while waiting for the fill it was
waiting on. The slot is never hidden or zero-sized now; only the border,
padding and dismiss button wait for the creative, and an empty slot has
no height of its own, so the nav ends where it always did.
It is the last thing in a column that otherwise ends in breathing room,
so it should not sit flush against the bottom.
Squares off every slot, drops the sidebar unit, the two closing multiplex
grids and the half-page rail tower, and makes the rail's last unit the one
that travels with the visitor.

- No radius or overflow clipping on a slot: the box only centres the unit
  and reserves its request-time height, so a taller creative grows it.
- The sidebar goes back to production's, forced open. The nav no longer
  takes a trailing node at all, so nothing ad-shaped reaches the shared
  component the extension also renders.
- The rail unit after the further reading widget pins under the fixed
  chrome. Sticky is bounded by the containing block, so it moved out of
  FurtherReading and back to rail level, where the stretched column gives
  it the whole page to travel.
- Slot numbers 1, 8, 9 and 10 are retired rather than reused, so their
  AdSense reporting rows stay readable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The onFilledChange callback, the multiplex unit type and the window latch
declaration all lost their last consumer in the placement trim. The fill
observer now also disconnects after the one impression event it exists to
send, instead of watching a refreshing unit mutate forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The slot moves from after the whole further-reading widget to between its
two sections. FurtherReading flattens to `contents` while hosting it, so
the sticky containing block stays the full-height rail and the unit keeps
the whole page to travel; consumers without a slot keep their original box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
expandSidebar forced only the rendered state, so the menu row showed
collapsed affordances on an expanded sidebar, the label disappeared and
clicking the toggle silently flipped a preference nothing was reading.
It now seeds the stored preference once per mount, after settings load —
the sidebar opens on landing while the toggle, its label, analytics and
persistence keep production behavior, and MainLayout's padding follows
the real setting on both layout variants.

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

Extracts the AdSense engine from the arbitrage template into
features/monetization/ProgrammaticAd, preserving the current engine
semantics exactly (CSS-only unfilled collapse, mount-at-eligibility,
one-shot outcome observer, module test-mode latch) and adding what the
engine never reported: request, unfilled, push-error and MRC viewable
impression events, all with one standardized extras shape. Callers pass
a remount key when unit identity changes, and a ref guard means an ins
can never be pushed twice. ArbitrageAdSlot stays as the thin per-surface
wrapper so /read pages cannot enroll in the organic experiment.

The ad article gains /articles/[id] (307 from /posts/:id/read,
X-Robots-Tag noindex on both routes), drops the comments prefetch and
JSON-LD a noindexed page cannot use, gates the image preload on the
image existing, and forces light mode while mounted — display-only, the
stored preference is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The custom floating leaderboard was a publisher-implemented sticky ad,
which AdSense caps at 300px wide, desktop only, always-present and one
per viewport — our 728x90/320x50 bar, mounting ten seconds in beside a
sticky rail unit, was outside the spec on every count. Google's own
Anchor format serves a full-width bottom leaderboard on every
breakpoint, renders its own dismiss control (which we never touch), and
is exempt from the publisher-sticky rules because Google draws it.

Slot 13 retires with the component, along with everything that existed
only to serve it: the footer-nav height offset, the column-geometry
custom properties and their ResizeObserver publisher, and the
mount-delay branch of useTimedRelease. Enabling "Anchor ads" under Auto
ads in the AdSense account is the remaining account-side step; the
script's hard-navigation boundary already confines anchors to the ad
pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The share preview now carries the article's own cover instead of no
image at all — an ad-bought click should land on exactly the image that
sold it. The sidebar goes entirely: it has carried no ad unit since the
placement trim, and its post-boot mount was the page's last source of
layout shift. showSidebar: false also releases the reserved left
padding, so the article column centres cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TCF stub loaded for every visitor worldwide, and Google's ad tags
hold every request on any page where __tcfapi exists until the CMP
delivers a terminal answer — which iubenda only produces once the
banner is actioned. That gated ads on consent in countries where no
consent is required and where Google mandates no CMP at all: an
Israeli or US visitor saw no ads until they interacted with the banner.

The TCF layer (stub, safe-tcf, enableTcf, Google Additional Consent)
now loads only for GDPR-covered visitors per the boot geo, failing safe
to covered when the geo is unknown. Everyone still sees the banner and
their preference is still recorded; ads outside GDPR scope simply no
longer wait for it. EEA/UK/CH behavior is unchanged: certified CMP,
consent-gated requests, Limited ads on rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous fix scoped the TCF layer to checkIfGdprCovered, which
counts everyone outside a two-entry allowlist (US, IL) as GDPR-covered
— so a South African visitor still had ads held until they actioned the
banner. Google's certified-CMP mandate covers the EEA, the UK and
Switzerland; continent Europe is the superset that errs safe. Everyone
else gets the banner without the ad gate, and an unknown geo keeps the
full treatment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
window.__gpp is a consent API Google's tags can hold requests on, just
like __tcfapi — and the US state privacy laws it carries only apply to
US visitors. Everyone else now gets neither consent API, so nothing on
the page can make an ad request wait for the banner outside the regions
whose law requires it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The redesigned banner clamps .iubenda-banner-content to a scrollable
max-height, and iubenda refuses Accept/Reject while that element is
scrollable and not scrolled to bottom — the press-again counter. The
one-time poke at onBannerShown resolved that state for the initial
render only; every re-measure (resize, webfonts landing) could re-clamp
after the poke and swallow the next consent press. Poking on each
measure keeps the first press binding under all of them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consent scope (blocker 2): the certified-CMP gate keys on an explicit
EEA + UK + Switzerland country list (EU outermost regions included by
their own ISO codes) instead of continent Europe, which missed GDPR
territory on three other continents. enableUspr is scoped with the GPP
stub it depends on, and the spec now covers all four regimes: mandate,
outside it, US, unknown geo.

Preconditions in the map (blockers 3 and 4): slot 7's TODO now gates its
layoutKey on a visible ad label and a phone density re-measure with the
interval live; the Auto-ads anchor TODO requires the /articles URL-group
scoping and a real-phone check against the fixed footer bar before the
account switch flips.

Cleanup: the implementation moves to pages/articles/[id].tsx so the
X-Robots-Tag header fires on the shipping route and no SPA transition
can render the template under the legacy URL; comment rows keep one
stable element type so a moving interleave boundary cannot remount a
live comment subtree; expandSidebar leaves MainLayout with its last
consumer gone; the beforePopState reset carries its single-global-slot
caveat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The round-3 precondition for slot 7's layoutKey included a visible ad
label; a precondition that lives in a comment can be missed on the day
the id is pasted in. ProgrammaticAd now renders the policy-permitted
"Advertisements" caption above every inFeed unit inside the collapsing
wrapper, and the slot-map gate marks that item done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rebelchris
rebelchris force-pushed the claude/ad-arbitrage-assets-analytics-87b2db branch from a39ffbe to 8295abc Compare August 25, 2026 09:32
@rebelchris
rebelchris merged commit e1e5a3f into main Aug 25, 2026
12 checks passed
@rebelchris
rebelchris deleted the claude/ad-arbitrage-assets-analytics-87b2db branch August 25, 2026 09:48
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