TL;DR: creé una propiedad de GA4 por sitio (la landing y el blog), puse el ID de medición en cada uno y verifiqué que la etiqueta solo sale en el build de producción. En la landing de React agregué un evento seccion_vista para saber no solo cuántos abren la página, sino cuántos llegan al Portafolio, al Proceso o al Equipo. Todo en unos 20 minutos.
Qué quería saber
Tengo dos sitios: la landing de mi empresa (React + Vite) y este blog (Hugo). Quería responder preguntas muy concretas:
- ¿Cuántas personas entran y cuántas regresan?
- ¿Desde qué país y ciudad?
- ¿Llegan por Google, por LinkedIn, por WhatsApp o escribiendo la dirección?
- En la landing: ¿cuánta gente baja hasta ver mi propuesta (portafolio, proceso, equipo) y cuánta se va en el primer pantallazo?
Las cuatro primeras las responde GA4 solo, sin configurar nada. La última necesita un evento propio.

Paso 1 · Una propiedad por sitio (no una para todo)
En GA4 la jerarquía es Cuenta → Propiedad → Flujo de datos. Decidí una propiedad por sitio porque:
- Los informes de cada sitio salen limpios, sin filtrar por dominio cada vez.
- Puedo dar acceso a alguien solo a uno de los dos.
- Los objetivos son distintos: la landing busca contactos; el blog, lectura.
El asistente tiene cuatro pasos:

