- {es ? 'No es asesoría legal. Borrador actualizado: 27 de septiembre de 2026.' : 'Not legal advice. Draft updated: September 27, 2026.'}
+ {es ? 'No es asesoría legal. Borrador actualizado: 13 de octubre de 2026.' : 'Not legal advice. Draft updated: October 13, 2026.'}
);
From 349cd1a43c023400ff513cd2b896678179d59365 Mon Sep 17 00:00:00 2001
From: Sebastian Becerra <207095575+sbc1-code@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:41:47 -0700
Subject: [PATCH 2/7] Match cache-busted feed requests in local crossing
browser fixtures
---
scripts/check-local-crossings.mjs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/scripts/check-local-crossings.mjs b/scripts/check-local-crossings.mjs
index 301d70b0c..d5ad42f5b 100644
--- a/scripts/check-local-crossings.mjs
+++ b/scripts/check-local-crossings.mjs
@@ -8,13 +8,14 @@ export async function checkLocalCrossings(page, context, baseUrl) {
if (!['240202', '240201', '240104', '240204', '240801', '240401'].includes(crossing.port_number)) continue;
crossing.hours = '24 hrs/day';
crossing.port_status = 'Open';
+ crossing.current_wait_time = crossing.port_number === '240801' ? 0 : 30;
crossing.lanes = {
passenger_standard: { status: 'Delay', lanes_open: 1, delay_minutes: crossing.port_number === '240801' ? 0 : 30, update_time: '12:00 PM MDT' },
passenger_sentri: { status: 'No Delay', lanes_open: 1, delay_minutes: 0, update_time: '12:00 PM MDT' },
};
}
- const dataPattern = '**/data/crossings.json';
- const apiPattern = '**/api/public/crossings';
+ const dataPattern = '**/data/crossings.json*';
+ const apiPattern = '**/api/public/crossings*';
const feed = route => route.fulfill({ contentType: 'application/json', body: JSON.stringify(fixture) });
await page.route(dataPattern, feed);
await page.route(apiPattern, feed);
From 39d7de7f6a8f93befbb9ee2722b10f2121a9c915 Mon Sep 17 00:00:00 2001
From: Sebastian Becerra <207095575+sbc1-code@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:59:51 -0700
Subject: [PATCH 3/7] Prioritize local crossing decisions and deliver visible
report HTML
---
CHANGELOG.md | 18 ++++++
DECISIONS.md | 10 +++
ROADMAP.md | 8 +++
package.json | 2 +-
scripts/check-local-crossings.mjs | 7 +-
scripts/lib/crossing-snapshot-html.mjs | 41 ++++++++++++
scripts/prerender.mjs | 9 ++-
scripts/test-crossing-snapshot.mjs | 64 +++++++++++++++++++
.../dashboard/LocalCrossingComparison.jsx | 7 +-
src/lib/crossingNames.js | 25 ++++++++
src/pages/CrossingDetail.jsx | 8 +--
src/pages/Dashboard.jsx | 37 +++++++++--
12 files changed, 219 insertions(+), 17 deletions(-)
create mode 100644 scripts/lib/crossing-snapshot-html.mjs
create mode 100644 scripts/test-crossing-snapshot.mjs
create mode 100644 src/lib/crossingNames.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 877dfefdb..c924edb0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# CHANGELOG
+## 2026-10-13 — UX and crawlable-report QC (prepared)
+
+- Captured the live competitor and BorderPulse homepage in the cloud browser.
+ The observed first-screen gap is local decisions versus border-wide charts.
+ Moved the summary/chart behind an accessible disclosure, added quick local
+ choices, and made corridor selection a shareable `?area=` URL with Back
+ navigation and canonical homepage identity preserved.
+- Added familiar El Paso–Juárez crossing names and accent-insensitive aliases
+ for local search, headings and search titles without changing port identity.
+- Homepage and all 42 canonical crossing pages now deliver visible report HTML
+ before JavaScript. The original retrieval timestamp, raw lane clocks, missing
+ reports and closure state stay explicit. Other routes do not inherit this
+ homepage snapshot; no new page inventory or guessed rich-result schema.
+- Added snapshot/no-JavaScript assertions and expanded local-area browser
+ checks. No ranking, Core Web Vitals or acquisition improvement is claimed;
+ current Search Console reads were unavailable because the connected provider
+ reported an ended trial. Exact release evidence belongs to the pull request.
+
## 2026-10-13 — Local comparison and camera feature batch (prepared)
- Added nine named crossing corridors, remembered in browser storage and
diff --git a/DECISIONS.md b/DECISIONS.md
index 4685aeeca..20c117755 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -1,5 +1,15 @@
# DECISIONS
+## 2026-10-13 · Local decisions lead; published reports remain useful without JS
+
+Use familiar names as display/search aliases, preserving canonical CBP identity.
+Keep the border-wide chart available behind a disclosure and make local area
+selection shareable without creating duplicate indexable URLs. Deliver visible,
+timestamped HTML inside the React root on the homepage and crossing pages;
+createRoot replaces it with the refreshing app. Do not hide different report
+content for crawlers, imply a published snapshot is live, or expand thin SEO
+routes while current query/ranking evidence is unavailable.
+
## 2026-10-13 · Local comparisons use eligible lanes; cameras load on request
The owner requested competitor-inspired crossing features. Compare named
diff --git a/ROADMAP.md b/ROADMAP.md
index ff203e28f..2ef29ed98 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -21,6 +21,14 @@ cross-border experience; it is a product hypothesis, not a claim of demand.
### Now: earn trust in the free entry point
+- [~] QC the October 13 locality-first flow, shareable area URLs and familiar
+ crossing names. Deliver useful visible homepage/crossing snapshots without
+ JavaScript, preserving timestamps and canonical identity. Validation and
+ preview evidence belong to PR #132; not published yet.
+- [ ] Reconnect an existing authorized Search Console data path to measure
+ query/page impressions, clicks, CTR and area-to-crossing actions after release.
+ GSC Wizard reported an ended trial during QC; no subscription was started.
+
- [~] October 13 competitor feature batch: nine remembered local corridor
filters, comparisons of the selected eligible lane and click-loaded public
camera views at five Juárez–El Paso bridges. Prepared on the feature branch;
diff --git a/package.json b/package.json
index 7a0b93088..0d98883d4 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"test": "npm run test:ads && npm run test:writer && npm run test:refresh && npm run test:publish && npm run test:hours && npm run test:trust && npm run test:mdx && npm run test:metadata && npm run test:audit && npm run test:bundle && npm run test:routes",
- "test:metadata": "node --test scripts/test-faq-scope.mjs scripts/test-hreflang.mjs",
+ "test:metadata": "node --test scripts/test-faq-scope.mjs scripts/test-hreflang.mjs scripts/test-crossing-snapshot.mjs",
"test:ads": "node --test scripts/test-ad-slot-diagnostics.mjs",
"test:publish": "node --test scripts/test-publish-data.mjs scripts/test-public-crossings.mjs",
"publish:validate": "node scripts/validate-publish-data.mjs",
diff --git a/scripts/check-local-crossings.mjs b/scripts/check-local-crossings.mjs
index d5ad42f5b..4b721557f 100644
--- a/scripts/check-local-crossings.mjs
+++ b/scripts/check-local-crossings.mjs
@@ -30,6 +30,7 @@ export async function checkLocalCrossings(page, context, baseUrl) {
await page.reload();
const chooser = page.getByLabel(lang === 'es' ? 'Tu zona de cruce' : 'Your crossing area');
await chooser.selectOption('el-paso-juarez');
+ await page.waitForURL(url => url.searchParams.get('area') === 'el-paso-juarez');
const panel = page.getByRole('region', { name: lang === 'es' ? 'Comparación local' : 'Local comparison', exact: true });
await panel.waitFor();
await panel.getByText(/Santa Teresa/).first().waitFor();
@@ -55,7 +56,10 @@ export async function checkLocalCrossings(page, context, baseUrl) {
await chooser.selectOption('san-diego-tijuana');
await panel.getByRole('heading', { name: 'San Diego–Tijuana', exact: true }).waitFor();
await page.reload();
- assert.equal(await chooser.inputValue(), 'el-paso-juarez', 'blocked write must not claim persistence');
+ assert.equal(await chooser.inputValue(), 'san-diego-tijuana', 'the URL remains usable when browser storage is blocked');
+ assert.equal(await page.evaluate(() => localStorage.getItem('borderPulse_corridor')), 'el-paso-juarez', 'blocked storage does not claim a saved preference');
+ await chooser.selectOption('el-paso-juarez');
+ await panel.getByRole('heading', { name: 'El Paso–Juárez', exact: true }).waitFor();
const cameraLink = panel.getByRole('link', { name: lang === 'es' ? 'Ver cámaras' : 'View cameras', exact: true }).first();
await cameraLink.click();
await page.waitForURL(url => url.hash === '#cameras');
@@ -76,6 +80,7 @@ export async function checkLocalCrossings(page, context, baseUrl) {
await page.getByRole('button', { name: 'California', exact: true }).click();
assert.equal(await chooser.inputValue(), '');
assert.equal(await page.getByRole('region', { name: lang === 'es' ? 'Comparación local' : 'Local comparison', exact: true }).count(), 0);
+ assert.equal(new URL(page.url()).searchParams.has('area'), false);
}
} finally {
await page.unroute(dataPattern, feed);
diff --git a/scripts/lib/crossing-snapshot-html.mjs b/scripts/lib/crossing-snapshot-html.mjs
new file mode 100644
index 000000000..deab1a21c
--- /dev/null
+++ b/scripts/lib/crossing-snapshot-html.mjs
@@ -0,0 +1,41 @@
+import { availableLaneWait } from '../../src/lib/crossingAvailability.js';
+import { crossingDisplayName } from '../../src/lib/crossingNames.js';
+import { CORRIDORS } from '../../src/lib/corridors.js';
+import { camerasFor, CAMERA_SOURCE } from '../../src/lib/cameras.js';
+
+const esc = s => String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+
+// Visible, useful HTML for every visitor before the app loads, including when
+// JavaScript is unavailable. createRoot replaces it with the updating app.
+// The original snapshot timestamp is never rewritten into a "live" promise.
+export function snapshotHtml(crossings, portToSlug, fetchedAt, single = false) {
+ const heading = single
+ ? `${crossingDisplayName(crossings[0])} border wait times`
+ : 'US–Mexico border wait times';
+ const areas = single ? '' : `<\/div>/;
+ if (!target.test(html)) throw new Error('Expected one empty React root for snapshot injection');
+ return html.replace(target, `
${content}
`);
+}
diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs
index a8040f37b..598becef2 100644
--- a/scripts/prerender.mjs
+++ b/scripts/prerender.mjs
@@ -6,6 +6,9 @@ import { mdxBodyToHtml } from './lib/mdx-to-html.mjs';
import { hasPedestrianLane } from '../src/lib/crossingAvailability.js';
import { COMPARE_PAIRS } from '../src/lib/comparePairs.js';
import { pageHreflangs, blogHreflangs } from '../src/lib/hreflang.js';
+import { snapshotHtml, injectSnapshot } from './lib/crossing-snapshot-html.mjs';
+import { crossingSearchTitle } from '../src/lib/crossingNames.js';
+import { camerasFor } from '../src/lib/cameras.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
@@ -43,7 +46,7 @@ function faqItems(crossing, aggregate) {
function renderCrossingHead(crossing, slug, aggregate) {
// Title pattern matches what wins these SERPs: keyword-first, "today"/live
// freshness signal, current year. Brand goes last where truncation is cheap.
- const title = `${crossing.name} Border Wait Time Today: Live CBP Data (2026) | Border Pulse`;
+ const title = crossingSearchTitle(crossing, 'en', camerasFor(crossing.port_number).length > 0);
const desc = `Live ${crossing.name} border wait time from U.S. Customs and Border Protection, plus 30-day hour-by-hour patterns, port hours, and the best time to cross today.`;
const canonical = `${BASE}/crossing/${slug}/`;
const ogImage = `${BASE}/og/${slug}.png`;
@@ -489,7 +492,7 @@ async function main() {
const authors = readAuthors();
const indexWithLinks = injectCrawlableLinks(originalIndex, crossings, portToSlug, posts);
- fs.writeFileSync(indexPath, indexWithLinks);
+ fs.writeFileSync(indexPath, injectSnapshot(indexWithLinks, snapshotHtml(crossings, portToSlug, doc.fetched_at)));
const aggregatesDir = path.resolve(root, 'public/data/aggregates');
let crossingCount = 0;
@@ -504,7 +507,7 @@ async function main() {
} catch {}
}
const head = renderCrossingHead(c, slug, aggregate);
- const html = rewriteIndex(indexWithLinks, head);
+ const html = injectSnapshot(rewriteIndex(indexWithLinks, head), snapshotHtml([c], portToSlug, doc.fetched_at, true));
const outDir = path.resolve(distDir, 'crossing', slug);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.resolve(outDir, 'index.html'), html);
diff --git a/scripts/test-crossing-snapshot.mjs b/scripts/test-crossing-snapshot.mjs
new file mode 100644
index 000000000..58bc12f8b
--- /dev/null
+++ b/scripts/test-crossing-snapshot.mjs
@@ -0,0 +1,64 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import { snapshotHtml, injectSnapshot } from './lib/crossing-snapshot-html.mjs';
+import { crossingDisplayName, crossingSearchTerms, crossingSearchTitle } from '../src/lib/crossingNames.js';
+
+const crossing = { port_number: '240202', name: 'El Paso - Paso Del Norte (PDN)',
+ hours: '24 hrs/day', port_status: 'Open', lanes: { passenger_standard: { status: 'No Delay', lanes_open: 1, delay_minutes: 0, update_time: 'At 6:00 pm MDT' } } };
+const slugs = { '240202': 'el-paso-paso-del-norte-pdn' };
+const stamp = '2026-10-13T20:00:00.000Z';
+
+test('visible snapshot preserves zero, report clock, original retrieval and useful navigation', () => {
+ const html = snapshotHtml([crossing], slugs, stamp);
+ assert.match(html, /
US–Mexico border wait times<\/h1>/);
+ assert.match(html, /0 min/);
+ assert.match(html, /At 6:00 pm MDT/);
+ assert.match(html, new RegExp(stamp.replaceAll('.', '\\.')));
+ assert.match(html, /published snapshot, not a live feed/);
+ assert.match(html, /href="\/\?area=el-paso-juarez"/);
+ assert.match(html, /href="\/crossing\/el-paso-paso-del-norte-pdn\/"/);
+ assert.doesNotMatch(html, /aria-hidden|inert|left:-9999|display:none/);
+});
+test('closure overrides retained numbers; missing reports are not zero', () => {
+ assert.match(snapshotHtml([{ ...crossing, port_status: 'Closed' }], slugs, stamp), /Closed in this snapshot/);
+ assert.doesNotMatch(snapshotHtml([{ ...crossing, lanes: {} }], slugs, stamp), /0 min/);
+ assert.match(snapshotHtml([{ ...crossing, lanes: {} }], slugs, stamp), /No standard-lane report/);
+});
+test('single-crossing snapshot has local name and an official camera path', () => {
+ const html = snapshotHtml([crossing], slugs, stamp, true);
+ assert.match(html, /Santa Fe · Paso del Norte border wait times/);
+ assert.match(html, /puentesfronterizos.gob.mx\/camaras-en-vivo.php/);
+ assert.doesNotMatch(html, /Choose a crossing area/);
+});
+test('feed values are escaped and cannot inject active HTML', () => {
+ const html = snapshotHtml([{ ...crossing, name: '', port_number: 'unknown', hours: '
' }], { unknown: 'safe' }, stamp, true);
+ assert.doesNotMatch(html, /', port_number: 'unknown', hours: '
' }], { unknown: 'safe' }, stamp, true);
assert.doesNotMatch(html, /