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)
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.
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
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 /** * 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, ]); } } });
$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
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
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
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.
/* ═══════════════════════════ 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
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
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.
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.
$post_types del plugin.
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.
¿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

