32blogby StudioMitsu

Crea un ranking de posts populares con la API de GA4 en Next.js

Aprende a llamar la API de datos de Google Analytics 4 desde Next.js App Router para construir un ranking de posts populares basado en páginas vistas con caché ISR, paso a paso.

10 min read
Next.jsGoogle AnalyticsGA4APIISR
Contenido

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

  1. Inicia sesión en la Google Cloud Console
  2. Usa un proyecto existente o crea uno nuevo
  3. Ve a "APIs y servicios" → "Biblioteca" en el menú de la izquierda
  4. Busca "Google Analytics Data API" y haz clic en "Habilitar"

Crear la cuenta de servicio

  1. Ve a "APIs y servicios" → "Credenciales" en el menú de la izquierda
  2. Haz clic en "+ Crear credenciales" → "Cuenta de servicio"
  3. Ingresa un nombre (por ejemplo, ga4-reporting)
  4. Haz clic en "Crear y continuar." Puedes omitir el paso de asignación de rol
  5. Haz clic en la cuenta de servicio que acabas de crear, luego abre la pestaña "Claves"
  6. Haz clic en "Agregar clave" → "Crear nueva clave" → selecciona "JSON" y créala

Se descargará un archivo JSON. Se ve así:

json
{
  "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

  1. Abre Google Analytics
  2. Ve a Administración (icono de engranaje, abajo a la izquierda) → Propiedad → "Gestión de acceso a la propiedad"
  3. Haz clic en "+" → "Agregar usuarios"
  4. Pega el valor client_email del JSON descargado
  5. Establece el rol como "Lector" (acceso de solo lectura es suficiente)
  6. 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

bash
npm install @google-analytics/data

Configurar variables de entorno

Agrega lo siguiente a .env.local:

bash
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:

typescript
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 URL
  • metrics: [{ name: "screenPageViews" }] — cuenta páginas vistas
  • dateRanges — cubre los últimos 30 días
  • orderBys — 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.

typescript
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:

typescript
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:

typescript
// 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:

typescript
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:

bash
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:

NombreValor
GA_PROPERTY_IDTu ID de propiedad de GA4 (solo números)
GA_SERVICE_KEY_BASE64La cadena codificada en base64 de arriba

No necesitas envolver los valores en comillas en Vercel. Pégalos tal cual.

Desplegar y verificar

bash
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 de private_key no se parsearon correctamente. Cambia al enfoque de codificación base64 descrito arriba
  • PERMISSION_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ítala
  • PERMISSION_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 BetaAnalyticsDataClient de @google-analytics/data para obtener screenPageViews
  • Server Component: Llamamos a la API directamente desde un Server Component de App Router — sin necesidad de API route
  • Caché ISR: Usamos revalidate para 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: