⚠️
Importante antes de empezar: Este sistema está construido específicamente para Yoast SEO. Si usas Rank Math, All in One SEO u otro plugin SEO, el mini-plugin que vas a instalar necesitaría adaptaciones que no están incluidas en este tutorial.

Por qué editar el SEO en WordPress página a página es un problema real

Tienes una web con 30, 50 o 100 URLs. Un día revisas tu Google Search Console y ves que la mayoría de tus páginas tienen el mismo meta description genérico que generó WordPress automáticamente. O peor: sin meta description. O los títulos SEO son idénticos a los títulos de página, sin ninguna optimización.

Sabes que tienes que arreglarlo. Pero también sabes lo que significa arreglarlo: abrir WordPress, buscar la página, bajar hasta el bloque de Yoast, cambiar el campo, guardar, volver al listado, siguiente página. Y así cincuenta veces.

Cuánto tiempo pierdes editando meta descriptions una a una

Si cada edición te lleva 3 minutos — entre cargar el editor, encontrar el campo, escribir el texto y guardar — 50 páginas son 2,5 horas de trabajo repetitivo. Trabajo que no requiere creatividad ni decisión, solo ejecución mecánica.

Y eso suponiendo que lo haces de una sentada. En la práctica, lo vas aplazando porque es tedioso, y mientras tanto Google sigue rastreando tu web con campos vacíos o genéricos.

El miedo a tocar lo que funciona te paraliza (y Google lo nota)

Hay otro factor que nadie menciona: el miedo. Entrar en WordPress página a página, tocar campos que no entiendes del todo, guardar cambios sin saber exactamente qué va a pasar. Ese miedo es real y es legítimo, sobre todo si ya has tenido alguna experiencia de tocar algo y que deje de funcionar.

El resultado es parálisis: sabes que tienes que mejorar el SEO de tu web, pero no lo haces porque el proceso da demasiado respeto y consume demasiado tiempo.

Este tutorial resuelve exactamente eso.

Qué es sincronizar Google Sheets con WordPress y para qué sirve

La idea es simple: en lugar de entrar en WordPress para cada cambio, tienes una hoja de Google Sheets que actúa como panel de control de tu web. Editas los campos que quieres cambiar directamente en la hoja, marcas las filas, pulsas un botón del menú y el sistema envía los cambios a WordPress automáticamente.

Todo desde una sola pantalla, sin recargas, sin navegar por el panel de administración.

Qué campos puedes actualizar desde la hoja de cálculo

Campo Qué controla Dónde aparece
Title El título que ves en la pestaña del navegador y en los resultados de Google Yoast SEO → Título SEO
Meta description El texto descriptivo que aparece bajo el título en los resultados de búsqueda Yoast SEO → Meta descripción
Slug La parte de la URL que identifica la página (ej: /mis-servicios) WordPress → Permalink
Status Estado del contenido: publicado, borrador o privado WordPress → Visibilidad

Qué NO hace este sistema (para que no te lleves sorpresas)

✓ Lo que sí hace
Actualiza campos SEO de Yoast en masa
Funciona con posts, páginas y CPTs
Funciona con productos de WooCommerce
Procesa solo las filas que tú marcas
Registra el resultado de cada sincronización
✗ Lo que no hace
No crea posts nuevos en WordPress
No elimina contenido
No edita el cuerpo del artículo
No funciona con Rank Math ni otros plugins SEO
No trae cambios de WordPress a la hoja
💡
Importante: Este sistema solo actualiza posts y páginas que ya existen en WordPress, identificados por su ID. No puede crear contenido nuevo ni borrarlo. Eso lo convierte en una herramienta muy segura — literalmente no puedes romper nada.

Qué necesitas para conectar Google Sheets con WordPress

Antes de empezar, comprueba que tienes todo esto en orden. Son los únicos requisitos técnicos del sistema.

Cómo activar Application Passwords en WordPress

WordPress permite generar contraseñas específicas para aplicaciones externas — independientes de tu contraseña de acceso al panel. Es la forma segura de que el script de Google Sheets se autentique en tu WordPress.

Para activarlas: entra en WordPress → Usuarios → Tu perfil. Baja hasta la sección Application Passwords. Escribe un nombre descriptivo (por ejemplo: "Google Sheets Sync") y pulsa Add New Application Password. WordPress te mostrará la contraseña una sola vez — cópiala ahora y guárdala en un lugar seguro.

