Anchor Links and UTM Parameters: Technical Guide

UTMGuard Team
8 min readtechnical-guides

Anchor links and UTM parameters both live in URLs. But they follow different rules, serve different purposes, and when mixed incorrectly, destroy campaign tracking.

This is the complete technical guide to using them together correctly.

🚨 Not sure what's breaking your tracking?

Run a free 60-second audit to check all 40+ ways UTM tracking can fail.

Scan Your Campaigns Free

✓ No credit card ✓ See results instantly

What Are URL Fragments?

The fragment identifier (everything after #) was designed for in-page navigation:

https://docs.site.com/guide#installation
                            ↑
                        fragment

Purpose: Scroll the browser to the HTML element with id="installation".

Fragment Behavior (RFC 3986)

URL fragments follow specific rules defined in RFC 3986:

  1. Client-side only: Never sent in HTTP requests
  2. Browser-handled: Processed entirely by the browser
  3. After query string: Must come after ? parameters
  4. Single fragment: Only one # per URL (last one wins)

Why Fragments Aren't Sent to Servers

When you visit:

site.com/page?utm_source=email#section3

HTTP request sent to server:

GET /page?utm_source=email HTTP/1.1
Host: site.com

Fragment kept in browser:

#section3

The server never sees the fragment. This is by design—fragments are for client-side navigation only.

UTM Parameters: Server-Side Tracking

UTM parameters are query string parameters:

site.com?utm_source=facebook&utm_medium=cpc&utm_campaign=summer
         ↑
    query string (sent to server)

Purpose: Track campaign attribution in analytics.

Requirements:

  • Must start with ?
  • Multiple parameters joined with &
  • Must be URL-encoded
  • Must be sent to the server for tracking

The Conflict: When Fragments Come First

The Broken Structure

❌ site.com#pricing?utm_source=facebook&utm_campaign=sale

What happens:

  1. Browser parses URL
  2. Finds # first
  3. Treats everything after # as fragment: pricing?utm_source=facebook&utm_campaign=sale
  4. Sends to server: site.com
  5. Fragment (including UTMs): stays in browser

Result: Zero UTM parameters reach Google Analytics.

The Correct Structure

✅ site.com?utm_source=facebook&utm_campaign=sale#pricing

What happens:

  1. Browser parses URL
  2. Finds ? first → query string starts
  3. Parses UTM parameters until #
  4. Sends to server: site.com?utm_source=facebook&utm_campaign=sale
  5. Fragment: #pricing stays in browser for scroll behavior

Result: GA4 receives UTM parameters AND page scrolls to anchor.

😰 Is this your only tracking issue?

This is just 1 of 40+ ways UTM tracking breaks. Most marketing teams have 8-12 critical issues they don't know about.

• 94% of sites have UTM errors

• Average: $8,400/month in wasted ad spend

• Fix time: 15 minutes with our report

✓ Connects directly to GA4 (read-only, secure)

✓ Scans 90 days of data in 2 minutes

✓ Prioritizes issues by revenue impact

✓ Shows exact sessions affected

Get Your Free Audit Report

Complete URL Anatomy

RFC 3986 URL Structure

https://user:pass@host.com:8080/path/to/page?query=value&key=val#fragment
  ↑       ↑         ↑        ↑       ↑            ↑                 ↑
scheme  userinfo   host    port    path       query           fragment

ORDER MATTERS:
1. Scheme (https://)
2. Authority (user:pass@host.com:8080)
3. Path (/path/to/page)
4. Query (?query=value&key=val)      ← UTM parameters go here
5. Fragment (#fragment)               ← Anchor goes here

UTM Parameter Placement

✅ CORRECT:
https://site.com/products/shoes?utm_source=facebook&utm_medium=paid#featured
                               ↑                                    ↑
                           query starts                      fragment starts

❌ WRONG:
https://site.com/products/shoes#featured?utm_source=facebook&utm_medium=paid
                               ↑
                    fragment starts (consumes everything after)

Real-World Implementation Examples

Example 1: Product Launch Landing Page

Goal: Drive traffic to specific section with campaign tracking.

Correct URL:

https://shop.com/new-arrivals?utm_source=instagram&utm_medium=story&utm_campaign=spring2024&utm_content=swipe-up#hero-banner

Behavior:

  • User clicks in Instagram story
  • Lands on /new-arrivals page
  • Page scrolls to #hero-banner section
  • GA4 receives full campaign attribution

Example 2: Email Newsletter

Goal: Link to blog post section with tracking.

Correct URL:

https://blog.com/ultimate-guide?utm_source=newsletter&utm_medium=email&utm_campaign=weekly-digest&utm_content=section3-link#implementation-steps

Behavior:

  • User clicks in email
  • Opens blog post
  • Scrolls to #implementation-steps
  • Email campaign tracked in GA4

Example 3: Multi-Anchor Navigation

Common mistake:

❌ site.com#header?utm_source=facebook#main-content

Why it fails:

  • Only one fragment allowed per URL
  • Browser uses last # (main-content)
  • First # starts fragment, consuming ?utm_source=facebook

Correct approach:

✅ site.com?utm_source=facebook#main-content

Pick ONE anchor. You can't have multiple.

Technical Validation Methods

Method 1: Browser DevTools

// Open browser console on your landing page
const url = new URL(window.location.href);
 
console.log('Full URL:', url.href);
console.log('Query string:', url.search);
console.log('Fragment:', url.hash);
console.log('UTM Source:', url.searchParams.get('utm_source'));

Expected output (correct structure):

Full URL: https://site.com/page?utm_source=facebook#pricing
Query string: ?utm_source=facebook
Fragment: #pricing
UTM Source: facebook

Output when broken:

Full URL: https://site.com/page#pricing?utm_source=facebook
Query string: (empty string)
Fragment: #pricing?utm_source=facebook
UTM Source: null

Method 2: Server-Side Validation

# Python example - check what server receives
from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/landing')
def landing():
    utm_source = request.args.get('utm_source')
    print(f"Query string received: {request.query_string}")
    print(f"UTM Source: `{"{"}{"{"}utm_source{"}"}{"}"}}`")
 
    # If utm_source is None, fragment came before query
    if utm_source is None:
        print("ERROR: No UTM parameters received. Check URL structure.")

Method 3: Google Analytics Debugger

// Add to your site for debugging
window.addEventListener('load', () => {
  const params = new URLSearchParams(window.location.search);
  const hasUTMs = params.has('utm_source') || params.has('utm_medium');
 
  if (!hasUTMs && window.location.hash.includes('utm_')) {
    console.error('UTMs detected in fragment! They will not track.');
    console.error('Fragment:', window.location.hash);
  }
});

Edge Cases and Gotchas

// ❌ WRONG - builds fragment first
const link = `${"{"}{"{"}baseUrl{"}"}{"}"}}#${"{"}{"{"}anchor{"}"}{"}"}}?${"{"}{"{"}utmParams{"}"}{"}"}}`;
 
// ✅ CORRECT
const link = `${"{"}{"{"}baseUrl{"}"}{"}"}}?${"{"}{"{"}utmParams{"}"}{"}"}}#${"{"}{"{"}anchor{"}"}{"}"}}`;

Edge Case 2: URL Builder Concatenation

// ❌ WRONG - appends UTMs to existing fragment
function addUTMs(url, utms) {
  return url + '?' + utms;  // Dangerous if url has fragment
}
 
// ✅ CORRECT - checks for existing fragment
function addUTMs(url, utms) {
  const [base, fragment] = url.split('#');
  const separator = base.includes('?') ? '&' : '?';
  return `${"{"}{"{"}base{"}"}{"}"}}${"{"}{"{"}separator{"}"}{"}"}}${"{"}{"{"}utms{"}"}{"}"}}${fragment ? '#' + fragment : ''}`;
}

Edge Case 3: Third-Party URL Shorteners

Some URL shorteners preserve fragments, others don't:

bit.ly/abc123 → expands to → site.com?utm_source=x#pricing ✅
rebrandly.com/xyz → expands to → site.com#pricing?utm_source=x ❌

Solution: Always test shortened URLs before using in campaigns.

Best Practices Checklist

For Developers

✅ Use URL constructor APIs (not string concatenation) ✅ Validate URL structure in tests ✅ Add lint rules for URL building ✅ Document URL structure in style guide ✅ Use TypeScript for URL parameter types

For Marketers

✅ Always put ? before # in campaign URLs ✅ Test every campaign URL in GA4 Real-Time ✅ Use URL validation tools before launch ✅ Document URL structure in campaign templates ✅ Train new team members on correct structure

✅ Fixed this issue? Great! Now check the other 39...

You just fixed one tracking issue. But are your Google Ads doubling sessions? Is Facebook attribution broken? Are internal links overwriting campaigns?

Connects to GA4 (read-only, OAuth secured)

Scans 90 days of traffic in 2 minutes

Prioritizes by revenue impact

Free forever for monthly audits

Run Complete UTM Audit (Free Forever)

Join 2,847 marketers fixing their tracking daily

FAQ

Can I use multiple anchors in one URL?

No. Browsers only support one fragment per URL. If you have multiple # symbols, only the last one is used.

What happens to the fragment in server logs?

Fragments never appear in server logs because they're not sent in HTTP requests.

Do URL shorteners break fragments?

Most preserve fragments correctly. But some redirect services may strip or reorder them. Always test before using in production.

Can I track fragment interactions in GA4?

Yes, using custom JavaScript events. Fragments aren't automatically tracked, but you can send events when users click anchor links.

Does this affect SEO?

No. Google crawls query strings but ignores fragments (except in AJAX crawling, which is deprecated). Your UTM parameters don't affect SEO.

How do I validate URLs programmatically?

Use the URL API in JavaScript, or URL parsing libraries in other languages. Check that url.search contains UTMs and url.hash contains the anchor.

Conclusion

Anchor links and UTM parameters work together perfectly when structured correctly.

The rule:

domain.com/path?query_parameters#fragment

ALWAYS: ? before #

Implementation:

  • Use URL APIs, not string concatenation
  • Validate structure before launch
  • Test in GA4 Real-Time reports
  • Document in your style guide

Get this right once, and both your marketing tracking and user experience work flawlessly.


Technical Reference: Fragment Before Query Validation Rule