Telar: Docs Telar: 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 forzada:

body.embed-mode {
  // Siempre mostrar sugerencias de navegación móvil
  .nav-hint {
    display: block;
  }

  // Forzar botones de navegación en todos los tamaños de pantalla
  .arrow-nav-up,
  .arrow-nav-down {
    display: flex !important;
  }
}

Personalización de flechas en escritorio para embed:

body.embed-mode {
  @media (min-width: 768px) {
    .arrow-nav-up,
    .arrow-nav-down {
      // Diseño horizontal en la parte inferior
      left: 20%;
      bottom: max(8%, 1.5rem);

      // Efectos al pasar el cursor
      &:hover {
        transform: scale(1.1) translateY(-2px); // Flecha arriba
        box-shadow: 0 6px 16px rgba(0,0,0,0.3);
      }
    }
  }
}

Embeds de tamaño móvil (<768px):

// Media query a nivel raíz para especificidad apropiada
@media (max-width: 767px) {
  body.embed-mode .arrow-nav-up,
  body.embed-mode .arrow-nav-down {
    // Pila vertical en el lado derecho
    right: 1rem;
    bottom: 50%;
    transform: translateY(50%);
  }
}

Sistema de navegación

Archivo: assets/js/story.js

function initializeNavigation() {
  const isMobileViewport = window.innerWidth < 768;
  const isEmbedMode = window.telarEmbed?.enabled || false;

  if (isMobileViewport || isEmbedMode) {
    // Móvil o embed: Navegación por botones
    initializeEmbedNavigation();
  } else {
    // Escritorio: Acumulación de desplazamiento
    initializeDesktopNavigation();
  }
}

Modos de navegación

Escritorio (no embed):

Móvil (<768px):

Modo embed (todos los tamaños de pantalla):

Creación de botones

function createNavigationButtons() {
  const upBtn = document.createElement('button');
  upBtn.className = 'arrow-nav-up';
  upBtn.setAttribute('aria-label', 'Previous step');
  upBtn.innerHTML = '<span class="material-symbols-outlined">arrow_upward</span>';

  const downBtn = document.createElement('button');
  downBtn.className = 'arrow-nav-down';
  downBtn.setAttribute('aria-label', 'Next step');
  downBtn.innerHTML = '<span class="material-symbols-outlined">arrow_downward</span>';

  // Adjuntar detectores de eventos
  upBtn.addEventListener('click', () => navigateSteps('previous'));
  downBtn.addEventListener('click', () => navigateSteps('next'));

  document.body.appendChild(upBtn);
  document.body.appendChild(downBtn);
}

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:

Iframes anidados:

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