Skip to content

Same-Origin Subfolder Deployment

Most sGTM deployments use a subdomain: sgtm.example.com, metrics.example.com, collect.example.com. That works, and with a Cloudflare proxy in front it even survives Safari ITP reasonably well. But there is a stricter deployment topology that survives ITP better: serving sGTM as a subfolder of the main site — example.com/metrics/* — so there is literally one origin, one cookie scope, and no subdomain relationship for the browser to treat as tracker-adjacent.

Subfolder deployment is more operational work. You need a reverse proxy (Cloudflare Worker, Nginx, Vercel rewrite, Next.js middleware) that routes /metrics/* to the sGTM origin and everything else to the main site. In return you get:

  • Strongest cookie scoping. Cookies live on example.com with Path=/ and Domain=example.com, same as any other first-party cookie the site sets. ITP treats them exactly like the site’s own session cookies.
  • No CORS. sGTM requests are same-origin with the page. No preflight, no Access-Control-Allow-Origin negotiation.
  • Simpler CMP integration. Your cookie banner rules a single hostname. No subdomain exception.
  • Better first-party signal in ad-platform algorithms. Some platforms weight “same-registrable-domain first-party” signals higher than subdomain first-party signals. Whether that matters materially is disputed, but it’s a free upside.

The trade-offs are real: more infrastructure, a routing layer that becomes a dependency, potential path conflicts with your app’s own routes, and extra latency per request.

SituationRecommended topology
Standard sGTM on Stape / GCP with Cloudflare in frontSubdomain (simpler, good enough)
Safari-heavy traffic and cross-visit attribution mattersSubfolder
Ecommerce with 30+ day purchase cyclesSubfolder
Complex multi-team site where operations can’t own a reverse proxySubdomain
Already running Cloudflare Workers or Vercel with rewritesSubfolder (marginal additional effort)
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith('/metrics/')) {
// Strip /metrics prefix, route to sGTM origin
const sgtmUrl = new URL(request.url);
sgtmUrl.hostname = env.SGTM_ORIGIN; // e.g. gtm-xxxxxx-xx.a.run.app
sgtmUrl.pathname = url.pathname.replace(/^\/metrics/, '');
// Forward with original headers — critical for geo and consent
return fetch(sgtmUrl.toString(), {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual',
});
}
// Everything else — pass through to the main origin
return fetch(request);
},
};

Deploy with wrangler deploy and add a route rule in Cloudflare Dashboard → Workers → Routes: example.com/metrics/* → worker name.

  1. Server container URL — in GTM, set the server container URL to https://example.com/metrics (no trailing slash). This is what the client-side Google Tag uses when posting events.

  2. Client-side Google Tag configuration — in the GA4 Configuration tag or gtag.js config, set server_container_url to https://example.com/metrics. This is the toggle that makes client-side events flow through your sGTM.

  3. CMP integration — your cookie banner should treat example.com cookies as first-party (it already does). No special exception needed for subdomains, because there aren’t any.

  4. Verification — open DevTools, fire a test event. The outbound request should go to https://example.com/metrics/g/collect, not to a subdomain. The _ga cookie should be set by example.com with the full 2-year expiry.

If your application already uses /metrics for something (a Prometheus endpoint, an internal dashboard, a URL path that happens to be an analytics term), pick a different prefix. /s, /i, /t are all reasonable short options. /analytics is tempting but high-profile path names get flagged by ad-blockers more often than generic ones — the whole point of subfolder deployment is partly to be unremarkable. Short and generic wins.

If you pick /metrics and then later add a Prometheus endpoint at /metrics/prometheus, the routing rule intercepts it. Either be careful about prefix exclusivity or make the rule more specific:

# Only match sGTM's actual paths, not everything under /metrics
location ~ ^/metrics/(g/collect|j/collect|gtm\.js|gtag/js) {
# ... forward to sGTM
}

Forgetting to strip the path prefix in the rewrite. sGTM expects requests at /g/collect, not /metrics/g/collect. If the prefix isn’t stripped, sGTM returns 404s and the cause is invisible from the browser (which just sees a 404 response without detail).

Not forwarding headers. Geo enrichment relies on CF-IPCountry (or X-Forwarded-For). Consent logic may rely on custom headers. If your reverse proxy strips headers by default, both features silently break.

Caching the rewritten responses. The reverse proxy must not cache sGTM responses. Cloudflare caches by default on many tiers — add an explicit bypass for /metrics/*. Nginx has proxy_cache off per location; Vercel does not cache rewritten responses by default.

Setting the wrong Host header. Cloud Run routes by Host header. If the reverse proxy forwards the original example.com Host, Cloud Run sees an unexpected host and may reject the request. Set Host: gtm-xxxxxx-xx.a.run.app (or your custom domain mapping) explicitly.

Assuming Edge routing is free performance-wise. A Cloudflare Worker adds ~5–20ms per request. A same-region Nginx proxy adds ~1–3ms. Vercel rewrites are essentially free at the edge. Measure — don’t assume — and decide if the latency is acceptable for your setup.

Losing the mapping when infrastructure changes. The reverse-proxy rule is now a load-bearing piece of your tagging infrastructure. Document it in your runbook. When someone migrates the main site to a new platform, the subfolder rewrite has to move with it — otherwise sGTM goes silently offline.