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.
This commit is contained in:
parent
f34ab7ef96
commit
a5fbd70d50
7 changed files with 87 additions and 10 deletions
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thermograph — 2-year calendar</title>
|
||||
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta name="description" content="A two-year calendar of daily weather grades for any location — see at a glance which days were unusually hot, cold, wet, or normal versus ~45 years of local climate history." />
|
||||
<meta name="theme-color" content="#f0803c" />
|
||||
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
||||
og:image origins in from this request's scheme://host + base path —
|
||||
|
|
@ -12,13 +12,14 @@
|
|||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thermograph" />
|
||||
<meta property="og:title" content="Thermograph — 2-year calendar" />
|
||||
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta property="og:description" content="A two-year calendar of daily weather grades for any location — see at a glance which days were unusually hot, cold, wet, or normal versus ~45 years of local climate history." />
|
||||
<meta property="og:url" content="__ORIGIN__/calendar" />
|
||||
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/calendar" />
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thermograph — compare comfort</title>
|
||||
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta name="description" content="Compare how well two or more places hit your comfort temperature over any date range, side by side, each graded against its own ~45-year climate history." />
|
||||
<meta name="theme-color" content="#f0803c" />
|
||||
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
||||
og:image origins in from this request's scheme://host + base path —
|
||||
|
|
@ -12,13 +12,14 @@
|
|||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thermograph" />
|
||||
<meta property="og:title" content="Thermograph — compare comfort" />
|
||||
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta property="og:description" content="Compare how well two or more places hit your comfort temperature over any date range, side by side, each graded against its own ~45-year climate history." />
|
||||
<meta property="og:url" content="__ORIGIN__/compare" />
|
||||
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/compare" />
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thermograph — a single day</title>
|
||||
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta name="description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." />
|
||||
<meta name="theme-color" content="#f0803c" />
|
||||
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
||||
og:image origins in from this request's scheme://host + base path —
|
||||
|
|
@ -12,13 +12,14 @@
|
|||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thermograph" />
|
||||
<meta property="og:title" content="Thermograph — a single day" />
|
||||
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta property="og:description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." />
|
||||
<meta property="og:url" content="__ORIGIN__/day" />
|
||||
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/day" />
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thermograph — how unusual is your weather?</title>
|
||||
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta name="description" content="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." />
|
||||
<meta name="theme-color" content="#f0803c" />
|
||||
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
||||
og:image origins in from this request's scheme://host + base path —
|
||||
|
|
@ -12,13 +12,17 @@
|
|||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thermograph" />
|
||||
<meta property="og:title" content="Thermograph — how unusual is your weather?" />
|
||||
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta property="og:description" content="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." />
|
||||
<meta property="og:url" content="__ORIGIN__/" />
|
||||
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/" />
|
||||
<script type="application/ld+json">
|
||||
{"@context":"https://schema.org","@type":"WebApplication","name":"Thermograph","url":"__ORIGIN__/","applicationCategory":"WeatherApplication","operatingSystem":"Web, iOS, Android","browserRequirements":"Requires JavaScript","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"description":"See how today's weather compares to ~45 years of local climate history for any place on Earth — graded by percentile."}
|
||||
</script>
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thermograph — reading the grades</title>
|
||||
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta name="description" content="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." />
|
||||
<meta name="theme-color" content="#f0803c" />
|
||||
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
||||
og:image origins in from this request's scheme://host + base path —
|
||||
|
|
@ -12,13 +12,14 @@
|
|||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Thermograph" />
|
||||
<meta property="og:title" content="Thermograph — reading the grades" />
|
||||
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
||||
<meta property="og:description" content="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." />
|
||||
<meta property="og:url" content="__ORIGIN__/legend" />
|
||||
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/legend" />
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
|
|
@ -1432,3 +1432,71 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
|||
@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {
|
||||
#cmp-params, #cal-filters { transition: transform .28s ease; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Server-rendered content pages (climate hub / city / month /
|
||||
records / glossary / about). Text-first, minimal JS.
|
||||
============================================================ */
|
||||
.site-name { font-size: 20px; margin: 0; }
|
||||
.site-name a { color: var(--text); text-decoration: none; }
|
||||
main.content { max-width: 880px; margin: 0 auto; padding: 8px 20px 56px; line-height: 1.6; }
|
||||
main.content h1 { font-size: clamp(26px, 4vw, 36px); margin: 8px 0 4px; }
|
||||
main.content h2 { font-size: 22px; margin: 32px 0 10px; }
|
||||
main.content p { margin: 10px 0; }
|
||||
main.content a { color: var(--accent); }
|
||||
.content .lede { font-size: 18px; color: var(--text); }
|
||||
.breadcrumb { font-size: 13px; color: var(--muted); margin: 4px 0 12px; }
|
||||
.breadcrumb a { color: var(--muted); text-decoration: none; }
|
||||
.breadcrumb a:hover { color: var(--accent); }
|
||||
.breadcrumb .sep { opacity: .5; }
|
||||
|
||||
.content .cta {
|
||||
display: inline-block; margin-top: 6px; padding: 11px 18px; border-radius: 10px;
|
||||
background: var(--accent); color: #1a1206; font-weight: 700; text-decoration: none;
|
||||
}
|
||||
.content .cta:hover { filter: brightness(1.05); }
|
||||
|
||||
.today-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin: 12px 0; }
|
||||
.today-card {
|
||||
display: flex; flex-direction: column; gap: 3px; padding: 12px 14px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-left: 5px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.tc-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
|
||||
.tc-value { font-size: 17px; font-weight: 700; }
|
||||
.tc-grade { font-size: 12px; color: var(--muted); }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table.normals-table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 15px; table-layout: fixed; }
|
||||
table.normals-table th:first-child, table.normals-table td:first-child { width: 26%; }
|
||||
table.normals-table th, table.normals-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||||
table.normals-table thead th { color: var(--muted); font-weight: 600; font-size: 13px; }
|
||||
table.normals-table td a { text-decoration: none; }
|
||||
table.normals-table tbody tr:hover { background: var(--surface-2); }
|
||||
|
||||
.climate-records ul { margin: 8px 0; padding-left: 20px; }
|
||||
.climate-records li { margin: 4px 0; }
|
||||
.climate-foot { font-size: 14px; margin-top: 28px; border-top: 1px solid var(--border); padding-top: 16px; }
|
||||
|
||||
/* Hub / directory of cities */
|
||||
.hub-country { margin: 22px 0; }
|
||||
.hub-country h2 { margin: 0 0 8px; font-size: 18px; }
|
||||
.city-links { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0; padding: 0; list-style: none; }
|
||||
.city-links a { text-decoration: none; }
|
||||
|
||||
/* Glossary / about editorial */
|
||||
.glossary-term { border-bottom: 1px solid var(--border); padding: 16px 0; }
|
||||
.glossary-term h2 { margin: 0 0 6px; }
|
||||
|
||||
.site-footer {
|
||||
max-width: 880px; margin: 0 auto; padding: 24px 20px 48px; border-top: 1px solid var(--border);
|
||||
color: var(--muted); font-size: 14px;
|
||||
}
|
||||
.site-footer nav { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-bottom: 10px; }
|
||||
.site-footer a { color: var(--accent); text-decoration: none; }
|
||||
.site-footer p { margin: 0; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
main.content { padding: 8px 14px 40px; }
|
||||
.today-cards { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Thermograph logo" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="canonical" href="__ORIGIN__/alerts" />
|
||||
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue