DEV Community

rashid ks
rashid ks

Posted on

Setting Up GTM + GA4 on a Static HTML Site (No CMS, No Plugins)

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 -->
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

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)