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", ¶ms, &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 →