Type an item name to instantly filter results. Shows name, icon, and sell price (if available).
To connect real Blizzard data, point API_BASE to your proxy endpoint (see Worker snippet below).
// Cloudflare Worker (JavaScript)
// - Stores BLIZZARD_CLIENT_ID/SECRET as Worker Secrets
// - Exposes: GET /search?q=hearth
// - Returns: [{ id, name, icon, sellPrice }] (sellPrice in copper or null)
//
// IMPORTANT: Blizzard API params can vary by game version/region.
// This uses the modern Game Data API item search + item media endpoints.
//
// wrangler.toml should set name + compatibility_date
// then add secrets:
// wrangler secret put BLIZZARD_CLIENT_ID
// wrangler secret put BLIZZARD_CLIENT_SECRET
//
// Usage from the HTML page:
// const API_BASE = "https://YOUR-WORKER.your-domain.workers.dev";
export default {
async fetch(req, env) {
const url = new URL(req.url);
const q = (url.searchParams.get("q") || "").trim();
// CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, {
headers: corsHeaders(req.headers.get("Origin"))
});
}
if (url.pathname === "/search") {
if (q.length < 2) return json([], req);
const region = "us"; // change if needed: us/eu/kr/tw
const locale = "en_US"; // en_US, etc.
const namespace = "static-us"; // static-us, static-eu, etc.
const token = await getToken(env);
// 1) Item search
// Modern WoW Game Data API has /data/wow/search/item
// Common query style: name.en_US=... and _pageSize=...
const searchUrl = new URL(`https://${region}.api.blizzard.com/data/wow/search/item`);
searchUrl.searchParams.set("namespace", namespace);
searchUrl.searchParams.set("locale", locale);
searchUrl.searchParams.set("_pageSize", "20");
// search operators vary; this is a common one:
searchUrl.searchParams.set(`name.${locale}`, q);
const searchRes = await fetch(searchUrl.toString(), {
headers: { Authorization: `Bearer ${token}` }
});
if (!searchRes.ok) {
const text = await searchRes.text();
return json({ error: "Search failed", status: searchRes.status, detail: text }, req, 502);
}
const searchJson = await searchRes.json();
const results = (searchJson.results || [])
.map(r => r.data)
.filter(Boolean)
.slice(0, 20);
// 2) For each item, fetch item details + media (icons)
// Keep it fast: do a small fan-out in parallel, but cap it.
const hydrated = await Promise.all(results.map(async (item) => {
const id = item.id;
const itemUrl = `https://${region}.api.blizzard.com/data/wow/item/${id}?namespace=${namespace}&locale=${locale}`;
const mediaUrl = `https://${region}.api.blizzard.com/data/wow/media/item/${id}?namespace=${namespace}&locale=${locale}`;
const [itemRes, mediaRes] = await Promise.all([
fetch(itemUrl, { headers: { Authorization: `Bearer ${token}` } }),
fetch(mediaUrl, { headers: { Authorization: `Bearer ${token}` } })
]);
if (!itemRes.ok) return null;
const itemData = await itemRes.json();
const mediaData = mediaRes.ok ? await mediaRes.json() : null;
// Media typically includes assets array; icon is often type "icon"
const icon =
(mediaData?.assets || []).find(a => a.key === "icon")?.value
|| (mediaData?.assets || [])[0]?.value
|| null;
// Sell price (in copper) is often present as "sell_price" on item
const sellPrice = Number.isFinite(itemData?.sell_price) ? itemData.sell_price : null;
return {
id,
name: itemData?.name || item?.name || `Item ${id}`,
icon,
sellPrice
};
}));
const cleaned = hydrated.filter(Boolean);
return json(cleaned, req);
}
return json({ ok: true, routes: ["/search?q="] }, req);
}
};
function corsHeaders(origin) {
return {
"Access-Control-Allow-Origin": origin || "*",
"Access-Control-Allow-Methods": "GET,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Max-Age": "86400"
};
}
function json(data, req, status = 200) {
const origin = req.headers.get("Origin");
return new Response(JSON.stringify(data), {
status,
headers: {
"content-type": "application/json; charset=utf-8",
...corsHeaders(origin)
}
});
}
let cachedToken = null;
let cachedTokenExp = 0;
async function getToken(env) {
const now = Date.now();
if (cachedToken && now < cachedTokenExp - 30_000) return cachedToken;
const body = new URLSearchParams();
body.set("grant_type", "client_credentials");
const res = await fetch("https://oauth.battle.net/token", {
method: "POST",
headers: {
"Authorization": "Basic " + btoa(`${env.BLIZZARD_CLIENT_ID}:${env.BLIZZARD_CLIENT_SECRET}`),
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
if (!res.ok) throw new Error("Failed to fetch Blizzard OAuth token");
const j = await res.json();
cachedToken = j.access_token;
cachedTokenExp = now + (j.expires_in * 1000);
return cachedToken;
}