# Manual para crear tu propia invitación — Madify Invita

Este documento explica cómo armar **tu propia página de invitación** (a mano o pidiéndosela a una
inteligencia artificial como Claude) para publicarla en Madify Invita. Tu página recibe los datos de tu
evento automáticamente y las confirmaciones de asistencia llegan a tu panel.

> **Si vas a usar una IA:** copia este documento completo en la conversación y pídele tu diseño. Al
> final hay un bloque «Prompt para la IA» listo para usar.

> Este manual incluye las opciones del diseño **Clásico dorado** (sección «Opciones del diseño»).

## 1. Qué vas a entregar

Un archivo **ZIP** con tu sitio:

```
mi-invitacion.zip
├── index.html        ← obligatorio (en la raíz del ZIP o dentro de UNA carpeta)
├── css/estilos.css
├── js/app.js
└── img/foto.webp
```

- `index.html` es la portada. Puedes tener más páginas (`galeria.html`); también reciben los datos.
- Usa **rutas relativas**: `img/foto.webp`, nunca `/img/foto.webp` ni rutas de tu computadora.
- Todo en **UTF-8**.

## 2. Reglas del ZIP

- Extensiones permitidas: avif, css, eot, gif, htm, html, ico, jpeg, jpg, js, json, m4a, map, mjs, mp3, mp4, ogg, otf, png, svg, ttf, txt, wav, webm, webmanifest, webp, woff, woff2.
- Máximo **50 MB** comprimido, **200 MB** descomprimido y **500 archivos**.
- Se ignoran `__MACOSX/`, `.DS_Store` y archivos ocultos. No se aceptan enlaces simbólicos ni rutas con `..`.
- Optimiza las imágenes (WebP o JPEG, menos de 400 KB cada una): la mayoría de tus invitados abrirá la invitación desde el celular.
- Si la página tiene un error de sintaxis en las variables, el sistema te dice el archivo y la línea.

## 3. Datos dinámicos (variables)

Tu HTML se procesa con el motor de plantillas de Django. Escribe `{{ variable }}` donde quieras que aparezca un dato y `{% if … %}…{% endif %}` para mostrar algo solo si existe.

Llaves principales:

| Variable | Qué es |
|---|---|
| `event` | Datos del evento (tabla «event») |
| `guest` | El invitado del link personal, o vacío en el link general (tabla «guest») |
| `rsvp` | La respuesta previa de ese invitado, o vacío (tabla «rsvp») |
| `rsvp_config` | Configuración del formulario de confirmación (tabla «rsvp_config») |
| `theme` | Solo en diseños del catálogo: las opciones que personaliza el cliente (`{{ theme.<clave> }}`) y `theme.design` (slug, name, version). Vacío en un ZIP propio |
| `now` | Fecha y hora actual del servidor |

### `event`

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `event.title` | texto | Título del evento | `Boda de Ana y Luis` |
| `event.slug` | texto | Path del evento en la URL | `boda-ana-y-luis` |
| `event.type` | texto | Tipo (clave): boda, xv_anos, bautizo, primera_comunion, baby_shower, cumpleanos, fiesta, graduacion, corporativo, otro | `boda` |
| `event.type_label` | texto | Tipo legible | `Boda` |
| `event.honorees` | texto | Festejado(s) | `Ana y Luis` |
| `event.hosts` | texto | Anfitriones / papás / padrinos | `Familia López` |
| `event.description` | texto | Mensaje principal (usa |linebreaksbr para saltos de línea) | `Queremos compartir contigo…` |
| `event.starts_at` | fecha y hora | Inicio del evento (usa el filtro date) | `{{ event.starts_at|date:"l j \d\e F" }} → sábado 24 de octubre` |
| `event.ends_at` | fecha y hora | Fin del evento (puede estar vacío) |  |
| `event.dress_code` | texto | Código de vestimenta | `Formal` |
| `event.contact_phone` | texto | WhatsApp del anfitrión (con lada) | `5214431234567` |
| `event.whatsapp_url` | URL | Link directo a WhatsApp del anfitrión (vacío si no hay teléfono) | `https://wa.me/5214431234567` |
| `event.rsvp_deadline` | fecha | Fecha límite para confirmar (puede estar vacía) | `{{ event.rsvp_deadline|date:"j \d\e F" }}` |
| `event.rsvp_open` | sí/no | Si todavía se puede confirmar | `True` |
| `event.allow_public_rsvp` | sí/no | Si se puede confirmar sin link personal | `True` |
| `event.allow_children` | sí/no | Si la invitación admite niños | `True` |
| `event.locations` | lista | Ubicaciones en orden (ver tabla de ubicaciones) | `{% for loc in event.locations %}…{% endfor %}` |
| `event.main_location` | ubicación | La primera ubicación (o vacío) | `{{ event.main_location.name }}` |
| `event.links` | lista | Links del evento (ver tabla de links) | `{% for link in event.links %}…{% endfor %}` |
| `event.gift_links` | lista | Solo los links de tipo «mesa de regalos» | `{{ event.gift_links.0.url }}` |
| `event.extra` | diccionario | Datos libres que captura el anfitrión | `{{ event.extra.hashtag }}` |
| `event.url` | texto | Ruta relativa de la invitación | `/boda-ana-y-luis/` |
| `event.public_url` | URL | Dirección completa de la invitación (dominio propio si lo hay) | `https://invite.madify.mx/boda-ana-y-luis/` |

