@seip/blue-bird 0.7.0 → 0.7.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/.env_example CHANGED
@@ -3,18 +3,23 @@ DEBUG=true
3
3
  PORT=3000
4
4
  HOST="localhost"
5
5
  APP_URL="http://localhost"
6
- STATIC_PATH="frontend/public"
7
6
  VERSION="1.0.0"
8
- BLUEBIRD_PROJECT_NAME="bluebird"
9
7
 
10
- #Docker /Swagger config
8
+ # Docker / Swagger Config
11
9
  TITLE="Blue-Bird"
12
10
  DESCRIPTION="Description project"
13
11
 
14
-
15
12
  # Security Configuration
16
13
  JWT_SECRET="JWT_SECRET"
17
14
 
15
+ # Redis Configuration
16
+ REDIS_HOST="localhost"
17
+ REDIS_PORT=6379
18
+ REDIS_PASSWORD=""
19
+
20
+ # PM2 Clustering Instances (integer or 'max')
21
+ PM2_INSTANCES=1
22
+
18
23
  # Database Configuration (Used only for local development outside Docker)
19
24
  # SQLite (Default)
20
25
  DATABASE_URL="file:./dev.db"
package/AGENTS.md CHANGED
@@ -131,7 +131,7 @@ router.post("/logout", async (req, res) => {
131
131
 
132
132
  ## 6. Performance Caching (Cache)
133
133
 
134
- If an Express route involves heavy processing or database queries, utilize the `Cache` middleware to cache the REST API JSON payload.
134
+ If an Express route involves heavy processing or database queries, utilize the `Cache` middleware to cache the REST API JSON or HTML payload.
135
135
 
136
136
  ```javascript
137
137
  import Cache from "@seip/blue-bird/core/cache.js";
@@ -141,6 +141,8 @@ router.get("/stats", Cache.middleware(60), (req, res) => {
141
141
  });
142
142
  ```
143
143
 
144
+ The Cache module integrates with Redis when `REDIS_HOST` is configured in the environment. If Redis is unavailable or fails, it transparently falls back to an in-memory cache system without interrupting requests.
145
+
144
146
  ## 7. Security (Helmet)
145
147
 
146
148
  Helmet is **not applied globally** by default. Apply it per-router where needed:
@@ -156,19 +158,28 @@ apiRouter.use(App.helmet());
156
158
 
157
159
  Blue Bird features a built-in Docker Compose CLI wrapper to deploy and manage containerized development databases and production stacks.
158
160
 
161
+ Production deployments always use Docker for orchestration, running:
162
+ - Nginx: Serves static files directly from `frontend/dist/client/` and blocks common scanner requests (`.env`, `.git`, etc.) with fallback to Express.
163
+ - Node.js App: Managed via PM2 in cluster mode using `PM2_INSTANCES` configuration (defaults to `1`, can be set to `max`).
164
+ - MySQL: Database service.
165
+ - Redis: Memory caching and session store.
166
+
159
167
  ```bash
160
168
  # Manage containers using blue-bird CLI
161
- npx blue-bird docker start # Starts production app + mysql database
169
+ npx blue-bird docker start # Starts production app stack (mysql, redis, app, nginx)
162
170
  npx blue-bird docker start mysql # Starts MySQL container only (useful for local development)
171
+ npx blue-bird docker start redis # Starts Redis container only
172
+ npx blue-bird docker start dbs # Starts both database containers (MySQL + Redis)
163
173
  npx blue-bird docker stop # Stops all running containers
164
174
  npx blue-bird docker build # Builds/rebuilds application image
165
175
  npx blue-bird docker ps # Shows status of active containers
166
176
  npx blue-bird docker logs # Tails Node.js app container logs
177
+ npx blue-bird docker pm2 [args] # Runs PM2 commands inside the app container (e.g. status, monit)
167
178
  npx blue-bird docker mysql # Runs interactive MySQL client terminal inside the container
168
179
  npx blue-bird docker prune # Cleans unused volumes, dangling images, and BuildKit caches
169
180
  ```
170
181
 
171
- The container names and virtual networks are namespaced by the `TITLE` environment variable parsed from `.env` to prevent resource collisions on VPS hosts.
182
+ The container names and virtual networks are namespaced by the `TITLE` environment variable parsed from `.env` to prevent resource collisions on VPS hosts. Alternatively, PM2 and other services can be run manually in standalone server environments.
172
183
 
173
184
  ## 9. AI Development Guidelines
174
185
 
package/README.md CHANGED
@@ -62,8 +62,8 @@ project/
62
62
  │ │ ├── index.astro
63
63
  │ │ └── about.astro
64
64
  │ ├── public/ # Static assets mapped to root of Astro build
65
- │ │ └── js/
66
- │ │ └── tailwind.js # Local Tailwind compiler
65
+ │ │ └── css/
66
+ │ │ └── app.css # Css files
67
67
  │ └── astro.config.mjs # Astro configuration file
68
68
  ├── docker/
69
69
  │ └── Dockerfile # Optimized production build file
@@ -122,8 +122,8 @@ const app = new App({
122
122
  serverEntry: "./frontend/dist/server/entry.mjs", // Path to compiled Astro server entrypoint
123
123
  client: false, // Set to true to serve static files from client build
124
124
  clientDir: "./frontend/dist/client", // Path to Astro client static assets
125
- base: "/" // Mount base path
126
- }
125
+ base: "/", // Mount base path
126
+ },
127
127
  });
