From a6615b0c14efb6aa79ee52be56dd47d9382c4445 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Nov 2026 06:58:14 +0000 Subject: [PATCH 01/12] Fix the deploy focus race, add a Spanish home, and refresh the no-JS snapshot. A late map focus callback could take the search field and fail the Pages test. /es/ is the indexable Spanish home. On Vercel the no-JS snapshot frame uses the cached CBP fetch instead of the last full site build. Co-authored-by: Sebastian Becerra --- CHANGELOG.md | 7 + DECISIONS.md | 8 + ROADMAP.md | 13 +- api/public/snapshot.js | 58 +++++++ index.html | 6 +- public/robots.txt | 2 + public/sitemap.xml | 12 ++ scripts/build-sitemap.mjs | 4 +- scripts/check-border-map.mjs | 1 + scripts/check-document-language.mjs | 4 + scripts/check-faq-scope.mjs | 3 +- scripts/check-hreflang.mjs | 13 +- scripts/check-nojs-reports.mjs | 10 +- scripts/lib/crossing-snapshot-html.mjs | 154 ++++++++++++++++-- scripts/prerender.mjs | 36 +++- scripts/test-crossing-snapshot.mjs | 21 ++- scripts/test-hreflang.mjs | 11 +- scripts/test-public-crossings.mjs | 36 ++++ src/App.jsx | 1 + src/Layout.jsx | 8 +- .../dashboard/CrossingMapExplorer.jsx | 38 +++-- src/lib/homeMeta.js | 6 + src/lib/hreflang.js | 16 +- src/pages/Dashboard.jsx | 26 ++- vercel.json | 8 +- 25 files changed, 434 insertions(+), 68 deletions(-) create mode 100644 api/public/snapshot.js create mode 100644 src/lib/homeMeta.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3085ea0f6..d9e7b00c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2026-11-02 - Deploy focus race, Spanish home, fresher no-JS snapshot + +- Keep map selection focus from taking the search field when its callback arrives after the visitor has started filtering. This was the only failure in the Pages deploy at 06:23 UTC. +- Add `/es/` as the indexable Spanish home, with lang, title, description, reciprocal hreflang and a sitemap entry. Shared crossing URLs stay one address. +- Serve the no-JS home snapshot from the cached official fetch on Vercel, and from the data-deploy file on Pages, still labeled as a published snapshot. +- Describe DIGITO as an independent creative studio. No home city added. + ## 2026-11-01 — Correct map report focus and selected-lane summaries - Reveal the selected report's focused Back control clear of the native header and nested pane clipping; restore the original list/document position and focus on Escape, button and browser Back, with immediate reduced-motion-safe scrolling. diff --git a/DECISIONS.md b/DECISIONS.md index 49d1d841b..beea41ceb 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,13 @@ # DECISIONS +## 2026-11-02 · Spanish home is one indexable entry, not a translated URL tree + +Search engines need a Spanish document for the home page. `/es/` is that entry: its own lang, title, description, canonical and reciprocal hreflang with `/`. Shared crossing URLs stay one address with the in-page language preference. This does not open per-crossing or best-time Spanish duplicates while query evidence is still thin. + +## 2026-11-02 · The no-JS home snapshot is not frozen to the last site build + +Vercel publishes the app daily and on application changes, so HTML baked at build time can lag the live CBP fetch by hours. The no-JS home frame reads `/snapshot/`, which on Vercel is the existing cached CBP function rendered as HTML. GitHub Pages, the rollback host, keeps the snapshot file written by that data deploy. The frame is labeled as a published snapshot and keeps the retrieval time. It does not add a collector or rebuild the site on every data commit. + ## 2026-10-30 · The crossing map uses the existing BorderPulse design The map is an ordinary BorderPulse page. Its presentation inherits the existing shell, type, surfaces, controls and blueprint map language; a separate cinematic visual identity is no longer the target. Keep useful geography, explicit clocks, lane comparisons, honest replay and user-entered driving arithmetic. Entry points belong in the existing navigation/dashboard and relevant crossing pages. This correction awaits exact-head review before release. diff --git a/ROADMAP.md b/ROADMAP.md index 56dccf02e..be79eb311 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ Status legend: `[ ]` open · `[~]` in flight · `[x]` shipped · `[-]` won't do BorderPulse is an independently branded product built by DIGITO. Its own domain, codebase, operating record and product decisions stay here; DIGITO may describe the product as proof of its work without folding the crossing experience into -the trades-focused DIGITO site. +the DIGITO site. DIGITO is an independent creative studio: Make more of technology. The long-term opportunity is practical, bilingual cross-border decision support for people who cross for work, family and everyday life. Wait times are the @@ -223,10 +223,13 @@ Work top to bottom; restore product trust before expanding the URL inventory. - [x] **Programmatic SEO `/compare/:slugA-vs-:slugB`** — shipped 2026-05-09. 15 seed pairs prerendered + sitemap'd. Add more via the single `COMPARE_PAIRS` inventory in `src/lib/comparePairs.js`. -- [ ] **Hreflang for ES variants** — currently EN/ES share the same URL - and toggle in localStorage. For better ES SEO, consider - `/mejor-hora/:slug` parallel routes with proper `` pointing. +- [~] **Spanish home entry:** `/es/` is prepared as the indexable Spanish + homepage, with its own lang, title, description and reciprocal hreflang. + The in-page toggle still changes shared URLs such as `/`. Not published. +- [ ] **Hreflang for other ES variants:** crossing, best-time and compare + URLs still share one address and toggle in the browser. Parallel + routes such as `/mejor-hora/:slug` stay deferred while current + query evidence is thin. - [ ] **Refactor: extract `useTypicalDelta` hook** — duplicated in `BorderCrossingCard.jsx` and `Embed.jsx`. Pull into `src/lib/` once a third caller emerges. Low priority, no user impact. diff --git a/api/public/snapshot.js b/api/public/snapshot.js new file mode 100644 index 000000000..a91f80a2d --- /dev/null +++ b/api/public/snapshot.js @@ -0,0 +1,58 @@ +import { CBP_URL, createCbpPayload } from '../../scripts/fetch-cbp.mjs'; +import { snapshotDocument } from '../../scripts/lib/crossing-snapshot-html.mjs'; + +// HTML form of the cached official snapshot. The no-JS home frame uses it so +// the published table is not stuck on the last full site build. Same five +// minute CDN window as /api/public/crossings. +export const config = { maxDuration: 10 }; + +function send(res, status, body, cache = false) { + res.status(status); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.setHeader('Cache-Control', cache ? 'public, max-age=0, must-revalidate' : 'no-store'); + res.setHeader('X-Robots-Tag', 'noindex'); + if (cache) { + res.setHeader('Vercel-CDN-Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600'); + res.setHeader('CDN-Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600'); + } + res.end(body); +} + +function requestedLang(req) { + const fromQuery = req.query?.lang; + if (fromQuery === 'es' || fromQuery === 'en') return fromQuery; + try { + return new URL(req.url || '/', 'https://borderpulse.com').searchParams.get('lang') === 'es' ? 'es' : 'en'; + } catch { + return 'en'; + } +} + +export default async function handler(req, res) { + if (req.method !== 'GET') return send(res, 405, 'Method not allowed'); + const lang = requestedLang(req); + try { + const upstream = await fetch(CBP_URL, { + headers: { 'User-Agent': 'borderpulse.com/1.0 (snapshot-cache)' }, + signal: AbortSignal.timeout(8_000), + }); + if (!upstream.ok) throw new Error(`CBP source returned ${upstream.status}`); + const payload = createCbpPayload(await upstream.json()); + return send(res, 200, snapshotDocument(payload, { lang, framed: true }), true); + } catch (error) { + console.error('[public-snapshot]', error); + const host = req.headers?.host; + if (host) { + try { + const bundled = await fetch(`https://${host}/data/crossings.json`, { signal: AbortSignal.timeout(8_000) }); + if (bundled.ok) { + const payload = await bundled.json(); + return send(res, 200, snapshotDocument(payload, { lang, framed: true }), false); + } + } catch (fallbackError) { + console.error('[public-snapshot] bundled fallback', fallbackError); + } + } + return send(res, 502, 'Official CBP data is temporarily unavailable'); + } +} diff --git a/index.html b/index.html index 63db50694..8d711f7e8 100644 --- a/index.html +++ b/index.html @@ -29,10 +29,10 @@ - + +