StudyBoost Docs

Feature: AdSense Ads System (FEAT-41)

Metadata

  • Issue ID: FEAT-41
  • Status: Done
  • Owner: Kzu0-afk
  • Related PRs: 41-implement-ads-system -> dev
  • Canonical feature doc: docs/features/FEAT-41-Implement-Ads-System.md

Overview

Implements a controlled ads layer for StudyBoost using Google AdSense with strict gating rules:

  • system-level gate via FEAT-35 feature flag enable_ads
  • user-level gate via FEAT-39 subscription state (free users only; active paid users excluded)

The integration keeps ads non-intrusive, fails silently when ad loading fails, and avoids security-sensitive routes.


Frontend Behavior

Verification Surfaces vs. Ad Serving

AdSense review (site ownership + authorized selling) is handled separately from ad serving. Verification surfaces are unconditional — reachable by an anonymous, logged-out crawler on every page — while ad serving stays gated:

  • GET /ads.txtfrontend/app/ads.txt/route.ts emits google.com, pub-8538225020218705, DIRECT, f08c47fec0942fa0.
  • <meta name="google-adsense-account" content="ca-pub-8538225020218705"> is added to the root metadata in frontend/app/layout.tsx.
  • Both read the publisher ID from frontend/lib/seo/adsense.ts (getAdSenseAccountId / getAdSensePublisherId), which uses NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID and falls back to the known publisher ID so verification never fails closed.

The original build only injected the AdSense loader for eligible free users, so Google's anonymous review crawler never saw the publisher ID — leaving the site in "Requires review" / "Ads.txt: Not found". Verification is now global; serving remains gated by the eligibility rules below.

Ad Serving

  • Adds a reusable ads module:
    • frontend/components/ads/ads-provider.tsx
    • frontend/components/ads/use-ads-eligibility.ts
    • frontend/components/ads/ad-slot.tsx
  • Loads AdSense script lazily only when all eligibility checks pass.
  • Hides ads while subscription/flag state is loading.
  • Hides ads if:
    • feature flag is disabled/missing
    • user is active paid (pro / plus)
    • AdSense env config is missing
    • provider script fails or is blocked
  • Renders conservative placements on:
    • home discovery page
    • courses index
    • course detail
    • document preview/detail

Backend Behavior

  • Extends FEAT-35 known flag enum with:
    • FeatureFlagName.ENABLE_ADS = 'enable_ads'
  • Reuses existing GET /feature-flags/status/:name endpoint for runtime ads gating.
  • No new Ads-specific endpoint added.
  • No new database table or migration added for v1.
  • Adds GET /ads.txt as a static Next.js route handler (app/ads.txt/route.ts) for AdSense authorized-sellers verification. It is served by the frontend app, not a static public/ asset, so the publisher ID stays sourced from one place.

QA Test Scenarios

Scenario IDDescriptionStepsInputExpected Result
FEAT-41-01Free user sees approved ad placementEnable enable_ads, configure AdSense env, sign in as free user, open courses pageFree plan, flag enabledAd slot renders in approved location without blocking course content
FEAT-41-02Paid user does not see adsEnable enable_ads, sign in as active pro or plus user, open same pageActive paid subscriptionNo ad slot renders and AdSense script is not loaded for ads
FEAT-41-03Flag disabled hides ads globallyDisable enable_ads, open approved ad route as free userFree plan, flag disabledNo ad slot renders anywhere
FEAT-41-04Missing flag safe defaultRemove or omit enable_ads, open approved route as free userFlag missingAds remain disabled by default
FEAT-41-05Subscription loading stateThrottle /subscriptions/my-subscription, open an approved routeDelayed subscription responseAds do not render until eligibility is known; no flicker from hidden-to-visible paid state
FEAT-41-06Provider script failureBlock AdSense script or simulate network failureFree plan, flag enabled, provider unavailablePage remains usable; no uncaught UI error; ad area does not break layout
FEAT-41-07Missing AdSense envRun frontend without AdSense client IDFree plan, flag enabled, missing envAds do not render; no provider request is attempted
FEAT-41-08Disallowed route suppressionOpen login, signup, pricing checkout, or billing-related flowAny user stateAds do not render on protected/auth/billing-sensitive pages
FEAT-41-09Document page non-intrusive placementOpen document detail/preview as eligible free userFree plan, flag enabledAd appears only in side or bottom area; document content remains readable
FEAT-41-10Layout stabilityLoad page with slow ad responseFree plan, flag enabledMain content does not shift significantly and remains interactive
FEAT-41-11Ad limit enforcementOpen page with multiple content sectionsFree plan, flag enabledNumber of ads stays within documented placement limits
FEAT-41-12No private data leakageInspect ad requests and console logs during eligible renderAuthenticated free userNo session token, user ID, document content, or private metadata is logged or sent intentionally
FEAT-41-13ads.txt reachableGET /ads.txt logged outAnonymous request200 text/plain with google.com, pub-8538225020218705, DIRECT, f08c47fec0942fa0
FEAT-41-14Verification meta presentView source of any page logged outAnonymous request<meta name="google-adsense-account" content="ca-pub-8538225020218705"> present regardless of flag/subscription
FEAT-41-15Verification independent of servingNEXT_PUBLIC_ADSENSE_ENABLED=false, load a page logged outServing disabled/ads.txt and meta resolve; no ad slot or adsbygoogle.js loader injected
FEAT-41-16Verification survives missing envUnset NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID, request /ads.txtEnv missingFalls back to known publisher ID; /ads.txt still returns the authorized line

QA Verification Notes

  • Scoped FEAT-41 backend lint and backend build passed.
  • Scoped FEAT-41 frontend lint passed.
  • QA bug fix: ads now remain hidden until the current route/config pair has its own resolved enable_ads status, preventing stale flag reuse after route/config transitions.
  • adsense-optimization pass: lint (app/layout.tsx, app/ads.txt/route.ts, lib/seo/adsense.ts) and a full pnpm build from frontend both passed. /ads.txt compiles as a static route and renders the authorized publisher line. The earlier /documents?...&sort=trending prerender blocker no longer reproduces.

Edge Cases

  • Missing flag response defaults to ads disabled.
  • Missing AdSense env vars disable ads safely.
  • Route not in placement allow-list suppresses ads.
  • User plan transitions from free to paid suppress ads after state refresh.
  • Ad blockers prevent script load; ads fail closed without breaking page flow.
  • Verification surfaces (/ads.txt, google-adsense-account meta) fail open via a hardcoded publisher fallback — the opposite of ad serving — because a missing config value must not block AdSense review.
  • AdSense-registered host (studyboost.com) must match the canonical host (www.studyboost.com) or 301-redirect to it, or verification will not resolve.

Notes

  • Uses existing FEAT-31 auth/session and FEAT-39 subscription data model.
  • Keeps ad rendering policy conservative to preserve study UX.
  • No mock ad data is introduced; slot IDs come from environment configuration.
  • To re-trigger Google review after deploying: confirm /ads.txt + the verification meta resolve on production, then AdSense → Sites → Request review and Search Console → Request indexing for /, /courses, /documents. Ads.txt status can take 24–48h to flip to Authorized. The full runbook lives in docs/features/FEAT-41-Implement-Ads-System.md.