@seip/blue-bird 0.6.1 → 0.6.3

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/AGENTS.md CHANGED
@@ -264,7 +264,26 @@ npx blue-bird docker prune # Cleans unused volumes, dangling images, an
264
264
 
265
265
  The container names and virtual networks are namespaced by the `TITLE` environment variable parsed from `.env` to prevent resource collisions on VPS hosts.
266
266
 
267
- ## 9. AI Development Guidelines
267
+ ## 9. Hybrid SPA & Preact Integration
268
+
269
+ Blue Bird includes a high-performance Hybrid SPA rendering feature.
270
+ 1. **Server Detection**: `Template.render` intercepts SPA requests (header `X-blueBird-SPA: true` or query `?source=frontend`). It returns a parsed JSON payload with `meta`, `body` (only content inside `#blueBird-spa-content`), and `css` stylesheets.
271
+ 2. **SPA Direct Caching**: SPA requests are cached separately (key `spa:<template>`). The server strips security headers (CSP, Frame-Options, Content-Type-Options) to reduce JSON payload bytes.
272
+ 3. **Preact Islands**: Use import maps to render dynamic reactive zones. Simply add scripts of type `module` inside `#blueBird-spa-content`, and they will automatically execute and render on each navigation.
273
+
274
+ ```html
275
+ <script type="importmap">
276
+ {
277
+ "imports": {
278
+ "preact": "https://esm.sh/preact@10.19.2",
279
+ "preact/hooks": "https://esm.sh/preact@10.19.2/hooks",
280
+ "htm/preact": "https://esm.sh/htm@3.1.1/preact"
281
+ }
282
+ }
283
+ </script>
284
+ ```
285
+
286
+ ## 10. AI Development Guidelines
268
287
 
269
288
  1. **Frontend**: Use static HTML templates inside `frontend/` (e.g. `.html` files). Standard static files belong in `frontend/public/`.
270
289
  2. **JSON Responses**: API endpoints should return standardized responses formatted as `{ message: "..." }` or `{ data: ... }`.
package/README.md CHANGED
@@ -298,6 +298,67 @@ To deploy in a standard Linux environment using PM2 process manager:
298
298
 
299
299
  ---
300
300
 
301
+ ---
302
+
303
+ ## ⚡ Hybrid SPA & Preact Integration
304
+
305
+ Blue Bird natively integrates high-performance Hybrid Single Page Application (SPA) support:
306
+ * **Server-Side:** The `Template` class intercepts SPA requests and returns only the `#blueBird-spa-content` section wrapped in an optimized JSON payload. These requests feature cleaned headers (removing strict CSP/Frame rules to minimize footprint) and are cached independently in RAM.
307
+ * **Client-Side (`blue-bird.js`):** The SPA engine intercepts local anchor link click events, plays animated *fadeOut* and *fadeIn* transitions using Anime.js, updates metadata, and injects the new HTML fragments.
308
+
309
+ ### Preact Integration (Reactive Component Structure)
310
+
311
+ You can inject rich interactivity by adding Preact and HTM via Import Maps directly, without any build steps or bundlers:
312
+
313
+ #### 1. Load the Import Map in your HTML head:
314
+ ```html
315
+ <head>
316
+ <!-- ... -->
317
+ <script type="importmap">
318
+ {
319
+ "imports": {
320
+ "preact": "https://esm.sh/preact@10.19.2",
321
+ "preact/hooks": "https://esm.sh/preact@10.19.2/hooks",
322
+ "htm/preact": "https://esm.sh/htm@3.1.1/preact"
323
+ }
324
+ }
325
+ </script>
326
+ <script src="/js/blue-bird.js"></script>
327
+ </head>
328
+ ```
329
+
330
+ #### 2. Declare the component inside the `#blueBird-spa-content` container:
331
+ ```html
332
+ <main id="blueBird-spa-content">
333
+ <div id="counter-app"></div>
334
+
335
+ <script type="module">
336
+ import { render } from 'preact';
337
+ import { useState } from 'preact/hooks';
338
+ import { html } from 'htm/preact';
339
+
340
+ function Counter() {
341
+ const [count, setCount] = useState(0);
342
+ return html`
343
+ <div class="p-6 bg-slate-900 border border-white/10 rounded-2xl">
344
+ <h2 class="text-lg font-bold">Preact Counter</h2>
345
+ <p class="text-3xl text-blue-400 font-extrabold my-2">${count}</p>
346
+ <button class="px-4 py-2 bg-blue-600 rounded-lg text-white font-semibold" onClick=${() => setCount(count + 1)}>
347
+ Increment
348
+ </button>
349
+ </div>
350
+ `;
351
+ }
352
+
353
+ render(html`<${Counter} />`, document.getElementById('counter-app'));
354
+ </script>
355
+ </main>
356
+ ```
357
+
358
+ When navigating between pages using the SPA engine, any scripts of type `module` inside the page body will automatically execute in the DOM, mounting and updating your Preact "interactivity islands".
359
+
360
+ ---
361
+
301
362
  ## 📄 License / Licencia