⚠️
Nunca pongas esta contraseña en el propio archivo del script si vas a compartirlo. En el script que encontrarás más abajo, la contraseña va en el bloque CONFIG. Si compartes el script, elimina esas líneas antes.

Por qué Yoast no expone los campos SEO en la API REST por defecto

La API REST de WordPress permite que aplicaciones externas lean y modifiquen el contenido de tu web. Por defecto expone campos como el título, el cuerpo del artículo o el slug. Pero los campos de Yoast SEO — el título SEO y la meta description — no están expuestos por defecto porque Yoast los guarda como metadatos privados del post.

Para poder actualizarlos desde fuera de WordPress, necesitas registrar esos campos como accesibles por la API. Eso es exactamente lo que hace el mini-plugin del paso 1.

Cómo funciona la sincronización entre Google Sheets y WordPress

Qué es la API REST de WordPress y cómo la usamos sin saber programar

La API REST de WordPress es una puerta trasera que permite que herramientas externas le hablen directamente. Cuando pulsas "Push selected" en el menú de la hoja, el script de Apps Script envía una petición a esa puerta con los datos que has editado en la fila. WordPress recibe la petición, verifica que estás autenticada, actualiza el campo y devuelve una confirmación.

Tú no interactúas con esa puerta directamente — el script lo hace por ti. Lo único que necesitas es configurar las credenciales una vez al principio.

Para qué sirve el mini-plugin y qué hace exactamente

El mini-plugin registra los campos de Yoast SEO (_yoast_wpseo_title y _yoast_wpseo_metadesc) como accesibles y modificables a través de la API REST de WordPress. Sin él, cualquier intento de actualizar esos campos desde fuera de WordPress será ignorado silenciosamente — sin error, simplemente no pasará nada.

El plugin no modifica el comportamiento de Yoast ni afecta a ninguna otra funcionalidad de tu web. Solo abre esos dos campos a la API REST, con autenticación obligatoria.

Cómo montar el sistema paso a paso

Paso 1 — Instalar el mini-plugin en WordPress

1
Crear e instalar el mini-plugin

En tu ordenador, crea una carpeta llamada mi-rest-meta. Dentro, crea un archivo llamado mi-rest-meta.php y pega el código que encontrarás más abajo.

Comprime la carpeta en un archivo .zip. En WordPress, ve a Plugins → Añadir nuevo → Subir plugin, sube el ZIP y actívalo.

Si ya tienes acceso FTP o al gestor de archivos de tu hosting, también puedes subir la carpeta directamente a wp-content/plugins/ y activar el plugin desde el panel.

PHP mi-rest-meta.php
<?php
/**
 * Plugin Name: Mi REST Meta
 * Description: Expone los campos de Yoast SEO en la API REST de WordPress.
 * Version: 1.0.0
 */

if (!defined('ABSPATH')) exit;

add_action('init', function () {
  // Añade aquí los tipos de contenido que quieres sincronizar.
  // 'product' es para WooCommerce. Elimínalo si no tienes tienda.
  $post_types = ['post', 'page', 'product'];

  $yoast_keys = [
    '_yoast_wpseo_title',
    '_yoast_wpseo_metadesc',
  ];

  foreach ($post_types as $pt) {
    foreach ($yoast_keys as $key) {
      register_post_meta($pt, $key, [
        'type'              => 'string',
        'single'            => true,
        'sanitize_callback' => 'sanitize_text_field',
        'auth_callback'     => function () {
          return current_user_can('edit_posts');
        },
        'show_in_rest'      => true,
      ]);
    }
  }
});
💡
Si tienes un CPT personalizado (por ejemplo, el portfolio de tu tema), añádelo al array $post_types. Si no sabes el slug exacto de tu CPT, entra en WordPress → Ajustes → Permalinks y guarda los cambios — el slug aparece en la URL del listado del CPT en el panel.

Paso 2 — Crear la Application Password en WordPress

2
Generar la contraseña de aplicación

Ve a WordPress → Usuarios → Tu perfil. Baja hasta la sección Application Passwords.

Escribe un nombre (por ejemplo: "Google Sheets") y pulsa Add New Application Password.

Copia la contraseña que aparece — tiene el formato xxxx xxxx xxxx xxxx xxxx xxxx. WordPress solo te la mostrará esta vez. Guárdala en un lugar seguro antes de cerrar la pantalla.

Paso 3 — Preparar la hoja de Google Sheets

3
Crear la estructura de columnas

Crea una hoja de Google Sheets nueva. Puedes tener varias pestañas — una para Posts, una para Pages, una para Portfolio, una para productos de WooCommerce. Todas deben tener las mismas columnas en el mismo orden.

Las columnas obligatorias en la fila 1, exactamente con estos nombres:

Actualizar | ID | Post Type | Title | Slug | Status | _yoast_wpseo_title | _yoast_wpseo_metadesc | last_synced_at | last_synced_hash | sync_result | sync_error

La columna Actualizar debe ser de tipo checkbox (Insertar → Casilla de verificación). El ID es el ID numérico del post en WordPress — lo ves en la URL cuando editas una entrada: /wp-admin/post.php?post=42&action=edit.

La forma más rápida de poblar la hoja con tus posts existentes es exportar desde WordPress → Herramientas → Exportar en formato XML, o instalar un plugin de exportación a CSV como WP All Export.

Paso 4 — Añadir el script en Apps Script

4
Pegar el script en Google Apps Script

En tu hoja de Google Sheets, ve a Extensiones → Apps Script. Se abrirá el editor. Borra el contenido que hay por defecto y pega el script completo que encontrarás a continuación.

Una vez pegado, localiza el bloque CONFIG al principio del script y cambia únicamente las cuatro líneas marcadas. No toques nada más.

Guarda el proyecto con Ctrl+S (o Cmd+S en Mac). La primera vez que ejecutes el script, Google te pedirá autorización para que el script acceda a tu hoja y haga peticiones externas — acepta los permisos.

El script de Apps Script para sincronizar Google Sheets con WordPress

Las 4 líneas que tienes que cambiar (y nada más)

El script está diseñado para que solo necesites tocar el bloque CONFIG. Las cuatro líneas marcadas son las únicas que dependen de tu web. El resto del código no necesita modificarse.

JavaScript Code.gs — Apps Script
/* ═══════════════════════════
   CONFIG — Solo cambia estas 4 líneas
   ═══════════════════════════ */
const CONFIG = {
  wpBaseUrl: 'https://tuweb.com',          // ← Tu URL sin slash final
  wpUser: 'TU_USUARIO_WP',              // ← Tu nombre de usuario de WordPress
  wpAppPassword: 'xxxx xxxx xxxx xxxx xxxx xxxx', // ← La Application Password del paso 2

  sheets: [
    { name: 'Posts',     postType: 'post',    endpoint: '/wp-json/wp/v2/posts' },
    { name: 'Pages',     postType: 'page',    endpoint: '/wp-json/wp/v2/pages' },
    { name: 'Portfolio', postType: 'mi_cpt',  endpoint: '/wp-json/wp/v2/mi_cpt' }, // ← Cambia mi_cpt por el slug de tu CPT
    { name: 'WooCommerce', postType: 'product', endpoint: '/wp-json/wp/v2/product' },
  ],

  requiredHeaders: ['Actualizar', 'ID', 'Post Type', 'Title', '_yoast_wpseo_title', '_yoast_wpseo_metadesc'],
  batchSize: 15,
  retryCount: 3,
  retryBaseSleepMs: 800,
  hashFields: ['Title', 'Slug', 'Status', '_yoast_wpseo_title', '_yoast_wpseo_metadesc'],
};

/* ═══════════════════════════
   MENÚ
   ═══════════════════════════ */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('WordPress Sync')
    .addItem('Push selected (Actualizar marcado)', 'syncPushSelected')
    .addItem('Push changed (solo cambios reales)', 'syncPushChanged')
    .addToUi();
}

/* ═══════════════════════════
   HELPERS
   ═══════════════════════════ */
function basicAuthHeader_() {
  const token = Utilities.base64Encode(`${CONFIG.wpUser}:${CONFIG.wpAppPassword}`);
  return { Authorization: `Basic ${token}` };
}

function getSheetByName_(name) {
  const sh = SpreadsheetApp.getActive().getSheetByName(name);
  if (!sh) throw new Error(`No existe la pestaña: ${name}`);
  return sh;
}

function getHeaderMap_(sh) {
  const headers = sh.getRange(1, 1, 1, sh.getLastColumn()).getValues()[0].map(String);
  const map = {};
  headers.forEach((h, i) => map[h.trim()] = i);
  return { headers, map };
}