### Cada ubicación (`event.locations`, `event.main_location`)

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `loc.kind` | texto | Clave: ceremonia, recepcion, evento, otro | `ceremonia` |
| `loc.kind_label` | texto | Tipo legible | `Ceremonia` |
| `loc.name` | texto | Nombre del lugar (puede estar vacío) | `Parroquia de San José` |
| `loc.address` | texto | Dirección | `Centro, Morelia` |
| `loc.maps_url` | URL | Link de Google Maps tal cual lo capturaron (puede estar vacío) | `https://maps.app.goo.gl/…` |
| `loc.maps_link` | URL | maps_url o, si está vacío, una búsqueda de Google Maps con lugar + dirección | `https://www.google.com/maps/search/?api=1&query=…` |
| `loc.starts_at` | fecha y hora | Hora de esta ubicación (puede estar vacía) | `{{ loc.starts_at|time:"g:i a" }}` |
| `loc.notes` | texto | Notas | `Estacionamiento en la parte trasera` |

### Cada link (`event.links`, `event.gift_links`)

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `link.kind` | texto | Clave: regalos, hospedaje, transmision, playlist, fotos, otro | `regalos` |
| `link.kind_label` | texto | Tipo legible | `Mesa de regalos` |
| `link.label` | texto | Texto del botón | `Ver mesa de regalos` |
| `link.url` | URL | Destino | `https://mesaderegalos.liverpool.com.mx/…` |

### `guest` (solo cuando se abre con link personal)

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `guest.name` | texto | Nombre de la invitación (persona, pareja o familia) | `Familia Pérez` |
| `guest.max_adults` | número | Lugares para adultos | `2` |
| `guest.max_children` | número | Lugares para niños | `1` |
| `guest.max_total` | número | Total de lugares | `3` |
| `guest.token` | texto | Código del link personal (?i=) | `VoVZx4P4BgNt` |

### `rsvp` (la respuesta previa de ese invitado, si ya confirmó)

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `rsvp.name` | texto | Nombre con el que confirmó | `Laura Pérez` |
| `rsvp.attending` | sí/no | Si asiste | `True` |
| `rsvp.adults` | número | Adultos confirmados | `2` |
| `rsvp.children` | número | Niños confirmados | `1` |
| `rsvp.message` | texto | Mensaje que dejó | `¡Felicidades!` |
| `rsvp.attendees` | lista | Acompañantes: name, type (adult/child), age | `{% for a in rsvp.attendees %}{{ a.name }}{% endfor %}` |
| `rsvp.updated_at` | fecha y hora | Última actualización de su respuesta |  |

### `rsvp_config`

| Variable | Tipo | Descripción | Ejemplo |
|---|---|---|---|
| `rsvp_config.endpoint` | URL | A dónde se manda la confirmación (POST JSON) | `/api/v1/public/events/boda-ana-y-luis/rsvp/` |
| `rsvp_config.token` | texto | Token del invitado (vacío en el link general) |  |
| `rsvp_config.mode` | texto | guest (link personal), public (link general), invite_only (solo con link personal) o closed (ya cerró) | `guest` |
| `rsvp_config.max_adults` | número | Máximo de adultos que puede confirmar quien abre la página | `2` |
| `rsvp_config.max_children` | número | Máximo de niños | `1` |
| `rsvp_config.allow_children` | sí/no | Si puede confirmar niños | `True` |
| `rsvp_config.deadline` | texto | Fecha límite ISO (o vacío) | `2026-10-15` |


### Opciones del diseño «Clásico dorado» (`theme`)

| Variable | Tipo | Descripción | Default |
|---|---|---|---|
| `theme.color_principal` | Color | Color principal | #b08d57 |
| `theme.color_fondo` | Color | Color de fondo | #fbf7f2 |
| `theme.tipografia` | Lista de opciones | Tipografía de títulos | serif |
| `theme.mostrar_flores` | Sí / no | Brillo decorativo en la portada | true |
| `theme.foto_portada` | Imagen | Foto de portada — Vertical, de preferencia sin texto encima. (1080×1350 px) | img/portada.svg |
| `theme.frase_superior` | Texto corto | Frase sobre los nombres | Nos casamos |
| `theme.mensaje` | Texto largo | Mensaje de bienvenida | Con la bendición de Dios y de nuestros padres, queremos compartir contigo este día. |