302
363
 
303
364
  Distributed under the **MIT License**. See `LICENSE` for more information.
@@ -11,8 +11,8 @@ routerFrontendExample.get("/", (req, res) => {
11
11
  metaTags: {
12
12
  titleMeta: "Home - Blue Bird",
13
13
  descriptionMeta: "Welcome to Blue Bird Framework",
14
- keywordsMeta: "blue bird, framework, express"
15
- }
14
+ keywordsMeta: "blue bird, framework, express",
15
+ },
16
16
  });
17
17
  });
18
18
 
@@ -21,8 +21,18 @@ routerFrontendExample.get("/about", (req, res) => {
21
21
  metaTags: {
22
22
  titleMeta: "About - Blue Bird",
23
23
  descriptionMeta: "About Blue Bird Framework",
24
- keywordsMeta: "about, blue bird, framework"
25
- }
24
+ keywordsMeta: "about, blue bird, framework",
25
+ },
26
+ });
27
+ });
28
+
29
+ routerFrontendExample.get("/preact_example", (req, res) => {
30
+ return Template.render(res, "preact_example", {
31
+ metaTags: {
32
+ titleMeta: "Preact Example - Blue Bird",
33
+ descriptionMeta: "Preact Example Blue Bird Framework",
34
+ keywordsMeta: "preact, example, blue bird, framework",
35
+ },
26
36
  });
27
37
  });
28
38
 
package/core/template.js CHANGED
@@ -75,19 +75,33 @@ class Template {
75
75
  metaTags = {},
76
76
  } = options;
77
77
 
78
- res.type("text/html");
78
+ const isSpaRequest =
79
+ res.req &&
80
+ (res.req.headers["x-bluebird-spa"] === "true" ||
81
+ res.req.query?.source === "frontend");
79
82
 
80
83
  const extraKey = res.req ? res.req.originalUrl : "";
84
+ const cachePrefix = isSpaRequest ? "spa:" : "html:";
81
85
  const finalCacheKey =
82
- cacheKey || buildCacheKey(`html:${templateOrContent}`, metaTags, extraKey);
86
+ cacheKey ||
87
+ buildCacheKey(`${cachePrefix}${templateOrContent}`, metaTags, extraKey);
83
88
 
84
- const cacheDuration = typeof cache === "number" ? cache : (cache ? 60 : 0);
89
+ const cacheDuration = typeof cache === "number" ? cache : cache ? 60 : 0;
85
90
  const isCacheEnabled = !props.debug && cacheDuration > 0;
86
91
 
87
92
  if (isCacheEnabled && CACHE_TEMPLATE[finalCacheKey]) {
88
93
  const cached = CACHE_TEMPLATE[finalCacheKey];
89
94
  if (cached.expiry === 0 || cached.expiry > Date.now()) {
90
- return res.send(cached.html);
95
+ if (isSpaRequest) {
96
+ res.removeHeader("Content-Security-Policy");
97
+ res.removeHeader("X-Frame-Options");
98
+ res.removeHeader("X-Content-Type-Options");
99
+ res.type("application/json");
100
+ return res.json(cached.json);
101
+ } else {
102
+ res.type("text/html");
103
+ return res.send(cached.html);
104
+ }
91
105
  }
92
106
  delete CACHE_TEMPLATE[finalCacheKey];
93
107
  }
