@seip/blue-bird 0.4.6 → 0.4.8

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
@@ -123,19 +123,57 @@ routerApi.post("/users", validateUser.middleware(), (req, res) => {
123
123
 
124
124
  ## 4. Authentication (Auth)
125
125
 
126
- The system includes built-in JWT handling. The framework assumes tokens are passed via Cookies or the `Authorization` header.
126
+ The system includes built-in JWT handling with AES-256-GCM encryption. The framework handles tokens via Cookies or the `Authorization` header.
127
+
128
+ ### Protecting Routes
129
+ Use `Auth.protect()` as a middleware to secure routes.
127
130
 
128
131
  ```javascript
129
132
  import Auth from "@seip/blue-bird/core/auth.js";
130
133
 
131
134
  // Protect an API route (returns 401 if failed):
132
135
  router.get("/profile", Auth.protect(), (req, res) => {
133
- // The user payload is attached to req.user
136
+ // The decrypted user payload is attached to req.user
134
137
  res.json({ user: req.user });
135
138
  });
136
139
 
137
140
  // Protect a React route (redirects to login):
138
141
  router.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => { ... });
142
+
143
+ // Custom cookie key and storage key:
144
+ router.get("/admin", Auth.protect({
145
+ cookieKey: "admin_session",
146
+ key: "admin"
147
+ }), (req, res) => {
148
+ res.json({ admin: req.admin });
149
+ });
150
+ ```
151
+
152
+ ### Login and Logout
153
+ The `Auth` class provides helpers to handle session management via cookies.
154
+
155
+ ```javascript
156
+ // Login a user
157
+ router.post("/login", async (req, res) => {
158
+ const user = { id: 1, name: "John" };
159
+
160
+ // Generates JWT and sets "auth" cookie (24h default)
161
+ await Auth.login(res, user);
162
+
163
+ res.json({ message: "Logged in" });
164
+ });
165
+
166
+ // Logout a user
167
+ router.post("/logout", async (req, res) => {
168
+ await Auth.logout(res);
169
+ res.json({ message: "Logged out" });
170
+ });
171
+
172
+ // Customizing Login (key, expiration, cookie options)
173
+ await Auth.login(res, user, "my_session", {
174
+ expiresIn: "7d",
175
+ cookie: { httpOnly: true, secure: true }
176
+ });
139
177
  ```
140
178
 
141
179
  ## 5. Performance Coaching (Cache)
package/backend/index.js CHANGED
@@ -2,12 +2,31 @@ import App from "@seip/blue-bird/core/app.js";
2
2
  import routerApiExample from "./routes/api.js";
3
3
  import routerFrontendExample from "./routes/frontend.js";
4
4
 
5
+ /**
6
+ * Main entry point for the Blue Bird application.
7
+ * Initializes the App instance with routes, configuration, and starts the server.
8
+ */
5
9
  const app = new App({
10
+ /**
11
+ * Array of router instances to be registered in the application.
12
+ * Each router can have its own base path.
13
+ */
6
14
  routes: [routerApiExample, routerFrontendExample],
15
+
16
+ /** CORS configuration. If empty, uses default settings. */
7
17
  cors: [],
18
+
19
+ /** Global middlewares to be applied before routes. */
8
20
  middlewares: [],
21
+
22
+ /** Base host URL for the application. */
9
23
  host: "http://localhost",
24
+
25
+ /** Server port, defaults to environment variable or fallback in config. */
10
26
  port: process.env.PORT,
11
27
  });
12
28
 
29
+ /**
30
+ * Starts the application server.
31
+ */
13
32
  app.run();
@@ -1,8 +1,11 @@
1
1
  import Router from "@seip/blue-bird/core/router.js";
2
2
  import Validator from "@seip/blue-bird/core/validate.js";
3
+ import Cache from "@seip/blue-bird/core/cache.js";
4
+ import Auth from "@seip/blue-bird/core/auth.js"
3
5
 
4
6
  const routerApiExample = new Router("/api");
5
7
 
8
+ /* simple test route */
6
9
  routerApiExample.get("/users", (req, res) => {
7
10
  const users = [
8
11
  {
@@ -16,7 +19,10 @@ routerApiExample.get("/users", (req, res) => {
16
19
  ];
17
20
  res.json(users);
18
21
  });
22
+ /* End simple test route */
19
23
 
24
+
25
+ /* Validation example */
20
26
  const loginSchema = {
21
27
  email: { required: true, email: true },
22
28
  password: { required: true, min: 6 },
@@ -27,5 +33,43 @@ const loginValidator = new Validator(loginSchema);
27
33
  routerApiExample.post("/login", loginValidator.middleware(), (req, res) => {
28
34
  res.json({ message: "Login successful", body: req.body });
29
35
  });
36
+ /* End Validation example */
37
+
38
+ /* Cache example */
39
+ routerApiExample.get("/cache", Cache.middleware(), async (req, res) => {
40
+ //in debug =false , testing /api/cache should take 2 seconds , and after that should take 0 seconds
41
+ await new Promise(resolve => setTimeout(resolve, 2000));
42
+ res.json({ message: "Cache successful" });
43
+ })
44
+ /* End Cache example */
45
+
46
+ /* Auth example */
47
+ routerApiExample.get("/auth_generate", async (req, res) => {
48
+ const token = await Auth.login(res, { id: 1, name: "John Doe" })
49
+ /*Or use
50
+ const token = Auth.generateToken({ id: user.id });//to generate token
51
+ res.cookie("auth", token,{
52
+ maxAge: 24 * 60 * 60 * 1000,
53
+ httpOnly: true,
54
+ secure: production,
55
+ sameSite: "strict",
56
+ path: "/",
57
+ });
58
+ */
59
+ res.json({ message: "Auth successful", token });
60
+
61
+ })
62
+
63
+ routerApiExample.get("/auth_logout", async (req, res) => {
64
+ await Auth.logout(res)
65
+ res.json({ message: "Auth successful" });
66
+ })
67
+
68
+ routerApiExample.get("/auth_verify", Auth.protect(), (req, res) => {
69
+ // req.user has inject with the jwt token payload and decoded by the Auth.protect() middleware
70
+ const userInfo = req.user
71
+ res.json({ message: "Auth successful", user: userInfo });
72
+ })
73
+ /* End Auth example */
30
74
 
31
75
  export default routerApiExample;
@@ -6,36 +6,42 @@ import seoData from "./seo.js";
6
6
  const routerFrontendExample = new Router();
7
7
  routerFrontendExample.use(App.helmet()); // Helmet for frontend router
8
8
 
9
+ /* Render HTML frontend/landing.html */
9
10
  routerFrontendExample.get("/landing", (req, res) => {
10
11
  return Template.renderHtml(res, "landing", {
11
- metaTags:{
12
- titleMeta:"Landing Example",
12
+ metaTags: {
13
+ titleMeta: "Landing Example",
13
14
  descriptionMeta: "Description meta",
14
15
  keywordsMeta: "keywordsMeta"
15
16
  }
16
17
  });
17
18
  });
19
+ /* End Render HTML frontend/landing.html */
18
20
 
21
+ /* SEO example */
19
22
  routerFrontendExample.seo(
20
23
  [
21
24
  {
22
25
  path: "/",
23
26
  component: "Home",
24
- seoKey: "home",
25
- props: { id: 1, name: "Name" },
27
+ seoKey: "home",//key in seo.js data
28
+ props: { id: 1, name: "Name" },// Props pass to react component or
26
29
  },
27
30
  {
28
31
  path: "/about",
29
32
  component: "About",
30
- seoKey: "about",
33
+ seoKey: "about",//key in seo.js data
31
34
  props: { id: 2, name: "Name 2" },
32
35
  },
33
36
  ],
34
37
  { languages: ["en", "es"], defaultLanguage: "en", seoData },
35
38
  );
39
+ /* End SEO example */
36
40
 
41
+ /* Render React example for '*' route (catch all routes) */
37
42
  routerFrontendExample.get("*", (req, res) => {
38
43
  return Template.renderReact(res, "App");
39
44
  });
45
+ /* End Render React example */
40
46
 
41
47
  export default routerFrontendExample;
package/core/app.js CHANGED
@@ -160,7 +160,7 @@ class App {
160
160
  this.app.use(middleware);
161
161
  });
162
162
 
163
- if (this.logger) this._middlewareLogger();
163
+ if (this.logger || props.debug) this._middlewareLogger(this.logger);
164
164
 
165
165
  this.app.use((req, res, next) => {
166
166
  res.setHeader("X-Powered-By", "Blue Bird");
@@ -202,13 +202,14 @@ class App {
202
202
  * Middleware that logs incoming HTTP requests to the console and to a log file.
203
203
  * @private
204
204
  */
205
- _middlewareLogger() {
205
+ _middlewareLogger(logger = false) {
206
206
  this.app.use((req, res, next) => {
207
207
  const method = req.method;
208
208
  const url = req.url.replace(
209
209
  /(password|token|authorization)=([^&]+)/gi,
210
210
  "$1=***",
211
211
  );
212
+ if (url.includes("chrome")) return;
212
213
  const params =
213
214
  Object.keys(req.params).length > 0
214
215
  ? ` ${JSON.stringify(req.params)}`
@@ -218,7 +219,8 @@ class App {
218
219
  const time = `${now.split("T")[0]} ${now.split("T")[1].split(".")[0]}`;
219
220
  let message = ` ${time} -${ip} -[${method}] ${url} ${params}`;
220
221
 
221
- this.loggerInstance.info(message);
222
+ if (logger) this.loggerInstance.info(message);
223
+
222
224
  if (props.debug) {
223
225
  message = `${chalk.bold.green(time)} - ${chalk.bold.cyan(ip)} -[${chalk.bold.red(method)}] ${chalk.bold.blue(url)} ${chalk.bold.yellow(params)}`;
224
226
  console.log(message);
@@ -290,13 +292,13 @@ class App {
290
292
  this.app.listen(this.port, () => {
291
293
  console.log(
292
294
  chalk.bold.blue("Blue Bird Server Online\n") +
293
- chalk.bold.cyan("App URL: ") +
294
- chalk.green(`${this.appUrl}`) +
295
- "\n" +
296
- chalk.bold.cyan("Internal: ") +
297
- chalk.green(`${this.host}:${this.port}`) +
298
- "\n" +
299
- chalk.gray("────────────────────────────────"),
295
+ chalk.bold.cyan("App URL: ") +
296
+ chalk.green(`${this.appUrl}`) +
297
+ "\n" +
298
+ chalk.bold.cyan("Internal: ") +
299
+ chalk.green(`${this.host}:${this.port}`) +
300
+ "\n" +
301
+ chalk.gray("────────────────────────────────"),
300
302
  );
301
303
  });
302
304
  })
package/core/auth.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import jwt from "jsonwebtoken";
2
2
  import crypto from "node:crypto";
3
+ import Config from "./config.js";
3
4
 
5
+ const propsConfig = Config.props();
6
+ const jwtSecret = propsConfig.jwtSecret;
7
+ const production = propsConfig.debug;
4
8
  /**
5
9
  * Auth class to handle JWT generation, verification and protection with AES-256-GCM encryption.
6
10
  */
@@ -49,13 +53,13 @@ class Auth {
49
53
  /**
50
54
  * Generates an encrypted JWT token.
51
55
  * @param {Object} payload - The data to store in the token.
52
- * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
56
+ * @param {string} [secret=process.env.JWT_SECRET] - The secret key .
53
57
  * @param {string} [expiresIn="24h"] - Expiration time.
54
58
  * @returns {string} The generated token.
55
59
  */
56
60
  static generateToken(
57
61
  payload,
58
- secret = process.env.JWT_SECRET,
62
+ secret = jwtSecret,
59
63
  expiresIn = "24h"
60
64
  ) {
61
65
  if (!secret)
@@ -70,7 +74,7 @@ class Auth {
70
74
  * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
71
75
  * @returns {Object|null} The decoded and decrypted payload or null if invalid.
72
76
  */
73
- static verifyToken(token, secret = process.env.JWT_SECRET) {
77
+ static verifyToken(token, secret = jwtSecret) {
74
78
  if (!secret)
75
79
  throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
76
80
  try {
@@ -84,31 +88,93 @@ class Auth {
84
88
 
85
89
  /**
86
90
  * Middleware to protect routes. Checks for token in Cookies or Authorization header.
87
- * @param {Object} options - Options for protection.
91
+ * @param {Object} [options={}] - Options for protection.
88
92
  * @param {string} [options.redirect=null] - URL to redirect if not authenticated.
89
93
  * @param {string} [options.key="user"] - Key to store the decoded token in the request.
94
+ * @param {string} [options.cookieKey="auth"] - The cookie key to look for the token.
90
95
  * @returns {Function} Express middleware.
96
+ * @example
97
+ * router.get("/profile", Auth.protect(), (req, res) => { ... });
98
+ * // Or with custom cookie key:
99
+ * router.get("/admin", Auth.protect({ cookieKey: "admin_session" }), (req, res) => { ... });
91
100
  */
92
- static protect(options = { redirect: null, key: "user" }) {
101
+ static protect(options = {}) {
102
+ const { redirect = null, key = "user", cookieKey = "auth" } = options;
103
+
93
104
  return (req, res, next) => {
94
105
  const token =
95
- req.cookies?.auth || req.headers.authorization?.split(" ")[1];
106
+ req.cookies?.[cookieKey] || req.headers.authorization?.split(" ")[1];
107
+
108
+ const isContentTypeJson =
109
+ req.headers["content-type"] === "application/json";
96
110
 
97
111
  if (!token) {
98
- if (options.redirect) return res.redirect(options.redirect);
99
- return res.status(401).json({ message: "Unauthorized" });
112
+ if (redirect && !isContentTypeJson) return res.redirect(redirect);
113
+ return isContentTypeJson
114
+ ? res.status(401).json({ message: "Unauthorized" })
115
+ : res.status(401).send();
100
116
  }
101
117
 
102
118
  const decoded = this.verifyToken(token);
103
119
  if (!decoded) {
104
- if (options.redirect) return res.redirect(options.redirect);
105
- return res.status(401).json({ message: "Unauthorized" });
120
+ if (redirect && !isContentTypeJson) return res.redirect(redirect);
121
+ return isContentTypeJson
122
+ ? res.status(401).json({ message: "Unauthorized" })
123
+ : res.status(401).send();
106
124
  }
107
125
 
108
- req[options.key || "user"] = decoded;
126
+ req[key || "user"] = decoded;
109
127
  next();
110
128
  };
111
129
  }
130
+
131
+ /**
132
+ * Logs in a user by setting an authentication cookie.
133
+ * @param {import('express').Response} res - The response object.
134
+ * @param {Object} data - The data to store in the token.
135
+ * @param {string} [key="auth"] - The key for the cookie.
136
+ * @param {Object} [options={}] - Options for the cookie and token.
137
+ * @param {string} [options.expiresIn="24h"] - Token expiration (e.g., "1h", "7d").
138
+ * @param {import('express').CookieOptions} [options.cookie] - Express cookie options.
139
+ * @returns {Promise<string>} The generated token.
140
+ * @example
141
+ * await Auth.login(res, { id: 1, name: "Admin" });
142
+ */
143
+ static async login(res, data, key = "auth", options = {}) {
144
+ const { expiresIn = "24h", cookie = {} } = options;
145
+
146
+ const token = this.generateToken(data, jwtSecret, expiresIn);
147
+
148
+ const defaultCookieOptions = {
149
+ maxAge: 24 * 60 * 60 * 1000,
150
+ httpOnly: true,
151
+ secure: production,
152
+ sameSite: "strict",
153
+ path: "/",
154
+ };
155
+
156
+ const finalCookieOptions = { ...defaultCookieOptions, ...cookie };
157
+
158
+ res.cookie(key, token, finalCookieOptions);
159
+ return token;
160
+ }
161
+
162
+ /**
163
+ * Logs out a user by clearing the authentication cookie.
164
+ * @param {import('express').Response} res - The response object.
165
+ * @param {string} [key="auth"] - The key for the cookie.
166
+ * @param {import('express').CookieOptions} [options={}] - Options for clearing the cookie.
167
+ * @returns {Promise<boolean>} True if the cookie was cleared successfully.
168
+ * @example
169
+ * await Auth.logout(res);
170
+ */
171
+ static async logout(res, key = "auth", options = {}) {
172
+ const defaultOptions = {
173
+ path: "/",
174
+ };
175
+ res.clearCookie(key, { ...defaultOptions, ...options });
176
+ return true;
177
+ }
112
178
  }
113
179
 
114
180
  export default Auth;
package/core/cache.js CHANGED
@@ -9,37 +9,40 @@ setInterval(() => {
9
9
  }
10
10
  }, 300000).unref();
11
11
  /**
12
- * Cache Middleware
13
- * @example
14
- * router.get("/stats",
15
- Cache.middleware(120),
16
- controller.stats
17
- );
18
- * */
12
+ * Simple in-memory Cache class to provide middleware for Express routes.
13
+ * Caches JSON responses based on the request URL.
14
+ */
19
15
  class Cache {
20
-
21
- static middleware(seconds = 60) {
22
- return (req, res, next) => {
23
-
24
- const key = req.originalUrl;
25
-
26
- if (CACHE[key] && CACHE[key].expiry > Date.now()) {
27
- return res.json(CACHE[key].data);
28
- }
29
-
30
- const originalJson = res.json.bind(res);
31
-
32
- res.json = (body) => {
33
- CACHE[key] = {
34
- data: body,
35
- expiry: Date.now() + seconds * 1000
36
- };
37
- return originalJson(body);
38
- };
39
-
40
- next();
16
+ /**
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
+ * });
24
+ */
25
+ static middleware(seconds = 60) {
26
+ return (req, res, next) => {
27
+ const key = req.originalUrl;
28
+
29
+ if (CACHE[key] && CACHE[key].expiry > Date.now()) {
30
+ return res.json(CACHE[key].data);
31
+ }
32
+
33
+ const originalJson = res.json.bind(res);
34
+
35
+ res.json = (body) => {
36
+ CACHE[key] = {
37
+ data: body,
38
+ expiry: Date.now() + seconds * 1000,
41
39
  };
42
- }
40
+ return originalJson(body);
41
+ };
42
+
43
+ next();
44
+ };
45
+ }
43
46
  }
44
47
 
45
48
  export default Cache;
package/core/config.js CHANGED
@@ -17,7 +17,10 @@ class Config {
17
17
  /**
18
18
  * Retrieves application properties from environment variables or default values.
19
19
  * Results are cached after first call for performance.
20
- * @returns {Object} The configuration properties object.
20
+ * @returns {{debug: boolean, descriptionMeta: string, keywordsMeta: string, titleMeta: string, authorMeta: string, description: string, title: string, version: string, langMeta: string, host: string, appUrl: string, port: number, static: {path: string, options: Object}}} The configuration properties object.
21
+ * @example
22
+ * const props = Config.props();
23
+ * console.log(props);
21
24
  */
22
25
  static props() {
23
26
  if (_cachedProps) return _cachedProps;
@@ -37,6 +40,7 @@ class Config {
37
40
  host: process.env.HOST || "http://localhost",
38
41
  appUrl: process.env.APP_URL || process.env.HOST || "http://localhost",
39
42
  port: Number.isNaN(portRaw) ? 3000 : portRaw,
43
+ jwtSecret: process.env.JWT_SECRET,
40
44
  static: {
41
45
  path: process.env.STATIC_PATH || "frontend/public",
42
46
  options: {},
package/core/seo.js CHANGED
@@ -20,7 +20,7 @@ class SEO {
20
20
  */
21
21
  static generateSitemap(routesConfig, options = {}) {
22
22
  const { languages = [], defaultLanguage = "en" } = options;
23
- const host = (props.appUrl || "http://localhost").replace(/\/$/, "");
23
+ const host = (props.appUrl || `${props.host}:${props.port}`).replace(/\/$/, "");
24
24
  const baseUrl = `${host}`;
25
25
  const date = new Date().toISOString().split("T")[0];
26
26
 
package/core/template.js CHANGED
@@ -184,6 +184,8 @@ class Template {
184
184
  : "";
185
185
  const skeletonHtml = skeleton ? this.skeletonHtml() : "";
186
186
 
187
+ const canonicalUrl = metaTags.canonicalUrl || props.appUrl || "";
188
+
187
189
  const ogTags = `
188
190
  <meta property="og:title" content="${title}" />
189
191
  <meta property="og:description" content="${description}" />
@@ -210,6 +212,7 @@ class Template {
210
212
  .replace(/__VITE_ASSETS__/g, this.vite_assets())
211
213
  .replace(/__SCRIPTS_BODY__/g, scriptsBodyTags)
212
214
  .replace(/__STYLES_SKELETON__/g, stylesSkeleton)
215
+ .replace(/__CANONICAL_URL__/g, canonicalUrl)
213
216
  .replace(/__SKELETON__/g, skeletonHtml);
214
217
 
215
218
  html = this.minifyHtml(html);
@@ -338,6 +341,8 @@ class Template {
338
341
  ${ogImage ? `<meta name="twitter:image" content="${ogImage}" />` : ""}
339
342
  `;
340
343
 
344
+ const canonicalUrl = metaTags.canonicalUrl || props.appUrl || "";
345
+
341
346
  html = html
342
347
  .replace(/__LANG__/g, this.escapeHtml(langHtml))
343
348
  .replace(/__TITLE__/g, title)
@@ -347,6 +352,7 @@ class Template {
347
352
  .replace(/__HEAD_OPTIONS__/g, headOptions + ogTags)
348
353
  .replace(/__CLASS_BODY__/g, classBody)
349
354
  .replace(/__VITE_ASSETS__/g, withAssets ? this.vite_assets() : "")
355
+ .replace(/__CANONICAL_URL__/g, canonicalUrl)
350
356
  .replace(/__STYLES_SKELETON__/g, "");
351
357
 
352
358
  if (html.includes("__LINK_STYLES__")) {
package/core/upload.js CHANGED
@@ -69,7 +69,8 @@ class Upload {
69
69
  * const url = Upload.url("file.jpg", "uploads");
70
70
  */
71
71
  static url(filename, folder = "uploads") {
72
- return `${props.host}:${props.port}/${folder}/${filename}`;
72
+ const appUrl = props.appUrl ?? `${props.host}:${props.port}`;
73
+ return `${appUrl}/${folder}/${filename}`;
73
74
  }
74
75
  }
75
76
 
@@ -1,22 +1,27 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="__LANG__">
3
+
3
4
  <head>
4
5
  <meta charset="UTF-8">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
7
  <title>__TITLE__</title>
8
+ <link rel="canonical" href="__CANONICAL_URL__" />
7
9
  <link rel="icon" href="/favicon.ico" />
8
10
  <meta name="description" content="__DESCRIPTION__" />
9
11
  <meta name="keywords" content="__KEYWORDS__" />
10
12
  <meta name="author" content="__AUTHOR__" />
11
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
15
+ <link
16
+ href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
17
+ rel="stylesheet">
14
18
  __HEAD_OPTIONS__
15
19
  __LINK_STYLES__
16
20
  __SCRIPTS_HEAD__
17
21
  __VITE_ASSETS__
18
22
  __STYLES_SKELETON__
19
23
  </head>
24
+
20
25
  <body class="__CLASS_BODY__">
21
26
  <div id="root" data-react-component="__COMPONENT__" data-props='__PROPS__'>
22
27
  __SKELETON__
@@ -5,6 +5,7 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>__TITLE__</title>
8
+ <link rel="canonical" href="__CANONICAL_URL__" />
8
9
  <meta name="description" content="__DESCRIPTION__">
9
10
  <meta name="keywords" content="__KEYWORDS__">
10
11
  <meta name="author" content="__AUTHOR__">
@@ -30,12 +30,10 @@ function generateRoutes(routes, languages = []) {
30
30
  const allRoutes = [];
31
31
 
32
32
  routes.forEach(({ path: routePath, element }) => {
33
- // Base path
34
33
  allRoutes.push(
35
34
  <Route key={routePath} path={routePath} element={element} />
36
35
  );
37
36
 
38
- // Language-prefixed paths (only if multiple languages are used)
39
37
  if (languages && languages.length > 0) {
40
38
  languages.forEach((lang) => {
41
39
  const langPath = `/${lang}${routePath === "/" ? "" : routePath}`;
@@ -56,7 +54,7 @@ export default function App(_props) {
56
54
  <ThemeProvider>
57
55
  <LanguageProvider initialLang={lang}>
58
56
  <Router>
59
- <SPAProvider languages={LANGUAGES} defaultLanguage={DEFAULT_LANGUAGE}>
57
+ <SPAProvider languages={LANGUAGES} defaultLanguage={DEFAULT_LANGUAGE} {..._props} >
60
58
  <Suspense fallback={<Skeleton />}>
61
59
  <Routes>
62
60
  {generateRoutes(ROUTES, LANGUAGES)}
@@ -28,7 +28,7 @@ const SPAContext = createContext({
28
28
  * @param {Array<string>} [props.languages=[]] - Supported language codes.
29
29
  * @param {string} [props.defaultLanguage="en"] - Default language.
30
30
  */
31
- export function SPAProvider({
31
+ export function SPAProvider({ props,
32
32
  children,
33
33
  languages = [],
34
34
  defaultLanguage = "en",
@@ -36,7 +36,7 @@ export function SPAProvider({
36
36
  const location = useLocation();
37
37
  const navigate = useNavigate();
38
38
  const { lang, setLang } = useLanguage();
39
- const [pageProps, setPageProps] = useState({});
39
+ const [pageProps, setPageProps] = useState(props);
40
40
  const [pageMeta, setPageMeta] = useState({});
41
41
  const [loading, setLoading] = useState(false);
42
42
  const isFirstRender = useRef(true);
@@ -136,7 +136,6 @@ export function SPAProvider({
136
136
  }
137
137
  });
138
138
 
139
- // Update OG tags
140
139
  const ogUpdates = {
141
140
  "og:title": meta.titleMeta,
142
141
  "og:description": meta.descriptionMeta,
@@ -153,21 +152,21 @@ export function SPAProvider({
153
152
  }, []);
154
153
 
155
154
  useEffect(() => {
156
- // Sync language from URL prefix if present
157
155
  const urlLang = detectLangFromPath(location.pathname);
158
156
  if (urlLang) {
159
157
  setLang(urlLang);
160
158
  } else if (languages.length > 0 && !isFirstRender.current) {
161
- // On subsequent navigations, if prefix is missing, revert to default
162
159
  setLang(defaultLanguage);
163
160
  }
164
161
 
165
- // Skip fetch on initial render
166
162
  if (isFirstRender.current) {
167
163
  isFirstRender.current = false;
168
164
  return;
169
165
  }
170
166
 
167
+ setPageProps({});
168
+ setPageMeta({});
169
+
171
170
  if (abortRef.current) {
172
171
  abortRef.current.abort();
173
172
  }
@@ -4,9 +4,13 @@ import { useLanguage } from '../blue-bird/contexts/LanguageContext';
4
4
 
5
5
  import Card from '../blue-bird/components/Card';
6
6
  import Typography from '../blue-bird/components/Typography'
7
+ import { useSPA } from '../blue-bird/contexts/SPAContext.jsx';
7
8
 
8
9
  export default function About() {
9
10
  const { t } = useLanguage();
11
+ const { navigateToLang, pageProps, pageMeta } = useSPA();
12
+ console.log("pageProps", pageProps);
13
+ console.log("pageMeta", pageMeta);
10
14
  return (
11
15
  <div
12
16
  className="bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 min-h-screen"
@@ -3,11 +3,16 @@ import Card from '../blue-bird/components/Card';
3
3
  import Header from '../components/Header';
4
4
  import { useLanguage } from '../blue-bird/contexts/LanguageContext';
5
5
  import Typography from '../blue-bird/components/Typography';
6
+ import { useSPA } from '../blue-bird/contexts/SPAContext.jsx';
6
7
 
7
8
  export default function Home() {
8
9
  const { t } = useLanguage();
10
+ const { navigateToLang, pageProps, pageMeta } = useSPA();
11
+ console.log("pageProps", pageProps);
12
+ console.log("pageMeta", pageMeta);
13
+
9
14
  useEffect(() => {
10
- // Example API call to the backend
15
+
11
16
  fetch("/api/login", {
12
17
  method: "POST",
13
18
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Express + React opinionated framework with SPA or API architecture and built-in JWT auth",
5
5
  "type": "module",
6
6
  "bin": {