Blog-CRO Article Page — Shopify + React Islands
Stack técnico
- Base: Shopify Theme (Online Store 2.0), Liquid — sections, templates, schema con bloques/settings
- UI dinámica: React 18 montado tipo "islands" con data-react-component="ComponentName" en el div raíz de cada sección
- Build: esbuild (npm run build) → assets/main.jsx → assets/main.js
- Estilos: CSS tradicional en assets/main.css con naming BEM. Adapta los estilos al design system existente del proyecto (variables --color-primary, --color-text, --color-bg-alt, --font-heading, --font-body, --radius-card, etc.)
- Para datos complejos (producto), usa el patrón ya establecido: <script id="blog-cro-data-{{ section.id }}" type="application/json">{ ... }</script> y lee con JSON.parse(document.getElementById(id).textContent) dentro del componente React
- Bilingual: usa el hook assets/components/hooks/useLanguage.jsx y campos _en en el schema, igual que el resto del tema
Estructura general de la página
Template: templates/article.json
Secciones en orden:
1. header (existente, ya registrado)
2. blog-cro-hero — encabezado del artículo
3. blog-cro-body — layout 2 columnas: artículo izquierda + panel sticky derecha
4. collection-carousel — primer carrusel de productos (existente, reutilizar)
5. collection-carousel — segundo carrusel (segunda instancia, misma sección)
6. footer (existente)
SECCIÓN 1 — Blog CRO Hero (sections/blog-cro-hero.liquid)
Sección Liquid pura (sin React). Usa directamente el objeto article de Shopify.
Estructura visual (de arriba a abajo):
- Breadcrumb — texto pequeño: "Blog / {article.title}" con links. Tipografía body 12px, color secondary.
- Category badge — pill/chip con el tag principal del artículo (primer tag que no sea "featured"). Background --color-bg-alt, texto uppercase pequeño, border-radius pill.
- Título — h1 con article.title. Tipografía heading, clamp(32px, 4vw, 56px), weight 900, line-height 1.0, letra tight. Máximo 3 líneas.
- Meta row — fila horizontal: avatar del autor (si existe article.author) + nombre del autor + fecha formateada + separador "·" + tiempo estimado de lectura (calcula palabras / 200, mínimo "1 min read"). Tipografía body 13px, color secondary.
- Imagen destacada — article.image a ancho completo, aspect-ratio 16/9, object-fit cover, border-radius --radius-card. Si no hay imagen, no renderizar el elemento (no placeholder vacío).
Schema settings:
- show_breadcrumb checkbox (default true)
- show_read_time checkbox (default true)
- read_time_label text / read_time_label_en text
- author_label text "Por" / author_label_en text "By"
SECCIÓN 2 — Body + Panel Sticky (sections/blog-cro-body.liquid)
Layout 2 columnas. Columna izquierda: Liquid puro. Columna derecha: React BlogCroPanel.
CSS del layout:
.blog-cro-layout { display: flex; gap: 60px; align-items: flex-start; max-width: var(--max-width); margin: 0 auto; padding: 60px var(--padding-x); }
.blog-cro-article { flex: 0 0 60%; min-width: 0; }
.blog-cro-panel-col { flex: 0 0 calc(40% - 60px); }
Columna izquierda — Contenido del artículo (Liquid):
Renderiza {{ article.content }} directamente en .blog-cro-article__content. Estilos tipográficos:
- p, li: font-size 16px, line-height 1.7, color --color-text-secondary
- h2, h3, h4: font-family heading, weight 800, color --color-text, margin-top 32px
- img: max-width 100%, border-radius --radius-card, display block
- a: color --color-primary, text-decoration underline
- blockquote: border-left 3px solid --color-primary, padding-left 20px, font-style italic
- strong: font-weight 700, color --color-text
Columna derecha — Data bridge en Liquid:
{%- assign panel_product = section.settings.panel_product -%}
{%- assign panel_variant = panel_product.selected_or_first_available_variant -%}
Script con id="blog-cro-panel-{{ section.id }}" type="application/json" conteniendo: product, selectedVariant, shopCurrencySymbol, whatsappPhone, whatsappMessageTemplate, whatsappMessageTemplateEn, ctaLabel, ctaLabelEn, atcLabel, atcLabelEn, panelTitle, panelTitleEn.
Div con data-react-component="BlogCroPanel" y data-panel-id="blog-cro-panel-{{ section.id }}".
COMPONENTE React — BlogCroPanel (assets/components/BlogCroPanel.jsx)
Lee el JSON con JSON.parse(document.getElementById(panelId).textContent).
El panel tiene position: sticky; top: 120px. Fondo blanco, border: 1px solid var(--color-border), border-radius: var(--radius-card), padding 24px.
Estructura interna (de arriba a abajo):
1. Panel header (solo si panelTitle está definido): texto pequeño uppercase bold, color secondary.
2. Product image: selectedVariant.featured_image.src o product.featured_image. aspect-ratio 4/5, object-fit cover, border-radius --radius-card. Hover: transform: scale(1.03), transition 0.3s ease.
3. Product info: product.title (heading 18px weight 800). Precio: currencySymbol + (price/100).toFixed(2). Si compare_at_price > price: precio original tachado + precio actual destacado.
4. WhatsApp CTA: full-width, fondo #25D366, texto blanco, height 50px. Icono SVG de WhatsApp inline. Construye URL con wa.me usando el template: "Hola! Quiero pedir: {product} — {price}. Link: {url}". Limpia el teléfono con .replace(/\D/g,''). Hover: filter brightness(1.08).
5. Add to Cart: full-width, fondo --color-text, texto blanco, height 48px. Estados: idle → loading (spinner) → success ("¡Agregado!") → idle (1.5s). fetch('/cart/add.json'). En success: document.dispatchEvent(new CustomEvent('dissent:cart-updated')).
6. Product link: "Ver producto →" centrado, 12px, color secondary, text-decoration underline.
SECCIONES 4 y 5 — Colecciones
No crear código nuevo. Solo añadir al article.json:
"collection-1": { "type": "collection-carousel", "settings": { "heading": "Productos en este artículo", "view_all_label": "Ver todo" } }
"collection-2": { "type": "collection-carousel", "settings": { "heading": "También te puede gustar", "view_all_label": "Ver todo" } }
Schema settings de blog-cro-body.liquid:
- panel_product: product picker
- panel_title / panel_title_en: text
- whatsapp_phone: text (número sin +, ej: "573001234567")
- whatsapp_message / whatsapp_message_en: textarea (placeholders: {product} {variant} {price} {url})
- cta_label default "Comprar por WhatsApp" / cta_label_en default "Buy on WhatsApp"
- atc_label default "Agregar al Carrito" / atc_label_en default "Add to Cart"
COMPORTAMIENTOS Y MICRO-ANIMACIONES
- Panel: position sticky top 120px en desktop. En mobile: position static.
- WhatsApp hover: filter brightness(1.08), transition 0.2s ease.
- ATC success: animación scale(0.97) → scale(1).
- Imagen hover: transform scale(1.03), overflow hidden en wrapper.
RESPONSIVE (Mobile ≤809px)
.blog-cro-layout { flex-direction: column; padding: 32px var(--padding-x-mobile); gap: 40px; }
.blog-cro-article, .blog-cro-panel-col { flex: none; width: 100%; }
.blog-cro-panel { position: static; }
El panel aparece debajo del artículo completo en mobile.
NOTAS IMPORTANTES
1. Reutilizar — no recrear: CollectionCarousel, useLanguage, useInView ya existen. No crear versiones nuevas.
2. Solo tocar main.jsx: añadir import BlogCroPanel from './components/BlogCroPanel.jsx' y registrar BlogCroPanel en el objeto REGISTRY.
3. article.content se renderiza con Liquid — no pasarlo a React.
4. El panel no tiene selector de variante — usa selected_or_first_available_variant directamente.
5. Limpiar el teléfono con .replace(/\D/g,'') antes de construir el wa.me URL.
6. @media (prefers-reduced-motion: reduce): desactivar todas las transitions y animations.