Docs Documentación

Referencia del sistema de inserción

Referencia técnica para el sistema de inserción mediante iframe de Telar.

Resumen de arquitectura

El sistema de inserción de Telar permite que las historias se muestren dentro de iframes en sitios web externos y plataformas LMS. El sistema está diseñado en torno a estos principios:

Contenedores de altura fija:

Sin redimensionamiento dinámico:

Compatibilidad universal:

Detección del modo embed

Parámetro de URL

El modo embed se activa mediante el parámetro de URL ?embed=true:

https://tusitio.com/stories/story-1/?embed=true

Manejo del parámetro:

Implementación JavaScript

Archivo: assets/js/embed.js

(function() {
  'use strict';

  // Analizar parámetros de URL
  const params = new URLSearchParams(window.location.search);
  const embedParam = params.get('embed');

  // Inicializar estado de embed
  window.telarEmbed = {
    enabled: embedParam === 'true'
  };

  // Esperar a que el DOM esté listo
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

  function init() {
    if (!window.telarEmbed.enabled) return;

    // Aplicar clase body de modo embed
    document.body.classList.add('embed-mode');

    // Crear banner "View full site"
    createEmbedBanner();
  }
})();

Temporización:

Modificaciones CSS

Estilos de modo embed

Archivo: assets/css/telar.scss

El selector body.embed-mode aplica estilos específicos de embed:

Elementos ocultos:

body.embed-mode {
  // Ocultar elementos de navegación
  .home-button {
    display: none !important;
  }

  .share-button {
    display: none !important;
  }

  // Ocultar texto de carga en contador de pasos
  .viewer-overlay {
    display: none;
  }
}

Visibilidad y ubicación de los botones:

body.embed-mode {
  // Los botones de navegación se muestran en cualquier ancho en modo embed,
  // centrados sobre la columna narrativa con ancho intrínseco
  .mobile-nav {
    position: fixed;
    left: 20%;
    right: auto;
    bottom: max(8%, 1.5rem);
    transform: translateX(-50%);
  }
}

Embeds angostos (en el límite del diseño vertical o por debajo):

// El mismo límite del diseño vertical de todo el sitio
// (_responsive.scss $telar-vertical-min-width, 1024px)
@media (max-width: $telar-vertical-min-width) {
  body.embed-mode .mobile-nav {
    // Apilados en el borde derecho
    right: 1rem;
    left: auto;
    bottom: 50%;
    transform: translateY(50%);
    flex-direction: column;
  }
}

Sistema de navegación

Archivo: assets/js/telar-story/main.js

Las historias insertadas usan siempre la navegación por botones, en cualquier ancho: los eventos de desplazamiento no se propagan de manera confiable dentro de un iframe, así que el motor de desplazamiento nunca se activa en un embed. Fuera de los embeds, el modo de navegación sigue el modo de diseño: el diseño horizontal usa el motor de desplazamiento (basado en Lenis) y el diseño vertical (pantallas de 1024px de ancho o menos, o más angostas que 3:4) usa los mismos botones de anterior/siguiente. El límite se declara una sola vez en _sass/_responsive.scss ($telar-vertical-min-width) y layout-mode.js lo lee en tiempo de ejecución.

Modos de navegación

Creación de botones

Archivo: assets/js/telar-story/navigation.js

initializeButtonNavigation() construye el contenedor .mobile-nav con sus botones .mobile-prev y .mobile-next y los conecta a la navegación por pasos; termina sin hacer nada si el contenedor ya existe.

Creación del banner

Archivo: assets/js/embed.js

function createEmbedBanner() {
  // Obtener cadenas de idioma de Jekyll
  const lang = window.telarLang?.embedBanner || {
    text: 'This story is part of {site_name}...',
    link: 'View the complete site'
  };

  const siteName = getSiteName();
  const siteUrl = getFullSiteUrl();

  const banner = document.createElement('div');
  banner.className = 'embed-banner';
  banner.innerHTML = `
    <span class="embed-banner-text">
      ${lang.text.replace('{site_name}', siteName)}
    </span>
    <a href="${siteUrl}" target="_blank" class="embed-banner-link">
      ${lang.link}
    </a>
    <button class="embed-banner-close" aria-label="Close banner">×</button>
  `;

  document.body.appendChild(banner);

  // Manejar descarte
  banner.querySelector('.embed-banner-close').addEventListener('click', () => {
    banner.remove();
  });
}

Estilos del banner

.embed-banner {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 1000;
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(8px);
  padding: 0.75rem 1rem;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  max-width: 380px;

  display: flex;
  align-items: center;
  gap: 0.75rem;
}

Comportamiento:

Panel de compartir e insertar

Generación de código de inserción

Archivo: assets/js/share-panel.js

function generateEmbedCode() {
  if (!currentStoryUrl) return '';

  const width = embedWidthInput.value.trim() || '100%';
  const height = embedHeightInput.value.trim() || '800';

  // Normalizar dimensiones
  const widthAttr = normalizeDimension(width);
  const heightAttr = normalizeDimension(height);

  // Construir URL de embed
  const embedUrl = addEmbedParameter(currentStoryUrl);

  // Obtener título de historia
  const storyTitle = getStoryTitle();

  // Generar código iframe
  const iframeCode = `<iframe src="${embedUrl}"
  width="${widthAttr}"
  height="${heightAttr}"
  title="${storyTitle}"
  frameborder="0">
</iframe>`;

  return iframeCode;
}

