Comprehensive Link Tracking
Most GTM containers have a separate trigger for outbound links, another for downloads, maybe one for email clicks, and so on. Each one has its own tag, its own conditions, its own maintenance burden. A comprehensive link tracking system collapses all of this into a single trigger and a single tag that categorizes every link click automatically.
The result is a cleaner container, consistent data in GA4, and a single place to add new link types.
The classification architecture
Section titled “The classification architecture”The strategy: one All Clicks trigger that fires on every click → one Custom JavaScript variable that classifies the link type → one GA4 Event tag that reads the classification.
Link types to classify:
outbound— links to external domainsdownload— links to file types you want to trackemail—mailto:linksphone—tel:linksanchor— same-page#fragmentlinkssocial— links to social media platformsinternal— links within your site (optionally track these too)
Step 1: The link classification variable
Section titled “Step 1: The link classification variable”Create a Custom JavaScript variable named CJS - Link Type:
function() { var url = {{Click URL}}; if (!url) return 'unknown';
var currentHost = window.location.hostname;
// Email links if (url.indexOf('mailto:') === 0) return 'email';
// Phone links if (url.indexOf('tel:') === 0) return 'phone';
// Anchor links (same-page) if (url.indexOf('#') === 0) return 'anchor';
// File downloads var downloadExtensions = /\.(pdf|xlsx?|docx?|pptx?|csv|zip|rar|7z|mp3|mp4|mov|exe|dmg|pkg|deb|apk)(\?|$)/i; if (downloadExtensions.test(url)) return 'download';
// Social media var socialDomains = /(twitter|x)\.com|linkedin\.com|facebook\.com|instagram\.com|youtube\.com|tiktok\.com|pinterest\.com/i; if (socialDomains.test(url)) return 'social';
// Outbound: external domain try { var linkHost = new URL(url).hostname; if (linkHost && linkHost !== currentHost && linkHost !== 'www.' + currentHost) { return 'outbound'; } } catch (e) { // URL parsing failed — relative URL or malformed return 'internal'; }
return 'internal';}Step 2: The trigger
Section titled “Step 2: The trigger”Create an All Clicks trigger (not Link Click — All Clicks fires on every click, giving the variable more reliable access to the element):
- Trigger type: All Elements
- This trigger fires on: Some Clicks
- Condition: Click URL — does not equal — “ (not empty)
Optionally, filter to exclude clicks that are clearly not links:
- Condition: Click Tag — matches RegEx —
^(a|button)$(fire only on anchors and buttons)
Step 3: The GA4 Event tag
Section titled “Step 3: The GA4 Event tag”GA4 - Event - Link Click
- Type
- Google Analytics: GA4 Event
- Trigger
- All Clicks - With URL
- Variables
-
CJS - Link TypeClick URLClick TextPage URL
Set event name to link_click and parameters:
link_type→{{CJS - Link Type}}link_url→{{Click URL}}link_text→{{Click Text}}page_location→{{Page URL}}outbound→ (optional Custom JS variable returningtruefor outbound and social)
Step 4: Optionally exclude internal link tracking
Section titled “Step 4: Optionally exclude internal link tracking”If you don’t want every internal navigation tracked (this is a lot of events for large sites), add a blocking condition:
- Add trigger condition:
CJS - Link Type— does not equal —internal - Or add a blocking trigger to the GA4 tag: a trigger that fires when
CJS - Link Typeequalsinternal
Handling missing Click URL on child element clicks
Section titled “Handling missing Click URL on child element clicks”A common problem: the user clicks an <img> or <span> inside an <a> tag. GTM’s {{Click URL}} may be empty because the click landed on the child element, not the anchor.
Add a fallback Custom JavaScript variable for the link URL:
// Custom JS Variable: "CJS - Link URL With Fallback"function() { var url = {{Click URL}}; if (url) return url;
// Walk up the DOM to find the nearest anchor var el = {{Click Element}}; while (el && el !== document.body) { if (el.tagName === 'A' && el.href) { return el.href; } el = el.parentElement; } return '';}Use {{CJS - Link URL With Fallback}} instead of {{Click URL}} in both your classification variable and your GA4 tag.
Capturing the link’s parent context
Section titled “Capturing the link’s parent context”For product links, call-to-action tracking, and anything where you want to know which section of the page the link was in:
// Custom JS Variable: "CJS - Link Section"function() { var el = {{Click Element}}; while (el && el !== document.body) { var section = el.getAttribute('data-section') || el.getAttribute('data-block'); if (section) return section; el = el.parentElement; } return 'unknown';}Add data-section="hero", data-section="footer", data-block="product_grid" to your page sections. Now every link click includes which section the user was in.
What this looks like in GA4
Section titled “What this looks like in GA4”After implementing, every link click fires as a link_click event with a link_type dimension. In GA4 Explorations, you can:
- See all outbound destinations by domain
- Compare email link clicks vs. phone link clicks by page
- Identify top-performing download pages
- Segment social media clicks by social platform and source traffic
The single event approach also simplifies conversion setup in GA4 — mark link_click events where link_type = download as a key event, and you have download conversions without a separate tag.
Common mistakes
Section titled “Common mistakes”Using the Link Click trigger instead of All Clicks
Section titled “Using the Link Click trigger instead of All Clicks”Link Click triggers rewrite the event target to the closest <a> ancestor when you click a child element. This is useful but changes timing slightly. For the classification approach above where you need reliable access to the raw element, All Clicks is more predictable.
Not handling relative URLs in the classification variable
Section titled “Not handling relative URLs in the classification variable”The new URL(url) constructor in the classification variable will throw on relative URLs like /about or #section. The try/catch handles this, but test your implementation with relative links.
Triggering on every mouseenter event
Section titled “Triggering on every mouseenter event”Ensure your trigger fires on Click events only. If you accidentally set it to fire on mouseovers or other events, you’ll get enormous event volumes in GA4.