Site Head Custom Code
What every block in the site-wide head custom code does: Google Consent Mode defaults, Osano, Google Tag Manager, the Webflow Analyze consent sync, and the global CSS overrides.
Last Updated: August 24, 2026 | Audience: Developers
What lives in the head
Site Settings > Custom Code > Head Code. This runs on every page of cas.org, before the page content renders.
Five things live here, and they are in a deliberate order:
- Google Consent Mode defaults, which set every tracking permission to denied
- Osano, the consent platform that asks the visitor what they allow
- Google Tag Manager, which fires the actual marketing and analytics tags
- A block that syncs the visitor's Osano answer into Webflow's own analytics
- A large stylesheet of global CSS overrides
The order is not cosmetic. Consent defaults must be set before Google Tag Manager loads, otherwise GTM fires tags before it knows what the visitor allows. If you add anything to the head, add it below the existing blocks unless you have a specific reason not to.
Part 1: Google Consent Mode defaults
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'personalization_storage': 'denied',
'functionality_storage': 'granted',
'security_storage': 'granted',
'wait_for_update': 3000
});
gtag('set', 'ads_data_redaction', true);This is the safety net. It tells Google's tags to assume the visitor has denied everything until told otherwise.
Reading it line by line:
dataLayeris a plain array that Google Tag Manager watches. Anything pushed into it is a message to GTM.gtag()is a small helper that pushes those messages. It is defined here by hand because GTM has not loaded yet.- The five
deniedvalues cover advertising and analytics storage. Nothing that identifies a visitor is allowed yet. functionality_storageandsecurity_storageare granted because they cover things the site needs to work at all, like remembering a language preference or preventing fraud. These are not tracking.wait_for_update: 3000tells Google's tags to hold for up to three seconds before acting on the defaults. That gives Osano time to load and report the visitor's real answer, which avoids firing tags in denied mode for someone who has already consented.ads_data_redactionstrips identifiers from ad requests while advertising consent is denied.
Nothing in this block ever grants permission. It only sets the starting position. Granting happens in part 2.
Part 2: Osano
<link rel="preload" href="https://cmp.osano.com/.../osano.js" as="script">
<script src="https://cmp.osano.com/.../osano.js"></script>Osano is the consent management platform. It shows the cookie banner, records what the visitor chose, remembers it on return visits, and reports that answer to anything that asks.
It loads synchronously, meaning the browser stops parsing the page until this file finishes downloading. That is a real performance cost and it is intentional. Osano has to be ready before any tag that might set a cookie.
Osano also does something less obvious that catches people out. It replaces the browser's cookie mechanism with its own and drops any cookie whose name is not registered in the Osano dashboard, silently and with no error. If a cookie you expect is simply not appearing, read the Osano cookie blocker section of the UTM tracker footer script doc before debugging anything else.
Once loaded, Osano pushes a consent update into the dataLayer reflecting the visitor's real choices, which is what lifts the denied defaults from part 1.
Part 3: Google Tag Manager
(function(w,d,s,l,i){
w[l] = w[l] || [];
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-5RS86Q');This is Google's standard loader snippet, unmodified. Do not hand-edit it. The only meaningful value in it is the container ID at the end, GTM-5RS86Q.
What it does: creates a script tag pointing at the GTM container, marks it async so it does not block rendering, and inserts it into the page. From that point on, GTM manages the actual tags, GA4, ad pixels, and anything else marketing has configured.
Almost nothing tracking-related should be added to this head block directly. If someone asks you to add a pixel or a tag, the answer is nearly always that it belongs in GTM, where it can be consent-gated and changed without a site publish.
Part 4: The Webflow Analyze consent sync
wf.ready(() => {
// Sync current state on page load
Osano.cm.addEventListener('osano-cm-initialized', () => {
const consent = window.Osano.cm.getConsent();
if (consent.ANALYTICS === 'ACCEPT') {
wf.allowUserTracking();
} else {
wf.denyUserTracking();
}
});
// React live when the visitor changes their preferences
Osano.cm.addEventListener('osano-cm-consent-changed', (change) => {
if (change.ANALYTICS === 'ACCEPT') {
wf.allowUserTracking();
} else if (change.ANALYTICS === 'DENY') {
wf.denyUserTracking();
}
});
});Webflow has its own built-in analytics product, Webflow Analyze, which is separate from Google Analytics and is not managed by GTM. It has to be told about consent independently. That is what this block is for.
wf is a global object Webflow injects into the page. Early on it is only a stub that queues callbacks:
window.wf = { r: [], ready: function (cb) { this.r.push(cb); } };Anything passed to wf.ready() sits in that queue until Webflow's main JavaScript bundle loads at the bottom of the page and drains it. So this block does not run when the browser reaches it. It runs much later.
The bug in this block
The first listener, the one labeled "sync current state on page load", never fires.
Here is the sequence. Osano loads synchronously near the top of the head and finishes initializing almost immediately. Webflow's bundle loads at the very bottom of the body, after jQuery, the Webflow chunks, and roughly twenty GSAP files. Only then does wf.ready() release this callback and register the listener.
By that point osano-cm-initialized fired long ago, and Osano does not replay that event to anything subscribing afterward. This was verified directly in the browser.
The practical effect: a returning visitor who previously accepted analytics never gets wf.allowUserTracking() called on page load. Live consent changes still work, because that is the second listener. Only the initial sync is dead.
The fix is to read the current state directly instead of waiting for an event that has already passed:
wf.ready(() => {
const consent = Osano.cm.getConsent();
if (consent && consent.ANALYTICS === 'ACCEPT') {
wf.allowUserTracking();
} else {
wf.denyUserTracking();
}
Osano.cm.addEventListener('osano-cm-consent-changed', (change) => {
if (change.ANALYTICS === 'ACCEPT') {
wf.allowUserTracking();
} else if (change.ANALYTICS === 'DENY') {
wf.denyUserTracking();
}
});
});Note that getConsent() takes no argument. Passing it a category name does nothing and returns the full object regardless. This block already reads the property correctly, which is the pattern to copy.
Part 5: The global stylesheet
The last block is a large minified <style> tag. It exists because these rules either cannot be expressed in the Webflow Designer or need to apply globally without being attached to a class.
It groups into six purposes.
Font rendering
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}Makes text render thinner and cleaner on macOS. The comment in the code asks you to keep it, and that is a reasonable request. Removing it makes type look noticeably heavier on Mac.
Element resets
a { text-decoration: none; }
button { background-color: unset; padding: unset; text-align: inherit; }
section { position: relative; }
:is(h1,h2,h3,h4,h5,h6,p,ul,ol):first-child { margin-top: 0 !important; }Strips browser defaults so Webflow styling is the only thing in play. The last rule is the useful one: it removes the top margin from the first element inside any container, which is what stops rich text from pushing itself away from the top of its box.
Layout container
:where([data-container]) {
width: 100%;
max-width: 90rem;
margin-left: auto;
margin-right: auto;
padding: var(--scale--scale-128px) var(--scale--scale-32px);
}A global content container driven by a data attribute rather than a class. :where() gives it zero specificity, so any Webflow class overrides it without a fight. The padding uses Client-First scale variables.
Rich text helpers
.w-richtext ul li::marker { content: "\2014"; }
.em-dash-list li:before {
content: "\2014";
color: #41b6e6;
display: inline-block;
width: 1em;
}\2014 is the em dash character. CAS uses em dashes instead of bullets in list markers. The second rule is a manual version for places where ::marker cannot be styled.
This section also carries .styled-table, a full table treatment in CAS blue with striped rows, and a simple two-column .grid with .box-1 and .box-2 in CAS blue and yellow that collapses to one column under 600px. These exist so content editors can build tables and simple layouts inside rich text embeds.
Language-specific typography
:lang(ko) body, :lang(ko) p, :lang(ko) h1 {
word-break: keep-all;
overflow-wrap: break-word;
}
:lang(ja) body { line-break: strict; }
:lang(es) p, :lang(pt) p { hyphens: auto; }These matter more than they look. Korean and Japanese have line-breaking rules that Western defaults get wrong, producing text that breaks mid-word or mid-phrase. Spanish and Portuguese words run long, so automatic hyphenation prevents ragged columns. See Translation & localization for the wider locale setup.
@media print {
@page { margin: 0; }
body { margin: 1cm; }
}Known issues
- The initial consent sync never runs. Described in part 4 above. Confirmed, with a fix provided.
.container:tinyis not valid CSS. There is no:tinypseudo-class. The rule is silently ignored by every browser. It is harmless but dead, and it should be removed or corrected to whatever it was meant to be.-o-font-smoothingdoes nothing. Opera never supported that property. Also harmless, also dead.- The Osano preload is close to pointless. A
<link rel="preload">immediately followed by a synchronous script tag for the same file buys almost nothing, because the parser blocks on the script anyway. Not worth removing urgently, but it is not doing what the comment claims.
How to test it
Load a published page, open the console, and check each layer.
Consent Mode is firing both a default and an update:
dataLayer.filter(e => e[0] === 'consent')You should see two entries. A default with everything denied, then an update reflecting the visitor's actual choice. If the update is missing, Osano's Google Consent Mode integration is misconfigured in the Osano dashboard.
Osano is loaded and reporting:
Osano.cm.getConsent()Webflow's runtime arrived and the queue drained:
typeof wf.allowUserTracking // 'function' means the real runtime loaded
wf.r // undefined means the stub was replacedGTM is present:
google_tag_manager['GTM-5RS86Q']Common mistakes
- Adding tracking scripts directly to the head. Almost everything belongs in GTM instead, where it can be consent-gated and changed without publishing the site.
- Adding anything above the Consent Mode defaults. Anything that fires before those defaults are set is running without knowing what the visitor allows.
- Passing a category name to
getConsent(). The argument is ignored and the full object is returned. Read the property instead. - Assuming
wf.ready()runs immediately. It queues until the Webflow bundle loads at the bottom of the page, which is late enough to miss early events. - Editing the GTM snippet by hand. It is Google's standard loader. The only thing worth changing is the container ID.
- Treating the CSS block as scratch space. It applies to every page in every locale. Anything expressible as a Webflow class belongs in the Designer.
Quick reference
- Location: Site Settings > Custom Code > Head Code
- GTM container:
GTM-5RS86Q - Consent platform: Osano, loaded synchronously
- Consent default: everything denied except functionality and security
- Consent wait window: 3000ms before tags act on defaults
- Webflow Analyze: gated separately through
wf.allowUserTracking() - Brand colors in the CSS:
#0032a0blue,#ffc72cyellow,#41b6e6light blue - New tags go in GTM, not in this block