function ensureHeaders_(headers) {
  CONFIG.requiredHeaders.forEach(h => {
    if (!headers.includes(h)) throw new Error(`Falta la columna: ${h}`);
  });
}

function getCell_(row, map, key) {
  const idx = map[key];
  return idx !== undefined ? row[idx] : '';
}

function setCell_(row, map, key, value) {
  const idx = map[key];
  if (idx !== undefined) row[idx] = value;
}

function normalize_(v) {
  if (v === null || v === undefined) return '';
  if (typeof v === 'boolean') return v ? 'true' : 'false';
  return String(v).trim();
}

function computeHash_(row, map) {
  const raw = CONFIG.hashFields.map(f => normalize_(getCell_(row, map, f))).join('||');
  const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, raw, Utilities.Charset.UTF_8);
  return digest.map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join('');
}

function buildPayload_(row, map) {
  const payload = {};
  const title  = normalize_(getCell_(row, map, 'Title'));
  const slug   = normalize_(getCell_(row, map, 'Slug'));
  const status = normalize_(getCell_(row, map, 'Status'));
  const yTitle = normalize_(getCell_(row, map, '_yoast_wpseo_title'));
  const yDesc  = normalize_(getCell_(row, map, '_yoast_wpseo_metadesc'));

  if (title)  payload.title  = title;
  if (slug)   payload.slug   = slug;
  if (status) payload.status = status;

  payload.meta = {};
  if (yTitle) payload.meta._yoast_wpseo_title    = yTitle;
  if (yDesc)  payload.meta._yoast_wpseo_metadesc  = yDesc;
  if (!Object.keys(payload.meta).length) delete payload.meta;

  return payload;
}

function wpRequest_(url, body) {
  const options = {
    method: 'post',
    headers: { ...basicAuthHeader_(), 'Content-Type': 'application/json' },
    payload: JSON.stringify(body),
    muteHttpExceptions: true,
  };
  let lastErr = null;
  for (let i = 0; i < CONFIG.retryCount; i++) {
    try {
      const r = UrlFetchApp.fetch(url, options);
      const code = r.getResponseCode();
      if (code >= 200 && code < 300) return { ok: true, code };
      if (code === 429 || code >= 500) {
        Utilities.sleep(CONFIG.retryBaseSleepMs * Math.pow(2, i));
        lastErr = `HTTP ${code}`;
        continue;
      }
      return { ok: false, code, error: `HTTP ${code}: ${r.getContentText().slice(0,300)}` };
    } catch(e) {
      lastErr = e.message;
      Utilities.sleep(CONFIG.retryBaseSleepMs * Math.pow(2, i));
    }
  }
  return { ok: false, code: 0, error: String(lastErr) };
}

/* ═══════════════════════════
   SYNC
   ═══════════════════════════ */
function syncPushSelected() { runSync_('selected'); }
function syncPushChanged()  { runSync_('changed');  }

function runSync_(mode) {
  const now = new Date().toISOString();

  CONFIG.sheets.forEach(cfg => {
    const sh = getSheetByName_(cfg.name);
    if (sh.getLastRow() < 2) return;

    const { headers, map } = getHeaderMap_(sh);
    ensureHeaders_(headers);

    const range  = sh.getRange(2, 1, sh.getLastRow() - 1, sh.getLastColumn());
    const values = range.getValues();

    const items = values
      .map((row, i) => ({ row, i, id: normalize_(getCell_(row, map, 'ID')), hash: computeHash_(row, map) }))
      .filter(({ row, id, hash }) => {
        if (!id) return false;
        if (normalize_(getCell_(row, map, 'Post Type')) !== cfg.postType) return false;
        if (mode === 'selected') return getCell_(row, map, 'Actualizar') === true;
        return hash !== normalize_(getCell_(row, map, 'last_synced_hash'));
      });

    items.forEach(({ row, i, id, hash }) => {
      const url = `${CONFIG.wpBaseUrl}${cfg.endpoint}/${id}`;
      const res = wpRequest_(url, buildPayload_(row, map));

      if (res.ok) {
        setCell_(row, map, 'last_synced_at',   now);
        setCell_(row, map, 'last_synced_hash', hash);
        setCell_(row, map, 'sync_result',      `OK ${res.code}`);
        setCell_(row, map, 'sync_error',       '');
        setCell_(row, map, 'Actualizar',       false);
      } else {
        setCell_(row, map, 'sync_result', `ERROR ${res.code}`);
        setCell_(row, map, 'sync_error',  res.error.slice(0, 500));
      }
    });

    range.setValues(values);
    SpreadsheetApp.getActive().toast(`${cfg.name}: ${items.length} filas procesadas`, 'WordPress Sync', 5);
  });
}

