# YoSoyVendedor Embed Module

Portable module for rendering a YoSoyVendedor-powered catalog from a small
Google Analytics-style snippet.

Source API documentation:
https://documenter.getpostman.com/view/4285719/2sBXwsKpe6

## Documented Merchant API

All documented calls are `POST https://api.yosoyvendedor.com/merchant/`. The
public embed authenticates with a `wco1` credential issued by `wao-credential-vault` with `module: catalog`; the catalog backend validates it server-to-server before calling the Merchant API.

| Module action | YoSoyVendedor operation | Notes |
| --- | --- | --- |
| `categories` | `categories` | Lists merchant categories. |
| `products` | `get-products` | Uses `items_page`, `page`, `url`, `order`, and `filter.brand_id`. |
| `item` | `item-data` | Uses `item` numeric id or `url` slug. |
| `business` | `business-data` | Gets merchant/business data. |

## Files

- `public/embed.js`: browser embed renderer.
- `public/product.php`: public product SEO endpoint and optional server-rendered detail wrapper.
- `public/seo-header.php`: server-side SEO header endpoint for installed catalog pages.
- `wao-gw-7c2/api.php`: PHP proxy from simple actions to the merchant API. The
  browser module always calls
  `https://plugins.waopay.app/catalog-m/wao-gw-7c2/api.php`; `apiBase` is not a
  public snippet option.
- `wao-gw-7c2/config.php`: endpoint, CORS, timeout, and optional server-side token.
- `snippet.example.html`: copyable snippet.

The renderer uses the same public class names as the Dipropol catalog/product
pages (`catalog-layout`, `catalog-sidebar`, `filter-option`, `product-card`,
`product-detail`, `product-gallery`, `product-specs`, `product-certs`) so it
inherits the site design when embedded in this project.

Catalog and product-detail images fill their containers with proportional cropping, so mixed image ratios do not leave empty space inside cards, gallery frames, or thumbnails.

Product detail navigation stays inside the same embed container. Product links
produce `product=` URLs for shareability/history, but clicks are intercepted and
resolved through the `item-data` API action. Because detail is in-container,
`detailUrl` is no longer needed in the snippet.

The `specs` and `certifications` fields from `get-products` and `item-data` are
decoded as Base64 JSON when present and used to enrich catalog cards and product
detail tables/badges.

## Basic Snippet

```html
<div id="waom-catalog"></div>

<script>
  window.WAOMEmbedConfig = {
    target: '#waom-catalog',
    credential: 'wco1.k2026.PASTE_THE_CATALOG_CREDENTIAL',
    itemsPage: 12,
    scrollOffset: 20,
    quoteUrl: '',
    seo: {
      shareBaseUrl: window.location.href,
      siteName: document.title || 'Catalogo'
    },
    theme: {
      primary: '#e8291c',
      buttonHover: '#c91f15',
      secondary: '#111111',
      accent: '#1565c0'
    },
    texts: {
      detailsButton: 'Ver detalles',
      addButton: 'Agregar a Cotizacion',
      cartButton: 'Ver cotizacion',
      sidebarButton: 'Ver cotizacion',
      relatedTitle: 'Productos Relacionados'
    },
    buttons: {
      add: { mode: 'quote', className: 'js-add-to-quote' },
      cart: { url: '' },
      sidebar: { enabled: true, url: '' }
    }
  };
</script>
<script async src="https://plugins.waopay.app/catalog-m/public/embed.js"></script>
```

## Customization

Pagination is enabled by default. Configure page size with:

```js
itemsPage: 12
```

Use `itemsPage` as a hard product limit without pagination:

```js
itemsPage: 4,
itemsPageMode: 'limit'
```

Use `itemsPageMode: 'paginate'` for the normal paginated behavior.

Set a fixed number of product columns, from 1 to 12. Use `0` for the automatic
responsive grid:

```js
productColumns: 4
```

The catalog sidebar and toolbar are enabled by default and can be controlled
independently:

```js
showSidebar: true,
showToolbar: true,
showViewToggle: true,
defaultCatalogView: 'grid'
```

When `showSidebar` is `false`, the category sidebar is not rendered and the
product grid uses the full width. When `showToolbar` is `false`, the product
count and search input are not rendered.

`showViewToggle` controls the grid/list icon selector inside the toolbar.
`defaultCatalogView` accepts `grid` or `list`.

Prices are enabled by default. They are rendered only when the product price is
greater than zero. Products with price `0` can still be shown and quoted by
custom catalog actions; checkout-m rejects them only when a purchase/cart flow is attempted:

```js
showPrices: true,
defaultCurrency: 'MXN',
priceLocale: 'es-MX'
```

Set `showPrices: false` to hide prices in catalog cards, related/featured
products, product detail, and Product schema. Product currency fields take
priority over `defaultCurrency`.

Internal module navigation scrolls back to the catalog container by default:

```js
scrollOnNavigate: true,
scrollOffset: 20,
scrollBehavior: 'smooth'
```

