Ad-Free Keyword Mixer for Google Ads (Paste, Mix, Export)
Ad-free Keyword Mixer you can host on your own site. Paste word lists, pick a match type, and instantly generate Google Ads-ready keyword combinations — with dedupe + CSV export.
Why it exists: Keyword tools are often bloated, slow, or plastered with ads and tracking. Sometimes you just want a clean “combine these lists” utility that respects your time (and your clipboard).
What it does:
- Mixes 2 or 3 lists into combinations (A×B or A×B×C).
- Supports match types: Broad, Phrase, Exact.
- Optional: prepend/append “modifiers” (like locations or intent words).
- Dedupe + trims whitespace, removes empties.
- Exports as a Google Ads-friendly CSV (one keyword per line, with match type formatting).
Install:
- Create:
/tools/keyword-mixer/ - Save the file below as:
/tools/keyword-mixer/index.html - Visit:
/tools/keyword-mixer/
Tip: Keep your lists “clean”: one item per line (no commas). This tool is meant to be fast and predictable.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Ad-Free Keyword Mixer for Google Ads</title>
<meta name="description" content="Ad-free keyword mixer: paste lists, mix into Google Ads-ready keywords, choose match type, dedupe, and export to CSV." />
<meta name="robots" content="index,follow" />
<style>
:root{
--bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
--text:#e8eef8; --muted:rgba(232,238,248,.72);
--line:rgba(255,255,255,.10); --accent:#2a8cff;
--shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
--ok:#7CFF9A; --warn:#FFD37C;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.wrap{max-width:1100px;margin:0 auto;padding:18px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
.top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center}
.pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
.muted{color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:12px}
.c4{grid-column:span 4}
.c12{grid-column:span 12}
@media(max-width:980px){.c4{grid-column:span 12}}
textarea{
width:100%;min-height:220px;resize:vertical;
padding:10px 12px;border-radius:14px;border:1px solid var(--line);
background:var(--panel2);color:var(--text);outline:none;
font:650 13px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
input,select{
width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
background:var(--panel2);color:var(--text);outline:none;
font:700 13px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
border:0;border-radius:12px;padding:10px 12px;background:var(--accent);
color:#06101a;font-weight:900;cursor:pointer;
}
.btn2{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:transparent;
color:var(--text);font-weight:900;cursor:pointer;
}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:780px){.row2{grid-template-columns:1fr}}
.kpi{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px}
.kpi b{font-size:16px}
.out{
white-space:pre-wrap;word-break:break-word;
padding:12px;border-radius:14px;border:1px solid var(--line);
background:var(--panel2);min-height:240px;
font:650 13px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="top">
<div>
<div class="pill">Ad-Free Tool</div>
<h1 style="margin:10px 0 6px;font-size:22px">Keyword Mixer for Google Ads</h1>
<div class="muted">Paste lists → mix → choose match type → export. No ads, no tracking, no bloat.</div>
</div>
</div>
<div class="kpi muted">
<div>Mode: <b id="modeLbl">A×B</b></div>
<div>Keywords: <b id="countLbl">0</b></div>
<div class="muted">Tip: keep lists one item per line.</div>
</div>
</div>
<div class="grid">
<div class="card c4">
<b>List A</b>
<div class="muted" style="margin:6px 0 10px">Example: services (plumber, dentist, lawyer)</div>
<textarea id="a" placeholder="plumber
electrician
roof repair"></textarea>
</div>
<div class="card c4">
<b>List B</b>
<div class="muted" style="margin:6px 0 10px">Example: intent or modifiers (near me, 24/7, emergency)</div>
<textarea id="b" placeholder="near me
24/7
emergency"></textarea>
</div>
<div class="card c4">
<b>List C (optional)</b>
<div class="muted" style="margin:6px 0 10px">Example: locations (Detroit, Chicago, Miami)</div>
<textarea id="c" placeholder="Detroit
Chicago
Miami"></textarea>
</div>
<div class="card c12">
<div class="row2">
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Match type</div>
<select id="match">
<option value="broad">Broad</option>
<option value="phrase">Phrase</option>
<option value="exact">Exact</option>
</select>
</div>
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Output format</div>
<select id="format">
<option value="lines">One keyword per line</option>
<option value="csv">CSV (Keyword)</option>
</select>
</div>
</div>
<div class="row2" style="margin-top:12px">
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Optional prepend (applies to every keyword)</div>
<input id="pre" placeholder="best" />
</div>
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Optional append (applies to every keyword)</div>
<input id="post" placeholder="services" />
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
<button class="btn" id="mixBtn" type="button">🔀 Mix keywords</button>
<button class="btn2" id="copyBtn" type="button">📋 Copy</button>
<button class="btn2" id="dlBtn" type="button">⬇️ Download CSV</button>
<button class="btn2" id="clearBtn" type="button">🧹 Clear</button>
</div>
</div>
<div class="card c12">
<b>Output</b>
<div class="muted" style="margin:6px 0 10px">Deduped, trimmed, ready to paste into Google Ads.</div>
<div id="out" class="out"></div>
</div>
</div>
</div>
<script>
(function(){
var $ = function(id){ return document.getElementById(id); };
function lines(s){
return String(s||"")
.split(/\r?\n/)
.map(function(x){ return x.trim(); })
.filter(function(x){ return x.length > 0; });
}
function dedupe(arr){
var seen = Object.create(null);
var out = [];
for (var i=0;i<arr.length;i++){
var k = arr[i];
if (seen[k]) continue;
seen[k] = 1;
out.push(k);
}
return out;
}
function applyMatchType(k, match){
if (match === "phrase") return '"' + k + '"';
if (match === "exact") return '[' + k + ']';
return k; // broad
}
function joinParts(pre, core, post){
var s = [pre, core, post].filter(Boolean).join(" ").replace(/\s+/g," ").trim();
return s;
}
function mix(){
var A = lines($("a").value);
var B = lines($("b").value);
var C = lines($("c").value);
var pre = $("pre").value.trim();
var post = $("post").value.trim();
var match = $("match").value;
var fmt = $("format").value;
var out = [];
if (!A.length || !B.length) {
$("out").textContent = "Add at least List A and List B.";
$("countLbl").textContent = "0";
$("modeLbl").textContent = "A×B";
return;
}
if (C.length) {
$("modeLbl").textContent = "A×B×C";
for (var i=0;i<A.length;i++){
for (var j=0;j<B.length;j++){
for (var k=0;k<C.length;k++){
var core = A[i] + " " + B[j] + " " + C[k];
out.push(joinParts(pre, core, post));
}
}
}
} else {
$("modeLbl").textContent = "A×B";
for (var i2=0;i2<A.length;i2++){
for (var j2=0;j2<B.length;j2++){
var core2 = A[i2] + " " + B[j2];
out.push(joinParts(pre, core2, post));
}
}
}
// normalize whitespace + dedupe
out = out.map(function(x){ return x.replace(/\s+/g," ").trim(); });
out = dedupe(out);
// apply match type
out = out.map(function(x){ return applyMatchType(x, match); });
$("countLbl").textContent = String(out.length);
if (fmt === "csv") {
// single-column CSV: Keyword
var csv = ["Keyword"].concat(out.map(function(x){
// CSV escape double quotes
var v = String(x).replace(/"/g,'""');
return '"' + v + '"';
})).join("\n");
$("out").textContent = csv;
$("dlBtn").disabled = false;
$("dlBtn").dataset.csv = csv;
} else {
$("out").textContent = out.join("\n");
$("dlBtn").disabled = true;
$("dlBtn").dataset.csv = "";
}
}
function copyOut(){
var t = $("out").textContent || "";
if (!t.trim()) return;
navigator.clipboard.writeText(t).catch(function(){
// fallback
var ta = document.createElement("textarea");
ta.value = t;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
});
}
function downloadCSV(){
var csv = $("dlBtn").dataset.csv || "";
if (!csv) return;
var blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "keywords.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function clearAll(){
$("a").value = "";
$("b").value = "";
$("c").value = "";
$("pre").value = "";
$("post").value = "";
$("out").textContent = "";
$("countLbl").textContent = "0";
$("modeLbl").textContent = "A×B";
}
$("mixBtn").addEventListener("click", mix);
$("copyBtn").addEventListener("click", copyOut);
$("dlBtn").addEventListener("click", downloadCSV);
$("clearBtn").addEventListener("click", clearAll);
// auto-mix on change (lightweight)
["a","b","c","pre","post","match","format"].forEach(function(id){
$(id).addEventListener("input", function(){
// don’t spam until they have A & B
if ($("a").value.trim() && $("b").value.trim()) mix();
});
$(id).addEventListener("change", function(){
if ($("a").value.trim() && $("b").value.trim()) mix();
});
});
})();
</script>
</body>
</html>
Comments (2)