diff --git a/CHANGELOG.md b/CHANGELOG.md index 719d8254..9d4ffda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Unreleased — guide-to-waits and return visits + +- Crossing detail pages now save favorites to the same browser-local list used by the dashboard. The star control previously appeared there but did nothing. A saved crossing rises to the top of the dashboard; no account or notification is implied. +- Blog guides now offer a direct route to current northbound waits and WhatsApp/copy sharing. Shared links carry only a channel-level UTM tag, with no person-level referral identifier. Umami records `blog-to-waits` and `blog-share` using the guide slug and fixed method values. +- Adsterra remains in its existing dashboard placement while ad creative quality and actual revenue are reviewed. No extra ad unit or recurring service was added. + ## 2026-09-27 — Crossing discovery and sharing ### Added diff --git a/src/lib/analytics.js b/src/lib/analytics.js index 7a8b6543..d0bbdde8 100644 --- a/src/lib/analytics.js +++ b/src/lib/analytics.js @@ -24,6 +24,8 @@ // embed-copy distribution / backlink funnel // share-status viral / WhatsApp distribution // crossing-share crossing-page distribution; method=native|copy|whatsapp +// blog-share guide distribution; slug and method=copy|whatsapp +// blog-to-waits guide reader opening the current waits dashboard // // No free-text is ever sent: slugs and enum values only, never a raw search // query, so an accidental paste cannot land in analytics. diff --git a/src/pages/BlogPost.jsx b/src/pages/BlogPost.jsx index 54456bd5..bd6faa51 100644 --- a/src/pages/BlogPost.jsx +++ b/src/pages/BlogPost.jsx @@ -1,12 +1,13 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Link, useParams, Navigate } from 'react-router-dom'; -import { ArrowLeft } from 'lucide-react'; +import { ArrowLeft, ArrowRight, Copy, MessageCircle } from 'lucide-react'; import { MDXProvider } from '@mdx-js/react'; import { Button } from '@/components/ui/button'; import { getPost, getAuthor, pillarLabel, listPosts } from '@/lib/blog-runtime'; import { mdxComponents } from '@/components/blog/MdxComponents'; import { LangContext } from '@/lib/LangContext'; import { updatePageMeta, resetPageMeta } from '@/lib/seo'; +import { track } from '@/lib/analytics'; const STRINGS = { en: { @@ -17,6 +18,12 @@ const STRINGS = { and: 'and', beforeTravel: 'before you travel.', alsoIn: 'Also available in', + crossingSoon: 'Crossing soon?', + nextStep: 'Check the latest official northbound wait report, then send this guide to someone who needs it.', + currentWaits: 'Current wait times', + copyLink: 'Copy guide link', + copied: 'Guide link copied.', + copyFailed: 'Copy failed. You can share this page from your browser.', }, es: { allPosts: 'Todos los posts', @@ -26,6 +33,12 @@ const STRINGS = { and: 'y', beforeTravel: 'antes de viajar.', alsoIn: 'Disponible en', + crossingSoon: '¿Cruzarás pronto?', + nextStep: 'Consulta el reporte oficial más reciente hacia EE. UU. y comparte esta guía con quien la necesite.', + currentWaits: 'Tiempos de espera actuales', + copyLink: 'Copiar enlace de la guía', + copied: 'Enlace de la guía copiado.', + copyFailed: 'No se pudo copiar. Puedes compartir esta página desde tu navegador.', }, }; @@ -49,6 +62,9 @@ function findTwin(post, allPosts) { export default function BlogPost() { const { slug } = useParams(); const post = getPost(slug); + const [copyStatus, setCopyStatus] = useState(''); + + useEffect(() => setCopyStatus(''), [slug]); useEffect(() => { if (!post) return; @@ -73,6 +89,21 @@ export default function BlogPost() { const allPosts = [...listPosts({ lang: 'en' }), ...listPosts({ lang: 'es' })]; const twin = findTwin(post, allPosts); const twinLangLabel = twin?.frontmatter.lang === 'es' ? 'Español' : 'English'; + const guideUrl = `https://borderpulse.com/blog/${slug}/`; + const whatsappUrl = `${guideUrl}?utm_source=whatsapp&utm_medium=share&utm_campaign=guide`; + const copyUrl = `${guideUrl}?utm_source=copy&utm_medium=share&utm_campaign=guide`; + const shareText = lang === 'es' + ? `Lee esta guía de Border Pulse: ${fm.title}` + : `Read this Border Pulse guide: ${fm.title}`; + const copyGuideLink = async () => { + try { + await navigator.clipboard.writeText(copyUrl); + setCopyStatus(t.copied); + track('blog-share', { slug, method: 'copy' }); + } catch { + setCopyStatus(t.copyFailed); + } + }; return (
@@ -117,6 +148,22 @@ export default function BlogPost() { +
+

{t.crossingSoon}

+

{t.nextStep}

+
+ track('blog-to-waits', { slug })} className="inline-flex min-h-11 items-center gap-2 rounded-md bg-emerald-700 px-3 py-2 text-sm font-medium text-white hover:bg-emerald-800"> + {t.currentWaits}
+ {copyStatus &&

{copyStatus}

} +