Theme colors are CSS variables applied inside the module shell:

```js
theme: {
  primary: '#e8291c',
  buttonHover: '#c91f15',
  secondary: '#111111',
  accent: '#1565c0',
  success: '#2f8f2f',
  text: '#111111',
  muted: '#667085',
  background: '#f5f7fa',
  surface: '#ffffff',
  border: '#e4e7ec',
  radius: '8px'
}
```

Visible text can be customized:

```js
texts: {
  detailsButton: 'Ver ahora',
  addButton: 'Agregar al carrito',
  cartButton: 'Ver carrito',
  backButton: 'Volver al catalogo',
  relatedTitle: 'Productos Relacionados',
  paginationPrev: 'Anterior',
  paginationNext: 'Siguiente'
}
```

Button actions can use defaults or custom functions:

```js
actions: {
  add: function (ctx) {
    // ctx.product, ctx.mode, ctx.config
    myCart.add(ctx.product);
  },
  cart: function (ctx) {
    // ctx.item, ctx.config
    openCartDrawer();
  },
  share: function (ctx) {
    // ctx.item, ctx.url, ctx.config
    navigator.clipboard.writeText(ctx.url);
  },
  details: function (ctx) {
    // Optional override. Omit to use in-container product detail.
    console.log(ctx.url);
  }
}
```

If no custom `actions.add` is provided, the module tries
`window.DipropolCart.addToCart(product)`. If that does not exist, it dispatches
`window` events named `waom:add`, `waom:cart`, `waom:share`, `waom:sidebar`, and `waom:details`
for external integrations. Legacy `YSVEmbedConfig`, `YSVEmbeds`, `[data-ysv-embed]`, `#ysv-catalog`, and `ysv:*` events are still accepted during the migration window.

Buttons accept extra classes and attributes for quote/cart integrations:

```js
buttons: {
  details: {
    text: 'Ver ahora',
    className: 'js-product-detail',
    attrs: { 'data-source': 'waom' }
  },
  add: {
    text: 'Agregar al carrito',
    mode: 'cart',
    className: 'js-add-to-cart',
    attrs: {
      'data-cart-type': 'shopping',
      onclick: 'window.myCartOpen && window.myCartOpen()'
    }
  },
  cart: {
    text: 'Ver carrito',
    url: '/cart',
    className: 'js-open-cart'
  },
  share: {
    enabled: true,
    text: 'Compartir',
    className: 'js-share-product'
  },
  sidebar: {
    enabled: true,
    text: 'Ver cotizacion',
    url: '',
    className: 'js-open-quote'
  }
}
```

The optional sidebar button appears at the bottom of the category filter.

## Product SEO

The JavaScript module updates product detail metadata when a product is opened:
title, description, canonical, Open Graph, Twitter tags, and Product JSON-LD.
The Product schema includes images, SKU, brand, category, material, specs, and
certifications when those fields are available from `item-data`.

Product detail links use the page where the module is mounted by default. To
force a specific catalog page URL, configure:

```js
seo: {
  shareBaseUrl: 'SITE_CATALOG_URL',
  siteName: 'SITE_NAME'
}
```

Social crawlers such as WhatsApp and Facebook do not execute the browser
snippet. For product-specific previews on the site URL, that page
must render the product meta tags server-side when `?product=...` is present.
The browser module can keep navigation and history in sync, but it cannot change
the HTML that crawlers receive after the page has already loaded.

Recommended: load the catalog SEO header from the server that renders the page
where the catalog is installed. This keeps the visible URL, canonical URL, and
Open Graph URL on the client domain, for example:

```txt
https://dominio.com/catalogo?product=4l-botella-1
```

The module endpoint is only an internal metadata provider:

```txt
https://plugins.waopay.app/catalog-m/public/seo-header.php?product=4l-botella-1&target=https%3A%2F%2Fdominio.com%2Fcatalogo&siteName=Mi%20tienda
```

It returns only `<head>` tags and sets `canonical` and `og:url` to the `target`
domain with the product query:

```html
<link rel="canonical" href="https://dominio.com/catalogo?product=4l-botella-1">
<meta property="og:url" content="https://dominio.com/catalogo?product=4l-botella-1">
```

Set `YSV_BEARER_TOKEN` or `wao-gw-7c2/config.php` `product_bearer_token` first.
If the token or product is missing, `seo-header.php` returns an empty response
so the client page head is not broken.

### Server-side SEO header examples

Add the generated tags inside the `<head>` of the page where the catalog is
installed.

PHP:

```php
<?php
if (!empty($_GET['product'])) {
    $seoUrl = 'https://plugins.waopay.app/catalog-m/public/seo-header.php'
        . '?product=' . urlencode((string) $_GET['product'])
        . '&target=' . urlencode('https://dominio.com/catalogo')
        . '&siteName=' . urlencode('Mi tienda');

    echo file_get_contents($seoUrl) ?: '';
}
?>
```

WordPress:

```php
add_action('wp_head', function () {
    if (empty($_GET['product'])) {
        return;
    }

    $url = add_query_arg([
        'product' => sanitize_text_field((string) $_GET['product']),
        'target' => home_url('/catalogo'),
        'siteName' => get_bloginfo('name'),
    ], 'https://plugins.waopay.app/catalog-m/public/seo-header.php');

    $response = wp_remote_get($url, ['timeout' => 5]);

    if (!is_wp_error($response)) {
        echo wp_remote_retrieve_body($response);
    }
});
```

Node.js / Express:

```js
app.get('/catalogo', async (req, res) => {
  let seoHead = '';

  if (req.query.product) {
    const url = new URL('https://plugins.waopay.app/catalog-m/public/seo-header.php');
    url.searchParams.set('product', req.query.product);
    url.searchParams.set('target', 'https://dominio.com/catalogo');
    url.searchParams.set('siteName', 'Mi tienda');

    const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
    seoHead = response.ok ? await response.text() : '';
  }

  res.render('catalogo', { seoHead });
});
```

Python / Flask:

```python
from flask import Flask, request, render_template
import requests

app = Flask(__name__)

@app.route('/catalogo')
def catalogo():
    seo_head = ''
    product = request.args.get('product')

    if product:
        response = requests.get(
            'https://plugins.waopay.app/catalog-m/public/seo-header.php',
            params={
                'product': product,
                'target': 'https://dominio.com/catalogo',
                'siteName': 'Mi tienda',
            },
            timeout=5,
        )
        if response.ok:
            seo_head = response.text

    return render_template('catalogo.html', seo_head=seo_head)
```

cURL test:

```bash
curl "https://plugins.waopay.app/catalog-m/public/seo-header.php?product=4l-botella-1&target=https%3A%2F%2Fdominio.com%2Fcatalogo&siteName=Mi%20tienda"
```

As a fallback for SEO and social sharing, you can use the server-rendered product
endpoint on the module domain:

```txt
https://plugins.waopay.app/catalog-m/public/product.php?product=4l-botella-1
```

Set `YSV_BEARER_TOKEN` or `wao-gw-7c2/config.php` `product_bearer_token` first. That PHP
page calls `item-data`, prints Open Graph and schema.org Product metadata
server-side, then mounts the same embed module.
If the server token is missing, social previews will fall back to generic
metadata because WhatsApp and other crawlers do not execute the browser snippet
or send the browser credential.

To share the module SEO endpoint instead of the site URL, configure:

```js
seo: {
  productBaseUrl: 'https://plugins.waopay.app/catalog-m/public/product.php',
  siteName: 'Catalogo'
}
```

The catalog still opens product detail in-place, but product card links point to
URLs such as
`https://plugins.waopay.app/catalog-m/public/product.php?product=4l-botella-1` so copied
or shared links have item-specific metadata. That server-rendered URL contains
the item title, image, short description, Open Graph tags, and Product schema in
the initial HTML.

The destination page URL, for example `/catalogo?product=...`, belongs to the
destination site. If that page is static, WhatsApp will only read its static
catalog metadata. Product-specific social previews on that URL require making
the destination site server-render its own product metadata.

The module SEO endpoint can also receive a target page for canonical/OG URL
metadata:

```txt
https://plugins.waopay.app/catalog-m/public/product.php?product=4l-botella-1&target=ENCODED_SITE_CATALOG_URL&siteName=SITE_NAME
```

That helps keep `og:url` pointed at the client site, but the shared URL itself
will still be the URL that the user copied unless the client site renders the
metadata directly.

## Detail View Options

Hide page elements while product detail is shown:

```js
hideOnDetail: ['.page-hero', '.footer-cta']
```

Breadcrumbs are enabled by default:

```js
breadcrumbs: {
  enabled: true,
  showCurrent: true,
  homeUrl: '/',
  catalogUrl: ''
}
```

Set `showCurrent: false` to omit elements with the
`waom-breadcrumb__current` class and their adjacent separator.

## Vault Credential

`catalog-m` requires `wao-credential-vault` validation. Configure the catalog module with:

```txt
CATALOG_CREDENTIAL_VAULT_URL=https://plugins.waopay.app/wao-credential-vault/api.php
CATALOG_CREDENTIAL_VAULT_MODULE_KEY=CATALOG_MODULE_KEY
CATALOG_CREDENTIAL_VAULT_MODULE=catalog
```

Generate a catalog credential from the vault with `module: "catalog"`. Do not reuse a `checkout` credential in this module; the vault will reject credentials whose module does not match.

The catalog browser API accepts only `credential`; raw Bearer tokens are not accepted through JavaScript configuration, request bodies, or Authorization headers.

## Migration

The API endpoint is fixed to:

```txt
https://plugins.waopay.app/catalog-m/wao-gw-7c2/api.php
```

To load the browser module, use:

```html
<script async src="https://plugins.waopay.app/catalog-m/public/embed.js"></script>
```
