// Curated blocked domains — static lists (no search) const { useState, useEffect, useMemo } = React; function cidrToMask(bits) { const n = parseInt(bits || "32", 10); if (n === 32) return "255.255.255.255"; return Array.from({ length: 4 }, (_, i) => (65280 >> Math.min(8, Math.max(0, n - i * 8))) & 255).join("."); } function buildKeeneticBat(group, allGroups) { const lines = ["@echo off", "rem Keenetic static routes — " + (group ? group.title : "все группы"), ""]; const src = group ? [group] : allGroups; const seen = new Set(); src.forEach((g) => { (g.ips || []).forEach((cidr) => { const [ip, bits] = cidr.split("/"); if (!ip || seen.has(cidr)) return; seen.add(cidr); const mask = cidrToMask(bits); lines.push(`route ADD ${ip} MASK ${mask} 0.0.0.0 & rem ${g.title}`); }); }); if (lines.length <= 3) { lines.push("rem IP-префиксы не заданы — используйте список доменов вручную в VPN"); } return lines.join("\r\n") + "\r\n"; } function downloadText(filename, text) { const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } async function copyText(text) { try { await navigator.clipboard.writeText(text); return true; } catch { const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); return true; } } function DomainGroupCard({ group, onCopyDomains, onCopyIps, onDownloadBat }) { const [open, setOpen] = useState(true); const [copied, setCopied] = useState(null); const domainText = group.domains.join("\n"); const ipText = (group.ips || []).join("\n"); async function handleCopy(kind) { const text = kind === "domains" ? domainText : ipText; if (!text) return; await copyText(text); setCopied(kind); onCopyDomains?.(kind); setTimeout(() => setCopied(null), 1800); } return ( {open && (
{group.v2fly_tags?.length > 0 && (
{group.v2fly_tags.map((t) => ( {t} ))}
)}
{group.domains.map((d) => (
{d}
))}
{(group.ips || []).length > 0 && (
IP / CIDR (префиксы)
{group.ips.map((ip) => ( {ip} ))}
)}
{(group.ips || []).length > 0 && ( )}
)}
); } function DomainsApp() { const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState(""); useEffect(() => { fetch("data/blocked-domains.json") .then((r) => r.json()) .then((data) => { setGroups(data.groups || []); setLoading(false); }) .catch(() => setLoading(false)); }, []); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); if (!q) return groups; return groups.map((g) => ({ ...g, domains: g.domains.filter((d) => d.includes(q) || g.title.toLowerCase().includes(q)), })).filter((g) => g.domains.length > 0); }, [groups, filter]); const allDomains = groups.flatMap((g) => g.domains).join("\n"); return (
Статические списки · без поиска

Популярные заблокированные домены

Кураторские списки для настройки VPN и маршрутизации на роутере Keenetic. Поиск по crt.sh на сайте отключён — только готовые пресеты по категориям.

setFilter(e.target.value)} placeholder="Фильтр по домену…" style={{ flex: "1 1 220px", height: 42, padding: "0 16px", borderRadius: 12, border: "1px solid var(--line)", fontSize: 14, }} />
{loading &&

Загрузка списков…

} {!loading && filtered.length === 0 && (

Ничего не найдено по фильтру.

)}
{filtered.map((g) => ( downloadText(`keenetic-${grp.id}.bat`, buildKeeneticBat(grp, groups))} /> ))}
); } ReactDOM.createRoot(document.getElementById("root")).render();