Commit graph

18 commits

Author SHA1 Message Date
Emi Griffith
959a077c63 Polish the mobile nav dropdown; rename Day to Day Detail (#121)
- Compact the mobile menu: narrower panel (208px), tighter rows, and the nav
  list now stretches to the panel width and left-aligns (it was centering because
  the base .view-nav align-self:center overrode the panel's stretch).
- Stylish separation between entries: an inset hairline between each nav item,
  suppressed around the active pill so nothing crosses it.
- Rename the "Day" view to "Day Detail" across the app nav, and add it to the
  climate/city page nav (base.html.j2), which was missing it.
2026-07-16 12:34:04 +00:00
Emi Griffith
f86100015d Fold the mobile header into a single hamburger menu (#119)
On phones the header spanned two rows: the title block plus a control bar of
the °F/°C toggle, bell, account and hamburger. Wrap the nav in a .nav-panel
inside the <details> menu and inject the °F/°C toggle and account/bell into that
panel instead of into the header row. On desktop the panel is display:contents,
so the inline top bar renders exactly as before (order restored via flex order);
on phones the panel becomes a right-anchored, viewport-clamped dropdown and the
header collapses to a single row (logo + name + hamburger, tagline hidden).

The viewport clamp also fixes the SEO/city pages, whose menu dropdown previously
ran off-screen.
2026-07-16 12:15:00 +00:00
Emi Griffith
31f68981a0 Mobile hamburger nav; crisp SVG logo; month travel-note callout (#117)
- Collapse the view navigation into a hamburger menu on phones. The nav is
  wrapped in a <details> (pure CSS, no JS — so it works on the crawlable SEO
  pages too): transparent (display:contents) on desktop so the tabs render
  inline, a dropdown behind a hamburger under 640px. The °F/°C toggle, bell and
  account stay in the header bar; units.js/account.js now anchor their injected
  controls on the wrapper.
- Replace the header logo: the ▚ glyph was a rotated Unicode char that fonts
  rendered inconsistently (warped). Use a crisp inline SVG of the two accent
  squares (currentColor) across all headers.
- Month pages: turn the plain "Visiting <city> in <month>?" line into a bordered
  callout with a CTA button, matching the city page's travel box.

Verified in Firefox and Chromium at desktop and 390px, light and dark: desktop
nav inline, mobile hamburger opens the dropdown, logo crisp, travel note styled.
2026-07-16 05:42:59 +00:00
Emi Griffith
4db6760ce9 City pages: keep "Read more →" from wrapping mid-phrase (#114)
The Wikipedia "Read more →" link in the notable-weather section could break
between "Read" and "more" at narrow widths. Join it with non-breaking spaces so
it stays on one line.

Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-16 05:25:58 +00:00
Emi Griffith
59f6e11694 Month pages: show record high and low for every metric (#113)
The per-month records section listed only the warmest and coldest temperature.
Extend it to a card per metric (High, Low, Feels-like, Humidity, Wind, Gust,
Precip), each showing that calendar month's record high and low with the date.

Precip is special-cased: a per-day record low is just zero, and the dry-streak
helper would bridge year boundaries on month-filtered rows, so the wettest and
driest sides use the month's largest and smallest total accumulation, dated to
the year. Partial current months (< 26 days) are excluded so an unfinished month
can't win "driest" on a fraction of its rainfall.

Reuses the records page's card grid, so it's already vertical on mobile.

Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-16 05:13:51 +00:00
Emi Griffith
da94b082c5 SEO: show both temperature extremes per metric in month/season records (#112)
The month and season records tables showed only the record high (of the daytime
high) and the record low (of the overnight low) — one extreme each. Show both
ends of both metrics: for the daytime high, its record hottest (▲) and the
coldest a day ever stayed (▼, its record-low high); for the overnight low, its
mildest night (▲, its record-high low) and its record low (▼). All four already
came out of all_time_records; they were just being dropped.

Restructure each table to Period · Daytime high · Overnight low, with each metric
cell carrying two tinted, dated chips (▲ warmest / ▼ coldest). Fits mobile in
three columns; the all-time cards already showed both extremes, so this brings
the per-period tables in line.
2026-07-16 04:57:13 +00:00
Emi Griffith
b3e7f75e50 City page: range strip spans 10th–90th percentile instead of average (#111)
The monthly temperature-range visualization now spans the 10th-percentile daily low
to the 90th-percentile daily high (from climatology's p10/p90) — the band most days
fall within — instead of just the average low to average high, giving a truer sense
of each month's spread. The normals table keeps the average high/low figures.
2026-07-16 04:50:14 +00:00
Emi Griffith
807f26262f Climate hub: client-side search + alphabetical results (#110)
Add a search box to /climate that filters to a flat, alphabetical list of matching
cities (by city or country name), shown with their country; clearing it restores the
collapsible directory. The index is built from the already-crawlable links, so it's
a pure progressive enhancement (no SEO impact). Also sort cities alphabetically
within each country in the directory (were population-ordered).
2026-07-16 04:44:18 +00:00
Emi Griffith
0d477427c1 SEO: fix season-record wrapping + trim range strip on mobile (#109)
Two mobile polish issues on the climate pages:

- The season records table's "(Dec–Feb)" span overflowed its narrow first
  column (nowrap) and slid under the tinted value cell. Stack the span under the
  season name on phones, top-align the record cells, and let the first column
  wrap — so the period label sits cleanly in its own column.
- The monthly temperature-range strip stretched edge-to-edge with the values
  floating far to the right. Cap the strip width so bars + values pack into a
  tight group, shrink the value labels to fit (compact "41°–66°", no reserved
  92px column). The overrides now sit after the base range rules so they win.
2026-07-16 04:23:22 +00:00
Emi Griffith
d1ec114b4a SEO: make month/season records tables mobile-friendly (#108)
The all-time-records redesign (#106) removed the min-width floor that let the
wide records tables scroll, so the month and season records tables — still five
columns (value + separate date, twice) — compressed below their content on
phones and overlapped/clipped.

Fold each record's date under its value so those tables become three columns
(period · record high · record low), which fit any width without horizontal
scroll. Also tighten all climate tables under 560px (smaller font + padding) so
a hot city's 3-digit °F values with °C in parens never collide in the city
page's normals table.
2026-07-16 04:14:19 +00:00
Emi Griffith
4e8d1e9e59 SEO: monthly & seasonal records + colour-coded climate pages (#107)
* SEO: monthly & seasonal records on the city records page

The /climate/<city>/records page showed only all-time records per metric.
Expand it into a full records page: record high/low for each of the 12
months (each linking its month page) and for each meteorological season,
alongside the existing all-time table.

Seasons are hemisphere-aware — Dec–Feb reads as winter for northern cities
and summer for southern ones (picked from the city's latitude). Records
reuse grading.all_time_records over a month-filtered archive, so no new
data fetching or warming is needed. Adds a Dataset + breadcrumb JSON-LD
block and richer title/description for the expanded coverage.

* SEO: colour-code climate & records pages (heat-map + range strip)

The city, records and month pages were plain muted tables — generic climate-site
styling. Bring the interactive grader's diverging cold→hot palette onto them so
they read as heat maps in the site's own visual language:

- Map absolute °F to the 9-tier palette (temp_class, a Jinja global) and tint the
  temperature cells across the monthly-normals, monthly/seasonal/all-time records
  tables. The value stays in the ink token; the tint is a wash behind it. Non-temp
  rows (humidity/wind/precip) and date columns stay untinted.
- Add a monthly temperature-range strip on the city page: one gradient bar per
  month spanning the average low→high on a shared −10..115°F axis, so a city's
  whole-year rhythm (and hot-vs-cold character) reads at a glance.
- Tint the month page's hero high/low and its record values inline.

Palette, light/dark, and 390–1920px layouts verified by rendering hot (Phoenix)
and cold (Anchorage) cities.
2026-07-16 03:50:59 +00:00
Emi Griffith
d24049a78d Records: color-accented cards, vertical on mobile (#106)
Replace the 5-column records table (which forced horizontal scrolling on mobile —
only high OR low visible) with a responsive card grid: one card per metric, each
with a red-accented 'Highest' row and a blue-accented 'Lowest' row (tier colors).
Grids on desktop, stacks to a single column on mobile with high/low stacked
vertically — no horizontal scroll. Precip reads Wettest / longest dry spell.
2026-07-16 03:37:51 +00:00
Emi Griffith
5f380bcadd Records: show longest dry streak instead of record-low precipitation (#105)
Record-low rainfall is meaningless (always ~0), so the precip row's low side now
shows the longest run of consecutive days without measurable rain and the date that
dry spell began (grading.longest_dry_streak), keeping the wettest day on the high
side. A footnote explains the substitution.
2026-07-16 03:28:26 +00:00
Emi Griffith
0e16ad2a93 SEO: climate hub — collapse all countries by default, order alphabetically (#104)
Drop the open-by-default on the first three countries (every <details> starts
collapsed) and order the country sections alphabetically instead of by largest
city's population.
2026-07-16 03:22:47 +00:00
Emi Griffith
586a7b1aa1 SEO: collapsible climate hub + mobile formatting fixes (#103)
- Climate hub (/climate) now uses native <details> per country — collapsible
  expand/shrink sections with a city count and a rotating marker (top 3 countries
  open by default). Links stay in the DOM, so crawling/SEO is unaffected.
- Fix nested F/C parentheses in the city lede (e.g. 'July (71°F (22°C) average
  high)') by rephrasing so the already-parenthesized temperature isn't wrapped in
  another paren.
- Fix the 5-column records table overlapping on mobile: give it a min-width so it
  scrolls inside .table-wrap on narrow screens instead of colliding.
2026-07-16 03:17:00 +00:00
Emi Griffith
a705c154cd SEO: hand-curated notable weather event per city (falls back to blurb) (#100)
Auto-sourcing a notable weather event from Wikipedia search proved unreliable
(wrong matches, low coverage), so city_events.py is a small, fact-checked list of
one iconic event per city (Katrina/New Orleans, Harvey/Houston, the 1952 Great
Smog/London, the 2021 heat dome/Seattle, ...) — 31 cities, each with a Wikipedia
link. The city page shows a 'Notable weather in {city}' callout when one exists and
otherwise just the flavor blurb. Tests check the curated slugs are all valid and the
callout renders (and is absent for uncurated cities).
2026-07-16 01:23:45 +00:00
Emi Griffith
c3a11ee994 SEO: per-city Wikipedia blurbs + travel/compare CTA on city pages (#98)
Add unique editorial content so the programmatic pages don't read as templated:
- gen_flavor.py seeds backend/cities_flavor.json with a short descriptive blurb per
  city from Wikipedia's free REST summary API (no key), validating each match by
  comparing article coordinates to the city's lat/lon so the wrong 'Springfield'
  never attaches. Retries + modest concurrency; ~700/750 cities get a blurb, the
  rest render without one. Text is CC BY-SA, attributed with a 'via Wikipedia' link.
- City pages show the blurb under the intro and a travel callout that deep-links to
  the compare page with the city pre-loaded (/compare#loc=lat,lon) — the visitor
  just adds their own city. Month pages get the same seasonal 'visiting in {month}?'
  compare link. Both add unique per-page text and internal links.
- cities.py gains flavor(slug); tests cover the blurb + attribution + compare CTA.
2026-07-16 00:39:47 +00:00
Emi Griffith
58ac3120b2 SEO: crawlable programmatic climate pages + technical hygiene (#96)
* SEO: generate curated city set for crawlable climate pages

gen_cities.py reuses the GeoNames index places.py already parses to select the top
~500 metros by population, assigns each a stable URL-safe slug (dropping admin1 when
it repeats the city name), and writes committed backend/cities.json. cities.py loads
it lazily with slug lookup, all_slugs(), display_name(), and by_country() grouping
for the upcoming hub + sitemap.

* SEO: rendering core, robots.txt, sitemap.xml, and metadata hygiene

- content.py: Jinja2 environment + HTML responder (ETag/304), dynamic /robots.txt
  (disallows /api and /alerts, points at the sitemap) and /sitemap.xml (enumerates
  the home/static pages plus every city, month, and records URL from cities.py).
  Registered on the app before the StaticFiles mount so the routes win.
- templates/base.html.j2: shared layout with unique title/description, self-
  referential canonical, Open Graph, favicon/manifest, header nav (adds a Climate
  link) and a footer link graph.
- Give each existing page a unique <meta description> (were 5x identical) and a
  self-referential <link rel=canonical>; add WebApplication JSON-LD to the home page.
- Pin jinja2.

* SEO: server-rendered per-city climate page (/climate/{slug})

The keystone crawlable page: for a city it snaps to the grid cell, loads the
archive (fetching once if missing, self-healing), and renders as real HTML — a
'how today compares' block (grade + percentile per metric from grade_day, tinted
by tier), a monthly normals table (climatology at each month's 15th, shown in °F
and °C), all-time records (new grading.all_time_records helper), a breadcrumb,
Dataset+Place+BreadcrumbList JSON-LD, self-referential canonical, and links into
the interactive tool + month/records pages. Content-page CSS added to style.css
(renamed the table class to avoid colliding with the app's .normals flex row).

* SEO: month (/climate/{slug}/{month}) and records (/climate/{slug}/records) pages

Month pages render the exact-month long-tail ('average weather in {city} in
{month}') with that month's average high/low, typical p10-p90 range, month-specific
records, and prev/next month links. Records pages show all-time record highs/lows
per metric with dates (grading.all_time_records). Shared _resolve_city helper; the
literal /records route is registered before the {month} param and month names are
validated (unknown month -> 404).

* SEO: climate hub, weather glossary, and about/methodology pages

- /climate: crawlable directory of all ~500 cities grouped by country — the
  internal-link graph that lets search engines discover every city page.
- /glossary + /glossary/{term}: plain-language definitions (climate normal,
  percentile, temperature anomaly, feels-like, heat index, wind chill, humidity,
  reanalysis) with DefinedTerm JSON-LD and cross-links into the tool.
- /about: methodology page (ERA5 data source, 45-year baseline, +/-7-day window,
  percentile grading) for E-E-A-T. All linked from the shared footer.

* SEO: archive warmer, content-page tests, and deploy docs

- warm_cities.py: paced, idempotent offline warmer that pre-fetches each city
  cell's archive so /climate pages serve from cache and a crawl can't burst the
  archive quota (pages self-heal if hit before warming).
- tests/test_content.py: city-set slug uniqueness/lookup, robots.txt, sitemap
  enumerating city/month/records URLs, and that a rendered city page carries the
  stats + canonical + Dataset JSON-LD in the HTML; plus month/records/hub/glossary/
  about routing and 404s.
- DEPLOY.md: document the content pages, the warm step, and submitting the sitemap.
2026-07-15 23:53:11 +00:00