128
128
  ```
129
129
 
@@ -201,6 +201,8 @@ router.get("/stats", Cache.middleware(60), (req, res) => {
201
201
  });
202
202
  ```
203
203
 
204
+ Integrates with Redis if `REDIS_HOST` is defined in the environment. Falls back to an in-memory cache automatically if Redis is not configured or not running.
205
+
204
206
  ---
205
207
 
206
208
  ### 6. Security Headers (`Helmet`)
@@ -228,12 +230,15 @@ npx blue-bird docker <command> [options]
228
230
 
229
231
  ### Supported Actions:
230
232
 
231
- - **`npx blue-bird docker start`**: Boots the production stack (Node.js App + MySQL).
233
+ - **`npx blue-bird docker start`**: Boots the production stack (Node.js App + Nginx + MySQL + Redis).
232
234
  - **`npx blue-bird docker start mysql`**: Boots the MySQL container only (great for local HTTP development).
235
+ - **`npx blue-bird docker start redis`**: Boots the Redis container only.
236
+ - **`npx blue-bird docker start dbs`**: Boots both database containers (MySQL + Redis).
233
237
  - **`npx blue-bird docker stop`**: Stops all active containers.
234
238
  - **`npx blue-bird docker build [--no-cache]`**: Builds or updates the Node.js production image.
235
239
  - **`npx blue-bird docker ps`**: Lists running project containers and ports.
236
240
  - **`npx blue-bird docker logs [app|mysql]`**: Tails logs for the specified container.
241
+ - **`npx blue-bird docker pm2 [args]`**: Runs PM2 commands inside the Node.js application container (e.g. `status`, `monit`, `reload all`).
237
242
  - **`npx blue-bird docker db`**: Connects into the container's interactive MySQL shell using credentials from `.env`.
238
243
  - **`npx blue-bird docker prune`**: Safely clears orphaned volumes, dangling build caches, and images.
239
244
 
@@ -243,9 +248,16 @@ npx blue-bird docker <command> [options]
243
248
 
244
249
  You can deploy Blue Bird applications to production using two main workflows:
245
250
 
