@onlistify/storefront 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,41 +1,132 @@
1
1
  # ONlistify Storefront
2
2
 
3
- App y SDK para mostrar el storefront de un tenant (tema, bloques, listado de propiedades).
3
+ SDK para integrar el listado y la ficha de propiedades de una inmobiliaria en cualquier web. Una sola llamada monta tema, filtros, paginación y bloques configurados.
4
4
 
5
- ## SDK
5
+ ## Instalación
6
6
 
7
- Build de la librería: `pnpm run build:sdk`. Salida en `dist/` (ESM, CJS y `.d.ts`).
7
+ ```bash
8
+ npm install @onlistify/storefront
9
+ ```
10
+
11
+ El paquete incluye el **SDK** (navegador) y la **CLI**. Desde cualquier carpeta donde tengas un `.onlistify`:
12
+
13
+ ```bash
14
+ npm install @onlistify/storefront
15
+ npx onlistify pull --output-dir ./mi-sitio --env draft
16
+ npx onlistify push
17
+ npx onlistify promote
18
+ npx onlistify preview --env draft
19
+ npx onlistify dev
20
+ ```
21
+
22
+ Sin instalar (npx descarga y ejecuta): `npx @onlistify/storefront pull --output-dir ./mi-sitio --env draft`
23
+
24
+ Ejemplo mínimo de `.onlistify`: solo `apiUrl` y `apiKey`. El webhook se llama a `apiUrl/webhook`. Opcional: `storefrontUrl`, `tenantSlug` (para preview).
25
+
26
+ ## Integración (cualquier URL)
8
27
 
9
- ### Uso rápido
28
+ Indica el **slug** del tenant y la **base** de la API. La ruta de tu página puede ser la que quieras (`/`, `/propiedades`, dominio propio, etc.):
10
29
 
11
30
  ```ts
12
- import { bootSiteRuntime } from 'onlistify-storefront';
31
+ import { bootSiteRuntime } from '@onlistify/storefront';
13
32
 
14
33
  const app = document.getElementById('app');
15
34
  if (app) {
16
- await bootSiteRuntime(app);
35
+ await bootSiteRuntime(app, {
36
+ slug: 'mi-inmobiliaria',
37
+ baseUrl: 'https://api.onlistify.com',
38
+ });
17
39
  }
18
40
  ```
19
41
 
20
- La URL debe ser `/tienda/:slug` (ej. `/tienda/mi-inmobiliaria`). El SDK obtiene el slug del pathname.
42
+ Con bundler (Vite, Webpack, etc.) o como módulo en el navegador. No hace falta usar una ruta tipo `/tienda/:slug`.
43
+
44
+ ## Opciones
45
+
46
+ | Opción | Descripción |
47
+ |---------------|-------------|
48
+ | `slug` | Slug del tenant (recomendado). Sin esto, se puede usar `getSlugFromPath`. |
49
+ | `baseUrl` | Base de la API (ej. `https://api.onlistify.com`). |
50
+ | `environment` | `'draft'` \| `'live'`. Por defecto `'live'` o `?environment=draft` en la URL. |
51
+ | `getSlugFromPath` | Función `(pathname) => string` para obtener el slug desde la ruta. Se ignora si se pasa `slug`. |
52
+ | `onError` | Callback cuando falle la carga de configuración o el runtime. |
53
+
54
+ ## App de desarrollo
55
+
56
+ En este repo, la app Vite usa variables de entorno para no depender de la ruta:
57
+
58
+ - `VITE_PUBLIC_STOREFRONT_SLUG` — slug del tenant (ej. `richistate`).
59
+ - `VITE_PUBLIC_API_URL` — base de la API.
60
+
61
+ Crea `.env` con esos valores y arranca con `pnpm dev`. La app funciona en la raíz (ej. `http://localhost:5174/`).
62
+
63
+ ## Conectar a ONlistify para enviar cambios
64
+
65
+ El **storefront solo lee** (config, propiedades, plugins). Para **enviar cambios** (editar tema, bloques, publicar) tienes dos opciones:
66
+
67
+ **1. Dashboard (recomendado)**
68
+ - En el repo `onlistify-front` crea o edita `.env` con la URL de tu backend:
69
+ ```bash
70
+ NEXT_PUBLIC_API_URL=https://tu-backend.onlistify.com
71
+ ```
72
+ (En local: `http://localhost:8888` si el backend corre ahí.)
73
+ - Arranca el dashboard (`pnpm dev`), inicia sesión y entra en **Configuración del sitio** o **API pública**. Los cambios se envían al backend desde la UI.
74
+
75
+ **2. API con clave**
76
+ - En el dashboard ve a **API pública** y crea una API key del tenant.
77
+ - Desde scripts o integraciones:
78
+ ```bash
79
+ export ONLISTIFY_API_URL=https://tu-backend.onlistify.com
80
+ export ONLISTIFY_API_KEY=onl_tu_clave
81
+ ```
82
+ - `GET` config: `curl -H "X-API-Key: $ONLISTIFY_API_KEY" "$ONLISTIFY_API_URL/api/v1/public/site-config?environment=draft"`
83
+ - `PUT` para guardar borrador: body JSON con la config completa.
84
+ - `POST .../site-config/promote` para publicar borrador a vivo.
85
+
86
+ El **storefront** solo necesita `baseUrl` (la misma URL del backend) y `slug` del tenant para mostrar datos; no envía cambios.
87
+
88
+ ## Build del SDK
89
+
90
+ ```bash
91
+ pnpm run build:sdk
92
+ ```
93
+
94
+ Salida en `dist/`: entrada principal (browser), entrada `node` (Node) y carpeta `dist/plugins/` con los plugins por defecto (hero, header).
95
+
96
+ ## SDK Node (descargar web, push, webhook)
97
+
98
+ Para scripts o la CLI (descargar la web como código, subir cambios, disparar un webhook):
99
+
100
+ ```ts
101
+ import {
102
+ pullSiteConfig,
103
+ pushSiteConfig,
104
+ promoteSiteConfig,
105
+ exportSiteProject,
106
+ triggerWebhook,
107
+ getDefaultPluginsDir,
108
+ } from '@onlistify/storefront/node';
109
+ ```
21
110
 
