Skip to content

Arquitectura General

┌──────────────────────────────────────────────────────────┐
│ USUARIO (Browser) │
└─────────────────────────┬────────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────┐
│ COMPONENTES VUE │
│ (AE* de ecom-components + componentes propios) │
└─────────┬──────────────────────────────────┬─────────────┘
│ │
┌─────────▼─────────┐ ┌──────────▼──────────┐
│ COMPOSABLES │ │ PINIA STORES │
│ (lógica de UI) │ │ (estado global) │
└─────────┬─────────┘ └──────────┬──────────┘
│ │
┌─────────▼──────────────────────────────────▼─────────────┐
│ useServices() │
│ (punto central de acceso a datos con fallback i18n) │
└─────────────────────────┬────────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────┐
│ VERTICAL CONFIG (inspiria.ts) │
│ (registra servicios, API URLs, colores, metadata) │
└─────────────────────────┬────────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────┐
│ SERVICES (fetchApi → Strapi CMS) │
│ + Stripe + Zoho + ElevenLabs │
└──────────────────────────────────────────────────────────┘

Principio clave: Inversión de dependencias

Section titled “Principio clave: Inversión de dependencias”

Los componentes nunca importan servicios directamente. El flujo siempre es:

  1. El componente llama a useServices('getChannels')
  2. useServices busca la función getChannels en visibleVertical.services
  3. La vertical (Inspiria) tiene registrada: getChannels: channelsService.getChannels
  4. El servicio usa httpClient que conoce la URL base de la API según la vertical

Beneficio: Cambiar de vertical (marca) no requiere tocar un solo componente ni composable. Solo se cambia la configuración.


Una vertical es una marca/producto educativo independiente que comparte el mismo código base. Cada vertical define:

  • Su dominio/subdominio propio
  • Sus colores y branding
  • Sus propias APIs y servicios
  • Sus traducciones específicas
  • Sus páginas exclusivas (algunas verticales no tienen “libros”, por ejemplo)
  • Su Google Analytics y Search Console
  • Su configuración de Zoho CRM

Actualmente existe una sola vertical activa: Inspiria Dental. El sistema está diseñado y preparado para agregar más sin modificar componentes.

Cómo se determina qué vertical está activa

Section titled “Cómo se determina qué vertical está activa”
EntornoMecanismoEjemplo
ProducciónSubdominio de la URLinspiriadental.com → vertical con subdomain: 'inspiriadental'
DesarrolloPuerto del servidor:3000 → índice 0 del array, :3001 → índice 1, etc.

Código relevante en app/core/utils.ts:

export function selectVertical(): Verticals {
return isProduction() ? verticalSearchEngine() : selectVerticalForPort()
}

Y en app/core/config.ts:

export const VERTICAL_LIST = [INSPIRIA] // Array de todas las verticales
export const visibleVertical = selectVertical() // La vertical activa para esta instancia
  1. Crear app/constants/verticals/nuevaVertical.ts implementando la interfaz Verticals
  2. Configurar: nombre, subdominio, colores, URLs de API, analytics
  3. Registrar las funciones de servicio en el objeto services (puedes reutilizar servicios existentes o crear nuevos)
  4. Agregar traducciones en app/locales/nuevaVertical/{es,en,pt-BR}/
  5. Agregar al array VERTICAL_LIST en app/core/config.ts
  6. Configurar subdominio en AWS CloudFront
  7. Crear el content-type correspondiente en Strapi

1. Nuxt Router detecta pages/courses/index.vue
2. Middleware auth.global.ts → valida sesión JWT
3. Middleware language.global.ts → aplica idioma (localStorage o browser)
4. La página ejecuta:
const { data, isPending } = useServices('getCourses')
5. useServices internamente:
a. Lee visibleVertical.services.getCourses → coursesService.getCourses
b. Llama con locale actual: getCourses('es')
c. coursesService usa httpClient.get({ resource: 'courses', params: QUERY })
d. httpClient construye: GET https://api.inspiriadental.com/api/courses?populate=*&locale=es
e. Si datos vacíos → reintenta con fallback ('en')
f. Merge profundo de ambas respuestas para rellenar campos sin traducir
6. data reactivo se actualiza → componente re-renderiza
1. Click "Comprar" → useBuyFlow.startBuyFlow(type, products)
2. ¿Está logueado?
NO → guarda {type, products} en localStorage → redirige a /login
Tras login → resumeBuyFlowAfterLogin(email)
SÍ → continúa
3. ¿Ya tiene el producto? → redirige al contenido
4. ¿Es candidato a suscripción? → muestra popup AESubscriptionPopupContent
5. useCheckout crea sesión Stripe con metadatos del producto
6. Redirige al usuario a Stripe checkout URL
7. Stripe redirige a /gracias tras pago exitoso
8. Zoho CRM recibe webhook con datos del evento

Cuando la app arranca, el plugin 01.initApp.client.ts ejecuta (en orden):

  1. applyFavicon() — pone el favicon de la vertical activa
  2. addStandardSeo() — meta tags SEO base
  3. addVerticalColors() — inyecta CSS variables de colores de la vertical en :root
  4. analyticsInjection() — carga Google Analytics con el código de la vertical
  5. searchConsoleInjection() — meta tag de verificación
  6. addPrimaryColor() — aplica color primario como tema

En cada navegación de página:

  • documentLanguage() — actualiza <html lang="...">

Componentes → nunca acceden directamente a: services, fetchApi, stores de auth
→ sí acceden a: useServices, composables propios, stores (read)
Composables → pueden usar: useServices, stores, otros composables
→ nunca acceden a: httpClient directamente
Stores → pueden usar: useServices (para fetch data)
→ nunca acceden a: httpClient directamente, otros stores (evitar circular)
Services → solo usan: httpClient, zohoClient
→ no tienen acceso a: stores, composables, componentes