@seip/blue-bird 0.7.6 → 0.9.0

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.
Files changed (40) hide show
  1. package/.env_example +34 -34
  2. package/AGENTS.md +174 -249
  3. package/LICENSE +21 -21
  4. package/README.md +331 -367
  5. package/{index.js → backend/index.js} +22 -30
  6. package/backend/routes/api.js +57 -57
  7. package/core/app.js +338 -402
  8. package/core/auth.js +262 -256
  9. package/core/cache.js +174 -174
  10. package/core/cli/docker.js +488 -457
  11. package/core/cli/init.js +333 -337
  12. package/core/cli/route.js +42 -42
  13. package/core/config.js +52 -52
  14. package/core/database.js +263 -263
  15. package/core/debug.js +248 -248
  16. package/core/logger.js +115 -115
  17. package/core/middleware.js +27 -27
  18. package/core/router.js +144 -144
  19. package/core/swagger.js +40 -40
  20. package/core/upload.js +77 -77
  21. package/core/validate.js +380 -380
  22. package/docker/Dockerfile +16 -16
  23. package/docker/docker-compose.dev.yml +6 -0
  24. package/docker/docker-compose.mysql.yml +92 -92
  25. package/docker/docker-compose.none.yml +68 -68
  26. package/docker/docker-compose.postgres.yml +93 -93
  27. package/docker/nginx.conf +98 -106
  28. package/docker-compose.yml +92 -92
  29. package/frontend/about.html +103 -0
  30. package/frontend/images/favicon.ico +0 -0
  31. package/frontend/index.html +141 -0
  32. package/frontend/js/tailwind.js +8 -0
  33. package/package.json +64 -71
  34. package/frontend/astro.config.mjs +0 -35
  35. package/frontend/public/css/app.css +0 -319
  36. package/frontend/public/favicon.ico +0 -0
  37. package/frontend/src/http/api.js +0 -29
  38. package/frontend/src/layouts/Layout.astro +0 -20
  39. package/frontend/src/pages/about.astro +0 -54
  40. package/frontend/src/pages/index.astro +0 -110
