Bloque de agregar al calendario
Un bloque de "agregar esto a mi calendario" de un solo toque, y la solución práctica para invitaciones entre zonas horarias distintas. Un correo enviado no tiene JavaScript para detectar la zona horaria propia de un destinatario, así que este bloque codifica el inicio y el fin del evento como un instante absoluto en lugar de una cadena de hora local precalculada. De esa forma, la app de calendario de cada asistente lo muestra correctamente en su propia hora local de manera automática.
Cómo funciona
El bloque es contenido puro, como Sede: no hay nada que responder, así que nunca aparece en el panel de respuestas ni en la exportación CSV. Los tres enlaces que renderiza comparten el mismo espacio genérico de seguimiento de clics que usa cualquier bloque simple con enlaces, en lugar de una capacidad hecha a medida.
Los enlaces están condicionados a tener un startsAt válido: un evento sin hora de inicio definida renderiza solo el texto de título y descripción, sin enlaces muertos que no llevan a ningún lado. Si startsAt está definido pero endsAt no lo está (o resuelve a algo anterior a startsAt), el bloque establece por defecto la duración en una hora después del inicio — completar solo "cuándo empieza" se trata como el caso común, y todo consumidor posterior (el archivo .ics, Google Calendar, Outlook.com) necesita que exista algún instante final. location es un campo de texto libre simple que completas a mano a partir del propio texto de tu bloque de Sede o Enlace de reunión; los tres bloques no están vinculados automáticamente en esta primera versión, así que actualízalo tú mismo si cambia la sede.
A partir de esos props se renderizan tres enlaces, que comparten un solo blockId para efectos de seguimiento de clics (el mapa de calor agrupa por bloque, no por enlace individual, la misma convención que usa Enlaces sociales para sus varios íconos):
- Descargar .ics — un enlace firmado hacia
GET /api/calendarque transmite un archivo RFC 5545 (iCalendar) construido a mano. No hay ninguna dependencia de npm detrás — un archivo.icses texto plano, construido de la misma forma "totalmente desde cero" que el codificador de GIF de cuenta regresiva. Los camposDTSTART/DTENDdel archivo se emiten como instantes UTC absolutos (unaZfinal, sin necesidad de un componenteTZID/VTIMEZONE) — esa es la corrección real para las zonas horarias, no algo calculado en el momento de renderizar, sino una consecuencia de questartsAt/endsAtdel bloque ya se almacenan como milisegundos de época desde el principio. ElUIDdel evento es un hash sha256 determinístico de su título/descripción/ubicación/inicio/fin, así que volver a descargar el mismo enlace firmado actualiza una entrada de calendario en lugar de crear un duplicado. El texto de escritura libre se escapa según el RFC 5545 §3.3.11 (barras invertidas, comas, puntos y comas, saltos de línea literales) y las líneas largas se pliegan en el límite de 75 octetos que exige la especificación, así que la descripción larga de un organizador no puede producir un archivo técnicamente inválido. La URL en sí está firmada de la misma forma que los endpoints de cuenta regresiva/fórmula/gráfico — toda la carga del evento se serializa en la cadena de consulta (sin lectura de Firestore en la ruta GET), separada por dominio de cualquier otra familia de URL firmada en la app mediante un prefijo"calendar:"incorporado en la entrada del HMAC, así que una firma acuñada para este endpoint nunca puede verificarse contra otro. - Agregar a Google Calendar — un enlace directo simple a
calendar.google.com/calendar/render?action=TEMPLATE&...construido en el momento del envío, sin ida y vuelta al servidor. - Agregar a Outlook.com — un enlace directo simple a
outlook.live.com/calendar/0/action/compose?..., también construido en el momento del envío.
El título, la descripción y la ubicación se recortan de forma defensiva a longitudes máximas fijas (200 / 2000 / 300 caracteres) antes de que se construya cualquiera de los tres enlaces, así que un campo de texto libre muy largo no puede producir un enlace .ics roto ni una cadena de consulta demasiado grande en los dos enlaces web.
Campos configurables:
- Título del evento.
- Descripción (opcional).
- Empieza en — obligatorio para que se renderice cualquier enlace.
- Termina en (por defecto, 1 hora después del inicio).
- Ubicación (opcional, texto libre).
- Color de fondo, relleno, borde y radio de borde en la tarjeta.
- Color/tamaño del título del evento, y las anulaciones compartidas de color/fondo de los tres enlaces.
Ejemplos
Subject
Save the date: our fall meetup
Todo proyecto de propósito "Evento" empieza con un bloque de Agregar al calendario ya colocado junto a Enlace de reunión, Sede y RSVP — mira Eventos y webinars para el patrón completo de invitación a recordatorio del que forma parte este bloque. Un club deportivo reutiliza el mismo bloque semana tras semana para los horarios de práctica y partidos, así el calendario de cada familia refleja correctamente el horario sin importar en qué zona horaria estén viajando — mira Equipos y clubes deportivos. Una serie de webinars puede incluir esto en un correo de confirmación enviado justo después del registro, así el destinatario lo agrega a su calendario en el mismo momento en que se registró, en lugar de depender de que lo recuerde después.
Cómo se ve la alternativa estática
<div style="margin:12px 0;background-color:transparent">
<p style="margin:0 0 8px;font-weight:600;font-size:16px;color:#111827">Q3 Product Launch</p>
<a href="https://mailinapp.com/api/calendar?d=eyJ0aXRsZSI6...&s=abc123..."
style="display:inline-block;padding:10px 20px;background-color:#4f46e5;color:#ffffff;
border-radius:6px;text-decoration:none;font-size:14px;font-weight:600;margin:0 8px 8px 0">
Download .ics
</a>
<a href="https://calendar.google.com/calendar/render?action=TEMPLATE&text=Q3+Product+Launch&dates=20260901T170000Z%2F20260901T180000Z"
style="display:inline-block;padding:10px 20px;background-color:#4f46e5;color:#ffffff;
border-radius:6px;text-decoration:none;font-size:14px;font-weight:600;margin:0 8px 8px 0">
Add to Google Calendar
</a>
</div>
Esto es todo el bloque enviado: tres enlaces simples (el tercero, Outlook.com, omitido aquí por brevedad) construidos a partir de los mismos detalles del evento, sin ninguna imagen ni contenido condicionado de por medio.