22
- ### Configuración
111
+ - `pullSiteConfig({ apiUrl, apiKey }, 'draft' | 'live')` — devuelve la config normalizada.
112
+ - `pushSiteConfig({ apiUrl, apiKey }, config)` — guarda borrador.
113
+ - `promoteSiteConfig({ apiUrl, apiKey })` — publica a live.
114
+ - `exportSiteProject({ apiUrl, apiKey }, { outputDir, environment?, includePlugins? })` — escribe en `outputDir` el `site-config.draft.json` y, si `includePlugins` es true, copia los plugins por defecto del SDK a `outputDir/plugins/`. Así los devs obtienen la config y el código físico de los bloques para editarlos.
115
+ - `triggerWebhook(url, payload?)` — POST a la URL (útil tras push/promote para desplegar).
116
+ - `getDefaultPluginsDir()` — ruta a `dist/plugins` del paquete.
23
117
 
24
- - **API base:** variable de entorno `VITE_PUBLIC_API_URL` o `bootSiteRuntime(app, { baseUrl: 'https://api.ejemplo.com' })`. También `setApiBase(url)` antes de cualquier `fetch*`.
25
- - **Entorno:** `bootSiteRuntime(app, { environment: 'draft' | 'live' })`. Por defecto usa `?environment=draft` en la URL o `live`.
26
- - **Slug custom:** `bootSiteRuntime(app, { getSlugFromPath: (pathname) => 'mi-tenant' })`.
27
- - **Errores:** `bootSiteRuntime(app, { onError: (err) => console.error(err) })`.
118
+ La CLI incluida en el paquete (`npx onlistify`) usa este entry: `pull --output-dir ./mi-sitio` exporta el sitio completo; `push` / `promote` envían cambios y, si está definido `webhookUrl` en `.onlistify`, se llama al terminar.
28
119
 
29
- ### API pública
120
+ ## API pública (browser)
30
121
 
31
- - `bootSiteRuntime(app, options?)` — monta la UI en el elemento.
122
+ - `bootSiteRuntime(app, options?)` — monta el storefront en el elemento.
32
123
  - `fetchSiteConfig(slug, environment?)` — configuración del sitio.
33
124
  - `fetchProperties(slug, params?)` — listado de propiedades.
34
125
  - `fetchPlugins(slug)` — plugins/bloques.
35
126
  - `setApiBase(url)` — define la base de la API.
