Skip to content

UTM Link Builder — Zig source

Build campaign tracking URLs with utm_source, utm_medium and utm_campaign parameters. Bulk mode processes a whole list, presets and import round-trip existing tracking URLs, and a validator flags attribution-breaking values — runs entirely in your browser.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// utm-link-builder — campaign URL builder with canonical utm_* ordering. Language: Zig (0.13, standard library only). Port of src/lib/utm.ts build(); lint/bulk/preset helpers live in the TypeScript/Go sources. Query pairs pass through raw (values must be pre-encoded).

const std = @import("std");

const Canonical = struct { field: []const u8, key: []const u8 };
const ORDER = [_]Canonical{
    .{ .field = "source", .key = "utm_source" },     .{ .field = "medium", .key = "utm_medium" },
    .{ .field = "campaign", .key = "utm_campaign" }, .{ .field = "term", .key = "utm_term" },
    .{ .field = "content", .key = "utm_content" },
};

const Param = struct { field: []const u8, value: []const u8 };

fn push(out: []u8, len: usize, s: []const u8) usize { // copy s at len, clamped
    const n = @min(s.len, out.len - len - 1);
    @memcpy(out[len .. len + n], s[0..n]);
    return len + n;
}

/// Port of src/lib/utm.ts build(): strip stale utm_* pairs, keep unrelated ones, append in canonical order. null when unparseable.
fn build(base: []const u8, params: []const Param, out: []u8) ?[]const u8 {
    var len: usize = 0;
    const rest = if (std.mem.indexOf(u8, base, "://")) |i| blk: {
        len = push(out, len, base[0 .. i + 3]);
        break :blk base[i + 3 ..];
    } else blk: { // coerce a scheme-less host
        len = push(out, len, "https://");
        break :blk base;
    };

    const auth_len = std.mem.indexOfAny(u8, rest, "/?#") orelse rest.len;
    if (auth_len == 0) return null; // empty host
    len = push(out, len, rest[0..auth_len]);

    const tail0 = rest[auth_len..];
    const hash = std.mem.indexOfScalar(u8, tail0, '#');
    const tail = if (hash) |h| tail0[0..h] else tail0;
    const fragment = if (hash) |h| tail0[h + 1 ..] else "";

    const qmark = std.mem.indexOfScalar(u8, tail, '?');
    len = push(out, len, tail[0 .. qmark orelse tail.len]);

    var wrote = false;
    if (qmark) |q| { // keep unrelated pairs raw; drop stale utm_* ones
        var query = tail[q + 1 ..];
        while (query.len > 0) {
            const amp = std.mem.indexOfScalar(u8, query, '&') orelse query.len;
            const pair = query[0..amp];
            const key = if (std.mem.indexOfScalar(u8, pair, '=')) |e| pair[0..e] else pair;
            if (key.len > 0 and !(key.len >= 4 and std.mem.eql(u8, key[0..4], "utm_"))) {
                if (wrote) len = push(out, len, "&");
                len = push(out, len, pair);
                wrote = true;
            }
            query = if (amp == query.len) query[amp..] else query[amp + 1 ..];
        }
    }
    for (params) |p| { // append in canonical order; empty values skipped
        if (p.value.len == 0) continue;
        for (ORDER) |c| {
            if (!std.mem.eql(u8, c.field, p.field)) continue;
            if (wrote) len = push(out, len, "&");
            len = push(out, len, c.key);
            len = push(out, len, "=");
            len = push(out, len, p.value);
            wrote = true;
        }
    }
    if (fragment.len > 0) { len = push(out, len, "#"); len = push(out, len, fragment); }
    return out[0..len];
}

pub fn main() !void {
    const params = [_]Param{ .{ .field = "source", .value = "twitter" },
        .{ .field = "medium", .value = "social" }, .{ .field = "campaign", .value = "" } };
    var buf: [512]u8 = undefined;
    const url = build("example.com/page?utm_source=stale&id=7", &params, &buf).?;
    try std.io.getStdOut().writer().print("{s}\n", .{url});
    // -> https://example.com/page?id=7&utm_source=twitter&utm_medium=social
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →