Current version: v0.15.8
Release notes are now a public page. Moved off the Admin-only "Releases"
tab onto /releases/, linked from the v{{ version }} tag in the footer
on every page. Same content (rendered from RELEASES), just visible
without an account.
Visited-lake pins update instantly on sign-in. Previously the main map and lake detail maps only checked visit history once, at page load — if you signed in without reloading, pins stayed blue until a manual refresh. Both maps now listen for the sign-in event and recolor immediately.
Contour lines could vanish entirely when zoomed out. The contour library silently draws nothing below the lowest configured zoom threshold, which was 11 — any view at zoom 10 or below (one click out from the default) showed no elevation lines at all, on both the main map and lake detail maps. Extended the threshold table down to zoom 8 so contours now render continuously across the map's full zoom range.
Main map GPS button over-zoomed past visible trails. Locating yourself jumped to zoom 15 by default — past where the low-zoom trail overlay cuts off — hiding trails right when they mattered most on-trail. Capped the GPS landing zoom to 12.
Main map view could reset unexpectedly while GPS tracking was on. A single stray GPS reading outside the park (common under tree cover or in canyons) snapped the map back to its pre-tracking position on every update. Now only checked once, on the first fix of a tracking session.
Main map lost its position, zoom, and satellite/vector mode on reload. Anything that reloaded the page — a backgrounded mobile tab, a screen lock, a lost WebGL context — silently dropped back to the default view. Map position and imagery mode now persist across reloads.
Reverted lake detail maps to vector view by default. Un-does the satellite default introduced in 0.15.6 based on further testing.
Corrected trailhead coordinates for Lake Granby access, Shadow Mountain TH / Grand Lake, and Near Grand Lake (road-accessible).
Updated lake hero photos for Finch Lake and Thunder Lake, and added photos for Pear Lake / Pear Reservoir and Hutcheson Lakes (Lower) via admin content overrides.
Lake detail map — trailhead pin hidden by default. On lakes where the trailhead sits a couple miles from the lake (e.g. Love Lake, ~2.5 mi via Forest Canyon Overlook), the auto-fit camera could place a marker directly underneath the map's control button stack (imagery/fullscreen/offline/ campsite/privy/peak toggles), making it invisible even though it was technically within the viewport. Increased the right-side fit padding to clear the full button column, and the map now backs off one more zoom level after fitting so both the lake and trailhead pins sit comfortably centered with margin on every lake page, not just the previously cramped ones.
Removed 5 mismatched lake hero photos — Azure Lake, Lake Amore, Forest Lake, Doughnut Lake, and Love Lake were showing unrelated stock/archival images (a Canadian tour-boat lake, a 19th-century train engraving, Moraine Lake in Banff, a "Mineral Springs" fountain illustration, and a Canadian Rockies meadow pond) — almost certainly Wikimedia Commons keyword collisions for these obscure, off-trail Gorge Lakes cluster names. All five now fall back to the existing "Photo coming soon" state instead of showing an incorrect photo.
deploy.py: workspace backup is now opt-in. Replaced --skip-backup
with --backup — the .tar.gz snapshot to s3://{bucket}/.backups/ no
longer uploads on every deploy by default; pass --backup to include it.http://, and the
Cache Storage API + Service Worker registration are only exposed by browsers
on a secure context (https:// or http://localhost) — on dev, caches is
not defined at all, so the service worker and the offline download panel
silently fail there. Test those features on prod or via a local server on
http://localhost instead.Roads hidden in satellite/imagery view — SATELLITE_KEEP_IDS only kept
trail and label layers visible when the imagery toggle was on, so every paved
road (Trail Ridge Rd, Bear Lake Rd, parking-lot/service roads, plus their
bridge and tunnel variants) disappeared in satellite mode, leaving only
floating road labels with no line underneath. Roads now stay visible in
imagery mode, on both the main map and lake detail maps.
Roads recoloured to grey in satellite mode — roads default to white/ near-white, which collided with the contour lines once those also switch to white for contrast over aerial imagery. Roads now render as two-tone grey (dark outline, lighter fill) in satellite mode so they stay visually distinct from contours; terrain mode is unchanged, and each road layer's original color is restored exactly on toggle-off.
trails_lowzoom layer added in 0.15.2 was missing from TRAIL_LAYER_IDS, the
list of layers kept visible when the imagery toggle is switched on. So the
z10–12 low-zoom trails would vanish in satellite view while the original z13+
trails stayed visible. Fixed on both the main map and lake detail maps.Visited lake pins on lake detail maps — lake detail pages now recolour visited lake pins to red for signed-in users, matching the main map. This includes both nearby lake pins and the selected lake's larger pin; marker size is preserved so the current lake still reads as primary.
README updated to document the lake-detail map visited-pin behaviour and to remove stale mention of the old home-page road-closure stamp.
Low-zoom trail overlay — the Protomaps basemap only includes hiking-path
geometry from zoom 13 up, so the dashed orange trails disappeared when the map
was zoomed out past z13. A new scripts/extract_trails.py reads the trail lines
out of the z13 tiles already in rmnp.pmtiles, simplifies them, and writes
src/static/tiles/trails.geojson (~433 KB / ~90 KB gzipped). style.json draws
that overlay at z10–12, so trails now stay visible about three zoom levels farther
out; the pmtiles keeps drawing them at z13+. Applies to the main map and every
lake detail map, and the overlay is cached in the offline core install.
requirements.txt — declared pmtiles and mapbox-vector-tile (used by the new
extraction script). README updated with the overlay and its refresh step.
New src/data/peaks.json — all 106 named summits in RMNP, with name,
latitude, longitude, and elevation (feet and meters). Sourced from the
NPS/USGS ROMO_GEOL_Summits GIS layer (GNIS data). Documented in the README
data-flow section.
Peak pins on the maps — the main map and every lake detail map can show the named peaks as dark-brown pins with a two-peak mountain icon (distinct from the campsite tent). The popup shows the peak name, elevation (ft and m), and a Google Maps link.
Show/hide peak toggle — a mountain-icon button in the control stack on the main map and every lake detail map, off by default like the campsite and privy toggles.
Longs Peak & the wandering moose — the moose easter egg now anchors to the authoritative Longs Peak summit coordinate and always renders on the top layer; the Longs Peak pin is hidden on the main map (the moose stands in for it) and its popup gained a Google Maps link. Longs Peak still appears as a normal peak pin on lake detail maps.
New src/data/campsites.json — all 120 backcountry (wilderness) campsites in
RMNP, with site code, area, site name, 20-year average snow-free date, number of
sites, trailhead(s), distance from trailhead, elevation gain, privy, water source,
wheelchair accessibility, latitude/longitude, site elevation, and a link to the
campsite's NPS detail page. Non-coordinate fields come from the NPS
site_details.htm table; coordinates come from the NPS ArcGIS
ROMO_FACILITY_WildernessCampsite point layer. 108 sites carry coordinates and an
NPS detail link; 12 (discontinued/unmapped) list null/Unknown.
Campsite pins on the maps — the main map shows orange, tent-topped pins with a popup summarizing the site (trailhead, distance, elevation, gain, snow-free date, number of sites, privy, water source) plus links to the NPS detail page and Google Maps. Lake detail maps show the same campsites as smaller pins.
Show/hide campsite toggle on the main map and every lake detail map — a tent button in the control stack, off by default on both desktop and mobile.
trailheads.json for campsite access that don't
serve lakes: East Portal TH and Tonahutu TH (coordinates from the NPS
trailheads GIS layer). 18 of the 20 NPS campsite trailheads mapped to existing
entries.#8e44ad), distinct from the green trailheads —
consistent across the main map and lake detail maps.Vector basemap included in the offline download — the orange dashed trails
(and roads, water, land cover, labels) all live in one ~8 MB vector file,
rmnp.pmtiles. The offline panel previously cached only the large aerial
imagery regions, leaving that basemap to the service-worker precache — which
iOS can silently kill mid-install, so trails often didn't render offline even
after "downloading the area." The panel (now titled Offline Maps) lists
"Base map — trails, water & labels" as its first item, and it is pulled
automatically whenever any imagery region or "Download All" is downloaded. It
is cached into the same ASSETS_CACHE that already makes imagery work offline
and is served by the existing handlePmtiles range handler.
Weak-signal hint — navigator.onLine only reports that a network interface
exists, not that it works, so in the backcountry the phone reports "online" on a
dead signal and the map hangs on failing requests instead of using the cache.
The map page now probes real reachability and, when it's dead while nominally
online, shows a dismissible banner suggesting Airplane Mode (which makes requests
fail instantly so cached tiles render immediately).
scripts/generate_descriptions.py — the description seed generator.
src/data/description.json has diverged substantially through hand edits and
admin-promoted overrides, so the generator was removed to prevent a regeneration
pass from clobbering those edits. Descriptions are now hand-maintained (edit the
JSON, or promote an /admin override via promote_overrides.py --descriptions).
README updated to document the origin, recovery from git history, and the current
workflow.Fall River / Chapin Pass TH on Old Fall River Road (5.0 mi). It is
geographically one of the Gorge Lakes — the closest of the cluster to the
access point — and is now routed from Forest Canyon Overlook (off-trail)
on Trail Ridge Road, matching its neighbors (Arrowhead, Azure, Inkwell, etc.).
Area changed to "Gorge Lakes"; distance re-estimated to 3.0 mi for the steep
off-trail canyon descent.scripts/promote_overrides.py — manual maintenance tool that bakes
admin content overrides (hero images and, with --descriptions, descriptions)
from DynamoDB/S3 into the static source, clears the override rows, and runs the
build/deploy. Standalone — not invoked by build.py or deploy.py. Documented
in README Appendix C.requirements.txt — declared three packages that scripts already import
but that were missing: boto3, certifi, and Pillow.The new tiers use custom inline SVG icons (rendered into #badge-icon) so they
look more premium than the emoji medals of the lower tiers. Gold's next
threshold changed from null to 40 so its progress bar now advances toward
Platinum instead of disappearing once Gold is reached.
rmnp.pmtiles. That file was being treated as a
heavy "deferred" asset by the service worker (DEFERRED_RE matched all
.pmtiles), so it was only cached opportunistically via background top-up or
the first online map view. If neither completed before going offline, the
backcountry map rendered terrain tiles and tracked location but showed no
trails.rmnp.pmtiles is only ~8 MB and is the essential vector basemap, so it is no
longer deferred — it is fetched during the service-worker install alongside
the app shell (core install grows from ~19 MB to ~27 MB). The genuinely heavy
files (terrain.pmtiles 23 MB, imagery*.pmtiles 100 MB+) remain excluded
from the precache and are cached on demand as before.
apple-touch-icon.png, favicon-192.png, and
favicon-512.png were rendered on a transparent canvas, leaving ~20% of
each icon (the corners outside the circular emblem) fully transparent.
iOS shows black behind transparent pixels and, since iOS 16.4 reads the
manifest icons (one of which is tagged maskable), the transparent
edge-to-edge artwork masked into a broken icon — so a freshly installed
home-screen app fell back to a page screenshot instead of the real icon.The home-screen / PWA icons are now flattened onto the brand green
(#1a4731, matching the outer ring) for a seamless, full-bleed icon.
favicon.ico keeps its transparency since browser tabs look best that way.
Regenerate with python3 scripts/generate_favicons.py.
Corrected Lawn Lake TH coordinates — all three Lawn Lake TH variants
(Lawn Lake TH, Lawn Lake TH (off-trail), Lawn Lake TH / Ypsilon Lake
Trail) were at a bogus location (40.4005, -105.665). Updated to the correct
trailhead coordinates (40.40716, -105.62621) on Fall River Road.
Removed bogus Ypsilon Lake TH — this entry duplicated the Lawn Lake TH parking lot (per its own note) and was at the same wrong coordinates. Removed the entry entirely.
Reassigned Ypsilon Lake, Chiquita Lake, Chipmunk Lake — these three
lakes were pointing to the removed Ypsilon Lake TH. They now correctly
reference Lawn Lake TH, which is the actual trailhead for the Ypsilon
Lake Trail.
Added West Alluvial Fan TH and East Alluvial Fan TH — two new trailhead entries for the Alluvial Fan area in Horseshoe Park. West Alluvial Fan TH (40.41069, -105.63755) has a restroom; East Alluvial Fan TH (40.40996, -105.63257) does not.
Default og:image / twitter:image — Browse, Map, About, and Park Access
pages now include social preview image tags (topo hero image) by setting a
default in base.html. Lake detail pages continue to use their own hero
photo as before.
Non-blocking MapLibre on the map page — MapLibre CSS, pmtiles.js, and
maplibre-contour.min.js were previously loaded in <head>, blocking
first paint on the full map page. They are now loaded inline just before
the map div using the same media="print" onload pattern already in use
on lake detail pages.
BreadcrumbList JSON-LD on the map page — the map page was the only public indexable page missing structured breadcrumb data. Matches the pattern already used on Browse, About, and Park Access.
The fullscreen collapse button was repositioning to top: 8px in fullscreen
mode, landing on top of the MapLibre zoom controls. Removed the override so
the button holds its natural position in the button stack at all times.
A fullscreen button (expand icon) now appears below the offline download button on the lake detail page map. Clicking it expands the map to fill the entire viewport. The icon swaps to a collapse icon; clicking it again or pressing Escape returns the map to its normal inline size.
Hiking trails now use the same bright orange (#ff8c00, full opacity) on
the standard terrain map as they do on the aerial imagery view. Previously
the terrain map used a muted brown (#b07840 at 80% opacity) that was hard
to distinguish from contour lines. Change applies to both the main map and
the lake detail page maps.
When viewing community-uploaded photos in the lightbox on a lake detail page, you can now navigate between photos without closing the overlay:
The offline experience was broken in two ways: cache-busted asset URLs
(main.css?v=…) never matched the service worker precache (pages rendered
unstyled offline), and the install-time precache tried to download all
~414 files (~51 MB) in one shot — iOS kills long service worker installs,
silently leaving holes, so unvisited pages errored offline.
/browse/?area=…) serve correctly offline.base.html, so coverage self-heals across app launches.Finch Lake / Allenspark TH to
Finch Lake TH with corrected coordinates.Favicons are now exposed at stable domain-root URLs so browsers, Googlebot, and Googlebot-Image can discover the same icon assets consistently:
/favicon-192.png — primary large PNG favicon for search result display/favicon-512.png — larger PNG fallback for high-resolution contexts/favicon.svg — vector browser favicon/favicon.ico — legacy browser fallback/apple-touch-icon.png — iOS home-screen iconbase.html now links to those root URLs directly, with the 192px PNG listed
first. build.py mirrors both large PNG favicon assets into the generated
site root alongside the existing SVG, ICO, and Apple touch icon.
scripts/generate_favicons.py now writes a true multi-resolution
favicon.ico. The regenerated ICO contains 16, 24, 32, 48, and 64 pixel
variants instead of only the 16px image, while the 192px and 512px PNG assets
were regenerated from the same source artwork.
Lake detail pages now use search-focused titles in the form
Sky Pond in Rocky Mountain National Park: Trail, Map & Photos. The generated
title is shared by the HTML <title>, Open Graph title, and Twitter title so
search engines and social previews receive the same clear page label.
build.py now generates a public_name for each lake and uses it across
public-facing templates. USGS-style names such as Clouds, Lake of the and
Clouds, of the Lake render as Lake of the Clouds on lake detail pages,
browse cards, home cards, map popups, nearby-lake cards, profile lookups,
breadcrumbs, meta descriptions, and structured data.
The raw source name is still retained as raw_name where useful for internal
lookup/search context, and Browse Lakes search includes both the public and raw
forms so existing searches continue to work.
og:image and
twitter:image tags.Internal links across every template were using …/index.html URLs (e.g.
/lakes/bear-lake/index.html) while canonical tags pointed to the clean
directory URL (/lakes/bear-lake/). This caused Google to crawl both forms,
creating duplicate content noise and potentially diluting link equity.
All index.html suffixes removed from internal hrefs in:
- base.html — nav links (Browse, Map, Park Access, About) and footer links
- home.html — lake cards, Browse/Map CTA buttons
- browse.html — lake card links
- lake.html — nearby lake cards, area badge, map popup links, browse filter link
- map.html — map popup "View Lake" link
- 404.html — "Open the map" link
All internal links now match the canonical URL format exactly. The canonical
tag itself (/lakes/{slug}/) was already correct — only the link hrefs needed
fixing.
exp claim) on every page loadgoogle.accounts.id.prompt()
to silently re-authenticate using the user's active Google session — no UI,
no interruption to the userauto_select: true added to GIS initialize() to enable silent sign-inGeolocateControl to both the main map and lake detail pages(pointer: coarse) media query so tablets are included but mouse-driven
desktops are not.user-outside-rmnp CSS class on the map
container. GPS continues running silently so the dot appears automatically
when the user enters the parkjumpTo if the first fix is outside
the park; prevents the map from flying to a home location far from ColoradoSettings → Privacy → Location Services →
Safari / app name)@media (min-width: 600px) overrideThe site is now a fully offline-capable Progressive Web App. After the first launch on a good connection, all core content is available with no network.
src/templates/sw.js — Jinja2 template rendered by build.py at every
build. The pre-cache manifest is injected with per-file MD5 hashes; any
change to any cached asset automatically rotates the SW version and
triggers a background update on next app open.{VERSION}-{content-hash} — ensures updates are
detected even without a VERSION bump.| Asset | Notes |
|---|---|
rmnp.pmtiles (7.3 MB) |
Full vector basemap |
| Terrain PNGs — 221 tiles (23 MB) | z5–z12 RMNP bbox + 1-tile buffer; used by maplibre-contour for offline contour lines |
| All 150 lake detail HTML pages | Full path /lakes/{slug}/index.html so in-app links match |
| 8 app shell pages | Home, Browse, Map, About, Access, Sign-in, Profile, 404 |
| CSS, JS, fonts, sprites, manifest | All static assets |
terrain.pmtiles excluded |
23 MB; runtime-cached on first map view to stay under iOS 50 MB limit |
| Lake photos excluded | 25 MB; combined with terrain data would exceed iOS limit |
request.mode === 'navigate'): checks pre-cache
first (app shell + all lake pages), then runtime pages cache, then networkrmnp.pmtiles from pre-cache, terrain.pmtiles from runtime assets cacheMapLibre GL JS and CSS are now vendored at src/static/js/maplibre-gl.js
and src/static/css/maplibre-gl.css (was unpkg CDN). Eliminates the last
external JS dependency and allows the map to render fully offline.
body.offline class toggled on network state change — used by CSS to hide
UI elements that require networkbody.offline .maplibregl-popup a[href*="google.com/maps"]build.py: file_hash() + generate_sw_manifest() functions compute
the pre-cache manifest after minification; sw.js rendered as last stepdeploy.py: extra sync pass for sw.js with Cache-Control: no-cache
(overrides the max-age=31536000 applied to all other .js files)src/static/manifest.json — Web App Manifest with name "Lakes of
RMNP", short name "RMNP Lakes", standalone display mode, dark green theme
color (#1a4731), and all three icon sizes (180×180, 192×192, 512×512)base.html (inherited by all 163 pages):<link rel="manifest"> pointing to /static/manifest.json<meta name="theme-color"> — colors the browser chrome and task switcherapple-mobile-web-app-capable and apple-mobile-web-app-status-bar-style
for iOS standalone modeapple-mobile-web-app-title set to "RMNP Lakes"maplibre-contour runs tile
fetching in a Web Worker created from a blob: URL. Relative paths and
root-relative paths (/static/…) cannot be resolved from that context.
Fixed by passing window.location.origin + '/static/tiles/terrain/…' as
the DemSource URL and setting worker: false to avoid the blob URL
resolution issue.maplibre-contour
fetches one tile outside the map viewport to avoid edge artifacts at tile
boundaries. Tiles just outside our original bbox (e.g. 11/420/771) were
returning 404. Expanded the download buffer by 1 tile on all edges — tile
count increased from 101 → 221. Updated scripts/download_terrain.py with
a buffer=1 parameter in tiles_for_bbox().terrain.pmtiles to include the buffered tile set (23 MB, was
12 MB).Contour lines previously fetched terrain data from the AWS public terrarium
tile service (s3.amazonaws.com/elevation-tiles-prod/…) at runtime — an
external dependency that would break offline and couldn't be cached. Fixed by:
terrain.pmtiles
as individual PNGs into src/static/tiles/terrain/{z}/{x}/{y}.pngmlcontour.DemSource in both map.html and lake.html at the
self-hosted path instead of the AWS URLscripts/download_terrain.py to also write individual PNG files
alongside the MBTiles archive on future tile refreshesThe site now has zero runtime dependency on any external tile service. All map data (basemap, terrain, contours) is served entirely from S3/CloudFront.
The green trailhead pins on the main map (/map/) were missing the "TH" text
label inside the white circle. Added to match the lake detail page pins.
Updated README section 8 (Maps) to reflect the v0.11.0 self-hosted PMTiles stack. Previous content described the old USGS raster tile approach.
rmnp.pmtiles, terrain.pmtiles, vendored JS
libs, style, fonts, and spritespmtiles.js to fall back to
downloading the full ~19 MB on first load. Documents the exact impact,
workaround (browser caching), and step-by-step fix (switch origin to S3
REST endpoint + OAC policy)Replaced the runtime USGS raster tile dependency with a fully self-hosted vector map stack served from S3/CloudFront. All map assets are bundled with the site — no external tile service is called at page load.
rmnp.pmtiles (7.3 MB) — Protomaps OpenStreetMap vector basemap,
RMNP bounding box, z0–z15, extracted via the Protomaps daily buildterrain.pmtiles (12 MB) — AWS terrarium-encoded elevation tiles,
RMNP bbox, z5–z12, downloaded and packed into PMTiles via a new
scripts/download_terrain.py utilitypmtiles.js (32 KB vendored) — PMTiles protocol adapter for MapLibre;
registers the pmtiles:// URL scheme so both files are loaded via HTTP
Range requests with no tile server requiredmaplibre-contour.min.js (30 KB vendored) — dynamic contour line
computation from terrain tiles in a Web Workermap.setTerrain() with 1.5× exaggeration; right-click
drag (desktop) or two-finger drag (mobile) tilts the map into perspectivesky property in style.jsonwater) — the
Protomaps tileset stores lake name labels as POIs (kind: water) in the
pois layerlake_labels layer reads from pois, sits at the top of the layer
stack, and uses text-allow-overlap + text-ignore-placement so names are
never hidden by collision detection, camera angle, or terrain occlusionstyle.json derived from Protomaps light theme with RMNP-specific
overrides: parchment base, green park fills, ice-blue glacier, warm-brown
trail dashes, navy lake labelsstatic/tiles/maxPitch: 85 on both map and lake detail maps to allow full sky/3D tilttext-pitch-alignment: viewport so labels float
above terrain and are never occludedThe static/tiles/*.pmtiles path requires a CloudFront cache behavior that
forwards the Range header for HTTP range requests to work correctly.
The green trailhead pin on each lake's detail map now displays "TH" in the white circle, matching the style of the grey context-trailhead pins added in 0.10.5. Both pin types now read at-a-glance as trailheads without requiring the user to open the popup.
scripts/generate_descriptions.py that still
referenced the old count of 138 lakes; updated to 150 to match current
catalog size.After cross-checking our database against the official NPS lake count (147), Kathryn Neugent's enumerated list (144), and the USGS GNIS / OpenStreetMap named-water-feature data inside the official park boundary polygon, we identified and individually verified 16 new entries while removing 4 grouped "cluster" placeholders.
Cascade Lake — small widening of Cow Creek east of Lawn Lake; ~9,000 ft,
accessed via the new Cow Creek / McGraw Ranch THPtarmigan Tarn #1 and Ptarmigan Tarn #2 — small alpine pools on
Bighorn Flats below Ptarmigan Point at ~11,800 ft, off-trail from
Flattop MountainHutcheson Lakes → Lower / Middle / Upper (NPS-verified distances 7.2,
7.5, 8.1 mi via Finch Lake TH)Twin Lakes → lower / upper (Wild Basin, off-trail from Thunder Lake)Fay Lakes → Lower / Middle / Upper (NPS-verified 10,740 / 11,020 /
11,220 ft; 5.3 / 5.8 / 6.1 mi)Spectacle Lakes → Lower / Upper (11,340 ft, 5.3 / 5.7 mi)Added new trailhead entry to trailheads.json:
- Cow Creek / McGraw Ranch TH at 40.43098, -105.50064 — historic 1880s
homestead the park purchased in 1988; gateway to the Cow Creek, Dark
Mountain, and North Boundary Trails
build.py now emits tiny meta-refresh redirect pages at each old cluster
slug so externally-indexed URLs continue to resolve:
/lakes/hutcheson-lakes/ → /lakes/hutcheson-lakes-lower//lakes/twin-lakes/ → /lakes/twin-lakes-lower//lakes/fay-lakes/ → /lakes/fay-lakes-lower//lakes/spectacle-lakes/ → /lakes/spectacle-lakes-lower/Each redirect page uses 0-second meta-refresh + <link rel="canonical"> +
JS location.replace so Google treats it as a 301; pages are noindex so
they don't compete with the new canonical URLs in search.
/signin/ page headline and meta-description updated to matchhttps://www.nps.gov/romo/planyourvisit/conditions.htm) in a new tab.
Hidden on viewports under 900px to keep the mobile hero uncluttered (same
breakpoint as the signed-out promo card). Stamp graphic is the standard
200x200 size matching the promo card next to it..access-cta-link
(dark-green pill button) and .access-note-small (muted disclaimer text)./favicon.ico, /favicon.svg, /apple-touch-icon.png) resolve.
Browsers, bots, search engines, and iOS probe these URLs at the root even
when <link rel="icon"> tags point elsewhere — previously they 404'd to
the cartoon-moose error page. The canonical copies still live at
/static/assets/ and are referenced by the <link> tags as before; the
root files are just additional copies emitted by build.py after the
static-copy step./signin/ page. Hidden on viewports ≤900px so it never dominates the hero
on phones/tablets. Built on a new generic .signed-out-only mechanism in
auth.js (adds/removes body.signed-out based on session state) so the
same gate can be reused for future signed-out-only UI./signin/ page — full-width promo with the same passport-stamp
branding, a large Google "Continue with Google" button rendered inline,
a 2×2 feature grid (log visits / map fills in / earn badges / track
progress), and a "keep browsing" escape link. noindex,follow so it
doesn't compete with home in search. Inline pre-paint script bounces
already-signed-in users back to the home page./signin/ redirects to the home page via
location.replace (so the signin URL doesn't sit in browser history).favicon.svg
(primary, vector), favicon.ico (multi-resolution 16/24/32/48/64 for
older browsers), apple-touch-icon.png (180×180, iOS home screen), and
favicon-192.png / favicon-512.png (Android / PWA). All wired into
base.html so they're picked up site-wide.scripts/generate_favicons.py — Pillow-based rasterizer that
regenerates every favicon PNG/ICO from the same geometry as
favicon.svg. No libcairo dependency required; re-run after editing
the SVG to keep raster outputs in sync.base.html (main.css?v=<epoch>, auth.js?v=<epoch>).
Each build rotates the value so browsers always pull the latest
styles/scripts even though the files are served with a one-year
Cache-Control: max-age for performance.docs/mockups/ for
reference (passport stamp, stats banner, bootprint badge, topo map
trail) plus a size-ladder for the chosen Concept 1.google.com/maps/search/?api=1&query=<lat>,<lon> deep link format which
opens the Google Maps app on mobile when installed.lakes.json.?area=... support to the Browse Lakes page so area-filtered links can
be shared or used from other pages.0-1 mi, 1-3 mi, 3-6 mi, and 6+ mi.generate_description.py script to create an updated src/data/description.json file that used used as part of build.py and deploy.pygenerate_description.py script that helps generate better unique messages leveraging some additional CURATED_FACT_SOURCES, WILDERNESS_SITE_FACTS, SPECIAL, AREA_PUBLIC_FACTS. This generated a file called src/data/descriptions.json. Then build.py was updated to leverage this new data.analytics_pageview Lambda now silently drops pageview records for blocked IPs and all admin pages before any DynamoDB write or geo lookupBLOCKED_IPS set added at module level — initial entry: 66.186.210.201; add IPs to this set and redeploy to extend the listpage starts with /admin are dropped regardless of IP — admin activity was never meaningful in visitor analyticslakesofrmnp-pageviews; 434 clean records retained<head> to inline in the body, just before the map divmedia="print" onload="this.media='all'" (non-blocking); <noscript> fallback includeddefer juggling<a> in base.html wrapped an SVG image with no text content, failing the "links without discernible names" Lighthouse auditaria-label="Lakes of RMNP — Home" to the anchor; applies to all pages via base.html"image" field upgraded from a plain URL string to a full ImageObject:
{"@type": "ImageObject", "url": "...", "width": 960, "height": 640}url prefers the admin override image when set; falls back to the static Wikimedia photoog:image and twitter:image meta tags also updated to prefer the override URL over the static photominify_static() added to build.py — walks output/static/ after the static copy step and minifies all .css and .js files in-place using rcssmin and rjsminrcssmin>=1.2.0 and rjsmin>=1.2.0 added to requirements.txtensure_cloudfront_compression() added to deploy.py; called before the cache invalidation step on any deploy with a CloudFront distributionDefaultCacheBehavior.Compress; enables it if not set; no-ops on subsequent deploys (no unnecessary API calls)build.py now fetches GET /content/overrides once per build and bakes the admin override image URL directly into each lake page's <img src> — the correct image is present on first paint with no JavaScript swap requiredlake.override_image_url from the baked JSON data — no extra API call needed"Dunraven / North Fork TH" and "North Fork / Dunraven TH" keys in trailheads.json had incorrect coordinates placing the map pin in the Estes Park valley40.4756381, -105.4604792 (Dunraven Glade Road, north of Estes Park)build.py — no runtime costget_nearby_lakes() helper added to build.py; skips lakes with missing coordinates and excludes the current lake{% if nearby_lakes %} guard)loading="lazy" and decoding="async"Place + TouristAttraction — name, description, URL, geo coordinates (lat/lon/elevation), difficulty, trailhead distance, park area, Continental Divide side, hero photo URL (when available), and wheelchair accessibility flag via amenityFeature; containedInPlace links to Rocky Mountain National Park / nps.gov/romoBreadcrumbList — Home › Browse Lakes › [Lake Name]; gives Google the breadcrumb trail to display in search results instead of a raw URLWebSite + Organization JSON-LD (name, URL, description, logo)BreadcrumbList JSON-LD{% block jsonld %} block in base.html, fully overridable per templateog:image and twitter:image tags added to all page templates{% block og_image_tag %} block in base.htmlfetchpriority="high" and loading="eager" added; explicit width="960" height="640" as browser layout hintsfetchpriority="high", loading="eager", width="1280", height="520" added; alt text was already descriptiveloading="lazy" and decoding="async" addedloading="lazy" and decoding="async" addedalt="" (decorative; username label is adjacent)base.html now emits a full set of SEO/social tags on every page:<meta name="description"> (overridable per template via the meta_description block)<meta name="robots"> (overridable via the robots block; defaults to index,follow)<link rel="canonical"> pointing at the prod host (https://lakesofrmnp.com) regardless of env, so dev never competes with prod for rankingog:site_name, og:type, og:title, og:description, og:urltwitter:card (summary_large_image), twitter:title, twitter:description{{ lake_count }} interpolationnoindex,nofollow applied to /admin/ and /profile/; noindex,follow applied to the 404 pageog:type=article set on lake detail pagesbuild.py now emits output/robots.txt:/admin/ and /profile/https://lakesofrmnp.com/sitemap.xmlbuild.py now emits output/sitemap.xml with 143 URLs:<lastmod> set to today's UTC date on every build<loc> entries use the prod host so the dev bucket is never advertised to crawlersapp.py files under backend/functions/ now carry a top-of-file module docstring explaining purpose, callers (public / auth / admin), DynamoDB tables and S3 buckets touched, and notable response shapeshome_feed scans the full Visits table on cache miss when it could read the precomputed #META#RECENT rowvisits_lake / visits_list queries are not paginated, so very heavy users or lakes silently truncate at the 1 MB Dynamo page limitadmin_content image uploads can orphan the previous S3 object when the file extension changesadmin_users user-delete and visits_delete do not decrement #COUNT / #META#RECENT aggregate rowsadmin_moderation comment-delete leaves the date in visit_dates, so the activity feed still surfaces an empty entryis_admin is baked into the 24-hour JWT, so demoting an admin only takes effect after the token expiresleft: 0; right: auto override inside the ≤600 px media query so the menu anchors to the avatar's left edge instead of extending leftwardsrc/templates/404.html rendered at build time to output/404.htmlmap-sidebar-header, sidebar-stat, diff-bar, etc.) moved from map.html inline <style> into main.css so both pages can use it without duplication<nav> → <div role="navigation"> to prevent global nav CSS rule from applying dark background/ticket/10086911 and /ticket/10086912)Page X of Y (start–end of total) indicator; hidden when total ≤ 25; column sort resets to page 1max-height + Chart.js maintainAspectRatio: falseRELEASES file as styled HTML; refreshed at every buildREADME.md file as styled HTML; refreshed at every build (sourced from README.md, displayed as "Design" in the UI)markdown package with tables, fenced_code, toc, and sane_lists extensionsmarkdown>=3.5 dependency added to requirements.txt/access/ with summary of park gate hours, entrance stations, timed entry permit system, fees, and seasonal road closuresaccessible: true field added to lakes.json; data sourced from official NPS accessibility page (nps.gov/romo/planyourvisit/accessibility.htm)display:none wrapper revealed by auth.js only when user.is_admin === true