best-practicesUpdated 2025

URL Builder Validation: Catch Errors Before Campaign Launch

Building campaign URLs manually? Add validation to catch duplicates, encoding errors, and syntax mistakes before they corrupt your GA4 data.

8 min readbest-practices

You build campaign URLs manually. Paste into ads. Launch.

Three days later: GA4 shows zero data. The URL had duplicate parameters.

URL builders need validation. Here's how to add it and prevent launch failures.

🚨 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

The Problem

Manual URL building introduces errors:

  1. Duplicate parameters - Same UTM added twice
  2. Encoding errors - Unencoded spaces, special characters
  3. Syntax errors - Multiple ?, fragment before query
  4. Case inconsistency - Mixed uppercase/lowercase
  5. Invalid characters - Reserved characters unencoded

Each error corrupts tracking. Validation catches them before launch.

Real Example

Company: Marketing agency Tool: Custom spreadsheet URL builder Error: Template didn't check for duplicate utm_campaign Result: 50+ client campaigns launched with duplicates

Code
Intended:
?utm_source=google&utm_medium=cpc&utm_campaign=spring

Actual (from broken template):
?utm_source=google&utm_medium=cpc&utm_campaign=spring&utm_campaign={"{"}{"{"}fallback_campaign{"}"}{"}"}}

Impact:

  • $180,000 combined client ad spend
  • GA4 data unpredictable (mixed campaign attribution)
  • Had to manually fix 50+ campaigns mid-flight
  • Client trust damaged

Adding validation would have prevented launch.

😰 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

Essential Validation Checks

Check 1: No Duplicate Parameters

Javascript
function checkDuplicates(url) {
    const params = new URL(url).searchParams;
    const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
    const errors = [];
 
    utmParams.forEach(param => {
        const count = params.getAll(param).length;
        if (count > 1) {
            errors.push({
                type: 'duplicate',
                param: param,
                count: count,
                severity: 'critical'
            });
        }
    });
 
    return errors;
}
 
// Usage
const url = 'site.com?utm_source=fb&utm_source=ig';
console.log(checkDuplicates(url));
// [{ type: 'duplicate', param: 'utm_source', count: 2, severity: 'critical' }]

Check 2: Proper Encoding

Javascript
function checkEncoding(url) {
    const errors = [];
    const queryString = url.split('?')[1] || '';
 
    // Check for unencoded spaces
    if (queryString.includes(' ')) {
        errors.push({
            type: 'encoding',
            issue: 'Unencoded spaces',
            fix: 'Replace spaces with %20 or underscores',
            severity: 'critical'
        });
    }
 
    // Check for double encoding
    if (queryString.includes('%25')) {
        errors.push({
            type: 'encoding',
            issue: 'Double-encoding detected (%25)',
            fix: 'Decode once before using',
            severity: 'warning'
        });
    }
 
    // Check for invalid percent encoding
    const invalidPercent = queryString.match(/%(?![0-9A-Fa-f]{2})/);
    if (invalidPercent) {
        errors.push({
            type: 'encoding',
            issue: 'Invalid percent-encoding',
            fix: 'Use valid %XX format',
            severity: 'critical'
        });
    }
 
    return errors;
}

Check 3: URL Syntax

Javascript
function checkSyntax(url) {
    const errors = [];
 
    // Check for multiple question marks
    const questionMarks = (url.match(/\?/g) || []).length;
    if (questionMarks > 1) {
        errors.push({
            type: 'syntax',
            issue: 'Multiple question marks',
            fix: 'Use only one ?, then & for parameters',
            severity: 'critical'
        });
    }
 
    // Check fragment position
    const fragmentIndex = url.indexOf('#');
    const queryIndex = url.indexOf('?');
    if (fragmentIndex !== -1 && queryIndex !== -1 && fragmentIndex < queryIndex) {
        errors.push({
            type: 'syntax',
            issue: 'Fragment (#) before query string (?)',
            fix: 'Move # to end of URL',
            severity: 'critical'
        });
    }
 
    return errors;
}

Check 4: Case Consistency