Limpieza de URL

function addEmbedParameter(url) {
  try {
    const urlObj = new URL(url);
    // Limpiar parámetros de consulta y hash existentes
    urlObj.search = '';
    urlObj.hash = '';
    // Agregar parámetro embed limpio
    urlObj.searchParams.set('embed', 'true');
    return urlObj.toString();
  } catch (e) {
    // Alternativa si el análisis de URL falla
    const cleanUrl = url.split(/[?#]/)[0];
    return cleanUrl + '?embed=true';
  }
}

Propósito:

Tamaños predefinidos

Archivo: assets/js/share-panel.js

const presets = {
  canvas: { width: '100%', height: '800' },
  moodle: { width: '100%', height: '700' },
  wordpress: { width: '100%', height: '600' },
  squarespace: { width: '100%', height: '600' },
  wix: { width: '100%', height: '550' },
  mobile: { width: '375', height: '500' },
  fixed: { width: '800', height: '600' }
};

Normalización de dimensiones

function normalizeDimension(value) {
  // Si es solo un número, agregar 'px'
  if (/^\d+$/.test(value)) {
    return value + 'px';
  }
  return value;
}

Formatos aceptados:

Soporte multilingüe

Inyección de cadenas de idioma

Archivo: _layouts/story.html

<script>
  window.telarLang = window.telarLang || {};
  window.telarLang.embedBanner = {
    text: null,
    link: null
  };
</script>

Proceso:

  1. Jekyll procesa etiquetas liquid durante la construcción
  2. Las cadenas de idioma se inyectan en window.telarLang
  3. JavaScript lee desde window.telarLang.embedBanner
  4. Alternativa al inglés si faltan datos de idioma

Archivos de idioma

Inglés: _data/languages/en.yml

embed_banner:
  text: "This story is part of {site_name}, a digital storytelling site built with <a href='https://github.com/UCSB-AMPLab/telar' target='_blank'>Telar</a>."
  link: "View the complete site"

Español: _data/languages/es.yml

embed_banner:
  text: "Esta historia forma parte de {site_name}, un sitio web creado con <a href='https://github.com/UCSB-AMPLab/telar' target='_blank'>Telar</a> para contar historias."
  link: "Ver el sitio completo"

Compatibilidad de navegadores

El modo embed de Telar funciona en todos los navegadores modernos:

Consideraciones de iframe

Mismo origen vs. origen cruzado:

Estructura del embed:

Rendimiento:

Probar el modo embed

Pruebas locales

Estructura del archivo de prueba:

<!DOCTYPE html>
<html>
<head>
  <title>Prueba de Embed</title>
</head>
<body>
  <h1>Canvas LMS (100% × 800px)</h1>
  <iframe src="http://localhost:4001/telar/stories/story-1/?embed=true"
    width="100%"
    height="800px"
    title="Historia de prueba"
    frameborder="0">
  </iframe>

  <!-- Probar otros tamaños predefinidos -->
</body>
</html>

Lista de verificación de pruebas:

Pruebas en producción

  1. Despliega el sitio a producción (GitHub Pages o dominio personalizado)
  2. Prueba el código de inserción en curso real de Canvas LMS
  3. Verifica en múltiples navegadores (Chrome, Firefox, Safari)
  4. Prueba en dispositivos móviles (iOS, Android)
  5. Verifica la consola en busca de errores de JavaScript

Consideraciones de seguridad

Política de seguridad de contenido

Si tu sitio usa encabezados CSP, asegúrate de que los iframes estén permitidos:

Content-Security-Policy: frame-ancestors 'self' https://canvas.instructure.com https://*.wordpress.com

GitHub Pages:

Compartir recursos de origen cruzado (CORS)

Teselas IIIF:

Manifiestos IIIF externos:

Atributo sandbox de iframe

No recomendado para Telar:

<!-- ❌ No hagas esto -->
<iframe src="..." sandbox="allow-scripts allow-same-origin">
</iframe>

Por qué:

Personalizar el comportamiento de embed

Estilos personalizados de embed

Agrega CSS personalizado para modo embed en assets/css/telar.scss:

body.embed-mode {
  // Tus estilos personalizados de embed

  .custom-element {
    // Ocultar en modo embed
    display: none;
  }
}

Detectar modo embed en código personalizado

if (window.telarEmbed?.enabled) {
  // Comportamiento personalizado para modo embed
  console.log('Ejecutando en modo embed');
}

Modificar la apariencia del banner

Sobrescribe estilos del banner en telar.scss:

.embed-banner {
  // Posición personalizada
  top: 2rem;
  left: 2rem;

  // Colores personalizados
  background: rgba(0, 0, 0, 0.8);
  color: white;
}

Limitaciones conocidas

Sin redimensionamiento dinámico de altura:

Sin enlaces profundos (v0.5.0):

Estado del visor no preservado:

Mejoras futuras

Planeadas para versiones futuras:

Documentación relacionada