diff --git a/CHANGELOG.md b/CHANGELOG.md
index f789decc..162d25b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# CHANGELOG
+## 2026-11-11 - Sponsor card stands out; self-serve "Feature your business" page
+
+- Sponsor card redesign: green-tinted card with an accent edge, larger business name, the services the business lists (as chips), an optional business offer with an end date (hidden after it), and WhatsApp as the main filled button. Still labeled Sponsored/Patrocinado with the "has not evaluated" line, still below the answer and never alongside an ad.
+- First sponsor now shows as "Dr. J Constandse" (the name on his site and signage) with implants, veneers and smile design, crowns and full oral rehabilitation, per his site, and his 25% offer through Nov 30, attributed to the clinic.
+- New bilingual page `/feature-your-business/` and `/anunciate/` (hreflang-paired, in the sitemap, linked from the footer and each sponsor card). Five cities (Ciudad Juárez, Tijuana, Los Algodones, Nogales, Nuevo Progreso) by six categories (dentist, pharmacy, medical clinic, food, auto, salon and beauty). Open spots link to a $99/month Stripe Payment Link with `client_reference_id` set to the slot; taken spots show as taken. Clicks log `sponsor-checkout` with the slot id only.
+- Sponsors are now set by city and category in `src/lib/sponsors.js`. A city's crossing pages show one sponsor; with several in a city, they rotate by UTC calendar day.
+- Checks: full `npm test` passes after a prerendered build. Entry JS 62.05 KB gzip (budget 62.25), CSS 19.92 KB (budget 20). Headless Chrome 390px: card on PDN and the Juárez dentist guide; feature page renders EN and ES.
+
## 2026-11-10 - Sponsor WhatsApp message names Border Pulse
- The sponsor card's WhatsApp button now opens a prefilled message saying the patient found the clinic on Border Pulse (EN/ES). The visitor can edit it before sending. This lets the business see the source in its own inbox, which analytics cannot show.
diff --git a/DECISIONS.md b/DECISIONS.md
index 48f5864b..9a98c4d4 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -585,3 +585,7 @@ A page carries a sponsor card or a network ad, never both, and always below the
## 2026-11-10 - Sponsor traffic is measured in three steps, and only those are claimed
Sponsor reporting counts what the existing Umami property records: clicks on the sponsor card (`sponsor-click`), visits to the sponsor's site that arrive with `utm_source=borderpulse`, and taps on that site's contact buttons by source. The WhatsApp button opens a prefilled, editable message naming Border Pulse so the business can see the source in its own inbox. Booked appointments and revenue are outside what is measured, so reports do not state them. The sponsor's site reports into the same Umami property (the plan allows one site); filter by hostname when reading Border Pulse numbers.
+
+## 2026-11-11 - Self-serve sponsor listings before 2 to 3 paying sponsors
+
+The roadmap said to wait for 2 to 3 paying sponsors before a self-serve page. Sebastian approved building it now so businesses can buy without a call. A listing is one business per category per city, $99/month through a Stripe Payment Link. Every paid listing is reviewed before it is added to `src/lib/sponsors.js`, and refunded if not approved. Rotation is by day, not per view, so prerendered pages and the client agree. Business offers are shown attributed to the business and with an end date.
diff --git a/ROADMAP.md b/ROADMAP.md
index b80bc791..e1ef69f3 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -344,7 +344,8 @@ Snapshot of features live on borderpulse.com today.
- Pharmacy sponsor for the medication guides (highest-traffic slot). Not being pitched yet; needs a trusted contact and a vetting pass first.
- City guides as sponsor inventory: Tijuana, Los Algodones, Nuevo Progreso, Nogales. One featured business per category section.
-- Self-serve "feature your business" page, after 2 to 3 paying sponsors and a real click number to show.
+- [x] Self-serve "feature your business" page (2026-11-11, shipped early at Sebastian's call).
+- Next: a city directory page per city listing every active sponsor by category (Phase 3), then the return-trip guide (Phase 4).
- Swap the first sponsor's link to its own domain once it has one (currently constandse-dental.vercel.app).
### Shipped 2026-11-10
diff --git a/scripts/build-sitemap.mjs b/scripts/build-sitemap.mjs
index ff60ca7b..27de5bf5 100644
--- a/scripts/build-sitemap.mjs
+++ b/scripts/build-sitemap.mjs
@@ -61,6 +61,11 @@ async function main() {
{ lang: 'es', href: `${BASE}/privacidad/` },
{ lang: 'x-default', href: `${BASE}/privacy/` },
];
+ const featureAlternates = [
+ { lang: 'en', href: `${BASE}/feature-your-business/` },
+ { lang: 'es', href: `${BASE}/anunciate/` },
+ { lang: 'x-default', href: `${BASE}/feature-your-business/` },
+ ];
const urls = [
{ loc: `${BASE}/`, changefreq: 'hourly', priority: '1.0', lastmod: today, alternates: homeHreflangs() },
{ loc: `${BASE}/es/`, changefreq: 'hourly', priority: '0.9', lastmod: today, alternates: homeHreflangs() },
@@ -70,6 +75,8 @@ async function main() {
{ loc: `${BASE}/privacidad/`, changefreq: 'yearly', priority: '0.2', lastmod: today, alternates: privacyAlternates },
{ loc: `${BASE}/methodology/`, changefreq: 'monthly', priority: '0.5', lastmod: today, alternates: methodologyAlternates },
{ loc: `${BASE}/metodologia/`, changefreq: 'monthly', priority: '0.5', lastmod: today, alternates: methodologyAlternates },
+ { loc: `${BASE}/feature-your-business/`, changefreq: 'monthly', priority: '0.4', lastmod: today, alternates: featureAlternates },
+ { loc: `${BASE}/anunciate/`, changefreq: 'monthly', priority: '0.4', lastmod: today, alternates: featureAlternates },
];
for (const c of crossings) {
const slug = portToSlug[c.port_number];
diff --git a/scripts/check-document-language.mjs b/scripts/check-document-language.mjs
index ad1dc243..4c1e8035 100644
--- a/scripts/check-document-language.mjs
+++ b/scripts/check-document-language.mjs
@@ -106,7 +106,7 @@ export async function checkDocumentLanguage(page, context, baseUrl) {
await click('/');
await inspect('/', 'es');
// Explicit EN/ES routes must not inherit a contradictory UI preference.
- for (const [route, lang] of [['/methodology/', 'en'], ['/metodologia/', 'es'], ['/privacy/', 'en'], ['/privacidad/', 'es']]) {
+ for (const [route, lang] of [['/methodology/', 'en'], ['/metodologia/', 'es'], ['/privacy/', 'en'], ['/privacidad/', 'es'], ['/feature-your-business/', 'en'], ['/anunciate/', 'es']]) {
await page.goto(new URL(route, baseUrl).href);
await inspect(route, lang);
await switchTo(lang === 'en' ? 'es' : 'en');
diff --git a/scripts/check-hreflang.mjs b/scripts/check-hreflang.mjs
index 23c9279a..720bb246 100644
--- a/scripts/check-hreflang.mjs
+++ b/scripts/check-hreflang.mjs
@@ -24,6 +24,8 @@ export function checkBuiltHreflangs(dist = path.join(root, 'dist')) {
['/metodologia/', pair('/methodology/', '/metodologia/')],
['/privacy/', pair('/privacy/', '/privacidad/')],
['/privacidad/', pair('/privacy/', '/privacidad/')],
+ ['/feature-your-business/', pair('/feature-your-business/', '/anunciate/')],
+ ['/anunciate/', pair('/feature-your-business/', '/anunciate/')],
]);
for (const post of posts) {
const twins = post.translationKey ? posts.filter((p) => p.translationKey === post.translationKey) : [];
diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs
index a648da2e..5e63855b 100644
--- a/scripts/prerender.mjs
+++ b/scripts/prerender.mjs
@@ -693,6 +693,36 @@ async function main() {
fs.writeFileSync(path.resolve(esDir, 'index.html'), esHtml);
}
+ // /feature-your-business + /anunciate — self-serve sponsor page, hreflang-paired.
+ {
+ const enCanonical = `${BASE}/feature-your-business/`;
+ const esCanonical = `${BASE}/anunciate/`;
+ const hreflangs = [
+ { lang: 'en', href: enCanonical },
+ { lang: 'es', href: esCanonical },
+ { lang: 'x-default', href: enCanonical },
+ ];
+ const heads = [
+ ['feature-your-business', {
+ lang: 'en',
+ title: 'Feature your business | Border Pulse',
+ desc: 'A labeled spot on Border Pulse crossing pages for one business per category in each border city. $99 a month.',
+ canonical: enCanonical, ogImage: `${BASE}/og-card.png`, jsonLd: [], hreflangs,
+ }],
+ ['anunciate', {
+ lang: 'es',
+ title: 'Anuncia tu negocio | Border Pulse',
+ desc: 'Un espacio patrocinado en las páginas de cruces de Border Pulse para un negocio por categoría en cada ciudad fronteriza. $99 dólares al mes.',
+ canonical: esCanonical, ogImage: `${BASE}/og-card.png`, jsonLd: [], hreflangs,
+ }],
+ ];
+ for (const [route, head] of heads) {
+ const outDir = path.resolve(distDir, route);
+ fs.mkdirSync(outDir, { recursive: true });
+ fs.writeFileSync(path.resolve(outDir, 'index.html'), rewriteIndex(indexWithLinks, head));
+ }
+ }
+
// /about — trust signal page with methodology + public stats + privacy
{
const aboutHead = {
diff --git a/src/App.jsx b/src/App.jsx
index 87aa43e0..98c920ad 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -23,6 +23,7 @@ const WalkOrDrive = lazy(() => import('@/pages/WalkOrDrive'));
const Privacy = lazy(() => import('@/pages/Privacy'));
const CrossingConsole = lazy(() => import('@/pages/CorridorRedirect'));
const LaneFlip = lazy(() => import('@/pages/LaneFlip'));
+const FeatureBusiness = lazy(() => import('@/pages/FeatureBusiness'));
function RouteFallback() {
return (
@@ -55,6 +56,8 @@ function LayoutRoutes() {
} />
} />
} />
+ } />
+ } />
} />
diff --git a/src/Layout.jsx b/src/Layout.jsx
index 43006b33..71a9d62c 100644
--- a/src/Layout.jsx
+++ b/src/Layout.jsx
@@ -48,7 +48,7 @@ export default function Layout({ children }) {
useEffect(() => {
const pageOwnsLanguage =
/^\/blog\/[^/]+\/?$/.test(location.pathname) ||
- /^\/(methodology|metodologia|privacy|privacidad|es)\/?$/.test(location.pathname);
+ /^\/(methodology|metodologia|privacy|privacidad|feature-your-business|anunciate|es)\/?$/.test(location.pathname);
if (pageOwnsLanguage) return;
document.documentElement.lang = language;
}, [language, location.pathname]);
@@ -221,6 +221,12 @@ export default function Layout({ children }) {
{language === 'en' ? 'About' : 'Acerca de'}
+
+ {language === 'es' ? 'Anuncia tu negocio' : 'Feature your business'}
+
·
track('sponsor-click', { sponsor: sponsor.id, placement, action });
+ const city = cityById(sponsor.city)?.name || sponsor.city;
+ const category = categoryById(sponsor.category)?.[lang] || '';
+ const services = sponsor.services?.[lang] || [];
+ const offer = offerFor(sponsor);
+ const wa = whatsappUrl(sponsor, language);
return (