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 (us vs metric) 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

ComponentPurpose
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 Location type now includes an optional units field ('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.astro lets users switch between °F and °C, persisting the choice in localStorage.
// 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

`