36
127
  - Tipos: `SiteConfig`, `BlockDef`, `PageDef`, `PublicPropertySummary`, `ListPropertiesParams`, `BootSiteRuntimeOptions`, `StorefrontConfigError`, `StorefrontNetworkError`, etc.
37
128
 
38
- ### Errores
129
+ ## Errores
39
130
 
40
- - `StorefrontConfigError`: fallo HTTP de configuración o propiedades (`status` opcional).
41
- - `StorefrontNetworkError`: fallo de red (`cause` con el error original).
131
+ - `StorefrontConfigError`: error HTTP de config o propiedades (`status` opcional).
132
+ - `StorefrontNetworkError`: error de red (`cause` con el error original).
package/dist/cli.js ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawn } from 'child_process';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ async function loadSdk() {
10
+ try {
11
+ return await import(path.join(__dirname, 'node.js'));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function loadConfig() {
18
+ const cwd = process.cwd();
19
+ let apiUrl = process.env.ONLISTIFY_API_URL;
20
+ let apiKey = process.env.ONLISTIFY_API_KEY;
21
+ let tenantSlug = process.env.ONLISTIFY_TENANT_SLUG;
22
+ let storefrontUrl = process.env.ONLISTIFY_STOREFRONT_URL;
23
+ const configPath = path.join(cwd, '.onlistify');
24
+ if (fs.existsSync(configPath)) {
25
+ try {
26
+ const data = JSON.parse(fs.readFileSync(configPath, 'utf8'));
27
+ apiUrl = apiUrl ?? data.apiUrl ?? data.ONLISTIFY_API_URL;
28
+ apiKey = apiKey ?? data.apiKey ?? data.ONLISTIFY_API_KEY;
29
+ tenantSlug = tenantSlug ?? data.tenantSlug ?? data.ONLISTIFY_TENANT_SLUG;
30
+ storefrontUrl = storefrontUrl ?? data.storefrontUrl ?? data.ONLISTIFY_STOREFRONT_URL;
31
+ } catch (_) {}
32
+ }
33
+ apiUrl = (apiUrl || 'http://localhost:8888').replace(/\/$/, '');
34
+ storefrontUrl = (storefrontUrl || 'http://localhost:5174').replace(/\/$/, '');
35
+ const webhookUrl = `${apiUrl}/webhook`;
36
+ return { apiUrl, apiKey, tenantSlug, storefrontUrl, webhookUrl };
37
+ }
38
+
39
+ function usage() {
40
+ console.log(`onlistify (@onlistify/storefront)
41
+ pull [--env draft|live] [--output FILE] Descarga config a archivo
42
+ pull --output-dir DIR [--env draft] Descarga sitio completo (config + plugins)
43
+ push [FILE] Sube site-config.draft.json como borrador
44
+ promote Publica borrador a live
45
+ preview [--env draft|live] Abre storefront en el navegador
46
+ dev [--config FILE] Watch y push al guardar
47
+
48
+ Config: .onlistify con apiUrl y apiKey (opcional: storefrontUrl, tenantSlug para preview). Webhook = apiUrl/webhook.
49
+ `);
50
+ }
51
+
52
+ async function api(apiUrl, apiKey, method, pathname, body = null) {
53
+ const url = `${apiUrl}/api/v1/public${pathname}`;
54
+ const opts = { method, headers: { 'X-API-Key': apiKey } };
55
+ if (body) {
56
+ opts.headers['Content-Type'] = 'application/json';
57
+ opts.body = typeof body === 'string' ? body : JSON.stringify(body);
58
+ }
59
+ const res = await fetch(url, opts);
60
+ if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
61
+ return res.json();
62
+ }
63
+
64
+ function parseArgs(args) {
65
+ const out = { env: 'draft', output: null, outputDir: null, file: null, config: null };
66
+ for (let i = 0; i < args.length; i++) {
67
+ if (args[i] === '--env' && args[i + 1]) out.env = args[++i];
68
+ else if (args[i] === '--output' && args[i + 1]) out.output = args[++i];
69
+ else if (args[i] === '--output-dir' && args[i + 1]) out.outputDir = args[++i];
70
+ else if (args[i] === '--config' && args[i + 1]) out.config = args[++i];
71
+ else if (!args[i].startsWith('-')) { out.file = args[i]; break; }
72
+ }
73
+ return out;
74
+ }
75
+
76
+ async function cmdPull(config, args) {
77
+ const { apiUrl, apiKey } = config;
78
+ if (!apiKey) {
79
+ console.error('Falta ONLISTIFY_API_KEY o apiKey en .onlistify');
80
+ process.exit(1);
81
+ }
82
+ const { env, output, outputDir } = parseArgs(args);
83
+ const envParam = env === 'live' ? 'live' : 'draft';
84
+ const sdk = await loadSdk();
85
+
86
+ if (outputDir && sdk?.exportSiteProject) {
87
+ const { configPath, pluginsPath } = await sdk.exportSiteProject(
88
+ { apiUrl, apiKey },
89
+ { outputDir, environment: envParam, includePlugins: true }
90
+ );
91
+ console.log('Sitio exportado:', configPath, pluginsPath ? `plugins: ${pluginsPath}` : '');
92
+ return;
93
+ }
94
+
95
+ if (sdk?.pullSiteConfig) {
96
+ const siteConfig = await sdk.pullSiteConfig({ apiUrl, apiKey }, envParam);
97
+ const outPath = path.resolve(process.cwd(), output || (envParam === 'live' ? 'site-config.live.json' : 'site-config.draft.json'));
98
+ fs.writeFileSync(outPath, JSON.stringify(siteConfig, null, 2), 'utf8');
99
+ console.log('Config guardada en', outPath);
100
+ return;
101
+ }
102
+
103
+ const data = await api(apiUrl, apiKey, 'GET', `/site-config?environment=${envParam}`);
104
+ const json = JSON.stringify(data?.data ?? data ?? { pages: [] }, null, 2);
105
+ const outPath = path.resolve(process.cwd(), output || (envParam === 'live' ? 'site-config.live.json' : 'site-config.draft.json'));
106
+ fs.writeFileSync(outPath, json, 'utf8');
107
+ console.log('Config guardada en', outPath);
108
+ }
109
+
110
+ async function cmdPush(config, args) {
111
+ const { apiUrl, apiKey, webhookUrl } = config;
112
+ if (!apiKey) {
113
+ console.error('Falta ONLISTIFY_API_KEY o apiKey en .onlistify');
114
+ process.exit(1);
115
+ }
116
+ const { file } = parseArgs(args);
117
+ const configPath = path.resolve(process.cwd(), file || 'site-config.draft.json');
118
+ let body;
119
+ try {
120
+ body = fs.readFileSync(configPath, 'utf8');
121
+ JSON.parse(body);
122
+ } catch (e) {
123
+ if (e.code === 'ENOENT') console.error('Archivo no encontrado:', configPath);
124
+ else console.error('JSON inválido:', e.message);
125
+ process.exit(1);
126
+ }
127
+
128
+ const sdk = await loadSdk();
129
+ if (sdk?.pushSiteConfig) {
130
+ await sdk.pushSiteConfig({ apiUrl, apiKey }, JSON.parse(body));
131
+ } else {
132
+ await api(apiUrl, apiKey, 'PUT', '/site-config?environment=draft', body);
133
+ }
134
+ console.log('Borrador guardado');
135
+ if (webhookUrl && sdk?.triggerWebhook) {
136
+ try {
137
+ await sdk.triggerWebhook(webhookUrl, { event: 'onlistify.push' });
138
+ console.log('Webhook llamado');
139
+ } catch (e) {
140
+ console.warn('Webhook:', e.message);
141
+ }
142
+ }
143
+ }
144
+
145
+ async function cmdPromote(config) {
146
+ const { apiUrl, apiKey, webhookUrl } = config;
147
+ if (!apiKey) {
148
+ console.error('Falta ONLISTIFY_API_KEY o apiKey en .onlistify');
149
+ process.exit(1);
150
+ }
151
+ const sdk = await loadSdk();
152
+ if (sdk?.promoteSiteConfig) {
153
+ await sdk.promoteSiteConfig({ apiUrl, apiKey });
154
+ } else {
155
+ await api(apiUrl, apiKey, 'POST', '/site-config/promote');
156
+ }
157
+ console.log('Config publicada en vivo');
158
+ if (webhookUrl && sdk?.triggerWebhook) {
159
+ try {
160
+ await sdk.triggerWebhook(webhookUrl, { event: 'onlistify.promote' });
161
+ console.log('Webhook llamado');
162
+ } catch (e) {
163
+ console.warn('Webhook:', e.message);
164
+ }
165
+ }
166
+ }
167
+
168
+ function cmdPreview(config, args) {
169
+ const { storefrontUrl, tenantSlug } = config;
170
+ const { env } = parseArgs(args);
171
+ const slug = tenantSlug || 'mi-inmobiliaria';
172
+ const envParam = env === 'live' ? '' : '?environment=draft';
173
+ const url = `${storefrontUrl}/tienda/${encodeURIComponent(slug)}${envParam}`;
174
+ const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
175
+ spawn(start, [url], { stdio: 'ignore', detached: true });
176
+ console.log('Abriendo', url);
177
+ }
178
+
179
+ async function cmdDev(config, args) {
180
+ const { apiUrl, apiKey, webhookUrl } = config;
181
+ if (!apiKey) {
182
+ console.error('Falta ONLISTIFY_API_KEY o apiKey en .onlistify');
183
+ process.exit(1);
184
+ }
185
+ const { config: configFile } = parseArgs(args);
186
+ const configPath = path.resolve(process.cwd(), configFile || 'site-config.draft.json');
187
+ if (!fs.existsSync(configPath)) {
188
+ console.error('Archivo no encontrado:', configPath);
189
+ process.exit(1);
190
+ }
191
+ const sdk = await loadSdk();
192
+ const push = async () => {
193
+ try {
194
+ const body = fs.readFileSync(configPath, 'utf8');
195
+ const parsed = JSON.parse(body);
196
+ if (sdk?.pushSiteConfig) await sdk.pushSiteConfig({ apiUrl, apiKey }, parsed);
197
+ else await api(apiUrl, apiKey, 'PUT', '/site-config?environment=draft', body);
198
+ console.log('[%s] Borrador actualizado', new Date().toISOString());
199
+ if (webhookUrl && sdk?.triggerWebhook) {
200
+ try { await sdk.triggerWebhook(webhookUrl, { event: 'onlistify.push' }); } catch (_) {}
201
+ }
202
+ } catch (e) {
203
+ console.error('Error:', e.message);
204
+ }
205
+ };
206
+ fs.watch(configPath, (eventType) => {
207
+ if (eventType === 'change') push();
208
+ });
209
+ console.log('Observando', configPath, '- Recarga la pestaña de preview al cambiar.');
210
+ await push();
211
+ }
212
+
213
+ const commands = { pull: cmdPull, push: cmdPush, promote: cmdPromote, preview: cmdPreview, dev: cmdDev };
214
+ const cmd = process.argv[2];
215
+ const cmdArgs = process.argv.slice(3);
216
+
217
+ if (!cmd || cmd === '-h' || cmd === '--help') {
218
+ usage();
219
+ process.exit(0);
220
+ }
221
+
222
+ const fn = commands[cmd];
223
+ if (!fn) {
224
+ console.error('Comando desconocido:', cmd);
225
+ usage();
226
+ process.exit(1);
227
+ }
228
+
229
+ const config = loadConfig();
230
+ Promise.resolve(fn(config, cmdArgs)).catch((e) => {
231
+ console.error(e.message);
232
+ process.exit(1);
233
+ });
@@ -0,0 +1,53 @@
1
+ type SiteEnvironment = 'draft' | 'live';
2
+ interface SiteConfigTheme {
3
+ primaryColor?: string;
4
+ accentColor?: string;
5
+ logoUrl?: string;
6
+ customCssVars?: Record<string, string>;
7
+ }
8
+ interface PropiedadCardBlockProps {
9
+ theme?: {
10
+ primaryColor?: string;
11
+ accentColor?: string;
12
+ badgeBackground?: string;
13
+ cardBorderRadius?: string;
14
+ imageAspectRatio?: '16/9' | '4/3' | '1/1';
15
+ };
16
+ showFields?: Record<string, boolean>;
17
+ badges?: {
18
+ featured?: {
19
+ enabled: boolean;
20
+ label?: string;
21
+ };
22
+ virtualTour?: {
23
+ enabled: boolean;
24
+ label?: string;
25
+ };
26
+ custom?: Array<{
27
+ key: string;
28
+ label: string;
29
+ when?: string;
30
+ }>;
31
+ };
32
+ layout?: 'compact' | 'default' | 'minimal';
33
+ linkTarget?: '_self' | '_blank';
34
+ openDetailIn?: 'same' | 'modal' | 'drawer';
35
+ }
36
+ interface BlockDef<TProps = Record<string, unknown>> {
37
+ id: string;
38
+ type: string;
39
+ props?: TProps;
40
+ }
41
+ interface PageDef {
42
+ id: string;
43
+ path: string;
44
+ blocks: BlockDef[];
45
+ }
46
+ interface SiteConfig {
47
+ version?: number;
48
+ theme?: SiteConfigTheme;
49
+ themeOverrides?: Record<string, unknown>;
50
+ pages?: PageDef[];
51
+ }
52
+
53
+ export type { BlockDef as B, PageDef as P, SiteEnvironment as S, SiteConfig as a, PropiedadCardBlockProps as b, SiteConfigTheme as c };
@@ -0,0 +1,53 @@
1
+ type SiteEnvironment = 'draft' | 'live';
2
+ interface SiteConfigTheme {
3
+ primaryColor?: string;
4
+ accentColor?: string;
5
+ logoUrl?: string;
6
+ customCssVars?: Record<string, string>;
7
+ }
8
+ interface PropiedadCardBlockProps {
9
+ theme?: {
10
+ primaryColor?: string;
11
+ accentColor?: string;
12
+ badgeBackground?: string;
13
+ cardBorderRadius?: string;
14
+ imageAspectRatio?: '16/9' | '4/3' | '1/1';
15
+ };
16
+ showFields?: Record<string, boolean>;
17
+ badges?: {
18
+ featured?: {
19
+ enabled: boolean;
20
+ label?: string;
21
+ };
22
+ virtualTour?: {
23
+ enabled: boolean;
24
+ label?: string;
25
+ };
26
+ custom?: Array<{
27
+ key: string;
28
+ label: string;
29
+ when?: string;
30
+ }>;
31
+ };
32
+ layout?: 'compact' | 'default' | 'minimal';
33
+ linkTarget?: '_self' | '_blank';
34
+ openDetailIn?: 'same' | 'modal' | 'drawer';
35
+ }
36
+ interface BlockDef<TProps = Record<string, unknown>> {
37
+ id: string;
38
+ type: string;
39
+ props?: TProps;
40
+ }
41
+ interface PageDef {
42
+ id: string;
43
+ path: string;
44
+ blocks: BlockDef[];
45
+ }
46
+ interface SiteConfig {
47
+ version?: number;
48
+ theme?: SiteConfigTheme;
49
+ themeOverrides?: Record<string, unknown>;
50
+ pages?: PageDef[];
51
+ }
52
+
53
+ export type { BlockDef as B, PageDef as P, SiteEnvironment as S, SiteConfig as a, PropiedadCardBlockProps as b, SiteConfigTheme as c };
package/dist/index.cjs CHANGED
@@ -526,7 +526,8 @@ PropiedadCard = __decorateClass([
526
526
 
527
527
  // src/sdk/site-runtime.ts
528
528
  var BUILTIN_PLUGIN_URLS = {
529
- "onl-hero": { js: "/plugins/example/hero-plugin.js", css: "/plugins/example/hero-plugin.css" }
529
+ "onl-hero": { js: "/plugins/example/hero-plugin.js", css: "/plugins/example/hero-plugin.css" },
530
+ "onl-header": { js: "/plugins/example/header-plugin.js", css: "/plugins/example/header-plugin.css" }
530
531
  };
531
532
  var loadedPlugins = /* @__PURE__ */ new Set();
532
533
  function getSlugFromPath(pathname) {
@@ -700,10 +701,9 @@ async function bootSiteRuntime(app, options = {}) {
700
701
  } else {
701
702
  setApiBase(getApiBase());
702
703
  }
703
- const getSlug = options.getSlugFromPath ?? getSlugFromPath;
704
- const slug = getSlug(typeof window !== "undefined" ? window.location.pathname : "");
704
+ const slug = options.slug?.trim() || (options.getSlugFromPath ?? getSlugFromPath)(typeof window !== "undefined" ? window.location.pathname : "");
705
705
  if (!slug) {
706
- app.innerHTML = "<p>URL debe ser /tienda/:slug (ej. /tienda/mi-inmobiliaria)</p>";
706
+ app.innerHTML = "<p>Indica el tenant con <code>slug</code> en opciones (recomendado) o con <code>getSlugFromPath</code>. Ej: bootSiteRuntime(app, { slug: 'mi-inmobiliaria', baseUrl: 'https://api.ejemplo.com' })</p>";
707
707
  return;
708
708
  }
709
709
  app.innerHTML = "<p>Cargando...</p>";