Quieres una sección de "posts populares" en tu blog. Google Analytics 4 (GA4) ya tiene los datos de páginas vistas. Todo lo que necesitas hacer es obtenerlos desde Next.js y mostrarlos.
Este artículo te guía paso a paso para llamar la API de datos de GA4 desde Next.js App Router y construir un ranking de posts populares basado en PV desde cero. Usamos caché ISR para mantener las llamadas a la API bajas mientras mostramos un ranking casi en tiempo real.
¿Qué es la API de datos de GA4?
La API de datos de GA4 te permite acceder programáticamente a los mismos datos que ves en el panel de Google Analytics. En lugar de abrir un navegador, obtienes reportes desde código.
El método que usamos es runReport, que genera reportes como "qué páginas tuvieron más vistas." Es el equivalente en API de ver el reporte "Páginas y pantallas" en GA4.
Es gratis. Para propiedades GA4 Standard (tier gratuito), la API de datos no tiene costo adicional. Hay una cuota de 200,000 tokens por día, pero una función de ranking para un blog apenas usará una fracción de eso.
Crear una cuenta de servicio de GCP
Para usar la API de datos de GA4, necesitas una cuenta de servicio de Google Cloud Platform (GCP) con acceso a tu propiedad de GA4. Esta es una configuración que solo se hace una vez.
Preparar el proyecto de GCP
- Inicia sesión en la Google Cloud Console
- Usa un proyecto existente o crea uno nuevo
- Ve a "APIs y servicios" → "Biblioteca" en el menú de la izquierda
- Busca "Google Analytics Data API" y haz clic en "Habilitar"
Crear la cuenta de servicio
- Ve a "APIs y servicios" → "Credenciales" en el menú de la izquierda
- Haz clic en "+ Crear credenciales" → "Cuenta de servicio"
- Ingresa un nombre (por ejemplo,
ga4-reporting) - Haz clic en "Crear y continuar." Puedes omitir el paso de asignación de rol
- Haz clic en la cuenta de servicio que acabas de crear, luego abre la pestaña "Claves"
- Haz clic en "Agregar clave" → "Crear nueva clave" → selecciona "JSON" y créala
Se descargará un archivo JSON. Se ve así:
{
"type": "service_account",
"project_id": "your-project-id",
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "ga4-reporting@your-project-id.iam.gserviceaccount.com",
"client_id": "...",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
Los dos campos que necesitas son client_email y private_key.
Otorgar acceso a tu propiedad de GA4
- Abre Google Analytics
- Ve a Administración (icono de engranaje, abajo a la izquierda) → Propiedad → "Gestión de acceso a la propiedad"
- Haz clic en "+" → "Agregar usuarios"
- Pega el valor
client_emaildel JSON descargado - Establece el rol como "Lector" (acceso de solo lectura es suficiente)
- Haz clic en "Agregar"
Encontrar tu ID de propiedad de GA4
En el panel de administración de GA4, ve a "Configuración de la propiedad" → "Detalles de la propiedad." El ID de propiedad (solo números, por ejemplo, 123456789) se muestra ahí. Necesitarás este valor más adelante.
Obtener datos de GA4 desde Next.js
Con GCP configurado, es hora de llamar a la API desde tu proyecto de Next.js.
Instalar el paquete
npm install @google-analytics/data
Configurar variables de entorno
Agrega lo siguiente a .env.local:
GA_PROPERTY_ID=123456789
GA_CLIENT_EMAIL=ga4-reporting@your-project-id.iam.gserviceaccount.com
GA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...(truncated)...\n-----END PRIVATE KEY-----\n"
Para GA_PRIVATE_KEY, pega el valor private_key del archivo JSON tal cual. Envuélvelo en comillas dobles. Los caracteres \n son secuencias de escape de nueva línea — pégalos tal cual. El código usa .replace(/\\n/g, '\n') para convertirlos a nuevas líneas reales en tiempo de ejecución.
Crear el cliente de GA4
Crea lib/ga4.ts:
import { BetaAnalyticsDataClient } from "@google-analytics/data";
function getCredentials() {
// Base64-encoded JSON key (recommended for Vercel)
if (process.env.GA_SERVICE_KEY_BASE64) {
return JSON.parse(
Buffer.from(process.env.GA_SERVICE_KEY_BASE64, "base64").toString()
);
}
// Fallback for local development
return {
client_email: process.env.GA_CLIENT_EMAIL,
private_key: process.env.GA_PRIVATE_KEY?.replace(/\\n/g, "\n"),
};
}
const client = new BetaAnalyticsDataClient({
credentials: getCredentials(),
});
const propertyId = process.env.GA_PROPERTY_ID;
export type PageViewEntry = {
path: string;
views: number;
};
export async function getTopPages(
limit: number = 10,
days: number = 30
): Promise<PageViewEntry[]> {
const [response] = await client.runReport({
property: `properties/${propertyId}`,
dimensions: [{ name: "pagePath" }],
metrics: [{ name: "screenPageViews" }],
dateRanges: [{ startDate: `${days}daysAgo`, endDate: "today" }],
orderBys: [
{
metric: { metricName: "screenPageViews" },
desc: true,
},
],
limit,
});
if (!response.rows) return [];
return response.rows.map((row) => ({
path: row.dimensionValues?.[0]?.value ?? "",
views: parseInt(row.metricValues?.[0]?.value ?? "0", 10),
}));
}
getCredentials() soporta dos métodos de autenticación. Si GA_SERVICE_KEY_BASE64 está definido, decodifica en base64 la clave JSON completa. De lo contrario, recurre a variables de entorno individuales. El enfoque base64 es el recomendado para Vercel (se explica más adelante).
Esto es lo que hace cada parámetro de runReport:
dimensions: [{ name: "pagePath" }]— agrupa resultados por ruta de URLmetrics: [{ name: "screenPageViews" }]— cuenta páginas vistasdateRanges— cubre los últimos 30 díasorderBys— ordena por vistas en orden descendente (más vistas primero)limit— devuelve solo los 10 mejores resultados
Construir un componente de posts populares
Ahora construye un Server Component para mostrar los datos. Con los Server Components de App Router, puedes llamar funciones async directamente dentro del componente. No se necesita una API route separada.
import Link from "next/link";
import { getTopPages } from "@/lib/ga4";
export async function PopularPosts() {
const pages = await getTopPages(10, 30);
// Filtrar páginas que no son artículos (inicio, páginas de categoría, etc.)
const articles = pages.filter(
(p) => p.path.match(/^\/[a-z]{2}\/[^/]+\/[^/]+$/) && p.views > 0
);
if (articles.length === 0) return null;
return (
<section>
<h2 className="text-xl font-bold mb-4">Popular Posts</h2>
<ol className="space-y-3">
{articles.slice(0, 5).map((entry, i) => (
<li key={entry.path} className="flex items-baseline gap-3">
<span className="text-sm font-mono text-muted">{i + 1}</span>
<Link href={entry.path} className="text-sm hover:underline">
{entry.path}
</Link>
<span className="text-xs text-muted ml-auto">
{entry.views.toLocaleString()} PV
</span>
</li>
))}
</ol>
</section>
);
}
Este componente muestra pagePath (rutas de URL) directamente. Para mostrar títulos de artículos en su lugar, empareja los slugs con los metadatos de tus artículos:
import { getAllArticles } from "@/lib/content";
import { getTopPages } from "@/lib/ga4";
export async function getPopularArticles(locale: string, limit: number = 5) {
const [pages, articles] = await Promise.all([
getTopPages(50, 30),
Promise.resolve(getAllArticles(locale)),
]);
const viewMap = new Map(pages.map((p) => [p.path, p.views]));
return articles
.map((article) => ({
...article,
views: viewMap.get(`/${locale}/${article.category}/${article.slug}`) ?? 0,
}))
.filter((a) => a.views > 0)
.sort((a, b) => b.views - a.views)
.slice(0, limit);
}
Cachear llamadas a la API con ISR
Llamar a la API de GA4 en cada solicitud de página desperdiciaría cuota y ralentizaría las respuestas. ISR (Incremental Static Regeneration) te permite servir HTML cacheado mientras revalidas los datos en segundo plano.
Establece revalidate en la página que muestra el ranking:
// app/[locale]/popular/page.tsx
import { getPopularArticles } from "@/lib/ga4";
export const revalidate = 3600; // regenerate every hour
export default async function PopularPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const articles = await getPopularArticles(locale);
return (
<main>
<h1>Popular Articles</h1>
<ul>
{articles.map((article) => (
<li key={article.slug}>
<a href={`/${locale}/${article.category}/${article.slug}`}>
{article.title}
</a>
<span>{article.views.toLocaleString()} views</span>
</li>
))}
</ul>
</main>
);
}
revalidate = 3600 significa "cachea esta página hasta por 1 hora." Después de que el caché expira, la primera solicitud entrante activa una regeneración en segundo plano, y las solicitudes posteriores obtienen el HTML fresco.
Cachear a nivel de función
Si quieres cachear solo la llamada a la API de GA4 (no la página completa), usa la directiva use cache. Asegúrate de que cacheComponents: true esté configurado en next.config.ts:
import { cacheLife } from "next/cache";
import { getTopPages } from "@/lib/ga4";
export async function getCachedTopPages(limit: number, days: number) {
"use cache";
cacheLife("hours");
return getTopPages(limit, days);
}
Esto permite que múltiples páginas compartan los mismos datos de ranking cacheados mientras mantiene las llamadas a la API a una cada pocas horas.
Desplegar en Vercel
Localmente, almacenaste las credenciales en .env.local usando variables separadas. En Vercel, los caracteres de nueva línea (\n) en private_key pueden causar errores de parseo como DECODER routines::unsupported.
El enfoque recomendado es codificar en base64 el archivo JSON completo de la clave en una sola variable de entorno. Esto evita completamente los problemas con nuevas líneas.
Codificar la clave en base64
Convierte el archivo JSON de la clave descargado a base64:
base64 -w 0 your-service-account-key.json
Copia la cadena de salida.
Configurar variables de entorno
En el panel de Vercel, ve a Settings → Environment Variables y agrega:
| Nombre | Valor |
|---|---|
GA_PROPERTY_ID | Tu ID de propiedad de GA4 (solo números) |
GA_SERVICE_KEY_BASE64 | La cadena codificada en base64 de arriba |
No necesitas envolver los valores en comillas en Vercel. Pégalos tal cual.
Desplegar y verificar
git add .
git commit -m "feat: add GA4 popular posts ranking"
git push
Vercel construirá y desplegará automáticamente. Visita la página de ranking después del despliegue. Si los datos aparecen, ya terminaste.
Solución de problemas
Si las cosas no funcionan, verifica estos errores comunes:
DECODER routines::unsupported: Las nuevas líneas deprivate_keyno se parsearon correctamente. Cambia al enfoque de codificación base64 descrito arribaPERMISSION_DENIED: Google Analytics Data API has not been used in project...: La API de datos no está habilitada en tu proyecto de GCP. Ve a "APIs y servicios" → "Biblioteca" y habilítalaPERMISSION_DENIED: User does not have sufficient permissions...: El email de la cuenta de servicio no fue agregado como "Lector" en tu propiedad de GA4. Verifica la gestión de acceso a la propiedad en el admin de GA4- Las variables de entorno no surten efecto: Después de cambiar variables de entorno en Vercel, necesitas redesplegar. Ve a Deployments y haz clic en "Redeploy" en el último despliegue
Conclusión
Lo que construimos:
- Cuenta de servicio de GCP: Creamos una cuenta de servicio en Google Cloud Console y otorgamos acceso de Lector a la propiedad de GA4
- API de datos de GA4: Usamos
BetaAnalyticsDataClientde@google-analytics/datapara obtenerscreenPageViews - Server Component: Llamamos a la API directamente desde un Server Component de App Router — sin necesidad de API route
- Caché ISR: Usamos
revalidatepara limitar las llamadas a la API y conservar cuota - Despliegue en Vercel: Codificamos en base64 la clave JSON en una sola variable de entorno, evitando problemas de parseo con nuevas líneas
La API de datos de GA4 es gratuita. La cuota de 200,000 tokens por día es más que suficiente para un blog. Combinada con ISR, obtienes un ranking casi en tiempo real a costo cero.
Recursos oficiales:
- GA4 Data API Overview — resumen de la API y guías
- Dimensions & Metrics — dimensiones y métricas disponibles
- Data API Quotas — límites de cuota y detalles
- @google-analytics/data — biblioteca cliente de Node.js