246
- ### A. Docker Container Stack (Recommended)
251
+ ### A. Docker Container Stack (Highly Recommended)
252
+
253
+ Using the built-in Docker stack is the recommended deployment method because it sets up a complete, hardened production environment automatically:
254
+ - **Nginx Reverse Proxy:** Captures traffic on port 3000 (or custom PORT), serves Astro client-side assets directly from the filesystem to offload the Node.js server, and proxies the rest to Express.
255
+ - **PM2 Clustering:** Launches Node.js in cluster mode inside the container, utilizing all available CPU cores based on `PM2_INSTANCES` configuration (defaulting to 1).
256
+ - **Security Mitigation:** Nginx blocks common malicious scanners (e.g. `/.env`, `/.git`, `/wp-admin`) instantly using a 444 status code and implements a `10r/s` request rate-limit.
257
+ - **Services Stack:** MySQL and Redis are configured in the same bridge network automatically.
247
258
 
248
- 1. Configure `.env` with production keys,DEBUG=false and your custom `TITLE`.
259
+ To deploy via Docker:
260
+ 1. Configure `.env` with production keys, `DEBUG=false` and your custom `TITLE`.
249
261
  2. Build the production image:
250
262
  ```bash
251
263
  npx blue-bird docker build
@@ -255,9 +267,9 @@ You can deploy Blue Bird applications to production using two main workflows:
255
267
  npx blue-bird docker start prod
256
268
  ```
257
269
 
258
- ### B. Standard PM2 / Node.js Runtime
270
+ ### B. Standard Standalone PM2 / Node.js Runtime
259
271
 
260
- To deploy in a standard Linux environment using PM2 process manager:
272
+ If you choose to run outside of Docker, you must set up the reverse proxy and databases manually. To deploy in a standard Linux environment using PM2:
261
273
 
262
274
  1. Install PM2 globally:
263
275
  ```bash
@@ -265,7 +277,7 @@ To deploy in a standard Linux environment using PM2 process manager:
265
277
  ```
266
278
  2. Start the application under PM2:
267
279
  ```bash
268
- pm2 start index.js --name "bluebird-app"
280
+ pm2 start index.js --name "bluebird-app" --node-args="--env-file=.env" -i max
269
281
  ```
270
282
  3. Monitor status:
271
283
  ```bash
package/core/cache.js CHANGED
@@ -1,38 +1,101 @@
1
+ import fs from "node:fs";
2
+
1
3
  const CACHE = {};
2
4
 
5
+ let redisClient = null;
6
+ let isRedisConnected = false;
7
+ const redisHost = process.env.REDIS_HOST ?? false;
8
+ const redisPort = process.env.REDIS_PORT ?? 6379;
9
+ const redisPassword = process.env.REDIS_PASSWORD || "";
10
+ const redisUrl = redisPassword
11
+ ? `redis://:${redisPassword}@${redisHost}:${redisPort}`
12
+ : `redis://${redisHost}:${redisPort}`;
13
+
14
+ /**
15
+ * Initializes the Redis client connection if REDIS_HOST env is set.
16
+ * @returns {Promise<void>}
17
+ */
18
+ async function initRedis() {
19
+ if (redisClient) return;
20
+ if (!redisHost) return;
21
+
22
+ try {
23
+ const { createClient } = await import("redis");
24
+ let host = redisHost;
25
+ if (host === "localhost" && fs.existsSync("/.dockerenv")) {
26
+ host = "redis";
27
+ }
28
+ const url = redisUrl;
29
+
30
+ redisClient = createClient({ url });
31
+ redisClient.on("error", () => {
32
+ isRedisConnected = false;
33
+ });
34
+ await redisClient.connect();
35
+ isRedisConnected = true;
36
+ } catch (err) {
37
+ redisClient = null;
38
+ isRedisConnected = false;
39
+ }
40
+ }
41
+
42
+ initRedis().catch(() => {});
43
+
3
44
  setInterval(() => {
4
- const now = Date.now();
5
- for (const key in CACHE) {
6
- if (CACHE[key].expiry <= now) {
7
- delete CACHE[key];
8
- }
45
+ const now = Date.now();
46
+ for (const key in CACHE) {
47
+ if (CACHE[key].expiry <= now) {
48
+ delete CACHE[key];
9
49
  }
50
+ }
10
51
  }, 300000).unref();
52
+
11
53
  /**
12
- * Simple in-memory Cache class to provide middleware for Express routes.
13
- * Caches JSON responses based on the request URL.
54
+ * High-performance Caching class supporting both local memory and Redis backends.
14
55
  */
15
56
  class Cache {
16
57
  /**
17
- * Middleware to cache responses.
18
- * @param {number} [seconds=60] - Number of seconds to cache the response.
19
- * @returns {Function} Express middleware function.
20
- * @example
21
- * router.get("/stats", Cache.middleware(120), (req, res) => {
22
- * res.json({ ok: true });
23
- * });
58
+ * Express middleware to cache route JSON and HTML responses.
59
+ * @param {number} [seconds=60] - Expiry time in seconds.
60
+ * @returns {Function} Express middleware.
24
61
  */
25
62
  static middleware(seconds = 60) {
26
- return (req, res, next) => {
63
+ return async (req, res, next) => {
27
64
  const key = req.originalUrl;
28
65
 
29
- if (CACHE[key] && CACHE[key].expiry > Date.now()) {
30
- const cached = CACHE[key];
31
- if (cached.type === "json") {
32
- return res.json(cached.data);
33
- } else {
34
- res.type("text/html");
35
- return res.send(cached.data);
66
+ if (redisHost && !redisClient) {
67
+ await initRedis().catch(() => {});
68
+ }
69
+
70
+ if (isRedisConnected && redisClient) {
71
+ try {
72
+ const cachedData = await redisClient.get(key);
73
+ if (cachedData) {
74
+ const cached = JSON.parse(cachedData);
75
+ if (cached.type === "json") {
76
+ return res.json(cached.data);
77
+ } else {
78
+ res.type("text/html");
79
+ res.set("X-Blue-Bird-Cache", "HIT");
80
+ return res.send(cached.data);
81
+ }
82
+ }
83
+ } catch (err) {
84
+ isRedisConnected = false;
85
+ }
86
+ }
87
+
88
+ if (!isRedisConnected || !redisClient) {
89
+ if (CACHE[key] && CACHE[key].expiry > Date.now()) {
90
+ const cached = CACHE[key];
91
+ if (cached.type === "json") {
92
+ res.set("X-Blue-Bird-Cache", "HIT");
93
+ return res.json(cached.data);
94
+ } else {
95
+ res.type("text/html");
96
+ res.set("X-Blue-Bird-Cache", "HIT");
97
+ return res.send(cached.data);
98
+ }
36
99
  }
37
100
  }
38
101
 
@@ -40,33 +103,58 @@ class Cache {
40
103
  const originalSend = res.send.bind(res);
41
104
  let cachedInRequest = false;
42
105
 
43
- res.json = (body) => {
106
+ res.json = async (body) => {
44
107
  if (!cachedInRequest) {
45
- CACHE[key] = {
108
+ cachedInRequest = true;
109
+ const cacheObject = {
46
110
  type: "json",
47
111
  data: body,
48
112
  expiry: Date.now() + seconds * 1000,
49
113
  };
50
- cachedInRequest = true;
114
+ if (isRedisConnected && redisClient) {
115
+ try {
116
+ await redisClient.set(key, JSON.stringify(cacheObject), {
117
+ EX: seconds,
118
+ });
119
+ } catch (err) {
120
+ CACHE[key] = cacheObject;
121
+ }
122
+ } else {
123
+ CACHE[key] = cacheObject;
124
+ }
51
125
  }
126
+ res.set("X-Blue-Bird-Cache", "MISS");
52
127
  return originalJson(body);
53
128
  };
54
129
 
55
- res.send = (body) => {
130
+ res.send = async (body) => {
56
131
  if (!cachedInRequest && typeof body === "string") {
57
- CACHE[key] = {
132
+ cachedInRequest = true;
133
+ const cacheObject = {
58
134
  type: "html",
59
135
  data: body,
60
136
  expiry: Date.now() + seconds * 1000,
61
137
  };
62
- cachedInRequest = true;
138
+ if (isRedisConnected && redisClient) {
139
+ try {
140
+ await redisClient.set(key, JSON.stringify(cacheObject), {
141
+ EX: seconds,
142
+ });
143
+ } catch (err) {
144
+ CACHE[key] = cacheObject;
145
+ }
146
+ } else {
147
+ CACHE[key] = cacheObject;
148
+ }
63
149
  }
150
+ res.set("X-Blue-Bird-Cache", "MISS");
64
151
  return originalSend(body);
65
152
  };
66
153
 
154
+ res.set("X-Blue-Bird-Cache", "MISS");
67
155
  next();
68
156
  };
69
157
  }
70
158
  }
71
159
 
72
- export default Cache;
160
+ export default Cache;
@@ -58,27 +58,43 @@ async function startCommand(service) {
58
58
  checkComposeFile();
59
59
 
60
60
  if (service === "mysql" || service === "--mysql" || service === "dev") {
61
- console.log(chalk.cyan("Starting MySQL container (dev mode)..."));
61
+ console.log(chalk.cyan("Starting MySQL container..."));
62
62
  const code = await runCmd("docker", ["compose", "up", "-d", "mysql"]);
63
63
  if (code === 0) {
64
- console.log(chalk.green("MySQL started. Run your app locally with: npm run dev"));
65
- console.log(chalk.blue("To also run the app in Docker (production), use: npx blue-bird docker start prod"));
64
+ console.log(chalk.green("MySQL started."));
66
65
  } else {
67
66
  console.error(chalk.red("Error starting MySQL."));
68
67
  process.exit(1);
69
68
  }
69
+ } else if (service === "redis" || service === "--redis") {
70
+ console.log(chalk.cyan("Starting Redis container..."));
71
+ const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
72
+ if (code === 0) {
73
+ console.log(chalk.green("Redis started."));
74
+ } else {
75
+ console.error(chalk.red("Error starting Redis."));
76
+ process.exit(1);
77
+ }
78
+ } else if (service === "dbs" || service === "databases") {
79
+ console.log(chalk.cyan("Starting Database containers (MySQL + Redis)..."));
80
+ const code = await runCmd("docker", ["compose", "up", "-d", "mysql", "redis"]);
81
+ if (code === 0) {
82
+ console.log(chalk.green("Database containers started."));
83
+ } else {
84
+ console.error(chalk.red("Error starting databases."));
85
+ process.exit(1);
86
+ }
70
87
  } else if (service === "prod" || service === "app" || service === "--app" || !service) {
71
- console.log(chalk.cyan("Starting production stack (MySQL + Node.js app)..."));
88
+ console.log(chalk.cyan("Starting production stack (MySQL + Redis + App + Nginx)..."));
72
89
  const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
73
90
  if (code === 0) {
74
91
  console.log(chalk.green("Production stack started."));
75
- console.log(chalk.blue("View logs with: npx blue-bird docker logs"));
76
92
  } else {
77
93
  console.error(chalk.red("Error starting production stack."));
78
94
  process.exit(1);
79
95
  }
80
96
  } else {
81
- console.error(chalk.red(`Unknown service '${service}'. Use: mysql (or --mysql), prod (or --app), or no arguments for both.`));
97
+ console.error(chalk.red(`Unknown service '${service}'. Use: mysql, redis, dbs, prod.`));
82
98
  process.exit(1);
83
99
  }
84
100
  }
@@ -99,13 +115,18 @@ async function stopCommand(service) {
99
115
  await runCmd("docker", ["compose", "stop", "mysql"]);
100
116
  await runCmd("docker", ["compose", "rm", "-f", "mysql"]);
101
117
  console.log(chalk.green("MySQL stopped."));
118
+ } else if (service === "redis" || service === "--redis") {
119
+ console.log(chalk.cyan("Stopping Redis..."));
120
+ await runCmd("docker", ["compose", "stop", "redis"]);
121
+ await runCmd("docker", ["compose", "rm", "-f", "redis"]);
122
+ console.log(chalk.green("Redis stopped."));
102
123
  } else if (service === "app" || service === "--app") {
103
124
  console.log(chalk.cyan("Stopping Node.js app container..."));
104
125
  await runCmd("docker", ["compose", "--profile", "prod", "stop", "app"]);
105
126
  await runCmd("docker", ["compose", "--profile", "prod", "rm", "-f", "app"]);
106
127
  console.log(chalk.green("App container stopped."));
107
128
  } else {
108
- console.error(chalk.red(`Unknown service '${service}'. Use: all (default), mysql, app.`));
129
+ console.error(chalk.red(`Unknown service '${service}'. Use: all, mysql, redis, app.`));
109
130
  process.exit(1);
110
131
  }
111
132
  }
@@ -235,6 +256,21 @@ async function pruneCommand(forceOpt, allOpt) {
235
256
  await runCmd("docker", ["system", "df"]);
236
257
  }
237
258
 
259
+ /**
260
+ * Executes PM2 commands inside the Node.js application container.
261
+ * @param {string[]} pm2Args - Arguments to pass to PM2.
262
+ */
263
+ async function pm2Command(pm2Args = []) {
264
+ checkComposeFile();
265
+ const subCommand = pm2Args[0] || "status";
266
+ const cmdArgs = ["compose", "exec", "app", "pm2", subCommand, ...pm2Args.slice(1)];
267
+ const code = await runCmd("docker", cmdArgs);
268
+ if (code !== 0) {
269
+ console.error(chalk.red("Error running PM2 command. Make sure the production stack is started."));
270
+ process.exit(1);
271
+ }
272
+ }
273
+
238
274
  /**
239
275
  * Entry point for Blue Bird CLI Docker subcommands.
240
276
  */
@@ -286,6 +322,9 @@ async function main() {
286
322
  case "logs":
287
323
  await logsCommand(args[1], args[2]);
288
324
  break;
325
+ case "pm2":
326
+ await pm2Command(args.slice(1));
327
+ break;
289
328
  case "mysql":
290
329
  case "db": {
291
330
  let user, password, db, root = false;
@@ -312,7 +351,7 @@ async function main() {
312
351
  }
313
352
  default:
314
353
  console.log(chalk.yellow(`Unknown docker command: ${command}`));
315
- console.log("Available commands: start, stop, build, ps, logs, mysql/db, df/disk, prune/clean");
354
+ console.log("Available commands: start, stop, build, ps, logs, pm2, mysql/db, df/disk, prune/clean");
316
355
  }
317
356
  }
318
357
 
package/docker/Dockerfile CHANGED
@@ -1,25 +1,16 @@
1
- # ─────────────────────────────────────────────────────────────────────────────
2
- # Blue Bird Framework — Production Dockerfile
3
- # Node.js 24-alpine for lightweight, production-ready environment
4
- # ─────────────────────────────────────────────────────────────────────────────
5
1
  FROM node:24.7.0-alpine3.21
6
2
 
7
- # Set environment variables
8
3
  ENV NODE_ENV=production
9
4
 
10
5
  WORKDIR /app
11
6
 
12
- # Copy package files first (layer cache optimization)
13
7
  COPY package*.json ./
14
8
 
15
- # Install production dependencies
16
- RUN npm ci --omit=dev
9
+ RUN npm ci --omit=dev && npm install -g pm2
17
10
 
18
- # Copy project source
19
11
  COPY . .
20
12
 
21
- # Expose the application port
22
13
  EXPOSE 3000
23
14
 
24
- # Start the application
25
- CMD ["npm", "start"]
15
+ CMD ["sh", "-c", "pm2-runtime start index.js -i ${PM2_INSTANCES:-1}"]
16
+
@@ -0,0 +1,83 @@
1
+ user nginx;
2
+ worker_processes auto;
3
+
4
+ error_log /var/log/nginx/error.log notice;
5
+ pid /var/run/nginx.pid;
6
+
7
+ events {
8
+ worker_connections 1024;
9
+ }
10
+
11
+ http {
12
+ include /etc/nginx/mime.types;
13
+ default_type application/octet-stream;
14
+
15
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16
+ '$status $body_bytes_sent "$http_referer" '
17
+ '"$http_user_agent" "$http_x_forwarded_for"';
18
+
19
+ access_log /var/log/nginx/access.log main;
20
+
21
+ sendfile on;
22
+ tcp_nopush on;
23
+ tcp_nodelay on;
24
+ keepalive_timeout 65;
25
+ types_hash_max_size 2048;
26
+
27
+ gzip on;
28
+ gzip_disable "msie6";
29
+ gzip_vary on;
30
+ gzip_proxied any;
31
+ gzip_comp_level 6;
32
+ gzip_buffers 16 8k;
33
+ gzip_http_version 1.1;
34
+ gzip_min_length 256;
35
+ gzip_types
36
+ text/plain
37
+ text/css
38
+ application/json
39
+ application/javascript
40
+ application/x-javascript
41
+ text/xml
42
+ application/xml
43
+ application/xml+rss
44
+ text/javascript
45
+ image/svg+xml;
46
+
47
+ limit_req_zone $binary_remote_addr zone=bluebird_limit:10m rate=10r/s;
48
+
49
+ server {
50
+ listen 80;
51
+ server_name localhost;
52
+ root /app/frontend/dist/client;
53
+
54
+ resolver 127.0.0.11 valid=5s;
55
+
56
+ location ~* /(\.env|\.git|wp-content|wp-admin|xmlrpc\.php) {
57
+ return 444;
58
+ }
59
+
60
+ location / {
61
+ try_files $uri @node_app;
62
+ }
63
+
64
+ location /_astro/ {
65
+ expires max;
66
+ add_header Cache-Control "public, max-age=31536000, immutable";
67
+ try_files $uri =404;
68
+ }
69
+
70
+ location @node_app {
71
+ limit_req zone=bluebird_limit burst=20 nodelay;
72
+
73
+ set $upstream_target http://app:3000;
74
+ proxy_pass $upstream_target;
75
+ proxy_http_version 1.1;
76
+ proxy_set_header Connection "";
77
+ proxy_set_header Host $host;
78
+ proxy_set_header X-Real-IP $remote_addr;
79
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
80
+ proxy_set_header X-Forwarded-Proto $scheme;
81
+ }
82
+ }
83
+ }
@@ -1,28 +1,12 @@
1
- # ─────────────────────────────────────────────────────────────────────────────
2
- # Blue Bird Framework — Docker Compose
3
- #
4
- # DEVELOPMENT / DATABASE ONLY:
5
- # npx blue-bird docker start mysql → Only MySQL container
6
- #
7
- # PRODUCTION:
8
- # npx blue-bird docker start prod → MySQL + Node.js app container
9
- # npx blue-bird docker stop → Stop all containers
10
- #
11
- # Multi-project VPS support:
12
- # Each project uses TITLE (or sanitised TITLE) to name
13
- # containers and networks uniquely.
14
- # ─────────────────────────────────────────────────────────────────────────────
15
-
16
1
  services:
17
- # ── Node.js App (Production only) ─────────────────────────────────────────
18
2
  app:
19
3
  build:
20
4
  context: .
21
5
  dockerfile: docker/Dockerfile
22
6
  container_name: ${TITLE:-bluebird}-app
23
7
  restart: unless-stopped
24
- ports:
25
- - "${PORT:-3000}:${PORT:-3000}"
8
+ expose:
9
+ - "3000"
26
10
  volumes:
27
11
  - .:/app
28
12
  - /app/node_modules
@@ -32,15 +16,36 @@ services:
32
16
  - NODE_ENV=production
33
17
  - DEBUG=false
34
18
  - DATABASE_URL=mysql://root:${DB_PASSWORD:-root}@mysql:3306/${DB_NAME:-blue_bird}
19
+ - REDIS_HOST=redis
20
+ - REDIS_PORT=6379
21
+ - PORT=3000
35
22
  depends_on:
36
23
  mysql:
37
24
  condition: service_healthy
25
+ redis:
26
+ condition: service_healthy
27
+ networks:
28
+ - bluebird_net
29
+ profiles:
30
+ - prod
31
+
32
+ nginx:
33
+ image: nginx:1.27-alpine
34
+ container_name: ${TITLE:-bluebird}-nginx
35
+ restart: unless-stopped
36
+ ports:
37
+ - "${PORT:-3000}:80"
38
+ volumes:
39
+ - .:/app:ro
40
+ - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
41
+ depends_on:
42
+ app:
43
+ condition: service_started
38
44
  networks:
39
45
  - bluebird_net
40
46
  profiles:
41
47
  - prod
42
48
 
43
- # ── MySQL Database ─────────────────────────────────────────────────────────
44
49
  mysql:
45
50
  image: mysql:8.0
46
51
  container_name: ${TITLE:-bluebird}-mysql
@@ -61,10 +66,28 @@ services:
61
66
  networks:
62
67
  - bluebird_net
63
68
 
69
+ redis:
70
+ image: redis:7-alpine
71
+ container_name: ${TITLE:-bluebird}-redis
72
+ restart: unless-stopped
73
+ ports:
74
+ - "${REDIS_PORT:-6379}:6379"
75
+ volumes:
76
+ - redis_data:/data
77
+ networks:
78
+ - bluebird_net
79
+ healthcheck:
80
+ test: ["CMD", "redis-cli", "ping"]
81
+ interval: 5s
82
+ timeout: 3s
83
+ retries: 5
84
+
64
85
  volumes:
65
86
  mysql_data:
87
+ redis_data:
66
88
 
67
89
  networks:
68
90
  bluebird_net:
69
91
  name: ${TITLE:-bluebird}_network
70
92
  driver: bridge
93
+
@@ -15,5 +15,10 @@ export function apiUrl(path = "", requestUrl) {
15
15
  if (!requestUrl) {
16
16
  throw new Error("apiUrl: requestUrl is required in production (pass Astro.url)");
17
17
  }
18
- return new URL(`/${path}`, requestUrl).href;
18
+ const url = new URL(`/${path}`, requestUrl);
19
+ if (url.hostname === "localhost") {
20
+ url.hostname = "127.0.0.1";
21
+ url.port = port;
22
+ }
23
+ return url.href;
19
24
  }
@@ -4,11 +4,17 @@ import { apiUrl } from "../http/api";
4
4
 
5
5
  let apiData = null;
6
6
  try {
7
- const response = await fetch(apiUrl("api/", Astro.url));
7
+ const targetUrl = apiUrl("api/", Astro.url);
8
+ console.log("[index.astro] request URL:", Astro.url.href);
9
+ console.log("[index.astro] target URL:", targetUrl);
10
+ const response = await fetch(targetUrl);
8
11
  apiData = await response.json();
9
12
  console.log("[index.astro] /api/ response:", apiData);
10
13
  } catch (err) {
11
14
  console.error("[index.astro] fetch error:", err.message);
15
+ if (err.cause) {
16
+ console.error("[index.astro] fetch error cause:", err.cause);
17
+ }
12
18
  }
13
19
  ---
14
20
 
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Express opinionated framework with HTML rendering, API architecture, built-in JWT auth, validation, caching, and SEO",
5
5
  "type": "module",
6
+ "exports": {
7
+ "./*": "./*"
8
+ },
6
9
  "bin": {
7
10
  "blue-bird": "core/cli/init.js"
8
11
  },
@@ -62,6 +65,7 @@
62
65
  "helmet": "^8.1.0",
63
66
  "jsonwebtoken": "^9.0.2",
64
67
  "multer": "^2.0.2",
68
+ "redis": "^4.7.0",
65
69
  "xss": "^1.0.15"
66
70
  }
67
71
  }