package/package.json CHANGED
@@ -1,71 +1,64 @@
1
- {
2
- "name": "@seip/blue-bird",
3
- "version": "0.7.6",
4
- "description": "Express opinionated framework with HTML rendering, API architecture, built-in JWT auth, validation, caching, and SEO",
5
- "type": "module",
6
- "exports": {
7
- "./*": "./*"
8
- },
9
- "bin": {
10
- "blue-bird": "core/cli/init.js"
11
- },
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/seip25/Blue-bird.git"
15
- },
16
- "keywords": [
17
- "express",
18
- "framework",
19
- "html",
20
- "api",
21
- "jwt",
22
- "seo"
23
- ],
24
- "author": "Seip25",
25
- "license": "MIT",
26
- "bugs": {
27
- "url": "https://github.com/seip25/Blue-bird/issues"
28
- },
29
- "homepage": "https://seip25.github.io/Blue-bird/",
30
- "scripts": {
31
- "dev": "node --watch --env-file=.env index.js",
32
- "dev:astro": "astro dev --root frontend",
33
- "dev:api": "node --watch --env-file=.env index.js",
34
- "start": "node --env-file=.env index.js",
35
- "build": "astro build --root frontend",
36
- "init": "node core/cli/init.js",
37
- "route": "node core/cli/route.js",
38
- "swagger-install": "node core/cli/swagger.js",
39
- "docker": "node core/cli/init.js docker"
40
- },
41
- "publishConfig": {
42
- "access": "public"
43
- },
44
- "files": [
45
- "core",
46
- "backend",
47
- "frontend/src",
48
- "frontend/public",
49
- "frontend/astro.config.mjs",
50
- "docker",
51
- "docker-compose.yml",
52
- "AGENTS.md",
53
- "index.js",
54
- ".env_example"
55
- ],
56
- "dependencies": {
57
- "@astrojs/node": "^11.0.0",
58
- "astro": "^7.0.0",
59
- "chalk": "^5.6.2",
60
- "compression": "^1.8.1",
61
- "cookie-parser": "^1.4.7",
62
- "cors": "^2.8.6",
63
- "express": "^5.2.1",
64
- "express-rate-limit": "^8.2.1",
65
- "helmet": "^8.1.0",
66
- "jsonwebtoken": "^9.0.2",
67
- "multer": "^2.0.2",
68
- "redis": "^6.1.0",
69
- "xss": "^1.0.15"
70
- }
71
- }
1
+ {
2
+ "name": "@seip/blue-bird",
3
+ "version": "0.9.0",
4
+ "description": "Express opinionated API framework with built-in JWT auth, validation, and caching",
5
+ "type": "module",
6
+ "exports": {
7
+ "./*": "./*"
8
+ },
9
+ "bin": {
10
+ "blue-bird": "core/cli/init.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/seip25/Blue-bird.git"
15
+ },
16
+ "keywords": [
17
+ "express",
18
+ "framework",
19
+ "html",
20
+ "api",
21
+ "jwt",
22
+ "seo"
23
+ ],
24
+ "author": "Seip25",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/seip25/Blue-bird/issues"
28
+ },
29
+ "homepage": "https://seip25.github.io/Blue-bird/",
30
+ "scripts": {
31
+ "dev": "node --watch --env-file=.env backend/index.js",
32
+ "start": "node --env-file=.env backend/index.js",
33
+ "init": "node core/cli/init.js",
34
+ "route": "node core/cli/route.js",
35
+ "swagger-install": "node core/cli/swagger.js",
36
+ "docker": "node core/cli/init.js docker"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "files": [
42
+ "core",
43
+ "backend",
44
+ "frontend",
45
+ "docker",
46
+ "docker-compose.yml",
47
+ "AGENTS.md",
48
+ "index.js",
49
+ ".env_example"
50
+ ],
51
+ "dependencies": {
52
+ "chalk": "^5.6.2",
53
+ "compression": "^1.8.1",
54
+ "cookie-parser": "^1.4.7",
55
+ "cors": "^2.8.6",
56
+ "express": "^5.2.1",
57
+ "express-rate-limit": "^8.2.1",
58
+ "helmet": "^8.1.0",
59
+ "jsonwebtoken": "^9.0.2",
60
+ "multer": "^2.0.2",
61
+ "redis": "^6.1.0",
62
+ "xss": "^1.0.15"
63
+ }
64
+ }
@@ -1,35 +0,0 @@
1
- import { defineConfig } from 'astro/config';
2
- import node from '@astrojs/node';
3
- import { loadEnv } from 'vite';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, resolve } from 'path';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
-
9
- const env = loadEnv(
10
- process.env.NODE_ENV ?? 'development',
11
- resolve(__dirname, '..'),
12
- '',
13
- );
14
-
15
- const apiPort = env.PORT || '3000';
16
- const apiHost = (env.HOST || 'localhost').replace(/^["']|["']$/g, '');
17
- const apiTarget = `http://${apiHost}:${apiPort}`;
18
-
19
- export default defineConfig({
20
- output: 'server',
21
- adapter: node({
22
- mode: 'middleware',
23
- }),
24
- vite: {
25
- envDir: resolve(__dirname, '..'),
26
- server: {
27
- proxy: {
28
- '/api': {
29
- target: apiTarget,
30
- changeOrigin: true,
31
- },
32
- },
33
- },
34
- },
35
- });
@@ -1,319 +0,0 @@
1
- /* ============================================================
2
- Blue Bird - Base Stylesheet
3
- Vanilla CSS — no build step required.
4
- Import this file in your Astro layout.
5
- ============================================================ */
6
-
7
- /* -------------------------------------------------------
8
- Design Tokens
9
- ------------------------------------------------------- */
10
- :root {
11
- --color-bg: #020617;
12
- --color-bg-surface: #0f172a;
13
- --color-bg-muted: #1e293b;
14
- --color-border: rgba(255, 255, 255, 0.08);
15
- --color-text: #f1f5f9;
16
- --color-text-muted: #94a3b8;
17
- --color-text-subtle: #64748b;
18
- --color-primary: #3b82f6;
19
- --color-primary-h: #2563eb;
20
- --color-accent: #6366f1;
21
-
22
- --space-xs: 0.25rem;
23
- --space-sm: 0.5rem;
24
- --space-md: 1rem;
25
- --space-lg: 1.5rem;
26
- --space-xl: 2rem;
27
- --space-2xl: 3rem;
28
- --space-3xl: 4rem;
29
-
30
- --font-sans: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
31
- --font-mono: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
32
- --text-sm: 0.875rem;
33
- --text-base: 1rem;
34
- --text-lg: 1.125rem;
35
- --text-xl: 1.25rem;
36
- --text-2xl: 1.5rem;
37
- --text-3xl: 1.875rem;
38
- --text-4xl: 2.25rem;
39
- --text-5xl: 3rem;
40
- --text-6xl: 3.75rem;
41
-
42
- --radius-sm: 0.375rem;
43
- --radius-md: 0.75rem;
44
- --radius-lg: 1rem;
45
- --radius-xl: 1.25rem;
46
- --radius-2xl: 1.5rem;
47
- --radius-full: 9999px;
48
-
49
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
50
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
51
- --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.6);
52
- --shadow-glow-blue: 0 0 24px rgba(59, 130, 246, 0.2);
53
- }
54
-
55
- /* -------------------------------------------------------
56
- Reset & Base
57
- ------------------------------------------------------- */
58
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
59
- html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
60
- body {
61
- min-height: 100dvh;
62
- background-color: var(--color-bg);
63
- color: var(--color-text);
64
- font-family: var(--font-sans);
65
- font-size: var(--text-base);
66
- line-height: 1.6;
67
- -webkit-font-smoothing: antialiased;
68
- -moz-osx-font-smoothing: grayscale;
69
- }
70
- ::selection { background-color: var(--color-primary); color: #fff; }
71
-
72
- /* -------------------------------------------------------
73
- Typography
74
- ------------------------------------------------------- */
75
- h1, h2, h3, h4, h5, h6 { line-height: 1.2; font-weight: 700; letter-spacing: -0.02em; }
76
- h1 { font-size: clamp(2rem, 5vw, var(--text-6xl)); }
77
- h2 { font-size: clamp(1.5rem, 4vw, var(--text-4xl)); }
78
- h3 { font-size: clamp(1.25rem, 3vw, var(--text-3xl)); }
79
- h4 { font-size: var(--text-xl); }
80
- p { color: var(--color-text-muted); line-height: 1.75; }
81
- a { color: var(--color-primary); text-decoration: none; transition: color 0.2s ease; }
82
- a:hover { color: var(--color-primary-h); }
83
-
84
- code, kbd {
85
- font-family: var(--font-mono);
86
- font-size: 0.875em;
87
- background: var(--color-bg-muted);
88
- color: var(--color-text);
89
- padding: 0.15em 0.4em;
90
- border-radius: var(--radius-sm);
91
- }
92
- pre {
93
- font-family: var(--font-mono);
94
- background: var(--color-bg-surface);
95
- border: 1px solid var(--color-border);
96
- border-radius: var(--radius-lg);
97
- padding: var(--space-lg);
98
- overflow-x: auto;
99
- font-size: var(--text-sm);
100
- }
101
- pre code { background: none; padding: 0; font-size: inherit; }
102
- img, svg { display: block; max-width: 100%; }
103
-
104
- /* -------------------------------------------------------
105
- Layout
106
- ------------------------------------------------------- */
107
- .container {
108
- width: 100%;
109
- max-width: 80rem;
110
- margin-inline: auto;
111
- padding-inline: var(--space-lg);
112
- }
113
- .flex { display: flex; }
114
- .flex-col { flex-direction: column; }
115
- .items-center { align-items: center; }
116
- .justify-between { justify-content: space-between; }
117
- .justify-center { justify-content: center; }
118
- .flex-wrap { flex-wrap: wrap; }
119
- .gap-sm { gap: var(--space-sm); }
120
- .gap-md { gap: var(--space-md); }
121
- .gap-lg { gap: var(--space-lg); }
122
- .gap-xl { gap: var(--space-xl); }
123
- .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-xl); }
124
- .grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-xl); }
125
- .text-center { text-align: center; }
126
- .text-muted { color: var(--color-text-muted); }
127
- .text-subtle { color: var(--color-text-subtle); }
128
- .text-sm { font-size: var(--text-sm); }
129
- .text-lg { font-size: var(--text-lg); }
130
- .font-mono { font-family: var(--font-mono); }
131
- .font-bold { font-weight: 700; }
132
- .uppercase { text-transform: uppercase; letter-spacing: 0.08em; }
133
- .mt-sm { margin-top: var(--space-sm); }
134
- .mt-md { margin-top: var(--space-md); }
135
- .mt-lg { margin-top: var(--space-lg); }
136
- .mt-xl { margin-top: var(--space-xl); }
137
- .mt-2xl { margin-top: var(--space-2xl); }
138
- .mb-sm { margin-bottom: var(--space-sm); }
139
- .mb-md { margin-bottom: var(--space-md); }
140
- .mb-lg { margin-bottom: var(--space-lg); }
141
- .mb-xl { margin-bottom: var(--space-xl); }
142
- .py-lg { padding-block: var(--space-lg); }
143
- .py-xl { padding-block: var(--space-xl); }
144
- .py-2xl { padding-block: var(--space-2xl); }
145
- .py-3xl { padding-block: var(--space-3xl); }
146
- .px-md { padding-inline: var(--space-md); }
147
- .px-lg { padding-inline: var(--space-lg); }
148
-
149
- /* -------------------------------------------------------
150
- Gradient Text
151
- ------------------------------------------------------- */
152
- .gradient-text {
153
- background: linear-gradient(135deg, var(--color-primary), var(--color-accent), #fff);
154
- -webkit-background-clip: text;
155
- -webkit-text-fill-color: transparent;
156
- background-clip: text;
157
- }
158
- .gradient-text-blue {
159
- background: linear-gradient(90deg, #60a5fa, #a5b4fc);
160
- -webkit-background-clip: text;
161
- -webkit-text-fill-color: transparent;
162
- background-clip: text;
163
- }
164
-
165
- /* -------------------------------------------------------
166
- Header / Nav
167
- ------------------------------------------------------- */
168
- .site-header {
169
- position: sticky;
170
- top: 0;
171
- z-index: 50;
172
- padding: var(--space-sm) var(--space-lg);
173
- }
174
- .nav-bar {
175
- display: flex;
176
- align-items: center;
177
- justify-content: space-between;
178
- background: rgba(15, 23, 42, 0.7);
179
- backdrop-filter: blur(12px);
180
- -webkit-backdrop-filter: blur(12px);
181
- border: 1px solid var(--color-border);
182
- border-radius: var(--radius-2xl);
183
- padding: var(--space-sm) var(--space-lg);
184
- box-shadow: var(--shadow-md);
185
- max-width: 80rem;
186
- margin-inline: auto;
187
- }
188
- .nav-brand {
189
- display: flex;
190
- align-items: center;
191
- gap: var(--space-sm);
192
- font-size: var(--text-xl);
193
- font-weight: 700;
194
- color: var(--color-text);
195
- text-decoration: none;
196
- }
197
- .nav-brand .brand-icon {
198
- width: 2.5rem;
199
- height: 2.5rem;
200
- background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
201
- border-radius: var(--radius-md);
202
- display: flex;
203
- align-items: center;
204
- justify-content: center;
205
- box-shadow: var(--shadow-glow-blue);
206
- }
207
- nav.nav-links { display: flex; gap: var(--space-xl); }
208
- .nav-links a { font-size: var(--text-sm); font-weight: 500; color: var(--color-text-muted); transition: color 0.2s ease; }
209
- .nav-links a:hover { color: var(--color-primary); }
210
-
211
- /* -------------------------------------------------------
212
- Buttons
213
- ------------------------------------------------------- */
214
- .btn {
215
- display: inline-flex;
216
- align-items: center;
217
- gap: var(--space-sm);
218
- padding: 0.75rem 1.75rem;
219
- border-radius: var(--radius-xl);
220
- font-weight: 600;
221
- font-size: var(--text-base);
222
- cursor: pointer;
223
- border: none;
224
- text-decoration: none;
225
- transition: transform 0.2s ease, box-shadow 0.2s ease;
226
- }
227
- .btn:hover { transform: translateY(-2px); }
228
- .btn:active { transform: translateY(0); }
229
- .btn-primary {
230
- background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
231
- color: #fff;
232
- box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3);
233
- }
234
- .btn-primary:hover { color: #fff; box-shadow: 0 6px 28px rgba(59, 130, 246, 0.45); }
235
- .btn-outline {
236
- background: transparent;
237
- color: var(--color-text-muted);
238
- border: 1px solid var(--color-border);
239
- }
240
- .btn-outline:hover { background: var(--color-bg-surface); color: var(--color-text); }
241
-
242
- /* -------------------------------------------------------
243
- Cards
244
- ------------------------------------------------------- */
245
- .card {
246
- background: var(--color-bg-surface);
247
- border: 1px solid var(--color-border);
248
- border-radius: var(--radius-2xl);
249
- padding: var(--space-xl);
250
- transition: transform 0.25s ease, box-shadow 0.25s ease;
251
- }
252
- .card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
253
- .card-glass {
254
- background: rgba(15, 23, 42, 0.5);
255
- backdrop-filter: blur(16px);
256
- -webkit-backdrop-filter: blur(16px);
257
- border: 1px solid var(--color-border);
258
- border-radius: var(--radius-2xl);
259
- padding: var(--space-xl);
260
- }
261
-
262
- /* -------------------------------------------------------
263
- Badge
264
- ------------------------------------------------------- */
265
- .badge {
266
- display: inline-flex;
267
- align-items: center;
268
- padding: 0.3rem 0.9rem;
269
- border-radius: var(--radius-full);
270
- font-size: 0.7rem;
271
- font-weight: 600;
272
- letter-spacing: 0.1em;
273
- text-transform: uppercase;
274
- background: rgba(59, 130, 246, 0.1);
275
- color: #60a5fa;
276
- border: 1px solid rgba(59, 130, 246, 0.2);
277
- }
278
-
279
- /* -------------------------------------------------------
280
- Hero
281
- ------------------------------------------------------- */
282
- .hero {
283
- position: relative;
284
- overflow: hidden;
285
- padding-block: var(--space-3xl);
286
- text-align: center;
287
- }
288
- .hero-glow {
289
- position: absolute;
290
- border-radius: 50%;
291
- filter: blur(80px);
292
- pointer-events: none;
293
- z-index: 0;
294
- }
295
- .hero-glow-1 { width: 24rem; height: 24rem; top: -6rem; left: -6rem; background: rgba(59, 130, 246, 0.08); }
296
- .hero-glow-2 { width: 24rem; height: 24rem; bottom: -6rem; right: -6rem; background: rgba(99, 102, 241, 0.08); }
297
- .hero-inner { position: relative; z-index: 1; max-width: 56rem; margin-inline: auto; }
298
-
299
- /* -------------------------------------------------------
300
- Section / Footer
301
- ------------------------------------------------------- */
302
- .section { padding-block: var(--space-3xl); }
303
- .site-footer {
304
- border-top: 1px solid var(--color-border);
305
- padding-block: var(--space-xl);
306
- text-align: center;
307
- color: var(--color-text-subtle);
308
- font-size: var(--text-sm);
309
- background: var(--color-bg);
310
- margin-top: var(--space-3xl);
311
- }
312
-
313
- /* -------------------------------------------------------
314
- Scrollbar (Webkit)
315
- ------------------------------------------------------- */
316
- ::-webkit-scrollbar { width: 6px; }
317
- ::-webkit-scrollbar-track { background: var(--color-bg); }
318
- ::-webkit-scrollbar-thumb { background: var(--color-bg-muted); border-radius: var(--radius-full); }
319
- ::-webkit-scrollbar-thumb:hover { background: var(--color-primary); }
Binary file
@@ -1,29 +0,0 @@
1
- const defaultPort = (import.meta.env.PORT || "3000").replace(
2
- /^["']|["']$/g,
3
- "",
4
- );
5
- const host = (import.meta.env.HOST || "localhost").replace(/^["']|["']$/g, "");
6
-
7
- /**
8
- * Builds an absolute API URL for server-side fetch calls.
9
- *
10
- * @param {string} [path] - API path relative to the server root (e.g. "api/users").
11
- * @param {URL} [requestUrl] - Current request URL (pass `Astro.url` from the page). Required in production.
12
- * @returns {string} Absolute URL ready for fetch.
13
- * @example apiUrl('api/', Astro.url)
14
- */
15
- export function apiUrl(path = "", requestUrl) {
16
- if (!requestUrl) {
17
- throw new Error(
18
- "apiUrl: requestUrl is required in production (pass Astro.url)",
19
- );
20
- }
21
- const url = new URL(`/${path}`, requestUrl);
22
- const runtimePort =
23
- (typeof process !== "undefined" && process.env && process.env.PORT) ||
24
- defaultPort;
25
- url.hostname = "127.0.0.1";
26
- url.port = runtimePort;
27
- url.protocol = "http:";
28
- return url.href;
29
- }
@@ -1,20 +0,0 @@
1
- ---
2
- import { ClientRouter } from "astro:transitions";
3
-
4
- const { title = "Blue Bird" } = Astro.props;
5
- ---
6
-
7
- <html lang="en">
8
- <head>
9
- <meta charset="UTF-8" />
10
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
- <title>{title}</title>
12
- <link rel="stylesheet" href="/css/app.css" />
13
- <ClientRouter />
14
- </head>
15
- <body
16
- class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white"
17
- >
18
- <slot />
19
- </body>
20
- </html>
@@ -1,54 +0,0 @@
1
- ---
2
- import Layout from "../layouts/Layout.astro";
3
- ---
4
-
5
- <Layout title="About - Blue Bird">
6
- <header class="site-header">
7
- <div class="nav-bar">
8
- <a href="/" class="nav-brand">
9
- <div class="brand-icon">
10
- <svg width="20" height="20" fill="none" stroke="white" viewBox="0 0 24 24">
11
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
12
- d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
13
- </svg>
14
- </div>
15
- Blue Bird
16
- </a>
17
- <nav class="nav-links">
18
- <a href="/">Home</a>
19
- <a href="/about">About</a>
20
- </nav>
21
- </div>
22
- </header>
23
-
24
- <main class="container py-3xl">
25
- <a href="/" class="btn btn-outline text-sm mb-2xl" style="width:fit-content;">
26
- &larr; Back to Home
27
- </a>
28
-
29
- <div class="card-glass py-3xl px-lg" style="max-width:52rem;">
30
- <span class="badge mb-lg" style="display:inline-flex;">Framework</span>
31
- <h1 class="gradient-text mt-md">About Blue Bird</h1>
32
- <p class="mt-lg">
33
- Blue Bird is an Express-based framework with native support for Astro frontend rendering.
34
- It provides raw performance, developer efficiency, and a clean project layout — batteries
35
- included for auth, validation, caching, Docker orchestration, and more.
36
- </p>
37
-
38
- <div class="grid-2 mt-2xl">
39
- <div class="card">
40
- <h4 class="mb-md">Express Backend</h4>
41
- <p class="text-sm">REST APIs, JWT auth, rate limiting, CORS, and Helmet security — all preconfigured via a clean constructor API.</p>
42
- </div>
43
- <div class="card">
44
- <h4 class="mb-md">Astro Frontend</h4>
45
- <p class="text-sm">File-based routing, SSR, View Transitions, and island architecture. Use Tailwind or any CSS framework you prefer.</p>
46
- </div>
47
- </div>
48
- </div>
49
- </main>
50
-
51
- <footer class="site-footer">
52
- <p>Powered by Blue Bird Framework &amp; Astro. All rights reserved.</p>
53
- </footer>
54
- </Layout>