Automating Campaign Link Validation in CI/CD Pipelines
Campaign link validation in CI/CD catches broken URLs, missing Open Graph tags, and redirect failures before they reach production. The pattern is simple: install mlz in your pipeline, run a check against every campaign destination URL in your config, and fail the build if anything is wrong. Here's the complete setup for GitHub Actions and GitLab CI.
Why campaign links belong in your CI pipeline
Marketing teams and engineering teams share infrastructure more often than they think. Landing pages are deployed by engineers. Campaign configs referencing destination URLs often live in repositories. When a landing page deploy breaks a campaign's destination URL, the engineering team finds out from the marketing team hours or days later — after the damage is done.
The fix is to treat campaign link validation the same way you treat other integration tests: run it in CI, fail the build on failures, and require a green check before anything ships. This is what campaign link validation looks like as a first-class engineering practice.
The operational cost is low. mlz is an npm package that installs in seconds. Each validation check takes 1–3 seconds. A pipeline step that validates 10 campaign links adds roughly 30 seconds to a build. That's a reasonable price for catching a broken og:image or a redirect that strips UTM parameters before it ships to production.
GitHub Actions: validate every campaign link
This workflow runs on every push to main and on pull requests. It installs mlz, then validates a set of campaign destination URLs. If any check fails, the build fails and the PR cannot be merged.
name: Campaign Link Validation
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate-campaign-links:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install MissingLinkz
run: npm install -g missinglinkz
- name: Validate campaign destination URLs
env:
MLZ_API_KEY: ${{ secrets.MLZ_API_KEY }}
run: |
mlz check "https://yoursite.com/landing-spring" --format json \
| jq -e '.valid == true'
mlz check "https://yoursite.com/landing-q2" --format json \
| jq -e '.valid == true'
mlz inspect "https://yoursite.com/landing-spring" --format json \
| jq -e '.success == true'
A few things to note about this setup:
mlz checkvalidates URL resolution, SSL, redirects, and response time. It exits 0 on success and 1 on failure — the pipeline handles exit codes automatically.mlz inspectchecks Open Graph tags, Twitter Cards, viewport, canonical URL, and favicon. Run it against any landing page that will be shared on social media.- The
jq -eflag makesjqreturn a non-zero exit code when the filter evaluates to false, so the step fails ifvalidorsuccessisfalse. MLZ_API_KEYis optional for basic validation. Add it as a GitHub Actions secret if you want results stored in the MissingLinkz dashboard.
Validating links from a config file
Hardcoding each URL in the workflow file doesn't scale. A better pattern is to keep your campaign destination URLs in a JSON config file and loop over them in the pipeline step. Here's an example with a campaigns.json config and a shell loop:
{
"campaigns": [
{
"name": "spring-launch",
"destination": "https://yoursite.com/landing-spring"
},
{
"name": "q2-product",
"destination": "https://yoursite.com/landing-q2"
}
]
}
#!/bin/bash
set -e
FAILED=0
for url in $(jq -r '.campaigns[].destination' campaigns.json); do
echo "Checking: $url"
result=$(mlz check "$url" --format json)
valid=$(echo "$result" | jq -r '.valid')
if [ "$valid" != "true" ]; then
echo "FAIL: $url failed validation"
echo "$result" | jq '.checks[] | select(.status != "pass")'
FAILED=$((FAILED + 1))
else
echo "PASS: $url"
fi
done
if [ "$FAILED" -gt 0 ]; then
echo "$FAILED campaign link(s) failed validation. Blocking deploy."
exit 1
fi
echo "All campaign links passed validation."
This script collects failures across all URLs before exiting. This is preferable to failing fast because it gives you a complete picture of what's broken in a single CI run, rather than making you re-run the pipeline after fixing each failure one at a time.
GitLab CI example
The same pattern works in GitLab CI. Add a validate-links job to your .gitlab-ci.yml that runs before the deploy stage:
stages:
- build
- validate
- test
- deploy
validate-campaign-links:
stage: validate
image: node:20
script:
- npm install -g missinglinkz
- bash validate-links.sh
variables:
MLZ_API_KEY: $MLZ_API_KEY
only:
- main
- merge_requests
The validate stage runs after build but before test and deploy. A campaign link failure blocks the entire pipeline. Set MLZ_API_KEY as a GitLab CI/CD variable in your project settings.
Allowing warnings, blocking failures
Not every check should be a hard block. A warn on response time (page is slow but not critically so) might be acceptable for a staging environment but worth addressing before production. A fail on SSL or destination resolution should always block the deploy.
The mlz check and mlz inspect commands return structured JSON where each check has a status of "pass", "warn", or "fail". You can parse these to implement a tiered blocking policy:
result=$(mlz check "$URL" --format json)
# Hard failures — always block
failures=$(echo "$result" | jq '[.checks[] | select(.status == "fail")] | length')
# Warnings — log but don't block
warnings=$(echo "$result" | jq '[.checks[] | select(.status == "warn")] | length')
if [ "$warnings" -gt 0 ]; then
echo "WARNING: $warnings advisory check(s) — review before production"
echo "$result" | jq '.checks[] | select(.status == "warn")'
fi
if [ "$failures" -gt 0 ]; then
echo "FAIL: $failures check(s) failed. Blocking deploy."
echo "$result" | jq '.checks[] | select(.status == "fail")'
exit 1
fi
This pattern lets you surface warnings in CI logs without blocking the build, while hard failures (404, SSL error, completely missing OG tags) still prevent a broken deploy.
Caching tips for faster pipeline runs
The main cost in a campaign link validation step is network latency: each check makes an HTTP request to the destination URL. For destination URLs that haven't changed, you can skip re-checking them by caching results between pipeline runs.
In GitHub Actions, cache the mlz global install between runs:
- name: Cache npm global
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-global-mlz-${{ hashFiles('**/package-lock.json') }}
The mlz install itself is fast (under 5 seconds), so caching is optional. More impactful: scope your validation step to run only when files that reference campaign URLs change, using GitHub Actions' paths filter:
on:
push:
paths:
- 'campaigns.json'
- 'src/campaigns/**'
- 'content/**'
This prevents the validation job from running on every commit that touches unrelated files, reducing pipeline minutes and keeping CI fast for teams making frequent non-campaign changes.
For the full guide on what validation covers and why each check matters, see campaign link validation: the complete guide. For building UTM links programmatically as part of the same pipeline, see how to build UTM links programmatically.
FAQ
- Does mlz need an API key to run in CI?
- No.
mlz checkandmlz inspectwork without an API key for basic URL validation and OG tag inspection. An API key is only required if you want check results stored in the MissingLinkz dashboard for tracking over time. For CI use, setMLZ_API_KEYas a secret if you want storage; omit it if you only need the exit code and JSON output. - What happens when mlz check finds a failure?
- The command exits with a non-zero status code, which causes the CI step to fail. It also outputs structured JSON with a
checksarray where each failed check has astatus: "fail"and a human-readablemessagefield. You can parse this JSON to extract specific failure details for your pipeline logs. - Can I validate OG tags in CI before a deploy?
- Yes, with one caveat:
mlz inspectchecks the live URL. If your landing page hasn't deployed yet, the check will run against the current production version. For pre-deploy checks against a staging environment, point the validation at your staging URL before the production deploy step. - How do I handle links that are intentionally redirected?
mlz checkreports redirects as awarn(single hop) orfail(multiple hops) depending on the chain length. If your campaign links are expected to redirect (e.g., through a link shortener or vanity URL), parse the JSON output and filter for the specific checks you care about rather than using a blanket pass/fail check.- Should I validate every campaign link on every push?
- For small teams with a handful of campaign links, yes — validation is fast enough that blanket checking makes sense. For larger teams with dozens of campaign configs, use the
pathsfilter to run validation only when campaign-related files change, or run a nightly scheduled job instead of per-push validation.
Related reading
Add it to your next CI run
Install MissingLinkz and add a campaign link validation step to your pipeline. Catches broken destinations, missing OG tags, and redirect failures before they ship.
npm install -g missinglinkz
mlz check "https://yoursite.com/landing" --format json | jq -e '.valid == true'
Exits 0 on pass, 1 on failure — works in any CI system. See the docs for full options.