### Filtros útiles

```django
{{ event.starts_at|date:"l j \d\e F \d\e Y" }}   → sábado 24 de octubre de 2026
{{ event.starts_at|date:"g:i a" }}              → 1:00 p.m.
{{ event.starts_at|date:"c" }}                  → 2026-10-24T13:00:00-06:00 (para cuentas regresivas en JS)
{{ event.description|linebreaksbr }}            → respeta los saltos de línea
{{ event.honorees|upper }}                      → ANA Y LUIS
{{ guest.max_adults|pluralize:"es" }}           → «es» si es plural (lugar/lugares)
{{ event.extra.hashtag|default:"#AnayLuis" }}   → valor por defecto si está vacío
```

Ejemplos:

```django
<h1>{{ event.honorees }}</h1>
<p>{{ event.starts_at|date:"l j \d\e F" }} · {{ event.starts_at|date:"g:i a" }}</p>

{% if guest %}
  <p>¡Hola, {{ guest.name }}! Reservamos {{ guest.max_adults }} lugar{{ guest.max_adults|pluralize:"es" }}.</p>
{% endif %}

{% for loc in event.locations %}
  <h3>{{ loc.kind_label }}: {{ loc.name }}</h3>
  <p>{{ loc.address }}</p>
  {% if loc.maps_link %}<a href="{{ loc.maps_link }}">Ver mapa</a>{% endif %}
{% endfor %}

{% for link in event.gift_links %}<a href="{{ link.url }}">{{ link.label }}</a>{% endfor %}
```

**Si tu JavaScript usa llaves dobles** (Vue, Handlebars, etc.), enciérralo en `{% verbatim %}…{% endverbatim %}` para que no se procese.

## 4. Confirmación de asistencia

### Opción A — formulario listo

```django
<section id="confirmar">
  {% rsvp_form %}
</section>
{% rsvp_script %}   {# antes de </body> #}
```

Genera un formulario completo (nombre, asiste sí/no, adultos, niños, nombres de acompañantes, mensaje) que puedes estilizar con CSS: `.rsvp-form`, `.rsvp-field`, `.rsvp-submit`, `.rsvp-success`, `.rsvp-error`.

### Opción B — tu propio formulario

Pon `data-rsvp` en tu `<form>` y usa estos `name` en los campos:

| Campo (`name`) | Qué es |
|---|---|
| `name` | Nombre de quien confirma (obligatorio en el link general) |
| `attending` | Asiste: radio/select/checkbox con valores yes/no (también si/no, true/false) |
| `adults` | Número de adultos |
| `children` | Número de niños |
| `guests` | Total de personas (alternativa a adults/children; se cuentan como adultos) |
| `message` | Mensaje para los anfitriones |
| `phone` | Teléfono (opcional) |
| `email` | Correo (opcional) |
| `website` | Campo trampa anti-spam: agrégalo oculto y déjalo vacío |

Atributos que entiende el script:

| Atributo | Para qué |
|---|---|
| `data-rsvp` | En el <form>: lo conecta con la API (el endpoint sale de {% rsvp_script %}) |
| `data-rsvp-attendees` | Contenedor donde se generan los campos de nombre por cada adulto/niño |
| `data-rsvp-when-attending` | Visible solo si responde que sí asiste |
| `data-rsvp-when-declined` | Visible solo si responde que no |
| `data-rsvp-children` | Se oculta si la invitación no incluye niños |
| `data-rsvp-success` | Mensaje de éxito (se muestra al confirmar) |
| `data-rsvp-error` | Mensaje de error |
| `data-rsvp-closed` | Visible si la confirmación ya cerró |
| `data-rsvp-invite-only` | Visible si solo se acepta con link personal |
| `data-success-yes / data-success-no` | En el <form>: textos de agradecimiento personalizados |

```html
<form data-rsvp data-success-yes="¡Gracias! Nos vemos pronto." data-success-no="¡Te extrañaremos!">
  <input name="name" placeholder="Tu nombre" required>
  <label><input type="radio" name="attending" value="yes"> Sí asistiré</label>
  <label><input type="radio" name="attending" value="no"> No podré ir</label>
  <div data-rsvp-when-attending hidden>
    <input type="number" name="adults" min="1" value="1">
    <input type="number" name="children" min="0" value="0" data-rsvp-children>
    <div data-rsvp-attendees></div>
  </div>
  <textarea name="message"></textarea>
  <input type="text" name="website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
  <button type="submit">Enviar</button>
  <p data-rsvp-success hidden></p>
  <p data-rsvp-error hidden></p>
</form>
{% rsvp_script %}
```