- Nombre, zona horaria y moneda. Nombre descriptivo (
Mi empresa (midominio.com)), zona (GMT-06:00) Ciudad de México y peso mexicano. Ojo: si eliges «México», el asistente pone por defecto la hora de Tijuana; cámbiala o tus informes «del día» se cortan dos horas antes. - Datos de la empresa. Sector y tamaño. Solo afectan las comparativas que te sugiere Google.
- Objetivos. Para la landing elegí Generar oportunidades de venta + Conocer el tráfico web; para el blog, Conocer el tráfico + Interacción y retención. Cambian los informes que aparecen en el menú, nada más.
- Flujo web. URL del sitio y nombre del flujo. Al crearlo te da el ID de medición con forma
G-XXXXXXXXXX.
El ID de medición no es un secreto: aparece en el HTML de cualquier sitio que use Analytics. Puede ir en el código sin problema (a diferencia de una API key).
Paso 2 · El blog en Hugo (3 líneas)
Hugo ya trae la plantilla interna de Google Analytics. Si tu layout base la incluye ({{ template "_internal/google_analytics.html" . }}), solo falta el ID en config.toml:
# Google Analytics 4. Hugo solo inserta la etiqueta en el build de producción.
[services]
[services.googleAnalytics]
ID = "G-XXXXXXXXXX"
En versiones nuevas de Hugo la clave vieja googleAnalytics = "..." en la raíz ya no se usa: va dentro de [services].
Lo bueno: con hugo server no se inserta, así que tus propias visitas mientras escribes no ensucian las cifras.
Paso 3 · La landing en React + Vite
Aquí no hay plantilla, así que hice un módulo pequeño, src/analitica.js, y lo llamo una vez en main.jsx:
// Google Analytics 4. Solo en producción.
const ID = import.meta.env.VITE_GA_ID || "G-XXXXXXXXXX";
export function iniciarAnalitica() {
if (!import.meta.env.PROD || !ID) return; // nada en desarrollo
const s = document.createElement("script");
s.async = true;
s.src = `https://www.googletagmanager.com/gtag/js?id=${ID}`;
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments); // gtag exige el objeto arguments tal cual
};
window.gtag("js", new Date());
window.gtag("config", ID);
// Evento propio: la primera vez que cada sección entra en pantalla
const vistas = new Set();
const io = new IntersectionObserver((entradas) => {
entradas.forEach((e) => {
const id = e.target.id;
if (!e.isIntersecting || vistas.has(id)) return;
vistas.add(id);
window.gtag("event", "seccion_vista", { seccion: id });
});
}, { threshold: 0.35 });
// React monta las secciones después: esperar a que existan
setTimeout(() => document.querySelectorAll("section[id]").forEach((n) => io.observe(n)), 1200);
}
// main.jsx
import { iniciarAnalitica } from "./analitica.js";
iniciarAnalitica();
Tres detalles que importan:
import.meta.env.PROD: connpm run devno se carga nada.argumentsy no...args:gtagespera exactamente el objetoarguments; con un arreglo normal los eventos no llegan.threshold: 0.35: la sección cuenta como «vista» cuando se ve al menos un tercio, no cuando apenas asoma un pixel. Y cada sección se cuenta una sola vez por visita.
Paso 4 · Verificar el efecto, no solo el archivo
Escribir bien el archivo no prueba que funcione. Compilé los dos sitios a una carpeta temporal (para no tocar la de publicación) y busqué la etiqueta en el resultado:
# Blog (Hugo): ¿la etiqueta sale en el HTML?
hugo --quiet -d /tmp/prueba-hugo
grep -o "gtag/js?id=G-[A-Z0-9]*" /tmp/prueba-hugo/index.html # → la etiqueta con tu ID
grep -l "G-XXXXXXXXXX" /tmp/prueba-hugo/posts/*/index.html | wc -l # → todos los posts
# Landing (Vite): ¿el ID quedó en el bundle?
npx vite build --outDir /tmp/prueba-vite --emptyOutDir
grep -l "G-XXXXXXXXXX" /tmp/prueba-vite/assets/*.js
En mi caso la etiqueta apareció en la portada y en los 57 posts, y el ID quedó dentro del JavaScript de la landing.
Después de publicar, la prueba final está en Informes → Tiempo real: abre tu sitio desde el teléfono y en menos de un minuto debes verte como «1 usuario», con tu ciudad.
Paso 5 · Ver el evento por sección en los informes
GA4 recibe seccion_vista desde el primer día, pero para agrupar por el nombre de la sección hay que registrar el parámetro una vez:
- Administrar → Definiciones personalizadas → Crear dimensión personalizada.
- Nombre:
Sección· Alcance: Evento · Parámetro:seccion. - En Exploraciones cruza Sección con Usuarios: ahí ves cuánta gente llegó a «portafolio», a «proceso» o a «equipo».
Las dimensiones solo empiezan a llenarse desde que las creas: conviene hacerlo el mismo día que publicas.
Lo que GA4 te da sin configurar nada
| Pregunta | Dónde verla |
|---|---|
| ¿Cuántas personas entran? | Informes → Resumen (Usuarios activos) |
| ¿Desde qué país y ciudad? | Informes → Usuario → Atributos demográficos |
| ¿Por dónde llegan? | Informes → Adquisición → Adquisición de tráfico |
| ¿Qué páginas leen más? | Informes → Interacción → Páginas y pantallas |
| ¿Quién está ahora mismo? | Informes → Tiempo real |
Antes de publicar: privacidad
Analytics usa cookies. En México, la LFPDPPP pide un aviso de privacidad que diga qué datos recabas y para qué; agrega un párrafo sobre analítica. Si recibes visitas de Europa, el RGPD exige consentimiento previo: ahí conviene un banner con Consent Mode de Google. Para un sitio que apenas empieza, el aviso de privacidad es lo mínimo indispensable.
Resumen
- Una propiedad por sitio, con zona horaria de Ciudad de México (no la de Tijuana que pone por defecto).
- Hugo:
[services.googleAnalytics] ID = "G-..."y listo; solo sale en producción. - React + Vite: un módulo de 30 líneas con
gtag, solo enPROD, más un eventoseccion_vistaconIntersectionObserver. - Verifica compilando a una carpeta temporal y buscando el ID; después, Tiempo real.
- Registra la dimensión personalizada
seccionel mismo día.