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 + ZIPEl 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ón | Descripción | Importancia |
|---|---|---|
| Header | Encabezado de navegación | Alta |
| Hero | Sección principal de la homepage | Alta |
| Footer | Pie de página | Alta |
| CourseGrid | Listado de cursos | Media |
| CourseDetail | Detalle de un curso | Media |
| LessonView | Vista de una lección | Media |
| DashboardHome | Panel del estudiante | Media |
| DashboardLayout | Layout del dashboard | Media |
| FeaturedCourses | Cursos destacados en homepage | Baja |
| PathGrid | Listado de rutas de aprendizaje | Baja |
| PathDetail | Detalle de una ruta | Baja |
| BlogGrid | Listado del blog | Baja |
| BlogPostView | Artículo del blog | Baja |
| WorkshopGrid | Listado de workshops | Baja |
| WorkshopDetail | Detalle de un workshop | Baja |
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.mjsSube 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
- Ve a
Admin → Themes → Custom Themes - Haz clic en Conectar GitHub — se instalará la Wislab GitHub App en tu cuenta u organización
- Una vez conectado, en cada theme vinculado verás el botón Link repo
- Introduce la URL del repo (
https://github.com/owner/repo) y la rama de producción - 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 prohibido | Motivo |
|---|---|
| process.env | Acceso 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 |
| XMLHttpRequest | Llamadas de red desde el bundle |
| WebSocket | Conexiones de red desde el bundle |
| child_process | Acceso al sistema operativo |
| __dirname / __filename | APIs 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
#hexdirectamente. - Componentes de servidor por defecto. Usa
"use client"solo si necesitas interactividad real. - No asumas autenticación. Siempre verifica que
currentUserno 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
externalen esbuild. - No uses minify.
minify: falseen esbuild — algunos minificadores inline React y rompen la carga dinámica. - Commit
theme.mjsal 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