The homepage was a bare tool: a find-bar and an empty panel reading "Find a location to begin." A visitor arriving from a search result or a shared link learned nothing about what the product does before deciding to leave. Rebuild it around the Weekly view, which is untouched: - Hero with the question as the h1, and a grade card showing a real graded example — the most unusual city we're currently tracking, either tail. The card's frame and text slots are server-rendered with reserved heights, so app.js re-pointing it at the visitor's own place shifts nothing. - "Unusual right now" strip, CSS scroll-snap, no JS carousel. A cold-tail city is force-included whenever one qualifies. - Stance line, how-it-works, explore cards, and 12 city chips linking into the ~1000-page /climate surface. - Monthly digest form, in the footer of every page. Serve / from Jinja instead of a static file with placeholder substitution, so crawlers and no-JS readers get the whole page as real HTML. home.html.j2 extends base.html.j2 and carries app.js's DOM contract over verbatim; the brand degrades to a <p> so the hero owns the sole h1. frontend/index.html is deleted rather than left behind the static mount, where it would keep serving indexable duplicate content. "Where is it most unusual right now" has no cheap answer at request time — percentiles live inside zlib-compressed payload blobs with no column to sort on. So homepage.py sweeps the warm cache and writes data/homepage.json, read by the template. The sweep is strictly cache-only (climate.load_cached_recent_forecast is new, the sibling of load_cached_history), so grading ~1000 cities costs zero upstream requests. It rides the notifier's timer behind an hourly guard rather than starting a second daemon, and also runs at the tail of warm_cities so a fresh deploy has a populated feed. Instrumentation: metrics gains a product-event counter keyed by (event, referrer domain, UTC day), behind an allowlist and a per-IP rate limit, fed by POST /api/v2/event. The referrer is taken from the request's own header, never from the client. The beacon gets its own inbound category that record_inbound ignores, so reporting an interaction doesn't also count as traffic. The dashboard grows an events block with per-referrer attribution. Email is scaffolded but sends nothing yet. mailer.py talks stdlib smtplib to a local Postfix null client on 127.0.0.1:25 (deploy/provision-mail.sh), so the choice between direct-to-MX and relaying through a provider stays a Postfix config change with no code change. The backend defaults to "console", which logs and sends nothing, so dev and tests exercise the whole signup path safely. Signups land in pending_digest unconfirmed; collecting the list shouldn't wait on delivery. Also adds /privacy, linked from the footer and kept out of the sitemap. The strip's classes are named unusual-* rather than record-*: .record-card is already the SEO records page's, and reusing it leaked layout rules onto /climate/<slug>/records.
179 lines
8.3 KiB
Django/Jinja
179 lines
8.3 KiB
Django/Jinja
{% extends "base.html.j2" %}
|
|
{# The Weekly view — the product itself — with the distribution surfaces built
|
|
around it. Everything structural is server-rendered so crawlers and no-JS
|
|
readers get a complete page; app.js hydrates the live numbers in place.
|
|
|
|
The interactive controls below (#find-btn, #date-input, #panel, #results, …)
|
|
are app.js's DOM contract, carried over verbatim from the old static
|
|
frontend/index.html. Do not rename these ids.
|
|
|
|
section / brand_tag / main_class come from the route's render context
|
|
(content.home_page), not a child-template {% set %} — the SEO pages already
|
|
pass `section` that way. #}
|
|
|
|
{% block title %}Thermograph — how unusual is your weather?{% endblock %}
|
|
{% block description %}See how today's weather compares to decades of local climate history for any place on Earth — daily high, low, feels-like, humidity, wind and rain graded by percentile.{% endblock %}
|
|
{% block canonical_path %}/{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
{% endblock %}
|
|
|
|
{% block jsonld %}
|
|
<script type="application/ld+json">
|
|
{{ jsonld_str }}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{# --- Hero ------------------------------------------------------------
|
|
The grade card is the LCP element: its frame is server-rendered and its
|
|
slots are sized here, so hydrating the live numbers causes no layout
|
|
shift. Band colour is always paired with the band word — colour is never
|
|
the only signal. #}
|
|
<section id="hero" class="hero">
|
|
<div class="hero-copy">
|
|
<h1>How unusual is your weather?</h1>
|
|
<p class="hero-sub">Any day, anywhere on Earth — graded against 45 years of that place's own history.</p>
|
|
</div>
|
|
|
|
<div class="hero-card">
|
|
{# --cat carries the band colour, the same convention .normal-card uses,
|
|
so the band token stays the single source of truth. #}
|
|
<div class="grade-card" id="hero-grade" aria-live="polite"
|
|
{% if unusual %}style="--cat: var(--{{ unusual.cls }})"{% endif %}>
|
|
<p class="grade-where" id="hero-where">
|
|
{%- if unusual -%}
|
|
Today in {{ unusual.display }}
|
|
{%- else -%}
|
|
Today
|
|
{%- endif -%}
|
|
</p>
|
|
<p class="grade-band" id="hero-band">{{ unusual.grade if unusual else '—' }}</p>
|
|
<p class="grade-detail" id="hero-detail">
|
|
{%- if unusual -%}
|
|
{{ unusual.metric_label }} in the {{ unusual.percentile|ordinal }} percentile for {{ unusual.window_label }}
|
|
{%- else -%}
|
|
Pick a place to see how unusual its weather is today.
|
|
{%- endif -%}
|
|
</p>
|
|
{% if unusual and unusual.is_default %}
|
|
<p class="grade-why muted">the most unusual weather we're tracking{% if stale %} · as of {{ unusual.date }}{% endif %}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="hero-actions">
|
|
<button type="button" id="hero-locate" class="btn-primary">Use my location</button>
|
|
<button type="button" id="hero-pick" class="btn-ghost">Pick on the map</button>
|
|
</div>
|
|
<p class="hero-jump"><a href="#panel">See your week ↓</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="controls">
|
|
<div class="find-bar">
|
|
<button type="button" id="find-btn" class="find-btn"></button>
|
|
<span id="loc-label" class="loc-label" hidden></span>
|
|
</div>
|
|
<div class="date-row">
|
|
<label>Target date
|
|
<input id="date-input" type="date" />
|
|
</label>
|
|
<button type="button" id="today-btn" class="today-chip" title="Reset the target date to today">Today</button>
|
|
<span class="hint"><a href="{{ base }}/legend">What do the grades mean? →</a></span>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="panel" class="panel panel-full">
|
|
<div class="placeholder" id="placeholder">
|
|
<p>Find a location to begin.</p>
|
|
<p class="muted">Thermograph fetches decades of daily highs, lows, and precipitation
|
|
for your ~4 sq mi cell, builds a ±7-day seasonal distribution for each
|
|
day of the year, and grades recent weather by where it falls on that distribution.</p>
|
|
</div>
|
|
<div id="results" hidden></div>
|
|
</section>
|
|
|
|
{# Sits outside #panel on purpose: app.js rewrites results.innerHTML
|
|
wholesale on every grade, so anything inside it would be destroyed. #}
|
|
<p class="stance-line muted">
|
|
Free. No ads. No tracking. No account needed.
|
|
<a href="{{ base }}/about">How we work →</a>
|
|
</p>
|
|
|
|
{# --- Unusual right now ----------------------------------------------
|
|
Scroll-snap row, no JS carousel. Both tails are represented whenever a
|
|
cold-tail city qualifies — two-sided honesty is product identity. #}
|
|
{% if ranked %}
|
|
<section class="unusual-strip" aria-labelledby="records-title">
|
|
<h2 id="records-title">Unusual right now</h2>
|
|
<ul class="unusual-row">
|
|
{% for r in ranked %}
|
|
<li class="unusual-card" style="--cat: var(--{{ r.cls }})">
|
|
<a href="{{ base }}/day#lat={{ r.lat }}&lon={{ r.lon }}"
|
|
data-event="home.records_click">
|
|
<span class="unusual-city">{{ r.display }}</span>
|
|
<span class="unusual-pct">{{ r.percentile|ordinal }}</span>
|
|
<span class="unusual-band">{{ r.grade }}</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p class="unusual-more"><a href="{{ base }}/climate">See all records →</a></p>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# --- How it works ---------------------------------------------------- #}
|
|
<section class="how-it-works" aria-labelledby="how-title">
|
|
<h2 id="how-title">How it works</h2>
|
|
<ol class="how-steps">
|
|
<li>We keep 45 years of climate history (ERA5 reanalysis) for your exact ~2-mile grid square.</li>
|
|
<li>Today gets compared to the same two weeks of the year, every year back to 1980.</li>
|
|
<li>The result is a percentile and a plain-language grade — Near Record cold to Near Record hot.</li>
|
|
</ol>
|
|
<p class="muted">
|
|
<a href="{{ base }}/legend">Full methodology →</a>
|
|
· Data: ERA5 via Open-Meteo · NASA POWER · MET Norway
|
|
</p>
|
|
</section>
|
|
|
|
{# --- Explore --------------------------------------------------------- #}
|
|
<section class="explore" aria-labelledby="explore-title">
|
|
<h2 id="explore-title">Explore</h2>
|
|
<div class="explore-cards">
|
|
<a class="explore-card" href="{{ base }}/calendar" data-event="home.nav_calendar">
|
|
<span class="explore-name">Calendar</span>
|
|
<span class="explore-desc">Two years of your days, each colored by how unusual it was.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ base }}/compare" data-event="home.nav_compare">
|
|
<span class="explore-name">Compare</span>
|
|
<span class="explore-desc">Rank any places by comfort for a trip or a move.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ base }}/day" data-event="home.nav_day">
|
|
<span class="explore-name">Day</span>
|
|
<span class="explore-desc">Any date since 1980, anywhere: what was normal, what happened.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ base }}/alerts" data-event="home.nav_alerts">
|
|
<span class="explore-name">Alerts</span>
|
|
<span class="explore-desc">Free percentile alerts: get pinged when your weather goes statistically weird.</span>
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
{# --- City hub links --------------------------------------------------
|
|
Real HTML links funnelling homepage authority into the ~1000-page
|
|
/climate surface. #}
|
|
<section class="city-hub" aria-labelledby="cities-title">
|
|
<h2 id="cities-title">Climate context for 1,000 cities</h2>
|
|
<ul class="city-chips">
|
|
{% for c in cities %}
|
|
<li><a href="{{ base }}/climate/{{ c.slug }}" data-event="home.nav_city">{{ c.name }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p><a href="{{ base }}/climate" data-event="home.nav_cities">Browse all cities →</a></p>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block body_scripts %}
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script type="module" src="{{ base }}/app.js"></script>
|
|
{% endblock %}
|