Para desarrolladores

Crear themes para Wislab

Los themes de Wislab son bundles ESM con componentes React que se cargan dinámicamente por academia. Cada academia puede activar, personalizar o conectar su propio theme desde GitHub.

¿Qué es un theme?

Un theme es un bundle JavaScript (ESM) que exporta componentes React tipados. Wislab los carga dinámicamente para cada academia, permitiendo que cada tenant tenga una identidad visual única sin modificar el core de la plataforma.

Un theme solo necesita implementar las secciones que quiere personalizar. Las secciones no implementadas hacen fallback automáticamente al theme base.

Hay dos formas de distribuir un theme a una academia: subir un ZIP manualmente, o conectar un repositorio GitHub para CI/CD automático.

Estructura de archivos

Un workspace de theme standalone tiene esta estructura recomendada:

my-theme/
  manifest.json         # Metadatos: nombre, slug, tokens, defaults
  index.ts              # Exporta sections + manifest
  package.json          # esbuild + deps de desarrollo
  tsconfig.json
  sections/
    Header.tsx
    Hero.tsx
  types/
    contract.ts         # Tipos vendoreados del contrato
  scripts/
    build.ts            # Script de compilación → dist/theme.mjs + ZIP

El workspace es independiente del repo de Wislab. Puedes desarrollarlo en cualquier directorio y subirlo como ZIP o conectarlo a GitHub.

manifest.json

El manifest describe el theme y sus valores por defecto. Ejemplo completo con todos los campos requeridos:

{
  "name": "Mi Theme",
  "slug": "mi-theme",
  "description": "Descripción breve del theme.",
  "version": "1.0.0",
  "author": "Tu nombre o empresa",
  "defaultTokens": {
    "colors": {
      "primary": "#6366f1",
      "secondary": "#8b5cf6",
      "accent": "#06b6d4",
      "background": "#0a0a0a",
      "surface": "#111111",
      "surfaceSubtle": "#1a1a1a",
      "border": "#2a2a2a",
      "borderSubtle": "#222222",
      "textPrimary": "#ffffff",
      "textSecondary": "#a1a1aa",
      "textMuted": "#71717a"
    },
    "typography": {
      "fontFamily": "Inter, sans-serif",
      "headingWeight": "700",
      "baseSize": "16px"
    },
    "borders": {
      "radius": "0.75rem",
      "radiusSm": "0.375rem",
      "radiusLg": "1rem"
    }
  }
}

Los campos name, slug, description, author, version y defaultTokens son requeridos. El slug debe contener solo letras minúsculas, números y guiones (^[a-z0-9-]+$). defaultTokens debe incluir colors, typography y borders.

Secciones disponibles

El contrato define las secciones disponibles. Solo necesitas implementar las que tu theme personaliza. Las demás hacen fallback al theme base.

SecciónDescripciónImportancia
HeaderEncabezado de navegaciónAlta
HeroSección principal de la homepageAlta
FooterPie de páginaAlta
CourseGridListado de cursosMedia
CourseDetailDetalle de un cursoMedia
LessonViewVista de una lecciónMedia
DashboardHomePanel del estudianteMedia
DashboardLayoutLayout del dashboardMedia
FeaturedCoursesCursos destacados en homepageBaja
PathGridListado de rutas de aprendizajeBaja
PathDetailDetalle de una rutaBaja
BlogGridListado del blogBaja
BlogPostViewArtículo del blogBaja
WorkshopGridListado de workshopsBaja
WorkshopDetailDetalle de un workshopBaja

Props de cada sección

Cada componente recibe props tipadas desde la plataforma. Ejemplo del Header:

// Header.tsx
import type { HeaderProps } from "@/themes/_contract/sections";

export function Header({
  tenant,
  currentUser,
  settings,
  assets,
  customPages,
  signOut,
  searchModal,
}: HeaderProps) {
  return (
    <header>
      <a href="/">{tenant.name}</a>
      {currentUser ? (
        <form action={signOut}>
          <button type="submit">Cerrar sesión</button>
        </form>
      ) : (
        <a href="/sign-in">Entrar</a>
      )}
    </header>
  );
}

Las props clave de tenant incluyen: name, slug, logoUrl, primaryColor. currentUser puede ser null si el usuario no está autenticado.

El prop signOut es una Server Action inyectada por la plataforma. Siempre úsala como <form action={signOut}>.

CSS variables y tokens

La plataforma inyecta CSS variables en el root del layout basadas en los tokens del theme. Úsalas en tus componentes para respetar la personalización del tenant:

/* Colores */
var(--color-primary)
var(--color-background)
var(--color-surface)
var(--color-surface-subtle)
var(--color-border)
var(--color-border-subtle)
var(--color-text-primary)
var(--color-text-secondary)
var(--color-text-muted)

/* Tipografía */
var(--font-family)
var(--heading-weight)
var(--base-size)

/* Bordes */
var(--radius)
var(--radius-sm)
var(--radius-lg)

El tenant puede sobreescribir cualquier token desde /admin/themes/customize. No hardcodees colores en los componentes.

Fallback al theme base

El loader hace merge automático de tu theme con base. Solo necesitas exportar las secciones que override:

