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> ¶ms) {
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 →