Most "Google changed its policy" posts are summaries of a changelog. This one is a diff. We run ads and affiliate links on this site, we sit behind AdSense, and over the last two years the rules underneath that setup moved enough that our BaseLayout.astro, our _headers file, and our article frontmatter all had to change. Below is what moved, what we shipped in response, and — the part most posts skip — what we deliberately have not shipped yet.
The three changes that reach into your codebase
Google maintains two separate documents that publishers routinely merge into one mental bucket. Google Publisher Policies are the hard rules; breaking them can get ad serving disabled on a page or across a site. Google Publisher Restrictions are softer; violating them doesn't get you banned, it just narrows which advertisers will bid, so the page still serves ads at a lower rate. If you're triaging a Policy Center notice, read the label first. "Restricted" is a revenue problem. "Policy violation" is an existence problem.
Three specific changes actually require code, not just reading:
A certified CMP is mandatory for EEA and UK traffic. Since 16 January 2024, Google's EU user consent policy requires publishers serving ads to users in the EEA, the UK, and Switzerland to use a Consent Management Platform from Google's certified list, integrated with the IAB Transparency and Consent Framework. A hand-rolled cookie banner does not satisfy this, no matter how correct its logic is. The requirement is on the vendor, not just the behavior.
Consent Mode v2 added two signals. The original consent mode carried ad_storage and analytics_storage. V2 adds ad_user_data and ad_personalization. From March 2024, EEA traffic without these signals loses remarketing audience population and degrades conversion measurement — you keep serving ads, but your measurement quietly gets worse, which is a harder failure to notice than an outright block.
AdSense display moved from per-click to per-impression. Announced in November 2023 and rolled out through 2024, display ad payouts are now impression-based. At the same time Google restated the AdSense for content revenue split: the buy-side platform takes its fee first (Google described its own Google Ads buy-side fee as averaging about 20%), and the publisher keeps 80% of what remains. If you built dashboards or forecasts on a click-based model, they're measuring the wrong event now.
The invalid-traffic risk that bites small publishers isn't fraud, it's development. If your ad script loads in
bun run dev, in preview deploys, and in every local refresh while you're styling a component, you are generating impressions from a single IP against your own publisher ID. Gate the script on an environment variable, not onimport.meta.env.PROD, so preview builds are covered too.
What we changed on this site
Five concrete changes, in the order we made them.
1. ads.txt serves a 200, not a redirect. We keep one authorized-seller line in public/ads.txt. On Cloudflare Workers with static assets, the file is served directly from the asset bundle. This matters because the crawler that validates authorized sellers wants the file at the apex path; a redirect chain from www or a trailing-slash normalization rule can make it look absent. We check the raw response code, not the browser view.
2. The ad script is environment-gated at a single seam. BaseLayout.astro renders the verification meta tag and adsbygoogle.js only when PUBLIC_ADSENSE_CLIENT is set, and renders an actual ad unit only when PUBLIC_ADSENSE_SLOT and PUBLIC_AD_NETWORK are both set. There is exactly one place in the codebase that decides whether an ad network sees a page. That's deliberate — when a CMP decision needs to suppress the script, we want one branch to modify, not eleven components with their own conditions.
3. We have not shipped a certified CMP, and the mitigation is that the ad unit is off. This is the honest state. PUBLIC_ADSENSE_SLOT is unset, so no ad unit renders. The compliance question changes shape depending on which surface you're worried about, and we'd rather serve zero ads than serve non-compliant ones to EEA readers while we work through CMP selection. If you're in the same position, the useful framing is: what is the smallest thing I can turn off that makes the question moot?
4. Disclosure got split into three separate mechanisms. Affiliate relationship, AI assistance, and ad labeling are three different obligations to three different authorities, and collapsing them into one footer line satisfies none of them cleanly. We render an affiliate disclosure component near the top of commercial articles, and an AI-assisted note driven by an aiAssisted: true frontmatter flag on every article where a model wrote any part of the body. The flag is per-article and set at write time, so it can't drift out of sync with the content.
5. Machine-readable indexes are noindex. We publish llms.txt, llms-full.txt, and articles.json for AI crawlers, and every one of them carries X-Robots-Tag: noindex in public/_headers. Those files are a near-complete duplicate of the corpus in plain text. Useful to a crawler that wants structured access; a self-inflicted duplicate-content problem if Search indexes them alongside the articles.
The dependency you can't policy-proof
One organization owns the ad network, the search referral traffic, and the measurement stack. Every mitigation above is a way of reducing blast radius inside that dependency, not a way out of it.
The content-side version of this is Google's scaled content abuse policy, introduced with the March 2024 spam update. The rule is not "AI-generated content is penalized" — Google has been explicit that it targets content produced at scale primarily to manipulate rankings, regardless of how it was produced. The practical distinction is whether a human reviewed and stands behind each piece. That's why our AI-assistance flag is a per-article boolean rather than a site-wide banner: it forces a decision per piece of content.
The structural hedge is an audience you can reach without an intermediary's permission. An email list survives an ad policy change, a core update, and a Policy Center notice. It's the one distribution channel where nobody else's changelog can revoke your access.
Before changing any ad code, pull your current state from the source rather than memory: request your own
/ads.txtwithcurl -Iand check for a 200, open a page with an ad blocker disabled and confirm which scripts actually load, and read the Policy Center label on any existing notice to see whether it says "restricted" or "violation." Three commands, and they tell you which of the changes above you're already exposed to.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)