// index.ts — theme mínimo (solo overrides Header y Hero)
import type { ThemeManifest } from "@/themes/_contract";
import manifestJson from "./manifest.json";
import { Header } from "./sections/Header";
import { Hero } from "./sections/Hero";

export const sections = {
  Header,
  Hero,
  // CourseGrid, LessonView, etc. → fallback a base automáticamente
};

export const manifest: ThemeManifest = manifestJson;

Compilar con esbuild

El bundle de producción es un archivo theme.mjs compilado como ESM. Usa esta configuración de esbuild — cualquier desviación puede romper la carga dinámica:

// scripts/build.ts
import { build } from "esbuild";
import path from "path";

await build({
  entryPoints: ["index.ts"],
  bundle: true,
  format: "esm",
  platform: "browser",
  target: "es2020",
  jsx: "automatic",
  external: [
    "react",
    "react-dom",
    "react/jsx-runtime",
    "next",
    "next/*",
  ],
  loader: { ".json": "json" },
  minify: false,          // no minificar — puede romper la carga
  outfile: "dist/theme.mjs",
});

Ejecuta la compilación con: npx tsx scripts/build.ts (o pnpm build si tienes el script en package.json).

El resultado es dist/theme.mjs. Este archivo — junto con manifest.json — es todo lo que necesita Wislab.

Vía 1: Subir ZIP manualmente

Empaqueta los dos archivos en un ZIP y súbelo desde el panel de administración:

# Comprimir (los dos archivos deben estar en la raíz del ZIP):
zip -j dist/mi-theme.zip dist/theme.mjs manifest.json

# Verificar contenido:
unzip -l dist/mi-theme.zip
#   Archive:  dist/mi-theme.zip
#     manifest.json
#     theme.mjs

Sube el ZIP desde Admin → Themes → Subir theme. La plataforma valida el bundle con el security scanner, extrae el manifest y lo registra en el Theme Store de la academia.

Vía 2: GitHub CI/CD

La opción recomendada para temas activos: conecta un repositorio GitHub y Wislab deployará automáticamente cada vez que hagas push.

Requisitos del repositorio

El repo debe contener en su raíz (o en dist/):

mi-theme-repo/
  manifest.json     # requerido en la raíz
  theme.mjs         # bundle compilado (o dist/theme.mjs)

Wislab busca primero dist/theme.mjs; si no existe, busca theme.mjs en la raíz.

Flujo de conexión

  1. Ve a Admin → Themes → Custom Themes
  2. Haz clic en Conectar GitHub — se instalará la Wislab GitHub App en tu cuenta u organización
  3. Una vez conectado, en cada theme vinculado verás el botón Link repo
  4. Introduce la URL del repo (https://github.com/owner/repo) y la rama de producción
  5. Wislab crea el webhook automáticamente y lanza el primer deploy

Staging branch

Puedes configurar una rama de staging separada. Los deploys a esa rama van a stagingBundleUrl y se pueden previsualizar antes de promover a producción.

Deploy manual

Si necesitas forzar un redeploy sin hacer push, usa el botón Deploy ahora (o Reintentar si el último deploy falló) en la tarjeta del theme.

Restricciones del security scanner

Wislab pasa todos los bundles por un scanner de seguridad antes de almacenarlos. El bundle será rechazado si contiene cualquiera de los siguientes patrones:

Patrón prohibidoMotivo
process.envAcceso a variables de entorno del servidor
require(CommonJS — usa ESM
eval(Ejecución dinámica de código
new Function(Ejecución dinámica de código
fetch(Llamadas de red desde el bundle
XMLHttpRequestLlamadas de red desde el bundle
WebSocketConexiones de red desde el bundle
child_processAcceso al sistema operativo
__dirname / __filenameAPIs de Node.js no disponibles en browser
import(Dynamic imports (excepto react, next/*, @/themes/*)

Los dynamic imports sí están permitidos para react, next/* y @/themes/*.

Buenas prácticas

  • Usa CSS variables para todos los colores. Nunca hardcodees #hex directamente.
  • Componentes de servidor por defecto. Usa "use client" solo si necesitas interactividad real.
  • No asumas autenticación. Siempre verifica que currentUser no sea null antes de renderizar elementos del usuario.
  • Responsive por defecto. La plataforma se usa en móvil. Usa sm:, md:, lg: de Tailwind.
  • No incluyas React en el bundle. Declara React como external en esbuild.
  • No uses minify. minify: false en esbuild — algunos minificadores inline React y rompen la carga dinámica.
  • Commit theme.mjs al repo. Si usas GitHub CI/CD, el bundle compilado debe estar commiteado — Wislab lo descarga directamente del repo, no lo compila por su cuenta.
  • Prueba con datos vacíos. Las listas pueden estar vacías. Siempre maneja el estado length === 0.
  • Accesibilidad. Usa roles ARIA, alt en imágenes y contraste adecuado (mínimo AA).

¿Preguntas o quieres publicar tu theme?

Si creaste un theme y quieres publicarlo en el marketplace de Wislab, contáctanos. Revisamos cada theme antes de hacerlo público.

Contactar