Weather Map Tutorial

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}&current=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.