- Con link personal (`?i=…`) el nombre se llena solo, se respetan los lugares del invitado y, si ya había confirmado, su respuesta aparece para modificarla.
- Sin link personal (link general) se pide el nombre y se respetan los límites del evento.
- Los límites se validan también en el servidor: no se puede confirmar de más aunque alguien modifique la página.

## 5. Página de ejemplo completa (mínima)

```html
<!doctype html>
<html lang="es-MX">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ event.title }}</title>
  <link rel="stylesheet" href="css/estilos.css">
</head>
<body>
  <header>
    <p class="eyebrow">{{ event.type_label }}</p>
    <h1>{{ event.honorees }}</h1>
    <p>{{ event.starts_at|date:"l j \d\e F \d\e Y" }} · {{ event.starts_at|date:"g:i a" }}</p>
    {% if guest %}<p class="hola">¡Hola, {{ guest.name }}!</p>{% endif %}
  </header>

  {% if event.description %}<section><p>{{ event.description|linebreaksbr }}</p></section>{% endif %}

  <section>
    {% for loc in event.locations %}
      <article>
        <h2>{{ loc.kind_label }}</h2>
        {% if loc.name %}<h3>{{ loc.name }}</h3>{% endif %}
        <p>{{ loc.address }}{% if loc.starts_at %} · {{ loc.starts_at|time:"g:i a" }}{% endif %}</p>
        {% if loc.maps_link %}<a href="{{ loc.maps_link }}" target="_blank" rel="noopener">Cómo llegar</a>{% endif %}
      </article>
    {% endfor %}
  </section>

  {% if event.gift_links %}
  <section>
    <h2>Mesa de regalos</h2>
    {% for link in event.gift_links %}<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.label }}</a>{% endfor %}
  </section>
  {% endif %}

  <section id="confirmar">
    <h2>Confirma tu asistencia</h2>
    {% if event.rsvp_deadline %}<p>Antes del {{ event.rsvp_deadline|date:"j \d\e F" }}.</p>{% endif %}
    {% rsvp_form %}
  </section>

  {% rsvp_script %}
</body>
</html>
```

## 6. Prompt para la IA

Copia y pega esto después de este manual, cambiando lo que está entre corchetes:

```text
Con base en el manual anterior, crea mi invitación digital para [tipo de evento: boda, XV años, bautizo…]
con estilo [describe: elegante, minimalista, floral, rústico, colores dorado y verde olivo…].

Requisitos:
- Entrégame el contenido de cada archivo y la estructura del ZIP (index.html en la raíz, css/, img/).
- Usa SOLO las variables del manual ({{ event.… }}, {{ guest.… }}); no escribas datos fijos de fecha, lugar o nombres.
- Incluye una sección de confirmación con {% rsvp_form %} (o un formulario data-rsvp) y {% rsvp_script %} antes de </body>.
- Diseño mobile first, que cargue rápido (sin frameworks pesados); fuentes de Google Fonts permitidas.
- No cargues scripts de terceros (salvo Google Fonts) ni envíes datos a otros servidores.
- Si usas JavaScript con llaves dobles, enciérralo en {% verbatim %}.
- Rutas relativas para imágenes y CSS. Indica qué imágenes debo agregar y de qué tamaño.
Secciones que quiero: [portada con nombres y fecha, cuenta regresiva, mensaje, ubicaciones con mapa, mesa de regalos, galería, confirmación…].
```

## 7. Antes de enviar (checklist)

- [ ] `index.html` en la raíz del ZIP y rutas relativas.
- [ ] Probaste la página abriéndola en el celular (los datos aparecen como `{{ … }}` hasta que se publica: es normal).
- [ ] La sección de confirmación usa `{% rsvp_form %}` o un `form data-rsvp` y `{% rsvp_script %}`.
- [ ] Imágenes optimizadas y el ZIP pesa menos de 50 MB.
- [ ] Sin scripts externos ni formularios que manden datos a otros sitios.

## 8. Cómo enviarlo

1. Entra a tu panel: https://invite-app.madify.mx/admin/ → **Mi sitio** → **Enviar sitio**.
2. Elige tu evento y sube el ZIP. Si algo no cumple las reglas, verás el error al momento.
3. Lo revisamos (normalmente en menos de 24 horas) y te avisamos por correo cuando esté publicado. Si necesita cambios, te decimos cuáles.

> Necesitas un plan que incluya **sitio propio**. Consulta los planes en https://invite.madify.mx/precios/.

## 9. Qué no se permite

- Scripts que capturen datos de tus invitados y los manden a otros servidores, o formularios que apunten a otros sitios.
- Contenido ilegal, engañoso u ofensivo, o que use marcas o imágenes sin permiso.
- Intentar leer archivos del servidor o romper el sistema (la página se rechaza).

¿Dudas? .
