Building a Weather Widget for Firefox New Tab: API-Free Approach with wttr.in
Most weather widgets require API keys, rate limits, and sometimes credit cards. There's a better way: wttr.in — a free, open, consent-friendly weather service that requires zero authentication.
What is wttr.in?
wttr.in is a console-oriented weather service by Igor Chubin. It supports:
- Plain text output (for terminals)
- JSON API output (for apps)
- Auto-detects location from IP
- Supports city name, coordinates, airport codes, and more
The JSON API
https://wttr.in/London?format=j1
This returns a full JSON response with current conditions plus a 3-day forecast. No API key. No account. No rate limit (be reasonable).
Parsing the Response
async function fetchWeather(location = '') {
const url = `https://wttr.in/${encodeURIComponent(location)}?format=j1`;
const response = await fetch(url);
if (!response.ok) throw new Error(`Weather fetch failed: ${response.status}`);
const data = await response.json();
return parseWeatherData(data);
}
function parseWeatherData(data) {
const current = data.current_condition[0];
const today = data.weather[0];
const tomorrow = data.weather[1];
const dayAfter = data.weather[2];
return {
temperature: {
c: parseInt(current.temp_C),
f: parseInt(current.temp_F),
},
feelsLike: {
c: parseInt(current.FeelsLikeC),
f: parseInt(current.FeelsLikeF),
},
humidity: parseInt(current.humidity),
description: current.weatherDesc[0].value,
weatherCode: parseInt(current.weatherCode),
forecast: [
parseForecastDay(today),
parseForecastDay(tomorrow),
parseForecastDay(dayAfter),
],
location: data.nearest_area[0],
};
}
function parseForecastDay(day) {
return {
date: day.date,
maxC: parseInt(day.maxtempC),
minC: parseInt(day.mintempC),
maxF: parseInt(day.maxtempF),
minF: parseInt(day.mintempF),
description: day.hourly[4]?.weatherDesc[0]?.value || '',
weatherCode: parseInt(day.hourly[4]?.weatherCode || 0),
sunrise: day.astronomy[0]?.sunrise || '',
sunset: day.astronomy[0]?.sunset || '',
};
}
Weather Code → Icon Mapping
wttr.in uses standard WMO weather interpretation codes:
function getWeatherIcon(code) {
const icons = {
113: '☀️', // Sunny/Clear
116: '⛅', // Partly cloudy
119: '☁️', // Cloudy
122: '☁️', // Overcast
143: '🌫️', // Mist
176: '🌦️', // Patchy rain
179: '🌨️', // Patchy snow
182: '🌧️', // Patchy sleet
185: '🌧️', // Patchy freezing drizzle
200: '⛈️', // Thundery outbreaks
227: '🌨️', // Blowing snow
230: '❄️', // Blizzard
248: '🌫️', // Fog
260: '🌫️', // Freezing fog
263: '🌦️', // Light drizzle
266: '🌧️', // Drizzle
281: '🌧️', // Freezing drizzle
284: '🌧️', // Heavy freezing drizzle
293: '🌦️', // Light rain
296: '🌧️', // Rain
299: '🌧️', // Moderate rain
302: '🌧️', // Heavy rain
305: '🌧️', // Heavy rain at times
308: '🌧️', // Very heavy rain
311: '🌧️', // Light sleet
314: '🌧️', // Sleet
317: '🌨️', // Light snow/sleet
320: '🌨️', // Moderate or heavy sleet
323: '🌨️', // Light snow
326: '🌨️', // Snow
329: '❄️', // Moderate snow
332: '❄️', // Heavy snow
335: '❄️', // Snow blowing
338: '❄️', // Heavy snow
350: '🌧️', // Ice pellets
353: '🌦️', // Light rain shower
356: '🌧️', // Moderate or heavy rain shower
359: '🌧️', // Torrential rain shower
362: '🌨️', // Light sleet showers
365: '🌨️', // Moderate or heavy sleet showers
368: '🌨️', // Light snow showers
371: '🌨️', // Moderate or heavy snow showers
374: '🌨️', // Light showers of ice pellets
377: '🌨️', // Moderate or heavy showers of ice pellets
386: '⛈️', // Patchy light rain with thunder
389: '⛈️', // Moderate or heavy rain with thunder
392: '⛈️', // Patchy light snow with thunder
395: '⛈️', // Moderate or heavy snow with thunder
};
return icons[code] || '🌡️';
}
Auto-Location Detection
wttr.in detects location from the request IP by default — just omit the location parameter:
// Auto-detect location
const weather = await fetchWeather('');
// or
const weather = await fetchWeather('auto');
For browser extensions, this works great as the fetch goes from the user's IP.
Caching Strategy
Weather doesn't change every second. Cache aggressively:
const CACHE_DURATION_MS = 10 * 60 * 1000; // 10 minutes
async function fetchWeatherCached(location = '') {
const cacheKey = `weather_${location}`;
const cached = await browser.storage.local.get(cacheKey);
if (cached[cacheKey]) {
const { data, timestamp } = cached[cacheKey];
if (Date.now() - timestamp < CACHE_DURATION_MS) {
return data; // Return cached data
}
}
// Fetch fresh data
const data = await fetchWeather(location);
await browser.storage.local.set({
[cacheKey]: { data, timestamp: Date.now() }
});
return data;
}
Error Handling
async function fetchWeatherSafe(location = '') {
try {
return await fetchWeatherCached(location);
} catch (error) {
console.warn('Weather fetch failed:', error);
// Try to return stale cache rather than showing error
const cacheKey = `weather_${location}`;
const cached = await browser.storage.local.get(cacheKey);
if (cached[cacheKey]) {
return { ...cached[cacheKey].data, stale: true };
}
return null;
}
}
Why Not OpenWeatherMap?
| wttr.in | OpenWeatherMap Free Tier | |
|---|---|---|
| API Key | Not required | Required |
| Rate limit | Generous (no hard limit) | 60 calls/min, 1M/month |
| Setup friction | Zero | Account + key management |
| Privacy | IP-based location | Account linked |
| Self-hostable | Yes (open source) | No |
For a privacy-focused extension, wttr.in is a natural fit — no account, no tracking, no API key to manage or expire.
The Result
This approach powers the weather widget in Weather & Clock Dashboard — a free Firefox new tab extension. Zero API keys for users to configure, works immediately after install, and respects privacy.
Weather & Clock Dashboard — live weather + 3-day forecast, world clocks, search bar for your Firefox new tab. Free, no tracking, MIT licensed.
Top comments (0)