@seip/blue-bird 0.4.4 → 0.4.6
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 +26 -25
- package/AGENTS.md +199 -199
- package/LICENSE +21 -0
- package/README.md +79 -79
- package/backend/index.js +13 -13
- package/backend/routes/api.js +31 -31
- package/backend/routes/frontend.js +41 -41
- package/backend/routes/seo.js +39 -39
- package/core/app.js +328 -325
- package/core/auth.js +114 -83
- package/core/cache.js +44 -44
- package/core/cli/component.js +42 -42
- package/core/cli/init.js +119 -118
- package/core/cli/react.js +435 -435
- package/core/cli/route.js +42 -42
- package/core/cli/scaffolding-auth.js +1037 -0
- package/core/config.js +48 -47
- package/core/debug.js +248 -248
- package/core/logger.js +100 -100
- package/core/middleware.js +27 -27
- package/core/router.js +333 -333
- package/core/seo.js +95 -100
- package/core/swagger.js +40 -25
- package/core/template.js +472 -462
- package/core/upload.js +76 -76
- package/core/validate.js +380 -380
- package/frontend/index.html +26 -26
- package/frontend/landing.html +69 -69
- package/frontend/resources/css/tailwind.css +17 -17
- package/frontend/resources/js/App.jsx +70 -70
- package/frontend/resources/js/Main.jsx +18 -18
- package/frontend/resources/js/blue-bird/components/Button.jsx +67 -67
- package/frontend/resources/js/blue-bird/components/Card.jsx +18 -18
- package/frontend/resources/js/blue-bird/components/DataTable.jsx +126 -126
- package/frontend/resources/js/blue-bird/components/Input.jsx +21 -21
- package/frontend/resources/js/blue-bird/components/Label.jsx +12 -12
- package/frontend/resources/js/blue-bird/components/LanguageButton.jsx +23 -23
- package/frontend/resources/js/blue-bird/components/Link.jsx +15 -15
- package/frontend/resources/js/blue-bird/components/Modal.jsx +27 -27
- package/frontend/resources/js/blue-bird/components/Skeleton.jsx +44 -44
- package/frontend/resources/js/blue-bird/components/Translate.jsx +12 -12
- package/frontend/resources/js/blue-bird/components/Typography.jsx +69 -69
- package/frontend/resources/js/blue-bird/contexts/LanguageContext.jsx +41 -41
- package/frontend/resources/js/blue-bird/contexts/SPAContext.jsx +239 -237
- package/frontend/resources/js/blue-bird/contexts/SnackbarContext.jsx +38 -38
- package/frontend/resources/js/blue-bird/contexts/ThemeContext.jsx +49 -49
- package/frontend/resources/js/blue-bird/locales/en.json +47 -47
- package/frontend/resources/js/blue-bird/locales/es.json +47 -47
- package/frontend/resources/js/components/Header.jsx +55 -55
- package/frontend/resources/js/pages/About.jsx +31 -31
- package/frontend/resources/js/pages/Home.jsx +82 -82
- package/package.json +57 -57
- package/vite.config.js +22 -22
package/core/router.js
CHANGED
|
@@ -1,333 +1,333 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import Config from "./config.js";
|
|
3
|
-
import Template from "./template.js";
|
|
4
|
-
import SEO from "./seo.js";
|
|
5
|
-
|
|
6
|
-
const __dirname = Config.dirname();
|
|
7
|
-
const props = Config.props();
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Router wrapper class for handling Express routing logic.
|
|
11
|
-
*/
|
|
12
|
-
class Router {
|
|
13
|
-
/**
|
|
14
|
-
* Creates a new Router instance.
|
|
15
|
-
* @param {string} [path="/"] - The base path for this router.
|
|
16
|
-
* @example
|
|
17
|
-
* const router = new Router("/api")
|
|
18
|
-
* router.get("/", (req, res) => {
|
|
19
|
-
* res.json({ message: "Hello World!" })
|
|
20
|
-
* })
|
|
21
|
-
*/
|
|
22
|
-
constructor(path = "/") {
|
|
23
|
-
this.router = express.Router();
|
|
24
|
-
this.path = path;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Registers a middleware on this router.
|
|
29
|
-
* @param {...Function} middleware - Middleware functions.
|
|
30
|
-
* @example
|
|
31
|
-
* router.use(Auth.protect());
|
|
32
|
-
* router.use(App.helmet());
|
|
33
|
-
*/
|
|
34
|
-
use(...middleware) {
|
|
35
|
-
this.router.use(...middleware);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Registers a GET route handler.
|
|
40
|
-
* @param {string} path - The relative path for the GET route.
|
|
41
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
42
|
-
* @example
|
|
43
|
-
* router.get("/users", (req, res) => {
|
|
44
|
-
* const users = [
|
|
45
|
-
* {
|
|
46
|
-
* name: "John Doe",
|
|
47
|
-
* email: "john.doe@example.com",
|
|
48
|
-
* },
|
|
49
|
-
* {
|
|
50
|
-
* name: "Jane Doe2",
|
|
51
|
-
* email: "jane.doe2@example.com",
|
|
52
|
-
* },
|
|
53
|
-
* ]
|
|
54
|
-
* res.json(users)
|
|
55
|
-
* })
|
|
56
|
-
*/
|
|
57
|
-
get(path, ...callback) {
|
|
58
|
-
if (path === "/*" || path === "*") {
|
|
59
|
-
path = /.*/;
|
|
60
|
-
}
|
|
61
|
-
this.router.get(path, callback);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Registers a POST route handler.
|
|
66
|
-
* @param {string} path - The relative path for the POST route.
|
|
67
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
68
|
-
* @example
|
|
69
|
-
* router.post("/users", (req, res) => {
|
|
70
|
-
* return res.json({ message: "User created successfully" })
|
|
71
|
-
* })
|
|
72
|
-
*/
|
|
73
|
-
post(path, ...callback) {
|
|
74
|
-
if (path === "/*" || path === "*") {
|
|
75
|
-
path = /.*/;
|
|
76
|
-
}
|
|
77
|
-
this.router.post(path, callback);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Registers a PUT route handler.
|
|
82
|
-
* @param {string} path - The relative path for the PUT route.
|
|
83
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
84
|
-
* @example
|
|
85
|
-
* router.put("/users", (req, res) => {
|
|
86
|
-
* return res.json({ message: "User updated successfully" })
|
|
87
|
-
* })
|
|
88
|
-
*/
|
|
89
|
-
put(path, ...callback) {
|
|
90
|
-
this.router.put(path, callback);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Registers a DELETE route handler.
|
|
95
|
-
* @param {string} path - The relative path for the DELETE route.
|
|
96
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
97
|
-
* @example
|
|
98
|
-
* router.delete("/users", (req, res) => {
|
|
99
|
-
* return res.json({ message: "User deleted successfully" })
|
|
100
|
-
* })
|
|
101
|
-
*/
|
|
102
|
-
delete(path, ...callback) {
|
|
103
|
-
this.router.delete(path, callback);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Registers a PATCH route handler.
|
|
108
|
-
* @param {string} path - The relative path for the PATCH route.
|
|
109
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
110
|
-
* @example
|
|
111
|
-
* router.patch("/users", (req, res) => {
|
|
112
|
-
* return res.json({ message: "User patched successfully" })
|
|
113
|
-
* })
|
|
114
|
-
*/
|
|
115
|
-
patch(path, ...callback) {
|
|
116
|
-
this.router.patch(path, callback);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Registers an OPTIONS route handler.
|
|
121
|
-
* @param {string} path - The relative path for the OPTIONS route.
|
|
122
|
-
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
123
|
-
* @example
|
|
124
|
-
* router.options("/users", (req, res) => {
|
|
125
|
-
* return res.json({ message: "User options successfully" })
|
|
126
|
-
* })
|
|
127
|
-
*/
|
|
128
|
-
options(path, ...callback) {
|
|
129
|
-
this.router.options(path, callback);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Returns the underlying Express router instance.
|
|
134
|
-
* @returns {import('express').Router} The Express router object.
|
|
135
|
-
*/
|
|
136
|
-
getRouter() {
|
|
137
|
-
return this.router;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Returns the base path associated with this router.
|
|
142
|
-
* @returns {string} The router path.
|
|
143
|
-
*/
|
|
144
|
-
getPath() {
|
|
145
|
-
return this.path;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Resolves meta tags for a route, supporting simple meta, inline multilingual, or external seoData.
|
|
150
|
-
*
|
|
151
|
-
* @private
|
|
152
|
-
* @param {Object} route - The route configuration.
|
|
153
|
-
* @param {string} lang - Target language.
|
|
154
|
-
* @param {Object} [seoData=null] - External SEO data object.
|
|
155
|
-
* @param {Array<string>} [languages=[]] - Available languages.
|
|
156
|
-
* @returns {Object} Resolved meta tags object.
|
|
157
|
-
*/
|
|
158
|
-
_resolveMeta(route, lang, seoData = null, languages = []) {
|
|
159
|
-
const { meta = {}, seoKey } = route;
|
|
160
|
-
|
|
161
|
-
if (seoKey && seoData && seoData[seoKey]) {
|
|
162
|
-
const seoEntry = seoData[seoKey];
|
|
163
|
-
return seoEntry[lang] || seoEntry[Object.keys(seoEntry)[0]] || {};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const hasLangKeys = languages.some(
|
|
167
|
-
(l) => meta[l] && typeof meta[l] === "object",
|
|
168
|
-
);
|
|
169
|
-
if (hasLangKeys) {
|
|
170
|
-
return meta[lang] || meta[Object.keys(meta).find((k) => meta[k])] || {};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return meta;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Registers a single SEO route with Template.renderReact.
|
|
178
|
-
*
|
|
179
|
-
* @private
|
|
180
|
-
* @param {string} routePath - Express route path.
|
|
181
|
-
* @param {string} component - React component name.
|
|
182
|
-
* @param {Object} props - Props to pass to the component.
|
|
183
|
-
* @param {Object} metaData - Resolved meta data.
|
|
184
|
-
* @param {string} lang - Language code.
|
|
185
|
-
*/
|
|
186
|
-
_registerSeoRoute(routePath, component, props, metaData, lang, options = {}) {
|
|
187
|
-
const { seoData, languages } = options;
|
|
188
|
-
|
|
189
|
-
this.get(routePath, (req, res) => {
|
|
190
|
-
let activeLang = lang;
|
|
191
|
-
|
|
192
|
-
const isDefaultRoute = !languages.some(l => req.path.startsWith(`/${l}`));
|
|
193
|
-
if (isDefaultRoute && req.cookies?.blue_bird_lang && languages?.includes(req.cookies.blue_bird_lang)) {
|
|
194
|
-
activeLang = req.cookies.blue_bird_lang;
|
|
195
|
-
}
|
|
196
|
-
if (req.query.source === "frontend" && req.query.lang && languages?.includes(req.query.lang)) {
|
|
197
|
-
activeLang = req.query.lang;
|
|
198
|
-
res.cookie("blue_bird_lang", activeLang, {
|
|
199
|
-
maxAge: 31536000000,
|
|
200
|
-
httpOnly: false,
|
|
201
|
-
path: "/",
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
let activeMeta = metaData;
|
|
206
|
-
if (activeLang !== lang) {
|
|
207
|
-
activeMeta = this._resolveMeta(
|
|
208
|
-
{ meta: metaData, seoKey: options.seoKey },
|
|
209
|
-
activeLang,
|
|
210
|
-
seoData,
|
|
211
|
-
languages,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const dynamicProps = {
|
|
216
|
-
props: {
|
|
217
|
-
...props,
|
|
218
|
-
params: req.params,
|
|
219
|
-
query: req.query,
|
|
220
|
-
lang: activeLang,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const metaTags = {
|
|
225
|
-
titleMeta: activeMeta.titleMeta || activeMeta.title || "",
|
|
226
|
-
descriptionMeta: activeMeta.descriptionMeta || activeMeta.description || "",
|
|
227
|
-
keywordsMeta: activeMeta.keywordsMeta || activeMeta.keywords || "",
|
|
228
|
-
ogImage: activeMeta.ogImage || "",
|
|
229
|
-
ogType: activeMeta.ogType || "website",
|
|
230
|
-
twitterCard: activeMeta.twitterCard || "summary_large_image",
|
|
231
|
-
langMeta: activeLang,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
return Template.renderReact(res, component, dynamicProps, { metaTags });
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Registers multiple routes based on an SEO configuration array.
|
|
240
|
-
* Supports simple meta, inline multilingual meta, and external seoData files.
|
|
241
|
-
* Automatically generates language-prefixed routes and registers sitemap.xml and robots.txt.
|
|
242
|
-
*
|
|
243
|
-
* @param {Array<Object>} routesConfig - Array of route objects.
|
|
244
|
-
* @param {Object} [options={}] - Configuration options.
|
|
245
|
-
* @param {Array<string>} [options.languages=[]] - Languages to register (e.g., ["en", "es"]).
|
|
246
|
-
* @param {string} [options.defaultLanguage="en"] - The default language for unprefixed paths.
|
|
247
|
-
* @param {Object} [options.seoData=null] - External SEO data object (like seo.php pattern).
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* // Simple (no i18n)
|
|
251
|
-
* router.seo([
|
|
252
|
-
* {
|
|
253
|
-
* path: "/",
|
|
254
|
-
* component: "Home",
|
|
255
|
-
* meta: { titleMeta: "Home", descriptionMeta: "Welcome" },
|
|
256
|
-
* props: { id: 1 }
|
|
257
|
-
* }
|
|
258
|
-
* ]);
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* // Multilingual (inline)
|
|
262
|
-
* router.seo([
|
|
263
|
-
* {
|
|
264
|
-
* path: "/",
|
|
265
|
-
* component: "Home",
|
|
266
|
-
* meta: {
|
|
267
|
-
* en: { titleMeta: "Home", descriptionMeta: "Welcome" },
|
|
268
|
-
* es: { titleMeta: "Inicio", descriptionMeta: "Bienvenido" }
|
|
269
|
-
* }
|
|
270
|
-
* }
|
|
271
|
-
* ], { languages: ["en", "es"], defaultLanguage: "en" });
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* // Multilingual (external seoData file)
|
|
275
|
-
* import seoData from "./seo.js";
|
|
276
|
-
* router.seo([
|
|
277
|
-
* { path: "/", component: "Home", seoKey: "home" }
|
|
278
|
-
* ], { languages: ["en", "es"], defaultLanguage: "en", seoData });
|
|
279
|
-
*/
|
|
280
|
-
seo(routesConfig, options = {}) {
|
|
281
|
-
const { languages = [], defaultLanguage = null, seoData = null } = options;
|
|
282
|
-
|
|
283
|
-
const defaultLanguageOption=
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
SEO.registerRoutes(this.router, routesConfig, options);
|
|
287
|
-
|
|
288
|
-
routesConfig.forEach((route) => {
|
|
289
|
-
const { path: routePath, component, props = {}, seoKey } = route;
|
|
290
|
-
|
|
291
|
-
if (languages.length > 0) {
|
|
292
|
-
languages.forEach((lang) => {
|
|
293
|
-
const langMeta = this._resolveMeta(route, lang, seoData, languages);
|
|
294
|
-
const langPath = `/${lang}${routePath === "/" ? "" : routePath}`;
|
|
295
|
-
this._registerSeoRoute(
|
|
296
|
-
langPath,
|
|
297
|
-
component,
|
|
298
|
-
props,
|
|
299
|
-
langMeta,
|
|
300
|
-
lang,
|
|
301
|
-
{ seoData, languages, seoKey },
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const defaultMeta = this._resolveMeta(
|
|
306
|
-
route,
|
|
307
|
-
defaultLanguageOption,
|
|
308
|
-
seoData,
|
|
309
|
-
languages,
|
|
310
|
-
);
|
|
311
|
-
this._registerSeoRoute(
|
|
312
|
-
routePath,
|
|
313
|
-
component,
|
|
314
|
-
props,
|
|
315
|
-
defaultMeta,
|
|
316
|
-
defaultLanguageOption,
|
|
317
|
-
{ seoData, languages, seoKey },
|
|
318
|
-
);
|
|
319
|
-
} else {
|
|
320
|
-
const meta = this._resolveMeta(route, defaultLanguageOption, seoData, []);
|
|
321
|
-
this._registerSeoRoute(
|
|
322
|
-
routePath,
|
|
323
|
-
component,
|
|
324
|
-
props,
|
|
325
|
-
meta,
|
|
326
|
-
defaultLanguageOption,
|
|
327
|
-
{ seoData, languages: [], seoKey },
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
export default Router;
|
|
1
|
+
import express from "express";
|
|
2
|
+
import Config from "./config.js";
|
|
3
|
+
import Template from "./template.js";
|
|
4
|
+
import SEO from "./seo.js";
|
|
5
|
+
|
|
6
|
+
const __dirname = Config.dirname();
|
|
7
|
+
const props = Config.props();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Router wrapper class for handling Express routing logic.
|
|
11
|
+
*/
|
|
12
|
+
class Router {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new Router instance.
|
|
15
|
+
* @param {string} [path="/"] - The base path for this router.
|
|
16
|
+
* @example
|
|
17
|
+
* const router = new Router("/api")
|
|
18
|
+
* router.get("/", (req, res) => {
|
|
19
|
+
* res.json({ message: "Hello World!" })
|
|
20
|
+
* })
|
|
21
|
+
*/
|
|
22
|
+
constructor(path = "/") {
|
|
23
|
+
this.router = express.Router();
|
|
24
|
+
this.path = path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Registers a middleware on this router.
|
|
29
|
+
* @param {...Function} middleware - Middleware functions.
|
|
30
|
+
* @example
|
|
31
|
+
* router.use(Auth.protect());
|
|
32
|
+
* router.use(App.helmet());
|
|
33
|
+
*/
|
|
34
|
+
use(...middleware) {
|
|
35
|
+
this.router.use(...middleware);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registers a GET route handler.
|
|
40
|
+
* @param {string} path - The relative path for the GET route.
|
|
41
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
42
|
+
* @example
|
|
43
|
+
* router.get("/users", (req, res) => {
|
|
44
|
+
* const users = [
|
|
45
|
+
* {
|
|
46
|
+
* name: "John Doe",
|
|
47
|
+
* email: "john.doe@example.com",
|
|
48
|
+
* },
|
|
49
|
+
* {
|
|
50
|
+
* name: "Jane Doe2",
|
|
51
|
+
* email: "jane.doe2@example.com",
|
|
52
|
+
* },
|
|
53
|
+
* ]
|
|
54
|
+
* res.json(users)
|
|
55
|
+
* })
|
|
56
|
+
*/
|
|
57
|
+
get(path, ...callback) {
|
|
58
|
+
if (path === "/*" || path === "*") {
|
|
59
|
+
path = /.*/;
|
|
60
|
+
}
|
|
61
|
+
this.router.get(path, callback);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Registers a POST route handler.
|
|
66
|
+
* @param {string} path - The relative path for the POST route.
|
|
67
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
68
|
+
* @example
|
|
69
|
+
* router.post("/users", (req, res) => {
|
|
70
|
+
* return res.json({ message: "User created successfully" })
|
|
71
|
+
* })
|
|
72
|
+
*/
|
|
73
|
+
post(path, ...callback) {
|
|
74
|
+
if (path === "/*" || path === "*") {
|
|
75
|
+
path = /.*/;
|
|
76
|
+
}
|
|
77
|
+
this.router.post(path, callback);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registers a PUT route handler.
|
|
82
|
+
* @param {string} path - The relative path for the PUT route.
|
|
83
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
84
|
+
* @example
|
|
85
|
+
* router.put("/users", (req, res) => {
|
|
86
|
+
* return res.json({ message: "User updated successfully" })
|
|
87
|
+
* })
|
|
88
|
+
*/
|
|
89
|
+
put(path, ...callback) {
|
|
90
|
+
this.router.put(path, callback);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Registers a DELETE route handler.
|
|
95
|
+
* @param {string} path - The relative path for the DELETE route.
|
|
96
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
97
|
+
* @example
|
|
98
|
+
* router.delete("/users", (req, res) => {
|
|
99
|
+
* return res.json({ message: "User deleted successfully" })
|
|
100
|
+
* })
|
|
101
|
+
*/
|
|
102
|
+
delete(path, ...callback) {
|
|
103
|
+
this.router.delete(path, callback);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Registers a PATCH route handler.
|
|
108
|
+
* @param {string} path - The relative path for the PATCH route.
|
|
109
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
110
|
+
* @example
|
|
111
|
+
* router.patch("/users", (req, res) => {
|
|
112
|
+
* return res.json({ message: "User patched successfully" })
|
|
113
|
+
* })
|
|
114
|
+
*/
|
|
115
|
+
patch(path, ...callback) {
|
|
116
|
+
this.router.patch(path, callback);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Registers an OPTIONS route handler.
|
|
121
|
+
* @param {string} path - The relative path for the OPTIONS route.
|
|
122
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
123
|
+
* @example
|
|
124
|
+
* router.options("/users", (req, res) => {
|
|
125
|
+
* return res.json({ message: "User options successfully" })
|
|
126
|
+
* })
|
|
127
|
+
*/
|
|
128
|
+
options(path, ...callback) {
|
|
129
|
+
this.router.options(path, callback);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the underlying Express router instance.
|
|
134
|
+
* @returns {import('express').Router} The Express router object.
|
|
135
|
+
*/
|
|
136
|
+
getRouter() {
|
|
137
|
+
return this.router;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns the base path associated with this router.
|
|
142
|
+
* @returns {string} The router path.
|
|
143
|
+
*/
|
|
144
|
+
getPath() {
|
|
145
|
+
return this.path;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolves meta tags for a route, supporting simple meta, inline multilingual, or external seoData.
|
|
150
|
+
*
|
|
151
|
+
* @private
|
|
152
|
+
* @param {Object} route - The route configuration.
|
|
153
|
+
* @param {string} lang - Target language.
|
|
154
|
+
* @param {Object} [seoData=null] - External SEO data object.
|
|
155
|
+
* @param {Array<string>} [languages=[]] - Available languages.
|
|
156
|
+
* @returns {Object} Resolved meta tags object.
|
|
157
|
+
*/
|
|
158
|
+
_resolveMeta(route, lang, seoData = null, languages = []) {
|
|
159
|
+
const { meta = {}, seoKey } = route;
|
|
160
|
+
|
|
161
|
+
if (seoKey && seoData && seoData[seoKey]) {
|
|
162
|
+
const seoEntry = seoData[seoKey];
|
|
163
|
+
return seoEntry[lang] || seoEntry[Object.keys(seoEntry)[0]] || {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const hasLangKeys = languages.some(
|
|
167
|
+
(l) => meta[l] && typeof meta[l] === "object",
|
|
168
|
+
);
|
|
169
|
+
if (hasLangKeys) {
|
|
170
|
+
return meta[lang] || meta[Object.keys(meta).find((k) => meta[k])] || {};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return meta;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Registers a single SEO route with Template.renderReact.
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
* @param {string} routePath - Express route path.
|
|
181
|
+
* @param {string} component - React component name.
|
|
182
|
+
* @param {Object} props - Props to pass to the component.
|
|
183
|
+
* @param {Object} metaData - Resolved meta data.
|
|
184
|
+
* @param {string} lang - Language code.
|
|
185
|
+
*/
|
|
186
|
+
_registerSeoRoute(routePath, component, props, metaData, lang, options = {}) {
|
|
187
|
+
const { seoData, languages } = options;
|
|
188
|
+
|
|
189
|
+
this.get(routePath, (req, res) => {
|
|
190
|
+
let activeLang = lang;
|
|
191
|
+
|
|
192
|
+
const isDefaultRoute = !languages.some(l => req.path.startsWith(`/${l}`));
|
|
193
|
+
if (isDefaultRoute && req.cookies?.blue_bird_lang && languages?.includes(req.cookies.blue_bird_lang)) {
|
|
194
|
+
activeLang = req.cookies.blue_bird_lang;
|
|
195
|
+
}
|
|
196
|
+
if (req.query.source === "frontend" && req.query.lang && languages?.includes(req.query.lang)) {
|
|
197
|
+
activeLang = req.query.lang;
|
|
198
|
+
res.cookie("blue_bird_lang", activeLang, {
|
|
199
|
+
maxAge: 31536000000,
|
|
200
|
+
httpOnly: false,
|
|
201
|
+
path: "/",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let activeMeta = metaData;
|
|
206
|
+
if (activeLang !== lang) {
|
|
207
|
+
activeMeta = this._resolveMeta(
|
|
208
|
+
{ meta: metaData, seoKey: options.seoKey },
|
|
209
|
+
activeLang,
|
|
210
|
+
seoData,
|
|
211
|
+
languages,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const dynamicProps = {
|
|
216
|
+
props: {
|
|
217
|
+
...props,
|
|
218
|
+
params: req.params,
|
|
219
|
+
query: req.query,
|
|
220
|
+
lang: activeLang,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const metaTags = {
|
|
225
|
+
titleMeta: activeMeta.titleMeta || activeMeta.title || "",
|
|
226
|
+
descriptionMeta: activeMeta.descriptionMeta || activeMeta.description || "",
|
|
227
|
+
keywordsMeta: activeMeta.keywordsMeta || activeMeta.keywords || "",
|
|
228
|
+
ogImage: activeMeta.ogImage || "",
|
|
229
|
+
ogType: activeMeta.ogType || "website",
|
|
230
|
+
twitterCard: activeMeta.twitterCard || "summary_large_image",
|
|
231
|
+
langMeta: activeLang,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return Template.renderReact(res, component, dynamicProps, { metaTags });
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Registers multiple routes based on an SEO configuration array.
|
|
240
|
+
* Supports simple meta, inline multilingual meta, and external seoData files.
|
|
241
|
+
* Automatically generates language-prefixed routes and registers sitemap.xml and robots.txt.
|
|
242
|
+
*
|
|
243
|
+
* @param {Array<Object>} routesConfig - Array of route objects.
|
|
244
|
+
* @param {Object} [options={}] - Configuration options.
|
|
245
|
+
* @param {Array<string>} [options.languages=[]] - Languages to register (e.g., ["en", "es"]).
|
|
246
|
+
* @param {string} [options.defaultLanguage="en"] - The default language for unprefixed paths.
|
|
247
|
+
* @param {Object} [options.seoData=null] - External SEO data object (like seo.php pattern).
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* // Simple (no i18n)
|
|
251
|
+
* router.seo([
|
|
252
|
+
* {
|
|
253
|
+
* path: "/",
|
|
254
|
+
* component: "Home",
|
|
255
|
+
* meta: { titleMeta: "Home", descriptionMeta: "Welcome" },
|
|
256
|
+
* props: { id: 1 }
|
|
257
|
+
* }
|
|
258
|
+
* ]);
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* // Multilingual (inline)
|
|
262
|
+
* router.seo([
|
|
263
|
+
* {
|
|
264
|
+
* path: "/",
|
|
265
|
+
* component: "Home",
|
|
266
|
+
* meta: {
|
|
267
|
+
* en: { titleMeta: "Home", descriptionMeta: "Welcome" },
|
|
268
|
+
* es: { titleMeta: "Inicio", descriptionMeta: "Bienvenido" }
|
|
269
|
+
* }
|
|
270
|
+
* }
|
|
271
|
+
* ], { languages: ["en", "es"], defaultLanguage: "en" });
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* // Multilingual (external seoData file)
|
|
275
|
+
* import seoData from "./seo.js";
|
|
276
|
+
* router.seo([
|
|
277
|
+
* { path: "/", component: "Home", seoKey: "home" }
|
|
278
|
+
* ], { languages: ["en", "es"], defaultLanguage: "en", seoData });
|
|
279
|
+
*/
|
|
280
|
+
seo(routesConfig, options = {}) {
|
|
281
|
+
const { languages = [], defaultLanguage = null, seoData = null } = options;
|
|
282
|
+
|
|
283
|
+
const defaultLanguageOption = defaultLanguage || props.langMeta || "en";
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
SEO.registerRoutes(this.router, routesConfig, options);
|
|
287
|
+
|
|
288
|
+
routesConfig.forEach((route) => {
|
|
289
|
+
const { path: routePath, component, props = {}, seoKey } = route;
|
|
290
|
+
|
|
291
|
+
if (languages.length > 0) {
|
|
292
|
+
languages.forEach((lang) => {
|
|
293
|
+
const langMeta = this._resolveMeta(route, lang, seoData, languages);
|
|
294
|
+
const langPath = `/${lang}${routePath === "/" ? "" : routePath}`;
|
|
295
|
+
this._registerSeoRoute(
|
|
296
|
+
langPath,
|
|
297
|
+
component,
|
|
298
|
+
props,
|
|
299
|
+
langMeta,
|
|
300
|
+
lang,
|
|
301
|
+
{ seoData, languages, seoKey },
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const defaultMeta = this._resolveMeta(
|
|
306
|
+
route,
|
|
307
|
+
defaultLanguageOption,
|
|
308
|
+
seoData,
|
|
309
|
+
languages,
|
|
310
|
+
);
|
|
311
|
+
this._registerSeoRoute(
|
|
312
|
+
routePath,
|
|
313
|
+
component,
|
|
314
|
+
props,
|
|
315
|
+
defaultMeta,
|
|
316
|
+
defaultLanguageOption,
|
|
317
|
+
{ seoData, languages, seoKey },
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
const meta = this._resolveMeta(route, defaultLanguageOption, seoData, []);
|
|
321
|
+
this._registerSeoRoute(
|
|
322
|
+
routePath,
|
|
323
|
+
component,
|
|
324
|
+
props,
|
|
325
|
+
meta,
|
|
326
|
+
defaultLanguageOption,
|
|
327
|
+
{ seoData, languages: [], seoKey },
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export default Router;
|