Skip to content

UTM Link Builder — C++ 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 C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// utm-link-builder — campaign URL builder with canonical utm_* ordering. C++ (20) port of src/lib/utm.ts build(); lint/bulk/preset helpers live in the TypeScript/Go sources. Kept query pairs pass through verbatim; new utm values are percent-encoded.

#include <cctype>
#include <cstdio>
#include <cstring>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

namespace {

constexpr std::pair<std::string_view, std::string_view> kCanonical[] = {
    {"source", "utm_source"}, {"medium", "utm_medium"}, {"campaign", "utm_campaign"},
    {"term", "utm_term"}, {"content", "utm_content"},
};

std::string pctEncode(std::string_view value) {
    static constexpr char HEX[] = "0123456789ABCDEF";
    std::string out;
    for (char ch : value) {
        unsigned char c = static_cast<unsigned char>(ch);
        if (std::isalnum(c) || std::strchr("-_.~", ch)) out += ch;
        else out += {'%', HEX[c >> 4], HEX[c & 15]};
    }
    return out;
}

}  // namespace

std::optional<std::string> build(std::string_view baseUrl,
                                 const std::map<std::string, std::string> &params) {
    std::string text = baseUrl.find("://") == std::string_view::npos
                           ? "https://" + std::string(baseUrl) : std::string(baseUrl);
    const size_t schemeEnd = text.find("://");
    std::string_view rest(text);
    rest.remove_prefix(schemeEnd + 3);
    size_t authLen = rest.find_first_of("/?#"); // authority ends there
    if (authLen == 0) return std::nullopt;      // empty host: unparseable
    if (authLen == std::string_view::npos) authLen = rest.size();
    std::string out = text.substr(0, schemeEnd + 3) + std::string(rest.substr(0, authLen));

    std::string_view tail = rest.substr(authLen);
    size_t hash = tail.find('#');
    std::string_view fragment = hash == std::string_view::npos ? std::string_view{} : tail.substr(hash + 1);
    if (hash != std::string_view::npos) tail = tail.substr(0, hash);
    size_t qmark = tail.find('?');
    out += tail.substr(0, qmark == std::string_view::npos ? tail.size() : qmark);

    std::string query;
    bool wrote = false;
    auto add = [&](std::string_view kv) { if (wrote) query += '&'; query.append(kv); wrote = true; };
    if (qmark != std::string_view::npos) { // keep unrelated pairs, drop stale utm_*
        std::string_view q = tail.substr(qmark + 1);
        while (!q.empty()) {
            size_t amp = q.find('&');
            std::string_view pair = q.substr(0, amp);
            std::string_view key = pair.substr(0, pair.find('='));
            if (!key.empty() && (key.size() < 4 || key.substr(0, 4) != "utm_")) add(pair);
            q = amp == std::string_view::npos ? std::string_view{} : q.substr(amp + 1);
        }
    }
    for (const auto &[field, key] : kCanonical) { // canonical order; empty values skipped
        auto it = params.find(std::string(field));
        if (it != params.end() && !it->second.empty())
            add(std::string(key).append("=").append(pctEncode(it->second)));
    }
    if (wrote) { out += '?'; out += query; }
    if (!fragment.empty()) { out += '#'; out += fragment; }
    return out;
}

int main() {
    auto url = build("example.com/page?utm_source=stale&id=7",
                     {{"source", "twitter"}, {"medium", "social"}, {"campaign", ""}});
    std::puts(url ? url->c_str() : "(null)");
    // -> 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 →