WooCommerce headless no es una tienda nueva. wp-admin, stock, pedidos y TPV se quedan. El HTML que descarga el cliente ya no es un tema PHP. Astro 7 imprime el catálogo. El checkout, si no quieres reescribir Redsys, se queda en WooCommerce.
Una petición clásica de WooCommerce ejecuta PHP, la object cache, la lista de plugins y el tema, para una ficha de producto que no ha cambiado desde el martes. Una caché HTML delante del origin arregla buena parte de eso. Headless es el corte siguiente: el catálogo es un árbol estático, el carrito es una isla pequeña, el dinero sigue pegando contra el origin que ya tienes acotado para PCI.
Este artículo es el corte. Niveles de caché, TTFB y LCP de campo viven en Headless WooCommerce con Astro, rendimiento e-commerce. Los pillars comerciales son desarrollador WooCommerce y desarrollador Astro. La misma separación sin carrito está en la guía de arquitectura headless.
Portent midió una caída de conversión de alrededor del 4,42% por cada segundo extra de carga. Es una pendiente, no una promesa de que Astro imprime dinero. Vamos a headless cuando el tema y la lista de plugins son el techo, no cuando el briefing es una captura de PageSpeed.
En el Meetup WooCommerce de Madrid oímos el mismo briefing tres veces en 2025: «queremos headless para Core Web Vitals» y, dos sprints después, el TPV de Redsys seguía siendo el producto. El catálogo sí se puede sacar del PHP. El cobro, en España, casi nunca.
Qué se queda en WooCommerce
El back office no se mueve. Productos, variaciones, stock, cupones, pedidos, reembolsos, informes. Redsys, Bizum, Stripe, PayPal, Klarna. Envío por tablas, puntos de recogida, recargo de equivalencia si aún lo usas. Plugins de IVA e OSS. Conectores ERP que escriben al pedido. Eso es PHP, y debe seguir siendo PHP.
Lo que sale es el tema de tienda: header.php, la plantilla de producto, el widget del mini carrito, Elementor en la home, el zoom de imagen, el carrusel de «completa el look» que encola 200 KB para enseñar tres referencias.
Redacción sigue en el mismo wp-admin. Si no puede, no has ido a headless. Has montado un segundo PIM y lo vas a odiar al tercer mes. En una tienda de textil en Valencia el equipo de producto seguía publicando fichas en WooCommerce. El error fue darles también un CMS de catálogo «moderno» para traducir atributos. A la sexta semana pedían volver al editor de producto de siempre. Un solo origen de verdad de producto. El HTML es un consumidor, no un segundo almacén.
xmlrpc y la ruta pública de usuarios de REST necesitan el mismo endurecimiento que cualquier origin WordPress. Headless no esconde wp-admin. Pon Access o Zero Trust delante. La CDN del catálogo no es un cortafuegos.
Pedidos, notas internas, reembolsos parciales y el correo de «pedido recibido» salen de WooCommerce. El frontend Astro no firma esos correos. Si un agente de atención necesita cambiar un estado, lo hace en wp-admin, no en un panel React paralelo. Dos paneles de pedido es cómo se pierde un reembolso de Bizum.
Los informes de WooCommerce (y los que lee el ERP) siguen leyendo la tabla de pedidos. Headless no cambia el modelo de datos. Cambia quién imprime el HTML de la ficha. Si finanzas pide un CSV el viernes, sigue saliendo de PHP.
El plugin que muere con el tema
Este es el inventario que nadie quiere hacer, y es todo el riesgo.
Muere con el tema (se reconstruye como isla, o se tira):
- Swatches de variación que sustituyen el desplegable por puntos de color
- Vista rápida y añadir al carrito por Ajax en el loop
- Zoom de imagen y visores 360
- Popups de upsell, barras de escasez falsa, widgets de «otros también compraron» que pintan en
woocommerce_after_shop_loop - Page builders en la home y en landings de producto
- Plugins de checkout de una página que se apropian de las plantillas
checkout
Se queda (API o webhook, sin tema):
- Pasarelas de pago (Redsys, Bizum, Stripe, PayPal)
- Envío y recogida (SEUR, Correos, Correos Express, recogida local, tarifas por tabla)
- Impuestos (incluido OSS)
- Suscripciones y membresías, mientras el estado se consulta y no lo pinta el CSS del plugin
- Klaviyo / Mailchimp / Acumbamail por hooks de pedido
- ERP (ver el pillar de integración WooCommerce ERP)
Si los trucos de conversión de la tienda viven en la primera lista, headless es un rediseño, no un proyecto de velocidad. Presupuéstalo como rediseño. El alcance del desarrollador WooCommerce es honesto con ese corte. Hemos visto una tienda «pasar a headless» y reinstalar el tema antiguo en el checkout porque el plugin de una página era el producto de verdad. Eso son dos storefronts. No lo hagas.
Una prueba útil: en staging desactiva el tema y carga una URL de producto con un tema en blanco. Lo que sigue ahí (precio, stock, añadir al carrito vía Store API) es lo que te quedas. Lo que desapareció (swatches, badges, HTML del estimador de entrega) es trabajo. Si la lista desaparecida es la razón por la que compran, presupuesta islas. Si es decoración, tírala.
Productos compuestos, packs y add-ons están en la zona gris. Algunos exponen REST, otros solo filtran woocommerce_add_to_cart. Si añadir un pack al carrito da 404 en Store API, esa referencia no es catálogo estático hasta que alguien escriba el mapeo. No lo descubras la mañana del corte.
En una tienda de material deportivo en Zaragoza el «configurador» de talla y grabado era un plugin que enganchaba el loop y escribía en sesión PHP. Store API no conocía el grabado. El catálogo se publicó en Astro y el botón de compra devolvía un carrito con la talla y sin el texto. El arreglo no fue más GraphQL. Fue dejar esa ficha en PHP hasta tener un atributo real y un endpoint que lo acepte.
Los badges de «envío 24/48» que pinta el tema a partir de una meta son datos. Llévalos al payload de SSG. Los badges que pinta un plugin de escasez con un temporizador en JavaScript no son datos. Son un parche de conversión. En headless o los tiras, o los reescribes como isla con reglas que puedas explicar a legal (LSSI y publicidad engañosa no distinguen entre tema PHP y componente Astro).
Los shortcodes dentro de la descripción larga son el otro agujero. the_content de un producto no es un contrato. Si la ficha pega [delivery_estimator] o un formulario de tallas de un page builder, ese HTML espera un runtime PHP. O sustituyes el shortcode por un campo que Astro sepa pintar, o esa ficha no sale del tema.
Astro 7 para el catálogo
Astro 7 por defecto es HTML y CSS. JavaScript solo donde marcas una isla.
Catálogo que debe ser estático:
- Home, categoría, producto, contenidos, páginas legales de lectura
getStaticPathsdesde la lista de productos en el build- AVIF desde la biblioteca de medios,
sizesque coinciden con la rejilla, no100vwen una miniatura de 320 px
Islas:
- Añadir al carrito, cantidad, selector de variación
- Mini carrito
- Búsqueda que pega a un índice, no a PHP
?s= - Quizá Stripe Elements en un paso de pago a medida, si de verdad no puedes redirigir
client:visible en añadir al carrito. client:idle en la búsqueda. Una ficha que importa un pack entero de iconos dentro de la isla ya ha perdido el punto. La misma regla que cualquier otro front Astro 7: la isla es un componente, no una excusa para hidratar la página.
Rebuilds: un webhook updated de producto reconstruye esa ruta, o un conjunto pequeño (producto, sus categorías, la home si merchandising). Rebuilds de catálogo entero cada diez minutos son cómo se descubre que tienes 40.000 SKU. Incremental o bajo demanda. Cloudflare, o el host que ya usas. No «Vercel porque el tutorial lo decía» si el resto de la pila ya está en Cloudflare Pages.
Búsqueda: PHP ?s= es un hit PHP. En un catálogo estático, la búsqueda es un índice en cliente (catálogos pequeños), un índice externo (Algolia, Meilisearch, Typesense) alimentado con el mismo payload que el SSG, o una consulta Store API / REST desde una isla. No mandes la caja de búsqueda a WordPress si el resto de la tienda nunca lo hace. Las facetas que necesitan stock en vivo van junto a la caja de compra, no en el HTML del SSG.
Productos relacionados: si están merchandised en wp-admin, son datos. Consúltalos en el build. Si salían de woocommerce_output_related_products del tema, nunca fueron datos, fueron plantilla. Reconstrúyelos como franja estática de la misma categoría, o tíralos.
Las imágenes se quedan en la biblioteca de medios de WordPress. Astro lee URLs, o un paso de build copia AVIF. No inventes un segundo DAM salvo que ya lo tengas. La imagen LCP sigue siendo el hero de la ficha. Headless no elige el formato por ti. Un PNG de 3 MB en el origin sigue siendo un PNG de 3 MB en el edge. Eso se discute en la guía de rendimiento, no aquí.
La vista previa editorial necesita ver un borrador de producto en el host de Astro. Un webhook o un token de preview que pide un producto al origin basta. Si preview significa «entra en wp-admin y usa el personalizador del tema», no has terminado el corte, y van a seguir pidiendo el tema viejo.
El catálogo es un documento. El carrito es una aplicación. Mezclar las dos en el mismo runtime es cómo Next acaba hidratando la ficha para cambiar un desplegable de talla.
No hidrates reseñas, la tabla de tallas ni el bloque de envío «gratis a partir de» si esos datos ya venían en el payload de build. Una isla por ficha, no una SPA por ficha. El desarrollador Astro que trata cada sección como candidata a client:load está montando el tema PHP otra vez, con más pasos.
Store API frente a GraphQL
Dos tuberías. Mezclarlas es la herida autoinfligida habitual.
Build de catálogo: WPGraphQL + WooGraphQL, o el endpoint REST de productos, en tiempo de build. Una consulta por ficha. Quieres nombre, precios, estado de stock, imágenes, categorías, atributos. No quieres el carrito.
Carrito y sesión: WooCommerce Store API (/wp-json/wc/store/v1/cart). Posee la cookie del carrito, nonces, cupones, cálculo de envío. Esa cookie tiene que estar en el origin que más tarde recibirá el checkout. Si el sitio Astro es tienda.ejemplo.es y Woo es cms.ejemplo.es, tienes un problema de cookie antes de tener un problema de React. Mismo sitio, o un proxy documentado.
Vemos equipos añadir al carrito con mutaciones WooGraphQL porque el catálogo ya habla GraphQL. Luego la página de checkout en WooCommerce enseña el cesto vacío. La sesión no se ha movido. Store API para el carrito. GraphQL para el payload de SSG. Escríbelo en la pared del canal de desarrollo.
Cookies de mismo sitio: si Astro está en el apex y WordPress en wp., fija el dominio de la cookie del carrito de forma explícita y proxifica /wp-json/wc/store/ al origin. CORS en Store API no sustituye la cookie. SameSite=Lax en una redirección de checkout entre dos hosts parece «Klarna funciona en staging» (mismo host) y «carrito vacío en producción» (dos hosts). Un hostname para quien compra, hostname interno para wp-admin.
El nonce que caduca en una página estática es un bug real. La isla del carrito tiene que pedir un nonce fresco a Store API al hidratar, no hornear uno en el HTML del SSG de esta mañana. Los nonces horneados responden 403 a las pocas horas y el botón de añadir al carrito «no hace nada». En una tienda de cosmética en Barcelona ese síntoma se diagnosticó como «Astro está roto». No estaba roto. El HTML de las 06:00 llevaba un nonce de las 06:00.
El stock en el build es una foto. Una talla agotada que sigue en el HTML es una devolución. El webhook woocommerce_product_set_stock (o el equivalente de variación) tiene que invalidar esa ruta. Si no puedes hacerlo, no hagas SSG de la caja de compra. Píntala desde Store API en el momento de la petición, todavía como isla pequeña. El HTML del catálogo puede seguir estático.
Los cupones viven en Store API. Un código pegado en un banner estático está bien como merchandising. Aplicarlo es un POST al carrito, no un campo en el SSG. Lo mismo con el cálculo de portes: una tabla de península en la ficha es texto. La tarifa legal sale en checkout, con código postal. No congeles portes por provincia en HTML salvo que también congeles rutas por provincia y sepas invalidarlas.
GraphQL brilla cuando la ficha necesita producto, variaciones, atributos, imagen y tres relacionados en una sola ida. REST brilla en webhooks y escrituras. La Store API brilla porque ya habla el idioma del carrito. Tres herramientas, tres trabajos. Un cliente Apollo que muta el carrito es un cuarto trabajo que no pediste.
Redsys, Bizum y el TPV que se queda en WooCommerce
Esta es la decisión de operación que en España parte una demo de una tienda que sigue cobrando el mes que viene. El default es híbrido: catálogo estático, carrito isla, checkout PHP.
Redsys no es un botón que copias a una isla. Es un TPV virtual con número de comercio, terminal, clave SHA-256, URL de notificación y retorno del titular. El plugin (el de la entidad, el de aplazame, el que sea que ya tenéis en wp-admin) firma la operación en PHP, redirige al SIS y espera un POST al origin. Ese POST no puede caer en la CDN de Astro. Si «terminas el headless» reescribiendo /checkout/ en Astro, reimplementas:
- Campos de dirección y validación de provincia y código postal
- Métodos de envío que cambian con el CP (península, Baleares, Canarias si las sirves)
- IVA y recargo
- El JS y la redirección de Redsys, más 3-D Secure
- Bizum, que en la mayoría de comercios españoles viaja como método dentro de Redsys, no como widget suelto
- Reintentos de pago fallido y enlaces de «pagar pedido» que salen del correo
- La URL de notificación (
/?wc-api=o la del plugin) que marca el pedido como pagado
Hemos hecho checkout a medida cuando una marca necesitaba un paso que ninguna plantilla PHP aguantaba. Es un producto. No es el default. El default es redirigir a /checkout/ con la cookie de Store API ya puesta, caché HTML en bypass, y cada plugin de pasarela, de portes y de «pedido recibido» disparando como ayer.
Bizum, para quien compra en el móvil en España, no es un detalle. Es el método que espera ver junto a tarjeta. Mientras Bizum viva en el plugin de Redsys (o en Stripe, o en Adyen), el sitio correcto para mostrarlo es el checkout de WooCommerce. Pintar un botón Bizum en una isla sin el contrato del TPV es un prototipo. El pedido se queda en «pendiente de pago» y atención llama a desarrollo.
La factura no sale de Astro. El origen de la factura se queda en PHP: el pedido cambia de estado en WooCommerce y el conector de facturación (ERP, Holded, el que ya tengáis) emite. Verifactu habla con ese origen, no con el HTML del catálogo. Si el frontend genera un PDF «para que se vea más bonito», tienes dos documentos y uno solo es el que quiere la AEAT. No dupliques el origen.
Los widgets de SEUR, Correos y Correos Express (mapa de oficina, punto de recogida, número de bultos) suelen inyectarse en las plantillas de checkout. Mueren el día que sales de esas plantillas. En híbrido se quedan. Si insistes en un checkout Astro, reconstruyes el widget contra la API del transportista, con el mismo contrato de CP y de peso que ya calcula Woo. Eso no es un sprint de «poner un mapa». Es un segundo producto de envíos. La mayoría de tiendas no lo necesita.
La LSSI (Ley 34/2002) pide identificar al prestador: razón social, NIF, domicilio, correo, datos de inscripción registral. Esos textos legales viven junto al acto de contratar, no junto al hero del catálogo. El checkout de WooCommerce ya los enseña (condiciones, privacidad, identificador del prestador). Déjalos ahí, en una sola copia que legal y finanzas pueden editar sin un deploy de Astro. Un CIF congelado en el SSG de marzo es un CIF mentiroso en agosto, cuando la sociedad cambia de domicilio o de administrador. El footer estático puede repetir lo mínimo. El acto de compra lleva el texto vivo.
PCI se queda donde estaba. El titular introduce la tarjeta en el SIS de Redsys o en Elements de Stripe, no en tu origin y no en una isla que reenvía PAN. El híbrido no ensancha el alcance. El checkout a medida, sí, salvo que copies con milímetro el flujo alojado que ya tenías.
Webhooks del TPV (/?wc-api=, notificaciones Redsys, Stripe) pegan al origin, no a la CDN de Astro. Cada ruta de skip de caché tiene nombre. La misma disciplina que un WAF: si no tiene nombre, alguien la va a cachear. El detalle de subredes de Redsys, Canarias e IGIC no cabe aquí; este texto se queda en la topología: el TPV es PHP, el catálogo no.
IVA y OSS se calculan en origin. Un precio en EUR en la ficha es precio de merchandising. La línea de checkout es la legal. No hagas SSG por país salvo que también hagas rutas por país y sepas invalidarlas. La mayoría de tiendas enseña un precio de catálogo y deja que Woo aplique el impuesto al pagar. Eso vale. Enseñar un IVA español congelado a un huésped en Francia es cómo te escribe finanzas.
«Pedido recibido» y «Mi cuenta» pueden seguir en WooCommerce. Headless no significa «cada URL es Astro». Significa que las URLs que se anuncian son archivos. /mi-cuenta/ detrás de Access o detrás de login está bien como PHP.
Una tienda de iluminación en el área de Barcelona cortó el PHP de la ficha, dejó Redsys en /checkout/ y el argumento de conversión era «la ficha es el rebote». Lo era. El checkout no lo era. No «termines el headless» reescribiendo la única plantilla que ya cobra.
Stock y cache
Los archivos estáticos en una CDN escalan. El stock no. Dos clientes comprando la última unidad es un problema de tienda, no de PageSpeed.
El patrón que aguanta:
- El HTML del catálogo es estático, precio incluido, hasta que un webhook dice lo contrario.
- La caja de compra pregunta a Store API el
stock_statusal hidratar la isla, o aceptas una ventana corta de dato viejo y compensas en checkout (WooCommerce ya rechaza el sobreventa si lo configuraste). - La creación del pedido es origin. ERP y correo disparan ahí.
«Usuarios concurrentes ilimitados» es cierto para el HTML. Es falso para el checkout, para el stock, para la base de datos. Dimensiona PHP para el pico de checkout que de verdad tienes (un drop, una newsletter, el puente de diciembre, el 24 de noviembre). La CDN no pasa la tarjeta.
Una caché HTML delante de un tema clásico sigue siendo la primera palanca. Si el TTFB ya es bajo y el LCP es la imagen hero, lee el hermano de rendimiento. Headless no arregla un PNG de 3 MB.
El oversell de una talla 42 en una campaña de noviembre no se discute en el canal de frontend. Se discute en el de operaciones: ¿el webhook llegó, la ruta se invalidó, la isla preguntó, el checkout rechazó? Cuatro trampas, cuatro dueños. Si solo mides LCP, no ves la talla 42.
Los transients de stock en object cache y el HTML estático son dos relojes. Sincronízalos por evento (webhook), no por cron de diez minutos. El cron es cómo una talla agotada a las 10:03 sigue «añadir al carrito» hasta las 10:10. En drop de sneakers eso son pedidos que luego cancelas a mano.
Cloudflare (o el host que uses) puede reconstruir una ruta. No puede reconstruir el almacén. El ERP de la integración WooCommerce sigue siendo la verdad de existencias si así lo habéis pactado. WooCommerce es el esclavo que el checkout consulta. Astro no es un tercer almacén.
Cuándo no ir a headless
- Catálogo de unas pocas docenas de SKU, sin problema de tráfico, ediciones en el personalizador cada semana
- El storefront es la lista de plugins (builders, checkout de una página, chat embebido en el tema)
- Nadie del equipo va a ser dueño del repo de Astro después del lanzamiento
- El briefing es «PageSpeed 100» y la ficha ya tiene caché HTML y AVIF
Entonces: dieta de plugins, caché HTML, pipeline de imagen, hueco de consentimiento. Más barato, reversible. Headless es un corte de sistemas. Ahora tienes dos deploys, dos previews, dos modos de fallo. Compensa cuando el tema es el cuello y el catálogo es sobre todo lectura.
Suscripciones y membresías: el derecho vive en Woo. La página Astro puede enseñar «tienes acceso» si puede preguntar. Si el plugin de membresía solo pinta envolviendo the_content en PHP, ese contenido no está en tu SSG. O lo expones, o dejas esas URLs en WordPress.
Una tienda B2B en L’Hospitalet con precios por rol, mínimos de pedido y pago a 30 días no es un catálogo estático con una isla. El precio que ve el comprador depende de una sesión. Astro puede imprimir la ficha genérica. La caja de compra, el precio neto y el checkout se quedan en PHP, o montas un runtime por petición (y entonces pregúntate por qué llamas a esto headless).
Si marketing publica landings de campaña en Elementor cada jueves y espera verlas en producción el viernes, un catálogo Astro les quita esa palanca. O les das un contrato de bloques con preview, o dejas las landings en WordPress y solo sacas /producto/ y /categoria/. Headless por URL, no por religión.
Nadie dueño de TypeScript más un autónomo PHP que «ya se apañará el front» es la cola de incidencias del trimestre. Dos pipelines sin dueño no es arquitectura.
Secuencia de migración
- Inventario. Lista de plugins en se queda / muere / desconocido. Lo desconocido es un spike, no una sorpresa el día del corte.
- API en la tienda viva. Store API ya está en WooCommerce moderno. GraphQL si el build del catálogo lo quiere. El cliente no ve nada.
- Catálogo Astro en un host de staging, datos reales de producto, aún sin checkout. Compara permalinks. Aquí salen las peleas de barra final y de base de categoría, no en la semana ocho.
- Isla de carrito + cookie contra el origin de staging. Añadir, cupón, vaciar. Entonces, y solo entonces, enlaza el checkout.
- Checkout sigue en Woo. Bypass de caché. Pasarelas en sandbox de verdad (Redsys de pruebas, no un mock). Correos de pedido. Camino de reembolso. Un pago Bizum de 1 € en el terminal de test no es opcional.
- SEO. Mapa de 301, canónicas, JSON-LD de producto desde los mismos campos que la página, sitemap de URLs Astro. Inspecciona una URL de producto antes del corte. No inventes lastmod en 10.000 SKU.
- Corta el DNS del catálogo. Deja
wp-adminy checkout en origin. Mira los webhooks de stock una semana.
El blue-green del catálogo es fácil (son archivos). El blue-green del checkout es el de hoy: ya tienes uno. No inventes un segundo.
La semana del corte es sobre todo DNS y 301, no React. Deja una ventana corta en la que el tema viejo aún responde a un bypass de cabecera para comparar una referencia. El momento en que los dos storefronts añaden al carrito contra la misma Store API, duplicarás líneas desde pestañas viejas. Drena el tema antiguo: 301, luego 410 en los assets del tema, luego bórralo. Dual-run «por si acaso» es cómo vuelve el personalizador.
Mapea también los feeds. Google Merchant, Facebook Catalog, el XML que come el ERP: si leían permalinks del tema, ahora leen permalinks de Astro, o un 301. Un feed que sigue apuntando a una ficha del tema muerto con ?add-to-cart= es tráfico que no convierte.
Los cupones de campaña que marketing tenía en landings Elementor hay que recrearlos como URLs Astro o dejar esas landings en WordPress. No las pierdas en el corte porque «no eran catálogo». Eran adquisición.
Auditoría y lecturas
LCP de campo, niveles de caché y peso del payload de Store API: guía de rendimiento. El corte de sistemas: esta página. El pillar de desarrollador WooCommerce es el trabajo comercial de catálogo. Desarrollador Astro es el front. La integración ERP es lo que no puede romperse cuando muere el tema. La arquitectura headless es el mismo corte sin carrito.
El Handbook de WooCommerce (Store API, checkout blocks) y el canal #woocommerce de Make WordPress Slack son las referencias de contrato, no un curso genérico de «headless commerce». Redsys publica la documentación del TPV virtual en su portal de comercio; léela antes de pintar un botón Bizum en el front.
Conclusión
WooCommerce como tienda. Astro 7 como catálogo. Carrito como isla sobre Store API. Checkout en PHP. Los plugins que pintaban el tema se reconstruyen o se tiran. Los plugins que llevaban el negocio se quedan. GraphQL construye páginas. GraphQL no sostiene el cesto. En España, Redsys y Bizum son el argumento más corto para no sacar el TPV del origin.
Escribe con la lista de plugins y con si el checkout puede quedarse en WooCommerce. Si la respuesta es «tiene que verse idéntico al checkout de una página del tema», estás comprando un rediseño. La superficie de desarrollador WooCommerce es donde empieza ese alcance. El front, cuando el alcance es catálogo, está en desarrollador Astro.
La ficha rápida, el carrito pequeño, el cobro aburrido. Ese es el corte que aguanta un Black Friday en península. El resto es teatro de arquitectura. Si el tema ya entrega HTML de ficha en un tiempo que Portent no castigaría, no desacoples para una captura. Desacopla cuando el PHP de la ficha es el techo y el TPV, de hecho, puede quedarse donde está.






