How to Build a Custom Interactive Weather Map in WordPress (Without Plugins)
If you run a WordPress site, you already know the golden rule: too many plugins will ruin your page speed.
I recently needed to add a sleek, global weather dashboard and interactive map to the site. Every weather widget plugin I tested was either heavily bloated, required a paid API subscription, or simply looked terrible and broke the theme.
Instead of compromising, I decided to build my own from scratch.
In this tutorial, I’ll show you exactly how to build a fully responsive, real-time weather dashboard and interactive map using Vanilla JavaScript, Leaflet.js, and the Open-Meteo API. Best of all? It requires zero API keys, zero plugins, and completely bypasses WordPressโs annoying habit of breaking custom scripts.
The Tech Stack
To keep the widget lightweight and fast, we are avoiding heavy libraries like jQuery. We only need three things:
- HTML/CSS (Flexbox): To create the grid of weather cards that seamlessly wraps on mobile devices.
- Open-Meteo API: A fantastic, free, open-source weather API. It doesn’t require registration or API keys, meaning you won’t suddenly hit a paywall if your site gets a spike in traffic.
- Leaflet.js: The leading open-source JavaScript library for mobile-friendly interactive maps. We will use this to plot our weather data on a world map.
The WordPress JavaScript Trap (And How to Beat It)
If you have ever tried pasting custom JavaScript into a WordPress “Custom HTML” block, youโve likely watched it silently crash. This happens for two main reasons:
- Script Stripping: WordPress security protocols and caching plugins often block or defer external
<script src="...">tags, meaning your map engine never actually loads. - The wpautop Filter: The WordPress editor tries to be helpful by automatically inserting invisible paragraph
<p>and<br>tags wherever it sees an empty line in your code. This completely shatters JavaScript syntax.
The Workaround
To get around this, the code below uses a dynamic injection function. Instead of putting the Leaflet map scripts directly in the HTML, our Vanilla JavaScript sneaks them into the site’s <head> after the page loads.
We also heavily minify the core JavaScript logic. By removing the line breaks, WordPress has nowhere to insert its formatting tags, ensuring the code runs perfectly every time.
The Complete Custom Code
To add this to your site, simply open your WordPress page editor, add a Custom HTML block, and paste the entire block of code below. It is fully self-contained and pre-loaded with 24 global cities.
<div id="weather-dashboard" style="background: #f8fafc; padding: 15px; border-radius: 16px; font-family: Arial, sans-serif; width: 100%; box-sizing: border-box;">
<div style="text-align: center; margin-bottom: 20px;">
<button id="toggle-unit" style="padding: 8px 16px; cursor: pointer; border-radius: 8px; border: 1px solid #cbd5e1; background: #ffffff; font-weight: bold; color: #334155; transition: all 0.2s;">Switch to ยฐF</button>
<div id="status" style="margin-top: 8px; color: #3b82f6; font-size: 13px; font-weight: bold;">Loading map resources...</div>
</div>
<div id="weather-grid" style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-bottom: 20px;"></div>
<div id="weather-map" style="height: 450px; width: 100%; border-radius: 12px; border: 1px solid #e2e8f0; background: #e2e8f0; display: flex; align-items: center; justify-content: center; color: #64748b; z-index: 1;">
Waiting for map engine...
</div>
</div>
<style>
.w-card { background: #fff; padding: 12px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); text-align: center; border: 1px solid #e2e8f0; width: 135px; flex-grow: 1; max-width: 180px; box-sizing: border-box; }
.w-card h3 { font-size: 13px; margin: 0 0 6px 0; color: #1e293b; }
.w-card .time { font-size: 11px; color: #64748b; margin-bottom: 6px; }
.w-card .icon { font-size: 26px; margin: 6px 0; }
.w-card .temp { font-size: 18px; font-weight: bold; color: #0f172a; margin: 4px 0;}
.w-card .details { font-size: 11px; color: #64748b; margin-top: 2px; }
.m-popup { text-align: center; font-family: Arial, sans-serif; min-width: 130px; }
.m-popup h4 { margin: 0 0 4px 0; font-size: 14px; color: #1e293b; }
.m-popup .temp { font-size: 18px; font-weight: bold; color: #0f172a; margin: 4px 0; }
.m-popup .details { font-size: 11px; color: #64748b; margin-bottom: 4px; }
</style>
<script>
(function(){
let isCelsius=true; let cache=[]; let map=null; let markersLayer=null;
const cities=[{name:"๐บ๐ธ Los Angeles",lat:34.05,lon:-118.24},{name:"๐ฒ๐ฝ Mexico City",lat:19.43,lon:-99.13},{name:"๐จ๐ฆ Toronto",lat:43.65,lon:-79.38},{name:"๐บ๐ธ New York",lat:40.71,lon:-74.00},{name:"๐ฆ๐ท Buenos Aires",lat:-34.60,lon:-58.38},{name:"๐ง๐ท Rio",lat:-22.90,lon:-43.17},{name:"๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ Glasgow",lat:55.86,lon:-4.25},{name:"๐ฌ๐ง London",lat:51.50,lon:-0.12},{name:"๐ฌ๐ง Chelmsford",lat:51.73,lon:0.47},{name:"๐ซ๐ท Paris",lat:48.85,lon:2.35},{name:"๐ฎ๐น Rome",lat:41.90,lon:12.49},{name:"๐ฉ๐ช Berlin",lat:52.52,lon:13.40},{name:"๐ธ๐ช Stockholm",lat:59.33,lon:18.07},{name:"๐ฟ๐ฆ Cape Town",lat:-33.92,lon:18.42},{name:"๐ช๐ฌ Cairo",lat:30.04,lon:31.23},{name:"๐ฆ๐ช Dubai",lat:25.20,lon:55.27},{name:"๐ฎ๐ณ Mumbai",lat:19.07,lon:72.87},{name:"๐น๐ญ Bangkok",lat:13.75,lon:100.50},{name:"๐ธ๐ฌ Singapore",lat:1.28,lon:103.83},{name:"๐จ๐ณ Beijing",lat:39.90,lon:116.40},{name:"๐ฐ๐ท Seoul",lat:37.56,lon:126.98},{name:"๐ฏ๐ต Tokyo",lat:35.68,lon:139.69},{name:"๐ฆ๐บ Sydney",lat:-33.86,lon:151.20},{name:"๐ณ๐ฟ Auckland",lat:-36.85,lon:174.76}];
function getI(c){if(c===0)return"โ๏ธ";if(c<=3)return"โ๏ธ";if(c<=48)return"๐ซ๏ธ";if(c<=67)return"๐ง๏ธ";if(c<=82)return"๐ฆ๏ธ";return"โ๏ธ";}
function fmtT(c){return isCelsius?Math.round(c)+"ยฐC":Math.round(c*9/5+32)+"ยฐF";}
function renderUI(){
const grid=document.getElementById('weather-grid'); if(!grid||cache.length===0)return; let html="";
cache.forEach(c=>{
if(c.error){html+=`<div class="w-card"><h3>${c.name}</h3><div style="color:#ef4444;font-size:11px;">Offline</div></div>`;return;}
html+=`<div class="w-card"><h3>${c.name}</h3><div class="time">๐ ${c.time}</div><div class="icon">${getI(c.code)}</div><div class="temp">${fmtT(c.temp)}</div><div class="details">Feels like ${fmtT(c.feelsLike)}</div><div class="details">๐จ ${Math.round(c.wind)} km/h</div></div>`;
}); grid.innerHTML=html;
if(markersLayer){markersLayer.clearLayers();
cache.forEach(c=>{if(c.error)return;
const p=`<div class="m-popup"><h4>${c.name}</h4><div class="details">๐ Local: ${c.time}</div><div class="temp">${getI(c.code)} ${fmtT(c.temp)}</div><div class="details">Feels like: ${fmtT(c.feelsLike)}</div><div class="details">๐จ ${Math.round(c.wind)} km/h</div></div>`;
L.marker([c.lat,c.lon]).bindPopup(p).addTo(markersLayer);});}}
async function loadW(){
const st=document.getElementById('status'); st.textContent="Fetching live data...";
try{const proms=cities.map(async city=>{
try{const res=await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}¤t=temperature_2m,apparent_temperature,weather_code,wind_speed_10m&timezone=auto`);
if(!res.ok)throw new Error("API Err"); const data=await res.json();
let lTime="N/A"; try{lTime=new Date().toLocaleTimeString("en-GB",{timeZone:data.timezone,hour:"2-digit",minute:"2-digit"});}catch(e){lTime=new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});}
return{name:city.name,lat:city.lat,lon:city.lon,temp:data.current.temperature_2m,feelsLike:data.current.apparent_temperature,code:data.current.weather_code,wind:data.current.wind_speed_10m,time:lTime,error:false};
}catch(e){return{name:city.name,error:true};}});
cache=await Promise.all(proms); renderUI();
st.textContent="Last updated: "+new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}); st.style.color="#64748b";
}catch(err){st.textContent="Error connecting to weather API."; st.style.color="#ef4444";}}
function initApp(){
try{document.getElementById('weather-map').innerHTML="";
map=L.map('weather-map').setView([25,0],2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map);
markersLayer=L.layerGroup().addTo(map);
loadW();
const tBtn=document.getElementById('toggle-unit');
if(tBtn){tBtn.addEventListener('click',function(){isCelsius=!isCelsius; this.textContent=isCelsius?"Switch to ยฐF":"Switch to ยฐC"; renderUI();});}
setInterval(loadW,300000);
}catch(e){document.getElementById('status').textContent="Map initialization failed: "+e.message;}}
const loadL=()=>{
const css=document.createElement('link'); css.rel='stylesheet'; css.href='https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css'; document.head.appendChild(css);
const js=document.createElement('script'); js.src='https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js';
js.onload=()=>{initApp();};
js.onerror=()=>{document.getElementById('status').textContent="Network block: Cloudflare/CDN denied Map load."; document.getElementById('status').style.color="#ef4444";};
document.head.appendChild(js);};
loadL();
})();
</script>
Customizing Your Locations
You aren’t locked into the 24 cities I provided above. If you want to customize the map for your own audience, look for this variable inside the <script> tag:
const cities=[{name:"๐บ๐ธ Los Angeles",lat:34.05,lon:-118.24}, ... ];
To add a new city, simply grab its latitude and longitude coordinates from Google Maps, pick a flag emoji, and drop it into the array following the same format. The script handles the timezone math, the API fetching, and the map marker placement entirely on its own.
Happy coding, and enjoy your new blazing-fast weather map.