@@ -119,7 +133,8 @@ class Template {
119
133
  }
120
134
 
121
135
  const title = metaTags.titleMeta || props.titleMeta || "";
122
- const description = metaTags.descriptionMeta || props.descriptionMeta || "";
136
+ const description =
137
+ metaTags.descriptionMeta || props.descriptionMeta || "";
123
138
  const keywords = metaTags.keywordsMeta || props.keywordsMeta || "";
124
139
  const author = metaTags.authorMeta || props.authorMeta || "";
125
140
  const canonicalUrl = metaTags.canonicalUrl || props.appUrl || "";
@@ -154,13 +169,24 @@ class Template {
154
169
 
155
170
  for (const [k, v] of Object.entries(metaTags)) {
156
171
  if (typeof v !== "object" && !escapes[k] && !raws[k]) {
157
- finalHtml = finalHtml.replaceAll(`{{${k}}}`, this.escapeHtml(String(v)));
172
+ finalHtml = finalHtml.replaceAll(
173
+ `{{${k}}}`,
174
+ this.escapeHtml(String(v)),
175
+ );
158
176
  }
159
177
  }
160
178
 
161
179
  for (const [k, v] of Object.entries(options)) {
162
- if (k !== "metaTags" && typeof v !== "object" && !escapes[k] && !raws[k]) {
163
- finalHtml = finalHtml.replaceAll(`{{${k}}}`, this.escapeHtml(String(v)));
180
+ if (
181
+ k !== "metaTags" &&
182
+ typeof v !== "object" &&
183
+ !escapes[k] &&
184
+ !raws[k]
185
+ ) {
186
+ finalHtml = finalHtml.replaceAll(
187
+ `{{${k}}}`,
188
+ this.escapeHtml(String(v)),
189
+ );
164
190
  }
165
191
  }
166
192
 
@@ -179,12 +205,51 @@ class Template {
179
205
  finalHtml = this.minifyHtml(finalHtml);
180
206
  }
181
207
 
208
+ if (isSpaRequest) {
209
+ let bodyContent = "";
210
+ const match = finalHtml.match(
211
+ /<([a-zA-Z0-9\-]+)[^>]*id="blueBird-spa-content"[^>]*>([\s\S]*?)<\/\1>/i,
212
+ );
213
+ if (match) {
214
+ bodyContent = match[2];
215
+ } else {
216
+ bodyContent = finalHtml;
217
+ }
218
+
219
+ res.removeHeader("Content-Security-Policy");
220
+ res.removeHeader("X-Frame-Options");
221
+ res.removeHeader("X-Content-Type-Options");
222
+ res.type("application/json");
223
+
224
+ const spaData = {
225
+ meta: {
226
+ title: title,
227
+ description: description,
228
+ keywords: keywords,
229
+ author: author,
230
+ },
231
+ body: bodyContent,
232
+ css: options.linkStyles ? options.linkStyles.map((s) => s.href) : [],
233
+ };
234
+
235
+ if (isCacheEnabled) {
236
+ CACHE_TEMPLATE[finalCacheKey] = {
237
+ json: spaData,
238
+ expiry: cacheDuration > 0 ? Date.now() + cacheDuration * 1000 : 0,
239
+ };
240
+ }
241
+
242
+ return res.json(spaData);
243
+ }
244
+
182
245
  if (isCacheEnabled) {
183
246
  CACHE_TEMPLATE[finalCacheKey] = {
184
247
  html: finalHtml,
185
248
  expiry: cacheDuration > 0 ? Date.now() + cacheDuration * 1000 : 0,
186
249
  };
187
250
  }
251
+
252
+ res.type("text/html");
188
253
  res.send(finalHtml);
189
254
  } catch (error) {
190
255
  logger.error(`Error rendering HTML template: ${error.message}`);
@@ -22,7 +22,7 @@ services:
22
22
  container_name: ${TITLE:-bluebird}-app
23
23
  restart: unless-stopped
24
24
  ports:
25
- - "${PORT:-3000}:3000"
25
+ - "${PORT:-3000}:${PORT:-3000}"
26
26
  volumes:
27
27
  - .:/app
28
28
  - /app/node_modules