Cómo funciona el modo "push selected" y el modo "push changed"

El script tiene dos modos de sincronización que puedes elegir desde el menú WordPress Sync que aparece en tu hoja:

Push selected (Actualizar marcado): Solo sincroniza las filas que tienen el checkbox Actualizar marcado. Tú decides qué se envía. Ideal cuando estás editando contenido y quieres control total sobre qué llega a WordPress.

Push changed (solo cambios reales): El script calcula una huella digital de cada fila basada en los campos importantes. Si la huella ha cambiado respecto a la última sincronización, la fila se envía. No necesitas marcar nada. Ideal para un flujo de mantenimiento donde quieres que todo lo editado se sincronice automáticamente.

Paso 5 — Primera sincronización de prueba

5
Verificar que todo funciona

Antes de sincronizar en masa, haz una prueba con una sola fila. Elige un post de prueba, edita el campo _yoast_wpseo_title con un texto que puedas identificar fácilmente (por ejemplo: "TEST — Título de prueba"), marca el checkbox Actualizar en esa fila.

Ve a Extensiones → Apps Script, ejecuta onOpen una vez para que aparezca el menú. Vuelve a la hoja, abre el menú WordPress Sync → Push selected. La primera vez Google pedirá autorización — acepta.

Cuando termine, la columna sync_result debe mostrar OK 200. Entra en WordPress, busca ese post y verifica que el título SEO ha cambiado. Si es así, el sistema funciona correctamente.

Si ves ERROR 401: la contraseña de aplicación es incorrecta. Si ves ERROR 404: el endpoint o el ID del post no es correcto.

Errores comunes al sincronizar Google Sheets con WordPress

⚠️
El endpoint del CPT no existe o devuelve 404
El CPT de tu tema o plugin no está expuesto en la API REST de WordPress. Para verificarlo, abre en el navegador: tuweb.com/wp-json/wp/v2/types y busca tu CPT en la respuesta. Si no aparece, hay que registrarlo con show_in_rest => true en el código que lo define. Si no tienes acceso a ese código (es un tema de terceros), consulta la documentación del tema.
⚠️
La Application Password está mal copiada
Este es el error más frecuente. La contraseña tiene espacios entre grupos de caracteres (xxxx xxxx xxxx) — esos espacios forman parte de la contraseña y deben incluirse en el CONFIG exactamente como los copió WordPress. Si ves ERROR 401, este es el primer lugar donde mirar.
⚠️
La columna se llama diferente en distintas pestañas
El script busca las columnas por su nombre exacto, incluyendo mayúsculas y espacios. Si en la pestaña Posts la columna se llama Actualizar y en Portfolio se llama actualizar o Actu, el script lanzará un error de columna obligatoria no encontrada. Unifica los nombres en todas las pestañas.
⚠️
El mini-plugin no está activo o no incluye el tipo de contenido
Si el script devuelve OK 200 pero los campos de Yoast no cambian en WordPress, el problema casi siempre es el mini-plugin. Verifica que está activo en WordPress → Plugins. Verifica también que el tipo de contenido que intentas sincronizar está incluido en el array $post_types del plugin.
⚠️
WooCommerce: el endpoint /wp-json/wp/v2/product devuelve 404
WooCommerce registra el CPT de productos con show_in_rest => true por defecto, pero el endpoint puede variar según la versión. Verifica el endpoint exacto en tuweb.com/wp-json/wp/v2/types buscando el tipo product y consultando el campo rest_base.
Sistema Gestión · Método Goana

¿Prefieres tenerlo funcionando hoy sin montarlo desde cero?

En Método Goana tienes todo lo que has visto en este tutorial listo para descargar e instalar:

  • Plantilla Google Sheets con las pestañas configuradas — posts, páginas, portfolio y WooCommerce incluidos
  • Mini-plugin listo para subir a WordPress en un click
  • Script preconfigurado con instrucciones exactas de qué cambiar
  • Vídeo de instalación de 15 minutos: de cero a funcionando
  • Guía de los errores más comunes y cómo resolverlos
Acceder a Método Goana →