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.
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.
Table of contents
- The Problem
- Real Example
- Essential Validation Checks
- Check 1: No Duplicate Parameters
- Check 2: Proper Encoding
- Check 3: URL Syntax
- Check 4: Case Consistency
- Check 5: Required Parameters
- Complete Validation Function
- Integration Examples
- HTML Form Validation
- Google Sheets Integration
- React Component
- Pre-Launch Checklist
- FAQ
- Should I validate every URL before launch?
- Can I automate validation in my workflow?
- What if validation fails for a live URL?
- Do paid URL builders include validation?
- Conclusion
🚨 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:
- Duplicate parameters - Same UTM added twice
- Encoding errors - Unencoded spaces, special characters
- Syntax errors - Multiple
?, fragment before query - Case inconsistency - Mixed uppercase/lowercase
- 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
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
Essential Validation Checks
Check 1: No Duplicate Parameters
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
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
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
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
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
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
<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
// 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
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
// 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
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:
- ✅ No duplicate parameters
- ✅ Proper encoding
- ✅ Valid URL syntax
- ✅ Case consistency
- ✅ Required parameters present
Add validation to your workflow. Prevent launch failures.
Technical Reference: Duplicate UTM Parameters Validation Rule