Tema premium para NodeBB, construido sobre Harmony. Diseñado para la comunidad Vasak con foco en performance, accesibilidad y una experiencia de usuario moderna.
- Características
- Requisitos
- Instalación
- Desarrollo
- Estructura del proyecto
- Sistema de diseño
- Módulos JavaScript
- Service Worker
- Dark Mode
- Personalización
- Despliegue
- Solución de problemas
| Feature | Descripción |
|---|---|
| Lazy Loading | loading="lazy" nativo + IntersectionObserver para imágenes dinámicas |
| Skeleton Screens | Placeholders shimmer para feed, topics, notificaciones y posts |
| Critical CSS | Estilos above-the-fold inyectados inline en el <head> (evita FOUC) |
| Service Worker | Cache-First para assets, Network-First para páginas, SWR para API |
| Code Splitting | Módulos AMD cargados bajo demanda por página |
| Content Visibility | content-visibility: auto + DOM recycling para listas de 200+ items |
| Optimización de fuentes | System font stack, font-display: swap, size-adjust cross-platform |
| Debouncing | Timer compartido en búsqueda: 350ms input, 400ms filtros, 300ms tags |
| Feature | Descripción |
|---|---|
| Dark Mode | Toggle en sidebar, paleta Catppuccin Latte/Mocha, respeta prefers-color-scheme |
| Infinite Scroll UX | Barra de progreso tipo YouTube, back-to-top, estado "end of feed", retry en error |
| Reacciones | 👍❤️😂😮😢 en posts — upvote nativo de NodeBB + localStorage |
| Compartir | Web Share API en mobile, modal con 7 redes en desktop, clipboard con feedback |
| Autosave | Borrador del composer guardado cada 2s, recuperación automática, expira en 72h |
| Autocompletado | Sugerencias de topics en el header, historial de búsquedas, navegación por teclado |
| Notificaciones Push | Web Push API, banner opt-in no intrusivo, handler en Service Worker |
| Menciones | @usuario en el composer con dropdown de avatares y navegación por teclado |
| Modo Lectura | Vista limpia sin sidebar, tipografía optimizada, botón en sidebar del topic |
| Filtros de Feed | Filtrar posts por: Todo / Imágenes / Videos / Popular (≥5 votos), persistido |
| Estadísticas de Lectura | Barra de progreso de lectura + tracking de tiempo en localStorage |
| Indicador offline | Banner sutil online/offline + página offline branded con auto-reload |
| Keyboard shortcuts | ? cheatsheet, / búsqueda, n composer, j/k navegación, g+X secuencias |
| Prefetch inteligente | <link rel="prefetch"> al hover 200ms, respeta Save-Data y conexiones lentas |
| Toast system | vasak.toast.success/error/info/warning() — toasts integrados al diseño |
| User card | Popover con avatar, bio, stats y botón follow al hover sobre un usuario |
| Modo compacto | Toggle feed/lista compacto (solo título + meta), persistido en localStorage |
| Búsqueda en topic | Panel con input, resultados y navegación, activado con Ctrl+F |
| TOC automático | Tabla de contenidos en sidebar para posts con 3+ headings, scroll spy |
| Panel de preferencias | Modal accesible desde el sidebar: tema, fuente, densidad, shortcuts |
| Panel de admin mejorado | Secciones organizadas, estado VAPID, stats del SW, botones de caché |
| Compresión de storage | LZString para borradores largos del autosave (>512 bytes) |
| Carousels | Múltiples imágenes en posts se convierten automáticamente en carousels Bootstrap |
| Animaciones | Sistema completo: page transitions, slide-up en cards, ripple en botones, spring easing |
| Feature | Descripción |
|---|---|
| Sidebar expandible | Ancho 270px expandido / 64px colapsado, preferencia guardada por usuario |
| Composer modal | Centrado en desktop, full-screen en mobile, backdrop blur |
| Feed estilo LinkedIn | Tarjetas de posts con action bar, composer prompt, tabs For You / Following |
| Topic cards Reddit | Vote column, meta row, teaser, thumbnails, action bar |
| Notificaciones | Dropdown con skeleton, badge con pulse animation |
| Login LinkedIn | Pantalla de login con SSO, modo admin via ?admin=true |
- NodeBB
^4.0.0 - nodebb-theme-harmony
^2(peer dependency — se instala automáticamente) - Bun
^1.0(para desarrollo)
# En el directorio raíz de NodeBB
npm install @vasakgroup/nodebb-theme-vasak
./nodebb build# Clonar en la carpeta de plugins de NodeBB
cd /ruta/a/nodebb/node_modules
git clone https://github.com/vasak-group/nodebb-theme-vasak.git nodebb-theme-vasak
cd nodebb-theme-vasak
bun installLuego en el ACP de NodeBB:
- Apariencia → Temas → seleccionar "Vasak Community Theme"
- Hacer clic en Aplicar
- Reconstruir NodeBB
bun install# Verificar formato
bun run lint
# Aplicar formato automáticamente
bun run lint:fixEl proyecto usa Biome para formateo. La configuración está en
biome.json.
NodeBB compila el SCSS automáticamente al hacer ./nodebb build. Para desarrollo activo:
# En el directorio raíz de NodeBB
./nodebb devLos cambios en archivos .scss y .tpl se reflejan al recargar la página en modo dev.
# 1. Editar archivos en scss/, templates/, public/
# 2. Reconstruir assets
./nodebb build tpl && ./nodebb build css
# 3. Para cambios en theme.js (server-side)
./nodebb buildnodebb-theme-vasak/
│
├── scss/ # Estilos SCSS
│ ├── overrides.scss # Variables Bootstrap (compilado antes que Harmony)
│ ├── _tokens.scss # CSS custom properties: paleta Catppuccin + dark mode
│ ├── _variables.scss # SCSS bridge vars → apuntan a var(--use-*)
│ ├── _base.scss # Layout, tipografía base, dark mode global
│ ├── _typography.scss # Sistema tipográfico: fluid type, @font-face, utilidades
│ ├── _header.scss # Header sticky, search bar, autocomplete dropdown
│ ├── _buttons.scss # Sistema de botones
│ ├── _forms.scss # Inputs, dropdowns, modales, badges
│ ├── _cards.scss # Topic cards estilo Reddit (vote column + content)
│ ├── _sidebar.scss # Sidebar izquierdo, widgets
│ ├── _sidebar-user.scss # Sección de usuario en sidebar
│ ├── _categories.scss # Lista de categorías
│ ├── _feed.scss # Feed page, composer prompt, post cards
│ ├── _topic.scss # Página de topic, posts, quick reply
│ ├── _composer.scss # Modal del composer, autosave indicator
│ ├── _skeletons.scss # Skeleton screens (shimmer placeholders)
│ ├── _animations.scss # Keyframes, transiciones, micro-interactions
│ ├── _infinite-scroll.scss # Progress bar, back-to-top, end state, load more
│ ├── _content-visibility.scss # content-visibility: auto para listas largas
│ ├── _reactions.scss # Sistema de reacciones en posts
│ ├── _share.scss # Modal de compartir
│ ├── _push.scss # Banner de notificaciones push
│ ├── _extras.scss # Reader mode, feed filters, reading progress, connection banner, keyboard shortcuts
│ ├── _a11y.scss # Accesibilidad: skip links, focus ring, sr-only
│ └── _new-features.scss # Toast, user card, compact mode, topic search, TOC
│
├── public/
│ ├── admin.js # Panel de administración del tema
│ └── src/client/
│ ├── search.js # Búsqueda avanzada con debouncing
│ ├── category.js # Página de categoría
│ ├── world.js # Página world/categories
│ ├── topic-enhancements.js # Carousels, bookmark fix, parent nav, hover actions
│ ├── feed-enhancements.js # Composer prompt, category filter, share handlers
│ ├── list-enhancements.js # Voting en topic lists
│ ├── infinite-scroll-ux.js # Progress bar, back-to-top, end state, sentinel
│ ├── virtual-list.js # content-visibility + DOM recycling
│ ├── reactions.js # Reacciones rápidas en posts
│ ├── share-enhanced.js # Web Share API + modal con redes sociales
│ ├── composer-autosave.js # Autosave de borradores en localStorage
│ ├── search-autocomplete.js # Autocompletado en el header
│ ├── push-notifications.js # Web Push API opt-in
│ ├── composer-mentions.js # @menciones en el composer
│ ├── reader-mode.js # Modo lectura limpia
│ ├── feed-filters.js # Filtros visuales del feed (persistidos)
│ ├── reading-stats.js # Progreso y estadísticas de lectura
│ ├── accessibility.js # Skip links, focus management, aria-labels
│ ├── connection-status.js # Banner online/offline
│ ├── keyboard-shortcuts.js # Atajos de teclado globales + cheatsheet
│ ├── prefetch.js # Prefetch inteligente al hover
│ ├── toast.js # Sistema de toasts (vasak.toast.*)
│ ├── user-card.js # Popover de usuario al hover
│ ├── compact-mode.js # Modo compacto para feed/listas
│ ├── topic-search.js # Búsqueda dentro de un topic (Ctrl+F)
│ ├── topic-toc.js # Tabla de contenidos automática
│ ├── storage-utils.js # localStorage con compresión LZString
│ └── user-settings.js # Panel de preferencias del usuario
│ └── account/
│ └── categories.js # Página de categorías del perfil
│
├── static/
│ ├── critical.css # CSS above-the-fold (inyectado inline en <head>)
│ ├── sw.js # Service Worker
│ └── lib/
│ ├── theme.js # JS cliente principal (IIFE global)
│ └── theme.css # CSS compilado (generado por NodeBB)
│
├── templates/ # Overrides de templates de Harmony
│ ├── feed.tpl # Página de feed
│ ├── topic.tpl # Página de topic
│ ├── login.tpl # Página de login (LinkedIn SSO)
│ └── partials/
│ ├── header/brand.tpl # Header con search y notificaciones
│ ├── sidebar-left.tpl # Sidebar con logo, nav, dark mode toggle
│ ├── topics_list.tpl # Lista de topics estilo Reddit
│ ├── posts_list_item.tpl # Item de post en listas
│ ├── notifications_list.tpl # Lista de notificaciones
│ └── topic/
│ ├── post.tpl # Post individual en topic
│ └── sidebar.tpl # Sidebar del topic
│
├── theme.js # Lógica server-side (hooks de NodeBB)
├── theme.scss # Entry point SCSS
├── theme.json # Metadata del tema
├── plugin.json # Manifest de NodeBB (hooks, módulos, scripts)
├── package.json # Configuración npm/bun
└── biome.json # Configuración de Biome (formatter)
El tema usa la paleta Catppuccin — Latte en modo claro, Mocha en modo oscuro.
/* Modo claro (Latte) */
--use-primary: #dd7878; /* Catppuccin Red */
--use-ui-background: #eff1f5; /* Catppuccin Base */
--use-ui-surface: #ffffff; /* Blanco para cards */
--use-text-main: #4c4f69; /* Catppuccin Text */
/* Modo oscuro (Mocha) */
--use-primary: #eba0ac; /* Catppuccin Flamingo */
--use-ui-background: #1e1e2e; /* Mocha Base */
--use-ui-surface: #313244; /* Mocha Surface0 */
--use-text-main: #cdd6f4; /* Mocha Text */Para cambiar la paleta, editar scss/_tokens.scss.
Todos los componentes consumen CSS custom properties. Los SCSS bridge variables en _variables.scss apuntan a ellas:
// En _variables.scss — bridge vars que apuntan a CSS custom properties
$vsk-primary: var(--use-primary);
$vsk-surface: var(--use-ui-surface);
$vsk-text-main: var(--use-text-main);
// Spacing (estático — no cambia en dark mode)
$vsk-space-4: 16px;
$vsk-space-6: 24px;Sistema de fuentes nativo (cero requests de red):
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text",
"Segoe UI", system-ui, "Helvetica Neue", Arial, sans-serif;
--font-mono: "SF Mono", "Cascadia Code", "Fira Code",
"Consolas", "Liberation Mono", monospace;Escala fluida con clamp():
--text-xs: clamp(11px, 0.69rem + 0.1vw, 12px);
--text-sm: clamp(12px, 0.75rem + 0.15vw, 13px);
--text-base: clamp(14px, 0.875rem + 0.2vw, 15px);
--text-xl: clamp(18px, 1.1rem + 0.5vw, 22px);El JS del tema está dividido en módulos AMD que NodeBB carga bajo demanda:
| Módulo | Archivo | Se carga en |
|---|---|---|
forum/search |
search.js |
Página /search |
forum/category |
category.js |
Páginas de categoría |
forum/world |
world.js |
Página /categories |
forum/topic/vasak-enhancements |
topic-enhancements.js |
Páginas de topic |
forum/vasak-feed |
feed-enhancements.js |
Página /feed |
forum/vasak-list |
list-enhancements.js |
Listas de topics |
forum/vasak-scroll-ux |
infinite-scroll-ux.js |
Global |
forum/vasak-virtual-list |
virtual-list.js |
Global |
forum/vasak-reactions |
reactions.js |
Topics + Feed |
forum/vasak-share |
share-enhanced.js |
Global |
forum/vasak-autosave |
composer-autosave.js |
Global |
forum/vasak-autocomplete |
search-autocomplete.js |
Global |
forum/vasak-push |
push-notifications.js |
Global |
forum/vasak-mentions |
composer-mentions.js |
Global |
forum/vasak-reader |
reader-mode.js |
Global |
forum/vasak-feed-filters |
feed-filters.js |
Global |
forum/vasak-reading-stats |
reading-stats.js |
Global |
forum/vasak-a11y |
accessibility.js |
Global |
forum/vasak-connection |
connection-status.js |
Global |
forum/vasak-shortcuts |
keyboard-shortcuts.js |
Global |
forum/vasak-prefetch |
prefetch.js |
Global |
forum/vasak-toast |
toast.js |
Global |
forum/vasak-user-card |
user-card.js |
Global |
forum/vasak-compact |
compact-mode.js |
Global |
forum/vasak-topic-search |
topic-search.js |
Global |
forum/vasak-toc |
topic-toc.js |
Topics |
forum/vasak-storage |
storage-utils.js |
Global (dependencia) |
forum/vasak-settings |
user-settings.js |
Global |
El core static/lib/theme.js solo contiene comportamientos globales (sidebar, dark mode, lazy loading, animaciones) y carga los módulos de página bajo demanda con require().
El SW se sirve desde /vasak-sw.js (raíz del origen) con el header Service-Worker-Allowed: /, lo que le permite interceptar todas las requests del sitio.
| Tipo de recurso | Estrategia | Caché |
|---|---|---|
Assets del tema (/plugins/@vasakgroup/nodebb-theme-vasak/) |
Cache-First | vasak-static-v1 |
Assets compilados NodeBB (client.css, nodebb.min.js) |
Cache-First | vasak-static-v1 |
Fuentes (.woff2, .ttf) |
Cache-First | vasak-static-v1 |
| Imágenes | Cache-First | vasak-images-v1 |
| Páginas HTML | Network-First | vasak-pages-v1 |
API GET (/api/*) |
Stale-While-Revalidate | vasak-pages-v1 |
| API escritura (POST/PUT/DELETE) | Network-Only | — |
| Uploads de usuarios | Network-Only | — |
Cambiar CACHE_VERSION en static/sw.js:
const CACHE_VERSION = "v2"; // era "v1"Al hacer deploy, el SW detecta la nueva versión, activa SKIP_WAITING y recarga la página automáticamente.
Las notificaciones push requieren VAPID keys. Generarlas con:
# Instalar web-push globalmente
bun add -g web-push
# Generar el par de claves
bunx web-push generate-vapid-keysConfigurar las variables de entorno en NodeBB:
# En el entorno donde corre NodeBB
export VAPID_PUBLIC_KEY="tu_clave_publica_aqui"
export VAPID_PRIVATE_KEY="tu_clave_privada_aqui"O en el archivo de configuración de NodeBB (config.json):
{
"vapid_public_key": "tu_clave_publica_aqui",
"vapid_private_key": "tu_clave_privada_aqui"
}Si
VAPID_PUBLIC_KEYno está configurada, el endpoint/vasak-push/vapid-public-keydevuelve 503 y el módulo de push se desactiva silenciosamente. El resto del tema funciona con normalidad.
El tema no emite ningún output en consola en producción. Para activar los logs internos:
// En la consola del navegador
localStorage.setItem('VASAK_DEBUG', 'true');
location.reload();
// Para desactivar
localStorage.removeItem('VASAK_DEBUG');
location.reload();Los logs aparecen con el prefijo [Vasak].
// Estado de las cachés
navigator.serviceWorker.controller.postMessage(
{ type: 'GET_CACHE_STATUS' },
[new MessageChannel().port1]
);
// Limpiar todas las cachés
navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' });| Atajo | Acción |
|---|---|
? |
Mostrar/ocultar cheatsheet de atajos |
/ |
Enfocar la búsqueda del header |
n |
Abrir el composer (nueva discusión) |
r |
Responder al post activo (en topic) |
j / k |
Navegar entre posts/topics (abajo/arriba) |
g h |
Ir al feed |
g s |
Ir a búsqueda |
g r |
Ir a recientes |
g u |
Ir a no leídos |
Ctrl+F |
Buscar dentro del topic actual |
Escape |
Cerrar modales y dropdowns |
Los atajos se desactivan automáticamente cuando el foco está en un input o textarea.
El sistema de toasts está disponible globalmente como vasak.toast:
// Desde cualquier módulo o la consola del navegador
vasak.toast.success('Post guardado correctamente');
vasak.toast.error('No se pudo conectar al servidor');
vasak.toast.info('Hay nuevas respuestas en este topic');
vasak.toast.warning('Tu sesión expira en 5 minutos');
// Con duración personalizada (ms)
vasak.toast.show('Mensaje personalizado', 'info', 8000);El dark mode se activa añadiendo la clase .dark al elemento <html>. El JS en static/lib/theme.js gestiona:
- Preferencia guardada en
localStoragecon la keyvasak:theme - Fallback al sistema via
prefers-color-scheme: darksi no hay preferencia guardada - Toggle manual con el botón en el sidebar (ícono luna/sol)
// Activar dark mode programáticamente
document.documentElement.classList.add("dark");
localStorage.setItem("vasak:theme", "dark");
// Desactivar
document.documentElement.classList.remove("dark");
document.documentElement.classList.add("light");
localStorage.setItem("vasak:theme", "light");Editar scss/_tokens.scss:
:root {
--primary: #dd7878; // Color primario (modo claro)
--primary-dark: #eba0ac; // Color primario (modo oscuro)
}- Crear
scss/_micomponente.scss - Importar en
theme.scss:@import "./scss/micomponente";
- Reconstruir:
./nodebb build css
- Crear
public/src/client/mi-modulo.jscomo módulo AMD:define("forum/mi-modulo", [], function () { var MiModulo = {}; MiModulo.init = function () { /* ... */ }; return MiModulo; });
- Registrar en
plugin.json:"modules": { "forum/mi-modulo": "public/src/client/mi-modulo.js" }
- Cargar desde
static/lib/theme.js:require(["forum/mi-modulo"], function (mod) { mod.init(); });
- Copiar el template desde
node_modules/nodebb-theme-harmony/templates/ - Pegarlo en
templates/manteniendo la misma ruta relativa - Hacer los cambios necesarios
- Reconstruir:
./nodebb build tpl
# Publicar en npm
bun publish
# Instalar en NodeBB
npm install @vasakgroup/nodebb-theme-vasak
./nodebb build# Empaquetar (excluir node_modules y .git)
bun run pack # o: npm packLuego en el ACP: Extender → Instalar plugins → Subir plugin → seleccionar el .tgz.
En el ACP de NodeBB: Extender → Instalar plugins → ingresar vasak-group/nodebb-theme-vasak.
- Verificar que está activado en Apariencia → Temas
- Reconstruir NodeBB:
./nodebb build - Limpiar caché del navegador (
Ctrl+Shift+R)
# Reconstruir solo CSS
./nodebb build css
# Reconstruir todo
./nodebb buildEl SW requiere HTTPS (o localhost). En desarrollo local con HTTP, el SW no se registra — esto es comportamiento esperado del navegador.
Para forzar el registro en desarrollo:
chrome://flags/#unsafely-treat-insecure-origin-as-secure
bun installDesde la consola del navegador:
// Limpiar todas las cachés del tema
navigator.serviceWorker.controller.postMessage({ type: "CLEAR_CACHE" });O desde DevTools: Application → Storage → Clear site data.
Los atajos se desactivan cuando el foco está en un input. Si no responden en ningún lado, verificar que el módulo esté cargado:
// En la consola del navegador
localStorage.setItem('VASAK_DEBUG', 'true');
location.reload();
// Buscar "[Vasak]" en los logsEl TOC solo se genera si el primer post del topic tiene 3 o más headings (h1, h2, h3). Si el contenido usa texto plano sin headings, el TOC no se activa.
La user card requiere que el link del usuario tenga /user/ en la URL. Verificar que los avatares y nombres de autor usen el formato estándar de NodeBB.
# Fork + clonar
git clone https://github.com/vasak-group/nodebb-theme-vasak.git
cd nodebb-theme-vasak
bun install
# Crear rama
git checkout -b feature/mi-mejora
# Formatear antes de commitear
bun run lint:fix
# Commit y PR
git add .
git commit -m "feat: descripción de la mejora"
git push origin feature/mi-mejoraMIT — libre para usar, modificar y distribuir.
Construido con ❤️ para Vasak Community