Introduction
When I first set out to create a Dark Sky‑inspired weather dashboard, I wanted a stack that was:
- Zero‑ops – no servers to provision or maintain.
- Fast – HTML is rendered at the edge, and API calls are cached close to the user.
- Extensible – easy to add features like historical trends, unit toggles, and rate‑limiting without rewriting large parts of the code‑base.
The result is a single Cloudflare Workers project that bundles:
- An Astro front‑end for SSR and static assets.
- Hono‑based API routes handling
/api/*endpoints. - A data collector (
/collect) that pulls data from the PirateWeather API, caches it in KV, and stores historical snapshots in D1. - Time‑Machine rate‑limiting (10 requests / hour / IP) to protect the free tier of the weather API.
- Automatic unit detection (
usvsmetric) based on the user’s locale, with a manual toggle.
Below you’ll find a complete, runnable walkthrough of the updated codebase, an architecture diagram, and the rationale behind each change.
Project Overview
| Component | Purpose |
|---|---|
Astro (src/pages/*.astro) | Renders the UI, pulls the latest weather from the Worker‑side API, and applies responsive styling. |
Worker entry (src/index.ts) | Routes requests to either the Astro renderer, the Hono API (/api/*), or the data‑collector (/collect, /test-trends). |
Hono API (src/middleware/api.ts) | Implements /api/weather, /api/trends, and the Time‑Machine rate‑limiter. |
Data collector (src/data-collector.ts) | Called by a scheduled cron (or manually) to fetch fresh forecast data, cache it in KV, and write a row to D1 for trend analysis. |
Weather service (src/lib/weather-service.ts) | Low‑level wrapper around the PirateWeather API that includes API‑key failover and a 10‑minute KV cache. |
KV (WEATHER_CACHE) | Stores the latest forecast per location for fast reads. |
D1 (WEATHER_TRENDS) | Holds a daily snapshot of temperature/high‑low values for historical graphs. |
Global CSS (src/styles/global.css) | Minimal reset – Astro’s built‑in preflight handles the rest. |
Documentation (CLAUDE.md) | Guidance for Claude Code when it assists future contributors. |
Architecture Diagram
flowchart LR
subgraph Edge["Cloudflare Edge"]
A[Client (Browser)] --> B[Worker (index.ts)]
B -->|HTML/SSR| C[Astro Renderer]
B -->|API request| D[Hono Router (middleware/api.ts)]
B -->|Collect request| E[Data Collector (data-collector.ts)]
D --> F[Weather Service (lib/weather-service.ts)]
F -->|Cache miss| G[KV (WEATHER_CACHE)]
F -->|Cache miss| H[PirateWeather API (primary key)]
H -->|Failover| I[PirateWeather API (secondary key)]
E --> G
E --> J[D1 (WEATHER_TRENDS)]
end
style Edge fill:#f9f9f9,stroke:#333,stroke-width:2px
The diagram mirrors the actual file structure: index.ts is the entry point, middleware/api.ts hosts the Hono routes, and weather-service.ts does caching and failover.
Key Technical Enhancements
1. Time‑Machine Rate Limiting
- Goal – Prevent a single client from exhausting the free tier of the PirateWeather API.
- Implementation – A simple in‑memory map (stored in KV for persistence) tracks request counts per IP over a 1‑hour sliding window.
// src/middleware/api.ts (excerpt)
const RATE_LIMIT_MAX = 10; // requests
const RATE_LIMIT_WINDOW = 3600; // seconds (1 h)
interface RateLimitRecord {
count: number;
windowStart: number; // epoch seconds
}
/**
* Returns the client IP from Cloudflare‑specific headers.
*/
function getClientIP(c: any): string {
// Cloudflare injects the original client IP in this header.
return c.req.header('CF-Connecting-IP') ?? c.req.header('x-forwarded-for') ?? 'unknown';
}
The middleware checks this map on every /api/* request and returns 429 Too Many Requests when the limit is exceeded.
2. Automatic Unit Detection & Toggle
- The
Locationtype now includes an optionalunitsfield ('us' | 'metric'). - On the client side we detect the user’s locale (
navigator.language) and send the appropriate unit query param. - A UI toggle in
src/pages/index.astrolets users switch between °F and °C, persisting the choice inlocalStorage.
// src/types.ts (new field)
export interface Location {
lat: number;
lon: number;
timezone?: string;
/** 'us' for Fahrenheit, 'metric' for Celsius */
units?: 'us' | 'metric';
}
3. API‑Key Failover
PirateWeather offers two keys (primary & secondary). If the primary key returns a 401/403, we transparently retry with the secondary key.
// src/lib/weather-service.ts (excerpt)
const PIRATE_WEATHER_API_URL = '[REDACTED] base; …`) were unnecessary because Astro already injects a preflight reset. We replaced them with a single comment:
```css
/* src/styles/global.css */
/* Global styles – base reset handled in page component */
5. Documentation for Claude Code
`