Most GTM/GA4 tutorials assume you're on WordPress with a plugin doing the heavy lifting. When you've migrated to static HTML/CSS/JS — no CMS, no plugin ecosystem — you're wiring everything by hand. Here's the actual setup, including the parts most tutorials skip: custom event tracking for things a plugin would normally handle for you.
1. The base GTM snippet
Drop this in <head>, as high as possible, on every page:
<!-- Google Tag Manager -->
<script>(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-XXXXXXX');</script>
<!-- End Google Tag Manager -->
And the noscript fallback right after the opening <body> tag:
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
That's it for the container. GA4 itself gets configured inside GTM as a tag, not hardcoded on the page — this is the part people skip when coming from a plugin-based setup, where the plugin often injects the GA4 script directly instead of routing through GTM.
2. Firing custom events without a plugin
On WordPress, a plugin usually auto-tracks outbound clicks, file downloads, form submits. On static HTML, you push these to dataLayer manually:
document.querySelectorAll('a[href^="https://wa.me"]').forEach(link => {
link.addEventListener('click', () => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'whatsapp_click',
link_url: link.href
});
});
});
Then in GTM: create a Custom Event trigger listening for whatsapp_click, attach it to a GA4 Event tag, and map link_url as an event parameter via a Data Layer Variable. No plugin, same result.
3. Form submission tracking (Web3Forms example)
If you're using Web3Forms (or any fetch-based form handler) instead of a native <form action> submit, the page never actually navigates — so a simple "Thank You page visited" GA4 trigger won't fire. You need to push the event manually on the fetch success callback:
fetch('https://api.web3forms.com/submit', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.dataLayer.push({ event: 'form_submit_success' });
// redirect or show thank-you state
}
});
This is the single most common tracking gap I see on migrated static sites — the form works fine, but the conversion event silently never fires because nobody wired it to the actual async response.
4. Validating before you trust any of it
Don't assume it's working because the container published. Use GTM's Preview mode and actually click through every tracked interaction — WhatsApp link, form submit, phone tap — watching the tag fire in real time. Then cross-check in GA4's DebugView that the event lands with the parameters you expect attached.
Skipping this step is how sites end up with months of "working" analytics that were quietly firing wrong the whole time.
I do this kind of GTM/GA4 implementation work regularly for client migrations off WordPress — more at capareach.com if it's useful.
Top comments (0)