diff --git a/CHANGELOG.md b/CHANGELOG.md
index 962ad5bb5..b58d47ef1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# CHANGELOG
+## Unreleased — route-specific language alternates
+
+- Generated crossing, best-time, comparison and other single-URL pages replace
+ inherited homepage hreflangs with their own canonical URL. Existing published
+ EN/ES blog, methodology and privacy pairs remain reciprocal; browser language
+ preferences do not create translated URLs.
+- SPA metadata updates replace stale language alternates and restore homepage
+ alternates on reset, preserving canonical URLs and non-language alternate links.
+- Added all-generated-page and desktop/mobile navigation checks, including
+ language switching, translated article entry and Back. Providers remain blocked
+ or mocked in browser tests. Sitemap inventory is a separate follow-up; release
+ and production verification evidence belongs to the scoped pull request.
+
## 2026-10-12 — CBP report times and fetch labels
- Dashboard and crossing cards distinguish snapshot retrieval from the official
diff --git a/ROADMAP.md b/ROADMAP.md
index 76b825964..b19090ce4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -21,6 +21,12 @@ cross-border experience; it is a product hypothesis, not a claim of demand.
### Now: earn trust in the free entry point
+- [~] Correct route hreflangs in generated HTML and SPA navigation while
+ preserving actual EN/ES pairs. Publication requires independent review and
+ complete release checks; exact production verification belongs to the PR.
+- [ ] Investigate dynamic walk-or-drive sitemap inventory separately from the
+ language-alternate correction; do not expand routes in that scoped change.
+
- [x] Separate CBP lane report times from snapshot retrieval on dashboard and
crossing detail cards. Preserve raw report text and unknown dates; release
verification belongs to the scoped pull request.
diff --git a/package.json b/package.json
index 706d9514f..ca1023408 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",
+ "test:metadata": "node --test scripts/test-faq-scope.mjs scripts/test-hreflang.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-hreflang.mjs b/scripts/check-hreflang.mjs
new file mode 100644
index 000000000..89094833f
--- /dev/null
+++ b/scripts/check-hreflang.mjs
@@ -0,0 +1,119 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const origin = 'https://borderpulse.com';
+const enArticle = '/blog/san-ysidro-vs-otay-mesa/';
+const esArticle = '/blog/san-ysidro-vs-otay-cual-es-mas-rapida/';
+const single = (route, lang = 'en') => [{ lang, href: origin + route }, { lang: 'x-default', href: origin + route }];
+const pair = (en, es) => [{ lang: 'en', href: origin + en }, { lang: 'es', href: origin + es }, { lang: 'x-default', href: origin + en }];
+const attrs = (tag) => Object.fromEntries([...tag.matchAll(/([\w:-]+)\s*=\s*(["'])(.*?)\2/gs)].map((m) => [m[1].toLowerCase(), m[3]]));
+const links = (html) => [...html.matchAll(/]*>/gi)].map((m) => attrs(m[0]));
+const alternates = (html) => links(html).filter((a) => a.rel === 'alternate' && a.hreflang).map((a) => ({ lang: a.hreflang, href: a.href }));
+
+export function checkBuiltHreflangs(dist = path.join(root, 'dist')) {
+ const posts = JSON.parse(fs.readFileSync(path.join(dist, 'data/blog/index.json'), 'utf8')).posts;
+ const expected = new Map([
+ ['/methodology/', pair('/methodology/', '/metodologia/')],
+ ['/metodologia/', pair('/methodology/', '/metodologia/')],
+ ['/privacy/', pair('/privacy/', '/privacidad/')],
+ ['/privacidad/', pair('/privacy/', '/privacidad/')],
+ ]);
+ for (const post of posts) {
+ const twins = post.translationKey ? posts.filter((p) => p.translationKey === post.translationKey) : [];
+ const en = twins.find((p) => p.lang === 'en');
+ const es = twins.find((p) => p.lang === 'es');
+ const route = `/blog/${post.slug}/`;
+ expected.set(route, en && es ? pair(`/blog/${en.slug}/`, `/blog/${es.slug}/`) : single(route, post.lang || 'en'));
+ }
+ let count = 0;
+ const inspect = (dir) => {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const file = path.join(dir, entry.name);
+ if (entry.isDirectory()) { inspect(file); continue; }
+ if (entry.name !== 'index.html') continue;
+ const html = fs.readFileSync(file, 'utf8');
+ if (/ a.rel === 'canonical');
+ assert.deepEqual(canonical.map((a) => a.href), [origin + canonicalRoute], `${route}: canonical`);
+ const actual = alternates(html);
+ assert.deepEqual(actual, expected.get(route) || single(canonicalRoute), `${route}: route alternates`);
+ for (const alternate of actual) {
+ const target = new URL(alternate.href);
+ assert.equal(target.origin, origin);
+ const targetHtml = fs.readFileSync(path.join(dist, target.pathname, 'index.html'), 'utf8');
+ assert.ok(alternates(targetHtml).some((a) => a.href === origin + canonicalRoute), `${route}: missing reciprocal alternate on ${target.pathname}`);
+ }
+ count++;
+ }
+ };
+ inspect(dist);
+ console.log(`[hreflang] PASS: ${count} generated pages; exact route alternates, canonicals and reciprocal targets`);
+}
+
+export async function checkHreflangNavigation(page, baseUrl) {
+ await page.evaluate(() => {
+ localStorage.setItem('borderPulse_language', 'en');
+ localStorage.setItem('borderPulse_region', 'ALL');
+ });
+ await page.goto(new URL('/', baseUrl).href);
+ await page.getByRole('searchbox', { name: 'Search crossings by name or city', exact: true }).waitFor();
+ const marker = await page.evaluate(() => (window.hreflangDocument = crypto.randomUUID()));
+ const inspect = async (route, expected = single(route)) => {
+ await page.waitForURL((url) => url.pathname === route);
+ await page.waitForFunction(({ canonical, expected }) => {
+ const actual = Array.from(document.querySelectorAll('link[rel="alternate"][hreflang]'), (link) => ({ lang: link.hreflang, href: link.href }));
+ return document.querySelector('link[rel="canonical"]')?.href === canonical && JSON.stringify(actual) === JSON.stringify(expected);
+ }, { canonical: origin + route, expected });
+ assert.equal(await page.evaluate(() => window.hreflangDocument), marker, 'must stay in same SPA document');
+ assert.deepEqual(await page.locator('link[rel="canonical"]').evaluateAll((nodes) => nodes.map((node) => node.href)), [origin + route]);
+ };
+ const click = async (route) => page.locator(`#root a[href="${route}"]:visible`).first().click();
+ await inspect('/');
+ await page.getByRole('button', { name: 'Cambiar a español', exact: true }).filter({ visible: true }).click();
+ await page.waitForFunction(() => document.documentElement.lang === 'es');
+ await inspect('/'); // Shared URL; no invented Spanish route.
+ await page.getByRole('button', { name: 'Switch to English', exact: true }).filter({ visible: true }).click();
+ await page.getByRole('searchbox', { name: 'Search crossings by name or city', exact: true }).fill('San Ysidro');
+ await click('/crossing/san-ysidro/');
+ await inspect('/crossing/san-ysidro/');
+ await page.getByRole('button', { name: 'Cambiar a español', exact: true }).filter({ visible: true }).click();
+ await page.waitForFunction(() => document.documentElement.lang === 'es');
+ await inspect('/crossing/san-ysidro/');
+ await page.getByRole('button', { name: 'Switch to English', exact: true }).filter({ visible: true }).click();
+ await click('/best-time/san-ysidro/');
+ await inspect('/best-time/san-ysidro/');
+ await page.goBack();
+ await inspect('/crossing/san-ysidro/');
+ await click('/compare/san-ysidro-vs-otay-mesa/');
+ await inspect('/compare/san-ysidro-vs-otay-mesa/');
+ const menu = page.getByRole('button', { name: 'Open menu', exact: true });
+ if (await menu.isVisible()) await menu.click();
+ await page.getByRole('navigation', { name: 'Main navigation', exact: true }).getByRole('link', { name: 'Blog', exact: true }).click();
+ await page.getByRole('heading', { level: 1, name: 'Border Pulse Blog', exact: true }).waitFor();
+ await inspect('/blog/');
+ await click(enArticle);
+ await inspect(enArticle, pair(enArticle, esArticle));
+ await page.locator('#root article').getByRole('link', { name: 'Español', exact: true }).click();
+ await inspect(esArticle, pair(enArticle, esArticle));
+ await page.goBack();
+ await inspect(enArticle, pair(enArticle, esArticle));
+ await click('/');
+ await inspect('/');
+ // A fresh translated entry also has to clear its prerendered pair on exit.
+ await page.goto(new URL(esArticle, baseUrl).href);
+ await page.locator('#root article[lang="es"]').waitFor();
+ const entryExpected = pair(enArticle, esArticle);
+ assert.deepEqual(await page.locator('link[rel="alternate"][hreflang]').evaluateAll((nodes) => nodes.map((node) => ({ lang: node.hreflang, href: node.href }))), entryExpected);
+ await click('/');
+ await page.getByRole('searchbox', { name: 'Search crossings by name or city', exact: true }).waitFor();
+ assert.deepEqual(await page.locator('link[rel="alternate"][hreflang]').evaluateAll((nodes) => nodes.map((node) => ({ lang: node.hreflang, href: node.href }))), single('/'));
+ console.log('[hreflang] PASS: SPA home/language switch → crossing → best-time → Back → compare → blog → EN/ES article → Back → home');
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) checkBuiltHreflangs();
diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs
index 333813fbb..a8040f37b 100644
--- a/scripts/prerender.mjs
+++ b/scripts/prerender.mjs
@@ -5,6 +5,7 @@ import matter from 'gray-matter';
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';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
@@ -135,19 +136,7 @@ function renderBlogPostHead(post, author, allPosts) {
const datePublished = `${post.date}T12:00:00Z`;
const dateModified = `${post.updated || post.date}T12:00:00Z`;
- // Build hreflang pairs if this post has a translation twin.
- const hreflangs = [];
- if (post.translationKey && allPosts) {
- const en = allPosts.find(
- (p) => p.translationKey === post.translationKey && p.lang === 'en',
- );
- const es = allPosts.find(
- (p) => p.translationKey === post.translationKey && p.lang === 'es',
- );
- if (en) hreflangs.push({ lang: 'en', href: `${BASE}/blog/${en.slug}/` });
- if (es) hreflangs.push({ lang: 'es', href: `${BASE}/blog/${es.slug}/` });
- if (en) hreflangs.push({ lang: 'x-default', href: `${BASE}/blog/${en.slug}/` });
- }
+ const hreflangs = blogHreflangs(post, allPosts || []);
const blogPosting = {
'@context': 'https://schema.org',
@@ -290,15 +279,12 @@ function rewriteIndex(indexHtml, head) {
}
}
- // Replace any existing hreflang tags with post-specific ones (the shell has
- // homepage-default hreflangs from index.html that we must override per page).
- if (head.hreflangs && head.hreflangs.length) {
- html = html.replace(/\s*]*>/gi, '');
- const hreflangTags = head.hreflangs
- .map((h) => ` `)
- .join('\n');
- html = html.replace(/<\/head>/i, `${hreflangTags}\n `);
- }
+ // Always replace shell alternates, including routes without translated URLs.
+ html = html.replace(/\s*]*\brel=["']alternate["'])(?=[^>]*\bhreflang=)[^>]*>/gi, '');
+ const hreflangTags = (head.hreflangs ?? pageHreflangs(head.canonical, head.lang || 'en'))
+ .map((h) => ` `)
+ .join('\n');
+ html = html.replace(/<\/head>/i, `${hreflangTags}\n `);
// Escape any literal "" can't break out of the
diff --git a/scripts/smoke-routes.mjs b/scripts/smoke-routes.mjs
index 0384d1536..412b6a67c 100644
--- a/scripts/smoke-routes.mjs
+++ b/scripts/smoke-routes.mjs
@@ -9,6 +9,7 @@ import { checkComparisonGuide } from './check-comparison-guide.mjs';
import { checkReportTimes } from './check-report-times.mjs';
import { checkAdSlotDiagnostics } from './check-ad-slot-diagnostics.mjs';
import { assertNoGlobalFaq, checkBuiltFaqScope, checkFaqNavigation } from './check-faq-scope.mjs';
+import { checkBuiltHreflangs, checkHreflangNavigation } from './check-hreflang.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const distDir = path.resolve(root, 'dist');
@@ -339,6 +340,7 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
activeErrors = [];
try {
await checkFaqNavigation(page, baseUrl);
+ await checkHreflangNavigation(page, baseUrl);
if (activeErrors.length) throw new Error(activeErrors.join(' | '));
} catch (error) {
failures.push(`${profile.name} FAQ scope navigation: ${error.stack || error.message}`);
@@ -350,6 +352,7 @@ async function runProfile(browser, profile, manifest, expectedCrossingCount) {
async function main() {
checkBuiltFaqScope(distDir);
+ checkBuiltHreflangs(distDir);
const manifest = loadRouteManifest();
if (process.argv.includes('--growth-only')) {
manifest.canonicals = [];
diff --git a/scripts/test-hreflang.mjs b/scripts/test-hreflang.mjs
new file mode 100644
index 000000000..55a2b0f52
--- /dev/null
+++ b/scripts/test-hreflang.mjs
@@ -0,0 +1,66 @@
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { pageHreflangs, blogHreflangs } from '../src/lib/hreflang.js';
+
+const base = 'https://borderpulse.com';
+test('single URL routes point to themselves without inventing translations', () => {
+ for (const route of ['/', '/crossing/san-ysidro/', '/best-time/san-ysidro/', '/compare/san-ysidro-vs-otay-mesa/']) {
+ assert.deepEqual(pageHreflangs(base + route), [{ lang: 'en', href: base + route }, { lang: 'x-default', href: base + route }]);
+ }
+});
+test('only published EN/ES blog twins create reciprocal alternates', () => {
+ const en = { slug: 'english', lang: 'en', translationKey: 'guide' };
+ const es = { slug: 'espanol', lang: 'es', translationKey: 'guide' };
+ const expected = [{ lang: 'en', href: base + '/blog/english/' }, { lang: 'es', href: base + '/blog/espanol/' }, { lang: 'x-default', href: base + '/blog/english/' }];
+ assert.deepEqual(blogHreflangs(en, [en, es]), expected);
+ assert.deepEqual(blogHreflangs(es, [en, es]), expected);
+ for (const post of [en, es, { slug: 'unpaired', lang: 'es' }]) {
+ assert.deepEqual(blogHreflangs(post, [post]), [{ lang: post.lang, href: `${base}/blog/${post.slug}/` }, { lang: 'x-default', href: `${base}/blog/${post.slug}/` }]);
+ }
+});
+test('existing methodology and privacy twins remain reciprocal', () => {
+ for (const [en, es] of [['methodology', 'metodologia'], ['privacy', 'privacidad']]) {
+ assert.deepEqual(pageHreflangs(`${base}/${en}/`), pageHreflangs(`${base}/${es}/`, 'es'));
+ assert.equal(pageHreflangs(`${base}/${en}/`).length, 3);
+ }
+});
+
+test('SPA metadata replaces stale/duplicate hreflangs and preserves other alternate links', async () => {
+ const { updatePageMeta, resetPageMeta } = await import('../src/lib/seo.js');
+ const nodes = [];
+ const create = (tag, attrs = {}) => {
+ const node = {
+ tag, attrs: { ...attrs },
+ setAttribute(name, value) { this.attrs[name] = value; },
+ remove() { nodes.splice(nodes.indexOf(this), 1); },
+ };
+ return node;
+ };
+ const matches = (node, selector) => node.tag === selector.split('[')[0] &&
+ [...selector.matchAll(/\[([^=\]]+)(?:="([^"]*)")?\]/g)].every(([, key, value]) =>
+ value === undefined ? key in node.attrs : node.attrs[key] === value);
+ const previous = globalThis.document;
+ globalThis.document = {
+ documentElement: { lang: 'en' },
+ head: { appendChild(node) { nodes.push(node); } },
+ createElement: create,
+ querySelector: (selector) => nodes.find((node) => matches(node, selector)),
+ querySelectorAll: (selector) => nodes.filter((node) => matches(node, selector)),
+ };
+ const rss = create('link', { rel: 'alternate', type: 'application/rss+xml', href: base + '/rss.xml' });
+ nodes.push(rss, create('link', { rel: 'alternate', hreflang: 'en', href: base + '/' }), create('link', { rel: 'alternate', hreflang: 'en', href: base + '/' }));
+ const actual = () => nodes.filter((node) => 'hreflang' in node.attrs).map((node) => ({ lang: node.attrs.hreflang, href: node.attrs.href }));
+ try {
+ const canonical = base + '/crossing/san-ysidro/';
+ updatePageMeta({ canonical });
+ assert.deepEqual(actual(), pageHreflangs(canonical));
+ assert.ok(nodes.includes(rss));
+ updatePageMeta({ title: 'Partial update' });
+ assert.deepEqual(actual(), pageHreflangs(canonical));
+ updatePageMeta({ canonical: base + '/blog/example/', hreflangs: [] });
+ assert.deepEqual(actual(), []);
+ resetPageMeta();
+ assert.deepEqual(actual(), pageHreflangs(base + '/'));
+ assert.ok(nodes.includes(rss));
+ } finally { globalThis.document = previous; }
+});
diff --git a/src/lib/hreflang.js b/src/lib/hreflang.js
new file mode 100644
index 000000000..178d7453c
--- /dev/null
+++ b/src/lib/hreflang.js
@@ -0,0 +1,30 @@
+const BASE = 'https://borderpulse.com';
+
+// Only these routes have separately crawlable translated pages. A language
+// preference on a shared URL does not create a Spanish alternate URL.
+export function pageHreflangs(canonical, lang = 'en') {
+ for (const [en, es] of [['methodology', 'metodologia'], ['privacy', 'privacidad']]) {
+ const enUrl = `${BASE}/${en}/`;
+ const esUrl = `${BASE}/${es}/`;
+ if (canonical === enUrl || canonical === esUrl) {
+ return [{ lang: 'en', href: enUrl }, { lang: 'es', href: esUrl }, { lang: 'x-default', href: enUrl }];
+ }
+ }
+ return [{ lang, href: canonical }, { lang: 'x-default', href: canonical }];
+}
+
+export function blogHreflangs(post, posts) {
+ const canonical = `${BASE}/blog/${post.slug}/`;
+ if (post.translationKey) {
+ const en = posts.find((p) => p.translationKey === post.translationKey && p.lang === 'en');
+ const es = posts.find((p) => p.translationKey === post.translationKey && p.lang === 'es');
+ if (en && es) {
+ return [
+ { lang: 'en', href: `${BASE}/blog/${en.slug}/` },
+ { lang: 'es', href: `${BASE}/blog/${es.slug}/` },
+ { lang: 'x-default', href: `${BASE}/blog/${en.slug}/` },
+ ];
+ }
+ }
+ return pageHreflangs(canonical, post.lang || 'en');
+}
diff --git a/src/lib/seo.js b/src/lib/seo.js
index 75a34a338..1f4c9479b 100644
--- a/src/lib/seo.js
+++ b/src/lib/seo.js
@@ -2,6 +2,18 @@
* seo.js — lightweight helpers to set tags from SPA routes.
* No extra deps; manipulates head directly.
*/
+import { pageHreflangs } from './hreflang.js';
+
+function setHreflangs(hreflangs) {
+ document.querySelectorAll('link[rel="alternate"][hreflang]').forEach((link) => link.remove());
+ for (const { lang, href } of hreflangs) {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'alternate');
+ link.setAttribute('hreflang', lang);
+ link.setAttribute('href', href);
+ document.head.appendChild(link);
+ }
+}
function setMeta(name, content, attr = 'name') {
if (!content) return;
@@ -16,9 +28,10 @@ function setMeta(name, content, attr = 'name') {
/**
* Update page-level SEO tags. Call from useEffect in each page component.
- * Pass null/undefined for any field to skip it.
+ * Pass null/undefined for a field to skip it. A canonical update also replaces
+ * language alternates; explicit hreflangs (including []) override the defaults.
*/
-export function updatePageMeta({ title, description, ogTitle, ogDescription, ogImage, ogUrl, canonical, lang }) {
+export function updatePageMeta({ title, description, ogTitle, ogDescription, ogImage, ogUrl, canonical, lang, hreflangs }) {
if (lang) document.documentElement.lang = lang;
if (title) document.title = title;
if (description) setMeta('description', description);
@@ -34,6 +47,7 @@ export function updatePageMeta({ title, description, ogTitle, ogDescription, ogI
document.head.appendChild(link);
}
link.setAttribute('href', canonical);
+ setHreflangs(hreflangs ?? pageHreflangs(canonical, lang || 'en'));
}
}
@@ -41,6 +55,7 @@ export function updatePageMeta({ title, description, ogTitle, ogDescription, ogI
* Reset meta tags to defaults (call on unmount or route change).
*/
export function resetPageMeta() {
+ setHreflangs(pageHreflangs('https://borderpulse.com/'));
document.documentElement.lang = 'en';
document.title = 'Border Pulse | Real-Time US-Mexico Border Crossing Wait Times';
setMeta('description', 'Live wait times at US-Mexico border crossings. Official CBP data, refreshed regularly. Bilingual EN/ES.');
diff --git a/src/pages/BlogPost.jsx b/src/pages/BlogPost.jsx
index bd6faa516..41e5e4453 100644
--- a/src/pages/BlogPost.jsx
+++ b/src/pages/BlogPost.jsx
@@ -8,6 +8,7 @@ import { mdxComponents } from '@/components/blog/MdxComponents';
import { LangContext } from '@/lib/LangContext';
import { updatePageMeta, resetPageMeta } from '@/lib/seo';
import { track } from '@/lib/analytics';
+import { blogHreflangs } from '@/lib/hreflang';
const STRINGS = {
en: {
@@ -74,7 +75,10 @@ export default function BlogPost() {
const url = `https://borderpulse.com/blog/${slug}/`;
const imagePath = fm.ogImage || fm.hero;
const ogImg = imagePath ? `https://borderpulse.com${imagePath}` : undefined;
- updatePageMeta({ title, description: desc, ogTitle: title, ogDescription: desc, ogUrl: url, ogImage: ogImg, canonical: url, lang: fm.lang || 'en' });
+ const publishedPosts = [...listPosts({ lang: 'en' }), ...listPosts({ lang: 'es' })]
+ .map((p) => ({ ...p.frontmatter, slug: p.slug }));
+ const hreflangs = blogHreflangs({ ...fm, slug }, publishedPosts);
+ updatePageMeta({ title, description: desc, ogTitle: title, ogDescription: desc, ogUrl: url, ogImage: ogImg, canonical: url, lang: fm.lang || 'en', hreflangs });
return () => resetPageMeta();
}, [post, slug]);