@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 +20 -1
- package/README.md +61 -0
- package/backend/routes/frontend.js +14 -4
- package/core/template.js +73 -8
- package/docker-compose.yml +1 -1
- package/frontend/public/js/blue-bird.js +1465 -0
- package/frontend/templates/about.html +5 -1
- package/frontend/templates/index.html +7 -1
- package/frontend/templates/preact_example.html +80 -0
- package/package.json +1 -1
- package/frontend/public/js/spa.js +0 -183
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.
|
|
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
|
-
|
|
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 ||
|
|
86
|
+
cacheKey ||
|
|
87
|
+
buildCacheKey(`${cachePrefix}${templateOrContent}`, metaTags, extraKey);
|
|
83
88
|
|
|
84
|
-
const cacheDuration = typeof cache === "number" ? cache :
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 (
|
|
163
|
-
|
|
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}`);
|