Javascript
function checkCase(url) {
    const errors = [];
    const params = new URL(url).searchParams;
    const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
 
    utmParams.forEach(param => {
        const value = params.get(param);
        if (value && value !== value.toLowerCase()) {
            errors.push({
                type: 'case',
                param: param,
                value: value,
                suggestion: value.toLowerCase(),
                severity: 'warning'
            });
        }
    });
 
    return errors;
}

Check 5: Required Parameters

Javascript
function checkRequired(url) {
    const errors = [];
    const params = new URL(url).searchParams;
    const required = ['utm_source', 'utm_medium', 'utm_campaign'];
 
    required.forEach(param => {
        if (!params.has(param)) {
            errors.push({
                type: 'missing',
                param: param,
                severity: 'critical'
            });
        }
    });
 
    return errors;
}

Complete Validation Function

Javascript
function validateCampaignUrl(url) {
    const allErrors = [];
 
    try {
        // Run all checks
        allErrors.push(...checkDuplicates(url));
        allErrors.push(...checkEncoding(url));
        allErrors.push(...checkSyntax(url));
        allErrors.push(...checkCase(url));
        allErrors.push(...checkRequired(url));
 
        // Categorize by severity
        const critical = allErrors.filter(e => e.severity === 'critical');
        const warnings = allErrors.filter(e => e.severity === 'warning');
 
        return {
            valid: critical.length === 0,
            errors: allErrors,
            summary: {
                critical: critical.length,
                warnings: warnings.length,
                total: allErrors.length
            }
        };
 
    } catch (error) {
        return {
            valid: false,
            errors: [{
                type: 'parse_error',
                issue: 'Could not parse URL',
                message: error.message,
                severity: 'critical'
            }],
            summary: { critical: 1, warnings: 0, total: 1 }
        };
    }
}
 
// Usage
const url = 'https://site.com?utm_source=Facebook&utm_source=fb&utm_campaign=spring sale';
const result = validateCampaignUrl(url);
 
console.log(result);
// {
//   valid: false,
//   errors: [
//     { type: 'duplicate', param: 'utm_source', ... },
//     { type: 'encoding', issue: 'Unencoded spaces', ... },
//     { type: 'case', param: 'utm_source', value: 'Facebook', ... }
//   ],
//   summary: { critical: 2, warnings: 1, total: 3 }
// }

Integration Examples

HTML Form Validation

Html
<form id="urlBuilder">
    <input type="text" id="baseUrl" placeholder="https://site.com" required>
    <input type="text" id="utmSource" placeholder="facebook" required>
    <input type="text" id="utmMedium" placeholder="cpc" required>
    <input type="text" id="utmCampaign" placeholder="spring_sale" required>
 
    <button type="submit">Build URL</button>
 
    <div id="errors" class="error-container"></div>
    <div id="result" class="result-container"></div>
</form>
 
<script>
document.getElementById('urlBuilder').addEventListener('submit', function(e) {
    e.preventDefault();
 
    // Build URL
    const baseUrl = document.getElementById('baseUrl').value;
    const params = new URLSearchParams({
        utm_source: document.getElementById('utmSource').value,
        utm_medium: document.getElementById('utmMedium').value,
        utm_campaign: document.getElementById('utmCampaign').value
    });
 
    const fullUrl = `${"{"}{"{"}baseUrl{"}"}{"}"}}?${params.toString()}`;
 
    // Validate
    const validation = validateCampaignUrl(fullUrl);
 
    // Display errors
    const errorsDiv = document.getElementById('errors');
    if (!validation.valid) {
        errorsDiv.innerHTML = `
            <h3>❌ Validation Failed</h3>
            <ul>
                ${validation.errors.map(err => `
                    <li class="${err.severity}">
                        <strong>${err.type}:</strong> ${err.issue || err.message}
                        ${err.fix ? `<br><em>Fix: ${err.fix}</em>` : ''}
                    </li>
                `).join('')}
            </ul>
        `;
        document.getElementById('result').innerHTML = '';
    } else {
        errorsDiv.innerHTML = '<p class="success">✅ Validation Passed</p>';
        document.getElementById('result').innerHTML = `
            <h3>Your Campaign URL:</h3>
            <input type="text" value="${"{"}{"{"}fullUrl{"}"}{"}"}}" readonly style="width:100%">
            <button onclick="navigator.clipboard.writeText('${"{"}{"{"}fullUrl{"}"}{"}"}}')">Copy</button>
        `;
    }
});
</script>

