From 7e19a5c21b15d3895f31ef913c5a45266a98e585 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:13:19 -0800 Subject: [PATCH] Sponsor card stands out; self-serve Feature your business page Card: tinted, larger name, services the business lists, dated business offer, WhatsApp as primary button. New EN/ES page /feature-your-business/ and /anunciate/ selling one spot per category per city at $99/month via Stripe Payment Link. Sponsors keyed by city and category, rotating by day. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JB1Z5G9UQjTEerD8NWr4Sg --- CHANGELOG.md | 8 + DECISIONS.md | 4 + ROADMAP.md | 3 +- scripts/build-sitemap.mjs | 7 + scripts/check-document-language.mjs | 2 +- scripts/check-hreflang.mjs | 2 + scripts/prerender.mjs | 30 ++++ src/App.jsx | 3 + src/Layout.jsx | 8 +- src/components/sponsors/SponsoredCard.jsx | 90 +++++++---- src/lib/sponsors.js | 93 +++++++++-- src/pages/FeatureBusiness.jsx | 184 ++++++++++++++++++++++ 12 files changed, 388 insertions(+), 46 deletions(-) create mode 100644 src/pages/FeatureBusiness.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f789decc3..162d25b95 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 48f5864b5..9a98c4d4c 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 b80bc7917..e1ef69f31 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 ff60ca7b7..27de5bf51 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 ad1dc2430..4c1e8035b 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 23c9279ab..720bb2465 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 a648da2ee..5e63855ba 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 87aa43e02..98c920ad1 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 43006b332..71a9d62c1 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 ( ); } diff --git a/src/lib/sponsors.js b/src/lib/sponsors.js index 0d6346820..c7f1cbc05 100644 --- a/src/lib/sponsors.js +++ b/src/lib/sponsors.js @@ -1,16 +1,53 @@ -// Sponsored placements from local businesses. One sponsor per placement; every card -// is labeled Sponsored. Keep entries factual: name, city, links. No medical or -// price claims in Border Pulse's voice; the business owns its own claims. +// Sponsored placements from local businesses. One commercial spot per page; every +// card is labeled Sponsored. Keep entries factual: name, city, what the business +// itself lists, links. No medical or price claims in Border Pulse's voice; the +// business owns its own claims, and they are shown as the business's. // Remove an entry (or set `until`) to end a placement. + +// Cities a business can sponsor, and the crossing pages that show its card. +export const CITIES = [ + // PDN, BOTA, Ysleta, Stanton DCL, El Paso (combined). + { id: 'juarez', name: 'Ciudad Juárez', ports: ['240202', '240201', '240104', '240204', '240221'] }, + // San Ysidro, Otay Mesa. + { id: 'tijuana', name: 'Tijuana', ports: ['250401', '250601'] }, + // Andrade. + { id: 'algodones', name: 'Los Algodones', ports: ['250201'] }, + // DeConcini, Mariposa, Morley Gate. + { id: 'nogales', name: 'Nogales', ports: ['260401', '260402', '260403'] }, + // Progreso International Bridge, Donna. + { id: 'progreso', name: 'Nuevo Progreso', ports: ['230901', '230902'] }, +]; + +export const CATEGORIES = [ + { id: 'dental', en: 'Dentist', es: 'Dentista' }, + { id: 'pharmacy', en: 'Pharmacy', es: 'Farmacia' }, + { id: 'medical', en: 'Medical clinic', es: 'Clínica médica' }, + { id: 'food', en: 'Food', es: 'Comida' }, + { id: 'auto', en: 'Auto', es: 'Autos' }, + { id: 'beauty', en: 'Salon & beauty', es: 'Salón y belleza' }, +]; + +// Stripe Payment Link for the self-serve listing. The buyer picks city and +// category there; Border Pulse reviews each listing before adding it below. +export const LISTING_PRICE_USD = 99; +export const CHECKOUT_URL = 'https://buy.stripe.com/dRmaEW9c7fm97TMdLF9AA03'; + export const SPONSORS = [ { id: 'constandse-dental', - name: 'Constandse Esthetic & Dent', - category: { en: 'Dentist', es: 'Dentista' }, - city: 'Ciudad Juárez', - blurb: { - en: 'Dental clinic in Ciudad Juárez, a short trip from the El Paso bridges.', - es: 'Clínica dental en Ciudad Juárez, a poca distancia de los puentes de El Paso.', + name: 'Dr. J Constandse', + city: 'juarez', + category: 'dental', + // What the clinic's own site lists, in its order. Shown as "listed by the clinic". + services: { + en: ['Dental implants', 'Veneers and smile design', 'Crowns', 'Full oral rehabilitation'], + es: ['Implantes dentales', 'Carillas y diseño de sonrisa', 'Coronas', 'Rehabilitación oral completa'], + }, + // Optional offer from the business, shown with its end date and hidden after it. + offer: { + en: '25% off treatments through Nov 30, per the clinic.', + es: '25% de descuento en tratamientos hasta el 30 de nov., según la clínica.', + until: '2026-11-30', }, url: 'https://constandse-dental.vercel.app/', whatsapp: '19153007751', @@ -19,8 +56,6 @@ export const SPONSORS = [ en: 'Hi, I found your clinic on Border Pulse and would like to book an appointment.', es: 'Hola, encontré su consultorio en Border Pulse y quiero agendar una cita.', }, - // Ciudad Juárez crossings: PDN, BOTA, Ysleta, Stanton DCL, El Paso (combined). - ports: ['240202', '240201', '240104', '240204', '240221'], guides: [ 'bringing-prescription-medication-back-from-mexico', 'traer-medicamentos-con-receta-de-mexico', 'dentist-in-juarez-from-el-paso-new-mexico', 'dentista-en-juarez-desde-el-paso-nuevo-mexico', @@ -32,15 +67,39 @@ export const SPONSORS = [ const active = (s, today) => (!s.since || s.since <= today) && (!s.until || today <= s.until); const todayIso = () => new Date().toISOString().slice(0, 10); +const dayNumber = today => Math.floor(Date.parse(`${today}T00:00:00Z`) / 86400000); +export const cityById = id => CITIES.find(c => c.id === id) || null; +export const categoryById = id => CATEGORIES.find(c => c.id === id) || null; + +export function activeSponsors(today = todayIso()) { + return SPONSORS.filter(s => active(s, today)); +} + +// A city's crossing pages hold one spot. With several sponsors in a city, the +// spot rotates by calendar day (UTC), so each gets whole days, not split views. export function sponsorForPort(portNumber, today = todayIso()) { - return SPONSORS.find(s => active(s, today) && s.ports?.includes(String(portNumber))) || null; + const city = CITIES.find(c => c.ports.includes(String(portNumber))); + if (!city) return null; + const list = activeSponsors(today).filter(s => s.city === city.id); + if (!list.length) return null; + return list[dayNumber(today) % list.length]; } + export function sponsorForGuide(slug, today = todayIso()) { - return SPONSORS.find(s => active(s, today) && s.guides?.includes(slug)) || null; + return activeSponsors(today).find(s => s.guides?.includes(slug)) || null; +} + +export function slotTaken(cityId, categoryId, today = todayIso()) { + return activeSponsors(today).some(s => s.city === cityId && s.category === categoryId); } -// Outbound link with UTM so the clinic can see Border Pulse in its own analytics. +export function offerFor(sponsor, today = todayIso()) { + const o = sponsor?.offer; + return o && (!o.until || today <= o.until) ? o : null; +} + +// Outbound link with UTM so the business can see Border Pulse in its own analytics. export function sponsorUrl(sponsor, placement) { const u = new URL(sponsor.url); u.searchParams.set('utm_source', 'borderpulse'); @@ -48,3 +107,9 @@ export function sponsorUrl(sponsor, placement) { u.searchParams.set('utm_campaign', placement); return u.toString(); } + +export function whatsappUrl(sponsor, language = 'en') { + if (!sponsor.whatsapp) return null; + const text = sponsor.whatsappText?.[language === 'es' ? 'es' : 'en']; + return `https://wa.me/${sponsor.whatsapp}${text ? `?text=${encodeURIComponent(text)}` : ''}`; +} diff --git a/src/pages/FeatureBusiness.jsx b/src/pages/FeatureBusiness.jsx new file mode 100644 index 000000000..7b10c9471 --- /dev/null +++ b/src/pages/FeatureBusiness.jsx @@ -0,0 +1,184 @@ +import React, { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { ArrowLeft, Check } from 'lucide-react'; +import { updatePageMeta, resetPageMeta } from '@/lib/seo'; +import { track } from '@/lib/analytics'; +import SponsoredCard from '@/components/sponsors/SponsoredCard'; +import { CITIES, CATEGORIES, CHECKOUT_URL, LISTING_PRICE_USD, SPONSORS, slotTaken } from '@/lib/sponsors'; + +const COPY = { + 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.', + back: 'All crossings', + h1: 'Put your business in front of people crossing the border', + lead: 'Border Pulse shows official CBP wait times for the U.S.-Mexico border in English and Spanish. People check it right before they cross. A featured listing puts your business on your city’s crossing pages, below the wait times.', + proof: '51.1K Google search impressions in 28 days (Search Console, through Nov 10, 2026).', + getTitle: 'What you get', + get: [ + 'A labeled card on your city’s crossing pages, below the wait times. It replaces the ad there.', + 'Buttons to your WhatsApp and your website. WhatsApp opens with a message saying the customer found you on Border Pulse.', + 'One business per category per city. When a city has several sponsors, they take turns on its crossing pages by day.', + 'A monthly count of clicks on your card, and visits tagged from Border Pulse.', + ], + price: `$${LISTING_PRICE_USD} a month`, + priceNote: 'Billed monthly through Stripe.', + pickTitle: 'Pick your city and category', + available: 'Available', + taken: 'Taken', + stepsTitle: 'How it works', + steps: [ + 'Pick an open spot and pay through Stripe. You enter your business name, website or Instagram, and WhatsApp.', + 'We review the listing. If it is not approved, the payment is refunded.', + 'We email you before it goes live.', + ], + rulesTitle: 'The rules', + rules: [ + 'Every listing is labeled Sponsored.', + 'A sponsor never sits above the wait times, and a page never shows a sponsor and an ad together.', + 'Your card shows facts you give us: name, city, the services you list, links. Border Pulse does not rate or recommend businesses.', + ], + exampleTitle: 'Example', + }, + 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.', + back: 'Todos los cruces', + h1: 'Pon tu negocio frente a quienes cruzan la frontera', + lead: 'Border Pulse muestra los tiempos de espera oficiales de CBP en la frontera entre EE. UU. y México, en inglés y español. La gente lo revisa justo antes de cruzar. Un anuncio destacado pone tu negocio en las páginas de los cruces de tu ciudad, debajo de los tiempos de espera.', + proof: '51.1 mil impresiones en búsquedas de Google en 28 días (Search Console, hasta el 10 de nov. de 2026).', + getTitle: 'Qué incluye', + get: [ + 'Una tarjeta marcada como patrocinada en las páginas de los cruces de tu ciudad, debajo de los tiempos de espera. Reemplaza el anuncio de esa página.', + 'Botones a tu WhatsApp y a tu sitio. WhatsApp se abre con un mensaje que dice que el cliente te encontró en Border Pulse.', + 'Un negocio por categoría en cada ciudad. Si una ciudad tiene varios patrocinadores, se turnan por día en sus páginas de cruces.', + 'Un conteo mensual de clics en tu tarjeta y de visitas que llegan desde Border Pulse.', + ], + price: `$${LISTING_PRICE_USD} dólares al mes`, + priceNote: 'Cobro mensual con Stripe.', + pickTitle: 'Elige tu ciudad y categoría', + available: 'Disponible', + taken: 'Ocupado', + stepsTitle: 'Cómo funciona', + steps: [ + 'Elige un espacio libre y paga con Stripe. Ahí escribes el nombre de tu negocio, tu sitio o Instagram y tu WhatsApp.', + 'Revisamos el anuncio. Si no se aprueba, se reembolsa el pago.', + 'Te escribimos por correo antes de publicarlo.', + ], + rulesTitle: 'Las reglas', + rules: [ + 'Todo anuncio dice Patrocinado.', + 'Un patrocinador nunca va arriba de los tiempos de espera, y una página nunca muestra un patrocinador y un anuncio al mismo tiempo.', + 'Tu tarjeta muestra los datos que nos das: nombre, ciudad, los servicios que anuncias y tus enlaces. Border Pulse no califica ni recomienda negocios.', + ], + exampleTitle: 'Ejemplo', + }, +}; + +function checkoutHref(slot) { + const u = new URL(CHECKOUT_URL); + u.searchParams.set('client_reference_id', slot); + return u.toString(); +} + +export default function FeatureBusiness({ lang = 'en' }) { + const es = lang === 'es'; + const t = COPY[es ? 'es' : 'en']; + const example = SPONSORS[0]; + + useEffect(() => { + const canonical = `https://borderpulse.com/${es ? 'anunciate' : 'feature-your-business'}/`; + updatePageMeta({ title: t.title, description: t.desc, ogTitle: t.title, ogDescription: t.desc, ogUrl: canonical, canonical, lang: es ? 'es' : 'en' }); + return () => resetPageMeta(); + }, [es, t]); + + const section = 'rounded-xl border border-slate-200 bg-white/80 p-4 dark:border-gray-700 dark:bg-gray-900/50 sm:p-5'; + + return ( +
+
+ + {t.back} + +
+ +
+

{t.h1}

+

{t.lead}

+

{t.proof}

+
+ +
+
+
+

{t.getTitle}

+

{t.price}

+
+
    + {t.get.map(item => ( +
  • + ))} +
+

{t.priceNote}

+
+ +
+

{t.pickTitle}

+
+ {CITIES.map(city => ( + + ))} +
+
+ +
+

{t.stepsTitle}

+
    + {t.steps.map(s =>
  1. {s}
  2. )} +
+
+ +
+

{t.rulesTitle}

+
    + {t.rules.map(r =>
  • {r}
  • )} +
+
+ + {example && ( +
+

{t.exampleTitle}

+ +
+ )} +
+
+ ); +}