Google Sheets Integration

Javascript
// Google Apps Script for Sheets
function validateUrlInSheet(url) {
    const validation = validateCampaignUrl(url);
 
    if (!validation.valid) {
        // Return errors as array for display in adjacent cells
        return [
            ['❌ INVALID'],
            ...validation.errors.map(err => [
                `${err.type}: ${err.issue || err.message}`
            ])
        ];
    } else {
        return [['✅ VALID']];
    }
}
 
// Use as custom function in Sheets
// =validateUrlInSheet(A2)

React Component

Jsx
import { useState } from 'react';
 
function UrlBuilder() {
    const [url, setUrl] = useState('');
    const [validation, setValidation] = useState(null);
 
    const handleValidate = () => {
        const result = validateCampaignUrl(url);
        setValidation(result);
    };
 
    return (
        <div>
            <input
                type="text"
                value=`{"{"}{"{"}url{"}"}{"}"}}`
                onChange={(e) => setUrl(e.target.value)}
                placeholder="Enter campaign URL"
            />
            <button onClick={"{"}{"{"}handleValidate{"}"}{"}"}}>Validate</button>
 
            {validation && (
                <div className={validation.valid ? 'success' : 'error'}>
                    {validation.valid ? (
                        <p>✅ URL is valid!</p>
                    ) : (
                        <div>
                            <p>❌ Validation failed:</p>
                            <ul>
                                {validation.errors.map((err, i) => (
                                    <li key=`{"{"}{"{"}i{"}"}{"}"}}` className={err.severity}>
                                        <strong>{err.type}:</strong> {err.issue}
                                        {err.fix && <em> - {err.fix}</em>}
                                    </li>
                                ))}
                            </ul>
                        </div>
                    )}
                </div>
            )}
        </div>
    );
}

Pre-Launch Checklist

Javascript
// Complete pre-launch validation
function preLaunchCheck(url) {
    console.log('🔍 Validating URL:', url);
 
    const result = validateCampaignUrl(url);
 
    console.log('\n📊 Summary:');
    console.log(`  Critical errors: ${result.summary.critical}`);
    console.log(`  Warnings: ${result.summary.warnings}`);
 
    if (result.summary.critical > 0) {
        console.log('\n❌ CANNOT LAUNCH - Critical errors detected:');
        result.errors
            .filter(e => e.severity === 'critical')
            .forEach(err => {
                console.log(`  - ${err.type}: ${err.issue || err.message}`);
                if (err.fix) console.log(`    Fix: ${err.fix}`);
            });
        return false;
    }
 
    if (result.summary.warnings > 0) {
        console.log('\n⚠️  WARNINGS (optional fixes):');
        result.errors
            .filter(e => e.severity === 'warning')
            .forEach(err => {
                console.log(`  - ${err.type}: ${err.issue || err.message}`);
            });
    }
 
    console.log('\n✅ URL is ready for launch!');
    return true;
}
 
// Usage
preLaunchCheck('https://site.com?utm_source=facebook&utm_medium=cpc&utm_campaign=spring');

✅ 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

Should I validate every URL before launch?

Yes. Validation takes seconds, but fixing post-launch tracking failures takes days.

Can I automate validation in my workflow?

Yes. Add validation to:

  • URL builder forms
  • Spreadsheet templates
  • CI/CD pipelines
  • Campaign management tools

What if validation fails for a live URL?

Fix immediately. Validation catching errors in production means you should fix and update all placements.

Do paid URL builders include validation?

Most do, but custom builders (spreadsheets, scripts) often don't. Add validation to any custom solution.

Conclusion

URL builders need validation to catch errors before campaigns launch.

Essential checks:

  1. ✅ No duplicate parameters
  2. ✅ Proper encoding
  3. ✅ Valid URL syntax
  4. ✅ Case consistency
  5. ✅ Required parameters present

Add validation to your workflow. Prevent launch failures.


Technical Reference: Duplicate UTM Parameters Validation Rule

UTM

Get Your Free Audit in 60 Seconds

Connect GA4, run the scan, and see exactly where tracking is leaking budget. No credit card required.

Trusted by growth teams and agencies to keep attribution clean.