@lolyjs/core 0.2.0-alpha.3 → 0.2.0-alpha.30

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 (46) hide show
  1. package/README.md +1463 -761
  2. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  3. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  4. package/dist/cli.cjs +15701 -2448
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js +15704 -2441
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +17861 -4115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.mts +323 -55
  11. package/dist/index.d.ts +323 -55
  12. package/dist/index.js +17982 -4227
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.types-B9j4OQft.d.mts +222 -0
  15. package/dist/index.types-B9j4OQft.d.ts +222 -0
  16. package/dist/react/cache.cjs +107 -32
  17. package/dist/react/cache.cjs.map +1 -1
  18. package/dist/react/cache.d.mts +29 -21
  19. package/dist/react/cache.d.ts +29 -21
  20. package/dist/react/cache.js +107 -32
  21. package/dist/react/cache.js.map +1 -1
  22. package/dist/react/components.cjs +11 -12
  23. package/dist/react/components.cjs.map +1 -1
  24. package/dist/react/components.js +11 -12
  25. package/dist/react/components.js.map +1 -1
  26. package/dist/react/hooks.cjs +124 -74
  27. package/dist/react/hooks.cjs.map +1 -1
  28. package/dist/react/hooks.d.mts +6 -24
  29. package/dist/react/hooks.d.ts +6 -24
  30. package/dist/react/hooks.js +122 -71
  31. package/dist/react/hooks.js.map +1 -1
  32. package/dist/react/sockets.cjs +5 -6
  33. package/dist/react/sockets.cjs.map +1 -1
  34. package/dist/react/sockets.js +5 -6
  35. package/dist/react/sockets.js.map +1 -1
  36. package/dist/react/themes.cjs +61 -18
  37. package/dist/react/themes.cjs.map +1 -1
  38. package/dist/react/themes.js +63 -20
  39. package/dist/react/themes.js.map +1 -1
  40. package/dist/runtime.cjs +531 -104
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.d.mts +2 -2
  43. package/dist/runtime.d.ts +2 -2
  44. package/dist/runtime.js +531 -104
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +56 -14
package/README.md CHANGED
@@ -1,761 +1,1463 @@
1
- # Loly Framework
2
-
3
- <div align="center">
4
-
5
- **A modern, full-stack React framework with native WebSocket support, route-level middlewares, and enterprise-grade features**
6
-
7
- [![npm version](https://img.shields.io/npm/v/@lolyjs/core?style=flat-square)](https://www.npmjs.com/package/@lolyjs/core)
8
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg?style=flat-square)](https://opensource.org/licenses/ISC)
9
-
10
- *Built with React 19, Express, Rspack, Socket.IO, and TypeScript*
11
-
12
- </div>
13
-
14
- ---
15
-
16
- ## Getting Started
17
-
18
- Create a new Loly application in seconds:
19
-
20
- ```bash
21
- npx create-loly-app mi-app
22
- ```
23
-
24
- This will create a new project with all the necessary files and dependencies. For more information about the CLI, visit the [@lolyjs/cli package](https://www.npmjs.com/package/@lolyjs/cli).
25
-
26
- ---
27
-
28
- ## Overview
29
-
30
- Loly is a full-stack React framework that combines the simplicity of file-based routing with powerful server-side rendering, static site generation, and unique features like native WebSocket support and route-level middlewares.
31
-
32
- ### What Makes Loly Different?
33
-
34
- - 🔌 **Native WebSocket Support** - Built-in Socket.IO integration with automatic namespace routing
35
- - 🎯 **Route-Level Middlewares** - Define middlewares directly in your routes for pages and APIs
36
- - 📁 **Separation of Concerns** - Server logic in `server.hook.ts` separate from React components
37
- - 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
38
- - 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
39
- - **Performance** - Fast bundling with Rspack and optimized code splitting
40
-
41
- ---
42
-
43
- ## Quick Start
44
-
45
- ### Installation
46
-
47
- ```bash
48
- npm install @lolyjs/core react react-dom
49
- # or
50
- pnpm add @lolyjs/core react react-dom
51
- ```
52
-
53
- ### Create Your First Page
54
-
55
- ```tsx
56
- // app/page.tsx
57
- export default function Home() {
58
- return <h1>Hello, Loly!</h1>;
59
- }
60
- ```
61
-
62
- ### Add Server-Side Data
63
-
64
- ```tsx
65
- // app/page/server.hook.ts
66
- import type { ServerLoader } from "@lolyjs/core";
67
-
68
- export const getServerSideProps: ServerLoader = async (ctx) => {
69
- const data = await fetchData();
70
-
71
- return {
72
- props: { data },
73
- metadata: {
74
- title: "Home Page",
75
- description: "Welcome to Loly",
76
- },
77
- };
78
- };
79
- ```
80
-
81
- ```tsx
82
- // app/page.tsx
83
- import { usePageProps } from "@lolyjs/core/hooks";
84
-
85
- export default function Home() {
86
- const { props } = usePageProps();
87
- return <h1>{props.data}</h1>;
88
- }
89
- ```
90
-
91
- ### Start Development Server
92
-
93
- ```bash
94
- npx loly dev
95
- # Server runs on http://localhost:3000
96
- ```
97
-
98
- ---
99
-
100
- ## Key Features
101
-
102
- ### 🔌 Native WebSocket Support
103
-
104
- Loly includes built-in WebSocket support with automatic namespace routing. Define WebSocket events using the same file-based routing pattern as pages and APIs:
105
-
106
- ```tsx
107
- // app/wss/chat/events.ts
108
- import type { WssContext } from "@lolyjs/core";
109
-
110
- export const events = [
111
- {
112
- name: "connection",
113
- handler: (ctx: WssContext) => {
114
- console.log("Client connected:", ctx.socket.id);
115
- },
116
- },
117
- {
118
- name: "message",
119
- handler: (ctx: WssContext) => {
120
- const { data, actions } = ctx;
121
- // Broadcast to all clients
122
- actions.broadcast("message", {
123
- text: data.text,
124
- from: ctx.socket.id,
125
- });
126
- },
127
- },
128
- ];
129
- ```
130
-
131
- **Client-side:**
132
-
133
- ```tsx
134
- import { lolySocket } from "@lolyjs/core/sockets";
135
-
136
- const socket = lolySocket("/chat");
137
-
138
- socket.on("message", (data) => {
139
- console.log("Received:", data);
140
- });
141
-
142
- socket.emit("message", { text: "Hello!" });
143
- ```
144
-
145
- **Key Benefits:**
146
- - Automatic namespace creation from file structure
147
- - Same routing pattern as pages and APIs
148
- - Built-in broadcasting helpers (`emit`, `broadcast`, `emitTo`, `emitToClient`)
149
- - No manual configuration required
150
-
151
- ### 🎯 Route-Level Middlewares
152
-
153
- Define middlewares directly in your routes for fine-grained control:
154
-
155
- **For Pages:**
156
-
157
- ```tsx
158
- // app/dashboard/server.hook.ts
159
- import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
160
-
161
- export const beforeServerData: RouteMiddleware[] = [
162
- async (ctx, next) => {
163
- // Authentication
164
- const token = ctx.req.headers.authorization;
165
- if (!token) {
166
- ctx.res.status(401).json({ error: "Unauthorized" });
167
- return;
168
- }
169
- ctx.locals.user = await verifyToken(token);
170
- await next();
171
- },
172
- ];
173
-
174
- export const getServerSideProps: ServerLoader = async (ctx) => {
175
- const user = ctx.locals.user; // Available from middleware
176
- return { props: { user } };
177
- };
178
- ```
179
-
180
- **For API Routes:**
181
-
182
- ```tsx
183
- // app/api/protected/route.ts
184
- import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
185
-
186
- // Global middleware for all methods
187
- export const beforeApi: ApiMiddleware[] = [
188
- async (ctx, next) => {
189
- // Authentication
190
- const user = await verifyUser(ctx.req);
191
- ctx.locals.user = user;
192
- await next();
193
- },
194
- ];
195
-
196
- // Method-specific middleware
197
- export const beforePOST: ApiMiddleware[] = [
198
- async (ctx, next) => {
199
- // Validation specific to POST
200
- await next();
201
- },
202
- ];
203
-
204
- export async function GET(ctx: ApiContext) {
205
- const user = ctx.locals.user;
206
- return ctx.Response({ user });
207
- }
208
- ```
209
-
210
- **Key Benefits:**
211
- - Middlewares execute before loaders/handlers
212
- - Share data via `ctx.locals`
213
- - Method-specific middlewares for APIs
214
- - Clean separation of concerns
215
-
216
- ### 📁 File-Based Routing
217
-
218
- Routes are automatically created from your file structure:
219
-
220
- | File Path | Route |
221
- |-----------|-------|
222
- | `app/page.tsx` | `/` |
223
- | `app/about/page.tsx` | `/about` |
224
- | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
225
- | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
226
-
227
- **Nested Layouts:**
228
-
229
- **⚠️ Important**: Layouts should NOT include `<html>` or `<body>` tags. The framework automatically handles the base HTML structure. Layouts should only contain content that goes inside the body.
230
-
231
- ```tsx
232
- // app/layout.tsx (Root layout)
233
- export default function RootLayout({ children }) {
234
- return (
235
- <div>
236
- <nav>Navigation</nav>
237
- {children}
238
- <footer>Footer</footer>
239
- </div>
240
- );
241
- }
242
- ```
243
-
244
- ```tsx
245
- // app/blog/layout.tsx (Nested layout)
246
- export default function BlogLayout({ children }) {
247
- return (
248
- <div>
249
- <aside>Sidebar</aside>
250
- <main>{children}</main>
251
- </div>
252
- );
253
- }
254
- ```
255
-
256
- ### 🚀 Hybrid Rendering
257
-
258
- Choose the best rendering strategy for each page:
259
-
260
- **SSR (Server-Side Rendering):**
261
-
262
- ```tsx
263
- // app/posts/server.hook.ts
264
- export const dynamic = "force-dynamic" as const;
265
-
266
- export const getServerSideProps: ServerLoader = async (ctx) => {
267
- const posts = await fetchFreshPosts();
268
- return { props: { posts } };
269
- };
270
- ```
271
-
272
- **SSG (Static Site Generation):**
273
-
274
- ```tsx
275
- // app/blog/[slug]/server.hook.ts
276
- export const dynamic = "force-static" as const;
277
-
278
- export const generateStaticParams: GenerateStaticParams = async () => {
279
- const posts = await getAllPosts();
280
- return posts.map(post => ({ slug: post.slug }));
281
- };
282
-
283
- export const getServerSideProps: ServerLoader = async (ctx) => {
284
- const post = await getPost(ctx.params.slug);
285
- return { props: { post } };
286
- };
287
- ```
288
-
289
- **CSR (Client-Side Rendering):**
290
-
291
- ```tsx
292
- // app/dashboard/page.tsx (No server.hook.ts)
293
- import { useState, useEffect } from "react";
294
-
295
- export default function Dashboard() {
296
- const [data, setData] = useState(null);
297
-
298
- useEffect(() => {
299
- fetchData().then(setData);
300
- }, []);
301
-
302
- return <div>{data}</div>;
303
- }
304
- ```
305
-
306
- ### 🔌 API Routes
307
-
308
- Create RESTful APIs with flexible middleware support:
309
-
310
- ```tsx
311
- // app/api/posts/route.ts
312
- import type { ApiContext } from "@lolyjs/core";
313
- import { validate } from "@lolyjs/core";
314
- import { z } from "zod";
315
-
316
- const postSchema = z.object({
317
- title: z.string().min(1),
318
- content: z.string().min(1),
319
- });
320
-
321
- export async function GET(ctx: ApiContext) {
322
- const posts = await getPosts();
323
- return ctx.Response({ posts });
324
- }
325
-
326
- export async function POST(ctx: ApiContext) {
327
- const data = validate(postSchema, ctx.req.body);
328
- const post = await createPost(data);
329
- return ctx.Response({ post }, 201);
330
- }
331
- ```
332
-
333
- ### 🛡️ Built-in Security
334
-
335
- **Rate Limiting:**
336
-
337
- ```tsx
338
- // loly.config.ts
339
- import { ServerConfig } from "@lolyjs/core";
340
-
341
- export const config = (env: string): ServerConfig => {
342
- return {
343
- rateLimit: {
344
- windowMs: 15 * 60 * 1000, // 15 minutes
345
- max: 1000,
346
- strictMax: 5,
347
- strictPatterns: ["/api/auth/**"],
348
- },
349
- };
350
- };
351
- ```
352
-
353
- **Validation with Zod:**
354
-
355
- ```tsx
356
- import { validate, ValidationError } from "@lolyjs/core";
357
- import { z } from "zod";
358
-
359
- const schema = z.object({
360
- email: z.string().email(),
361
- age: z.number().int().min(0).max(150),
362
- });
363
-
364
- try {
365
- const data = validate(schema, req.body);
366
- } catch (error) {
367
- if (error instanceof ValidationError) {
368
- return Response({ errors: error.format() }, 400);
369
- }
370
- }
371
- ```
372
-
373
- **Automatic Sanitization:**
374
-
375
- Route parameters and query strings are automatically sanitized to prevent XSS attacks.
376
-
377
- **Security Headers:**
378
-
379
- Helmet is configured by default with CSP (Content Security Policy) and nonce support.
380
-
381
- ### 📝 Structured Logging
382
-
383
- ```tsx
384
- import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
385
-
386
- // Request logger (automatic request ID)
387
- export const getServerSideProps: ServerLoader = async (ctx) => {
388
- const logger = getRequestLogger(ctx.req);
389
- logger.info("Processing request", { userId: ctx.locals.user?.id });
390
- return { props: {} };
391
- };
392
-
393
- // Module logger
394
- const logger = createModuleLogger("my-module");
395
- logger.info("Module initialized");
396
- logger.error("Error occurred", error);
397
- ```
398
-
399
- ---
400
-
401
- ## Project Structure
402
-
403
- ```
404
- your-app/
405
- ├── app/
406
- │ ├── layout.tsx # Root layout
407
- │ ├── page.tsx # Home page (/)
408
- │ ├── server.hook.ts # Server logic for home
409
- │ ├── _not-found.tsx # Custom 404
410
- │ ├── _error.tsx # Custom error page
411
- │ ├── blog/
412
- │ │ ├── layout.tsx # Blog layout
413
- │ │ ├── page.tsx # /blog
414
- │ │ └── [slug]/
415
- │ │ ├── page.tsx # /blog/:slug
416
- │ │ └── server.hook.ts # Server logic
417
- │ ├── api/
418
- │ │ └── posts/
419
- │ │ └── route.ts # /api/posts
420
- │ └── wss/
421
- │ └── chat/
422
- │ └── events.ts # WebSocket namespace /chat
423
- ├── components/ # React components
424
- ├── lib/ # Utilities
425
- ├── public/ # Static files
426
- ├── loly.config.ts # Framework configuration
427
- ├── init.server.ts # Server initialization (DB, services, etc.)
428
- └── package.json
429
- ```
430
-
431
- ---
432
-
433
- ## API Reference
434
-
435
- ### Server Loader
436
-
437
- ```tsx
438
- import type { ServerLoader } from "@lolyjs/core";
439
-
440
- export const getServerSideProps: ServerLoader = async (ctx) => {
441
- const { req, res, params, pathname, locals } = ctx;
442
-
443
- // Fetch data
444
- const data = await fetchData();
445
-
446
- // Redirect
447
- return {
448
- redirect: {
449
- destination: "/new-path",
450
- permanent: true,
451
- },
452
- };
453
-
454
- // Not found
455
- return { notFound: true };
456
-
457
- // Return props
458
- return {
459
- props: { data },
460
- metadata: {
461
- title: "Page Title",
462
- description: "Page description",
463
- },
464
- };
465
- };
466
- ```
467
-
468
- ### API Route Handler
469
-
470
- ```tsx
471
- import type { ApiContext } from "@lolyjs/core";
472
-
473
- export async function GET(ctx: ApiContext) {
474
- return ctx.Response({ data: "value" });
475
- }
476
-
477
- export async function POST(ctx: ApiContext) {
478
- return ctx.Response({ created: true }, 201);
479
- }
480
-
481
- export async function DELETE(ctx: ApiContext) {
482
- return ctx.Response({ deleted: true }, 204);
483
- }
484
- ```
485
-
486
- ### WebSocket Event Handler
487
-
488
- ```tsx
489
- import type { WssContext } from "@lolyjs/core";
490
-
491
- export const events = [
492
- {
493
- name: "connection",
494
- handler: (ctx: WssContext) => {
495
- // Handle connection
496
- },
497
- },
498
- {
499
- name: "custom-event",
500
- handler: (ctx: WssContext) => {
501
- const { socket, data, actions } = ctx;
502
-
503
- // Emit to all clients
504
- actions.emit("response", { message: "Hello" });
505
-
506
- // Broadcast to all except sender
507
- actions.broadcast("notification", data);
508
-
509
- // Emit to specific socket
510
- actions.emitTo(socketId, "private", data);
511
- },
512
- },
513
- ];
514
- ```
515
-
516
- ### Client Hooks
517
-
518
- ```tsx
519
- import { usePageProps } from "@lolyjs/core/hooks";
520
- import { revalidate } from "@lolyjs/core/client-cache";
521
-
522
- export default function Page() {
523
- const { params, props } = usePageProps();
524
-
525
- const handleRefresh = async () => {
526
- await revalidate(); // Refresh current page data
527
- };
528
-
529
- return <div>{/* Your UI */}</div>;
530
- }
531
- ```
532
-
533
- ### Components
534
-
535
- ```tsx
536
- import { Link } from "@lolyjs/core/components";
537
-
538
- export default function Navigation() {
539
- return (
540
- <nav>
541
- <Link href="/">Home</Link>
542
- <Link href="/about">About</Link>
543
- <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
544
- My Post
545
- </Link>
546
- </nav>
547
- );
548
- }
549
- ```
550
-
551
- ---
552
-
553
- ## Configuration
554
-
555
- ### Framework Configuration
556
-
557
- Create `loly.config.ts` in your project root to configure the framework:
558
-
559
- ```tsx
560
- import { FrameworkConfig } from "@lolyjs/core";
561
-
562
- export default {
563
- directories: {
564
- app: "app",
565
- build: ".loly",
566
- static: "public",
567
- },
568
- server: {
569
- port: 3000,
570
- host: "localhost",
571
- },
572
- routing: {
573
- trailingSlash: "ignore",
574
- caseSensitive: false,
575
- basePath: "",
576
- },
577
- rendering: {
578
- framework: "react",
579
- streaming: true,
580
- ssr: true,
581
- ssg: true,
582
- },
583
- } satisfies FrameworkConfig;
584
- ```
585
-
586
- ### Server Configuration
587
-
588
- Configure server settings (CORS, rate limiting, etc.) in `loly.config.ts` by exporting a `config` function:
589
-
590
- ```tsx
591
- // loly.config.ts
592
- import { ServerConfig } from "@lolyjs/core";
593
-
594
- export const config = (env: string): ServerConfig => {
595
- return {
596
- bodyLimit: "1mb",
597
- corsOrigin: env === "production"
598
- ? ["https://yourdomain.com"]
599
- : "*",
600
- rateLimit: {
601
- windowMs: 15 * 60 * 1000,
602
- max: 1000,
603
- strictMax: 5,
604
- strictPatterns: ["/api/auth/**"],
605
- },
606
- };
607
- };
608
- ```
609
-
610
- ### Server Initialization
611
-
612
- Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
613
-
614
- ```tsx
615
- // init.server.ts
616
- import { InitServerData } from "@lolyjs/core";
617
-
618
- export async function init({
619
- serverContext,
620
- }: {
621
- serverContext: InitServerData;
622
- }) {
623
- // Initialize database connection
624
- await connectToDatabase();
625
-
626
- // Setup external services
627
- await setupExternalServices();
628
-
629
- // Any other initialization logic
630
- console.log("Server initialized successfully");
631
- }
632
- ```
633
-
634
- **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
635
-
636
- ---
637
-
638
- ## CLI Commands
639
-
640
- ```bash
641
- # Development server
642
- npx loly dev
643
-
644
- # Build for production
645
- npx loly build
646
-
647
- # Start production server
648
- npx loly start
649
- ```
650
-
651
- ---
652
-
653
- ## TypeScript Support
654
-
655
- Loly is built with TypeScript and provides full type safety:
656
-
657
- ```tsx
658
- import type {
659
- ServerContext,
660
- ServerLoader,
661
- ApiContext,
662
- WssContext,
663
- RouteMiddleware,
664
- ApiMiddleware,
665
- GenerateStaticParams,
666
- } from "@lolyjs/core";
667
- ```
668
-
669
- ---
670
-
671
- ## Production
672
-
673
- ### Build
674
-
675
- ```bash
676
- npm run build
677
- ```
678
-
679
- This generates:
680
- - Client bundle (`.loly/client`)
681
- - Static pages if using SSG (`.loly/ssg`)
682
- - Server code (`.loly/server`)
683
-
684
- ### Environment Variables
685
-
686
- ```bash
687
- PORT=3000
688
- HOST=0.0.0.0
689
- NODE_ENV=production
690
- PUBLIC_WS_BASE_URL=http://localhost:3000
691
- ```
692
-
693
- ---
694
-
695
- ## Exports
696
-
697
- ```tsx
698
- // Server
699
- import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
700
-
701
- // Types
702
- import type {
703
- ServerContext,
704
- ServerLoader,
705
- ApiContext,
706
- WssContext,
707
- RouteMiddleware,
708
- ApiMiddleware,
709
- GenerateStaticParams,
710
- } from "@lolyjs/core";
711
-
712
- // Validation
713
- import { validate, safeValidate, ValidationError } from "@lolyjs/core";
714
-
715
- // Security
716
- import { sanitizeString, sanitizeObject } from "@lolyjs/core";
717
- import {
718
- createRateLimiter,
719
- defaultRateLimiter,
720
- strictRateLimiter
721
- } from "@lolyjs/core";
722
-
723
- // Logging
724
- import {
725
- logger,
726
- createModuleLogger,
727
- getRequestLogger
728
- } from "@lolyjs/core";
729
-
730
- // Client
731
- import { Link } from "@lolyjs/core/components";
732
- import { usePageProps } from "@lolyjs/core/hooks";
733
- import { lolySocket } from "@lolyjs/core/sockets";
734
- import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
735
- ```
736
-
737
- ---
738
-
739
- ## License
740
-
741
- ISC
742
-
743
- ---
744
-
745
- ## Built With
746
-
747
- - [React](https://react.dev/) - UI library
748
- - [Express](https://expressjs.com/) - Web framework
749
- - [Rspack](https://rspack.dev/) - Fast bundler
750
- - [Socket.IO](https://socket.io/) - WebSocket library
751
- - [Pino](https://getpino.io/) - Fast logger
752
- - [Zod](https://zod.dev/) - Schema validation
753
- - [Helmet](https://helmetjs.github.io/) - Security headers
754
-
755
- ---
756
-
757
- <div align="center">
758
-
759
- **Made with ❤️ by the Loly team**
760
-
761
- </div>
1
+ # Loly Framework
2
+
3
+ <div align="center">
4
+
5
+ **A modern, full-stack React framework with native WebSocket support, route-level middlewares, and enterprise-grade features**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lolyjs/core?style=flat-square)](https://www.npmjs.com/package/@lolyjs/core)
8
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg?style=flat-square)](https://opensource.org/licenses/ISC)
9
+ ![Downloads](https://img.shields.io/npm/dm/@lolyjs/core)
10
+ <br>
11
+ [![Alpha](https://img.shields.io/badge/status-alpha-red.svg)](https://github.com/MenvielleValen/loly-framework)
12
+ [![Expermiental](https://img.shields.io/badge/phase-experimental-black.svg)](https://github.com/MenvielleValen/loly-framework)
13
+
14
+ _Built with React 19, Express, Rspack, Socket.IO, and TypeScript_
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## Getting Started
21
+
22
+ Create a new Loly application in seconds:
23
+
24
+ ```bash
25
+ npx @lolyjs/cli@latest my-app
26
+ ```
27
+
28
+ This will create a new project with all the necessary files and dependencies. For more information about the CLI, visit the [@lolyjs/cli package](https://www.npmjs.com/package/@lolyjs/cli).
29
+
30
+ ---
31
+
32
+ ## Overview
33
+
34
+ Loly is a full-stack React framework that combines the simplicity of file-based routing with powerful server-side rendering, static site generation, and unique features like native WebSocket support and route-level middlewares.
35
+
36
+ ### What Makes Loly Different?
37
+
38
+ - 🔌 **Native WebSocket Support** - Built-in Socket.IO integration with automatic namespace routing
39
+ - 🎯 **Route-Level Middlewares** - Define middlewares directly in your routes for pages and APIs
40
+ - 📁 **Separation of Concerns** - Server logic in `page.server.hook.ts` and `layout.server.hook.ts` separate from React components
41
+ - 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
42
+ - 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
43
+ - **Performance** - Fast bundling with Rspack and optimized code splitting
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ### Installation
50
+
51
+ ```bash
52
+ npm install @lolyjs/core react react-dom
53
+ # or
54
+ pnpm add @lolyjs/core react react-dom
55
+ ```
56
+
57
+ ### Create Your First Page
58
+
59
+ ```tsx
60
+ // app/page.tsx
61
+ export default function Home() {
62
+ return <h1>Hello, Loly!</h1>;
63
+ }
64
+ ```
65
+
66
+ ### Add Server-Side Data
67
+
68
+ ```tsx
69
+ // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
70
+ import type { ServerLoader } from "@lolyjs/core";
71
+
72
+ export const getServerSideProps: ServerLoader = async (ctx) => {
73
+ const data = await fetchData();
74
+
75
+ return {
76
+ props: { data },
77
+ metadata: {
78
+ title: "Home Page",
79
+ description: "Welcome to Loly",
80
+ // See "SEO & Metadata" section below for full metadata options
81
+ },
82
+ };
83
+ };
84
+ ```
85
+
86
+ ```tsx
87
+ // app/page.tsx
88
+ export default function Home({ props }) {
89
+ return <h1>{props.data}</h1>;
90
+ }
91
+ ```
92
+
93
+ ### Start Development Server
94
+
95
+ ```bash
96
+ npx loly dev
97
+ # Server runs on http://localhost:3000
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Key Features
103
+
104
+ ### 🔌 Native WebSocket Support (Realtime v1)
105
+
106
+ Loly includes production-ready WebSocket support with automatic namespace routing, authentication, validation, rate limiting, and multi-instance scaling. Define WebSocket events using the new `defineWssRoute()` API:
107
+
108
+ ```tsx
109
+ // app/wss/chat/events.ts
110
+ import { defineWssRoute } from "@lolyjs/core";
111
+ import { z } from "zod";
112
+
113
+ export default defineWssRoute({
114
+ // Authentication hook
115
+ auth: async (ctx) => {
116
+ const token = ctx.req.headers.authorization;
117
+ return await verifyToken(token); // Returns user or null
118
+ },
119
+
120
+ // Connection hook
121
+ onConnect: (ctx) => {
122
+ console.log("User connected:", ctx.user?.id);
123
+ },
124
+
125
+ // Event handlers with validation, guards, and rate limiting
126
+ events: {
127
+ message: {
128
+ // Schema validation (Zod/Valibot)
129
+ schema: z.object({
130
+ text: z.string().min(1).max(500),
131
+ }),
132
+
133
+ // Guard (permissions check)
134
+ guard: ({ user }) => !!user, // Require authentication
135
+
136
+ // Per-event rate limiting
137
+ rateLimit: {
138
+ eventsPerSecond: 10,
139
+ burst: 20,
140
+ },
141
+
142
+ // Handler
143
+ handler: (ctx) => {
144
+ ctx.actions.broadcast("message", {
145
+ text: ctx.data.text,
146
+ from: ctx.user?.id,
147
+ });
148
+ },
149
+ },
150
+ },
151
+ });
152
+ ```
153
+
154
+ **Client-side:**
155
+
156
+ ```tsx
157
+ import { lolySocket } from "@lolyjs/core/sockets";
158
+
159
+ const socket = lolySocket("/chat");
160
+
161
+ socket.on("message", (data) => {
162
+ console.log("Received:", data);
163
+ });
164
+
165
+ socket.emit("message", { text: "Hello!" });
166
+ ```
167
+
168
+ **Key Features:**
169
+
170
+ - ✅ **Production-ready**: Auth, validation, rate limiting, logging
171
+ - ✅ **Multi-instance**: Redis adapter for horizontal scaling
172
+ - ✅ **State Store**: Shared state across instances (memory/Redis)
173
+ - ✅ **Presence**: User-to-socket mapping for targeted messaging
174
+ - **Type-safe**: Full TypeScript support
175
+ - **Automatic namespace creation** from file structure
176
+ - **Same routing pattern** as pages and APIs
177
+ - ✅ **Built-in helpers**: `emit`, `broadcast`, `toUser()`, `toRoom()`, `join()`, `leave()`
178
+ - ✅ **No manual configuration required** (works out of the box for localhost)
179
+
180
+ **📖 For complete documentation, see [REALTIME.md](./docs/REALTIME.md)**
181
+
182
+ ### 🎯 Route-Level Middlewares
183
+
184
+ Define middlewares directly in your routes for fine-grained control. Middlewares run before `getServerSideProps` (pages) or API handlers and can modify `ctx.locals`, set headers, redirect, etc.
185
+
186
+ **For Pages:**
187
+
188
+ ```tsx
189
+ // app/dashboard/page.server.hook.ts (preferred) or app/dashboard/server.hook.ts (legacy)
190
+ import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
191
+
192
+ export const beforeServerData: RouteMiddleware[] = [
193
+ async (ctx, next) => {
194
+ // Authentication
195
+ const token = ctx.req.headers.authorization;
196
+ if (!token) {
197
+ ctx.res.redirect("/login");
198
+ return; // Don't call next() if redirecting
199
+ }
200
+ ctx.locals.user = await verifyToken(token);
201
+ await next(); // Call next() to continue to next middleware or getServerSideProps
202
+ },
203
+ ];
204
+
205
+ export const getServerSideProps: ServerLoader = async (ctx) => {
206
+ const user = ctx.locals.user; // Available from middleware
207
+ return { props: { user } };
208
+ };
209
+ ```
210
+
211
+ **For API Routes:**
212
+
213
+ ```tsx
214
+ // app/api/protected/route.ts
215
+ import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
216
+
217
+ // Global middleware for all methods (GET, POST, PUT, etc.)
218
+ export const beforeApi: ApiMiddleware[] = [
219
+ async (ctx, next) => {
220
+ // Authentication
221
+ const user = await getUser(ctx.req);
222
+ if (!user) {
223
+ return ctx.Response({ error: "Unauthorized" }, 401);
224
+ }
225
+ ctx.locals.user = user;
226
+ await next();
227
+ },
228
+ ];
229
+
230
+ // Method-specific middleware (only runs before POST)
231
+ export const beforePOST: ApiMiddleware[] = [
232
+ async (ctx, next) => {
233
+ // Validation specific to POST
234
+ await next();
235
+ },
236
+ ];
237
+
238
+ // Method-specific middleware (only runs before GET)
239
+ export const beforeGET: ApiMiddleware[] = [
240
+ async (ctx, next) => {
241
+ // Cache logic specific to GET
242
+ await next();
243
+ },
244
+ ];
245
+
246
+ export async function GET(ctx: ApiContext) {
247
+ const user = ctx.locals.user;
248
+ return ctx.Response({ user });
249
+ }
250
+
251
+ export async function POST(ctx: ApiContext) {
252
+ const user = ctx.locals.user;
253
+ const data = ctx.req.body;
254
+ return ctx.Response({ created: true }, 201);
255
+ }
256
+ ```
257
+
258
+ **Key Benefits:**
259
+
260
+ - Middlewares execute before loaders/handlers
261
+ - Share data via `ctx.locals`
262
+ - Method-specific middlewares for APIs
263
+ - Clean separation of concerns
264
+
265
+ ### 📁 File-Based Routing
266
+
267
+ Routes are automatically created from your file structure:
268
+
269
+ | File Path | Route |
270
+ | ----------------------------- | --------------------- |
271
+ | `app/page.tsx` | `/` |
272
+ | `app/about/page.tsx` | `/about` |
273
+ | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
274
+ | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
275
+
276
+ **Nested Layouts:**
277
+
278
+ **⚠️ Important**: Layouts should NOT include `<html>` or `<body>` tags. The framework automatically handles the base HTML structure. Layouts should only contain content that goes inside the body.
279
+
280
+ ```tsx
281
+ // app/layout.tsx (Root layout)
282
+ export default function RootLayout({ children, appName, navigation }) {
283
+ return (
284
+ <div>
285
+ <nav>{navigation}</nav>
286
+ {children}
287
+ <footer>{appName}</footer>
288
+ </div>
289
+ );
290
+ }
291
+ ```
292
+
293
+ ```tsx
294
+ // app/layout.server.hook.ts (Root layout server hook - same directory as layout.tsx)
295
+ import type { ServerLoader } from "@lolyjs/core";
296
+
297
+ export const getServerSideProps: ServerLoader = async (ctx) => {
298
+ return {
299
+ props: {
300
+ appName: "My App",
301
+ navigation: ["Home", "About", "Blog"],
302
+ },
303
+ };
304
+ };
305
+ ```
306
+
307
+ ```tsx
308
+ // app/blog/layout.tsx (Nested layout)
309
+ export default function BlogLayout({ children, sectionTitle }) {
310
+ return (
311
+ <div>
312
+ <h1>{sectionTitle}</h1>
313
+ <aside>Sidebar</aside>
314
+ <main>{children}</main>
315
+ </div>
316
+ );
317
+ }
318
+ ```
319
+
320
+ ```tsx
321
+ // app/blog/layout.server.hook.ts (Nested layout server hook - same directory as layout.tsx)
322
+ import type { ServerLoader } from "@lolyjs/core";
323
+
324
+ export const getServerSideProps: ServerLoader = async (ctx) => {
325
+ return {
326
+ props: {
327
+ sectionTitle: "Blog Section",
328
+ },
329
+ };
330
+ };
331
+ ```
332
+
333
+ **Layout Server Hooks:**
334
+
335
+ Layouts can have their own server hooks that provide stable data across all pages. Props from layout server hooks are automatically merged with page props:
336
+
337
+ - **Layout props** (from `layout.server.hook.ts`) are stable and available to both the layout and all pages
338
+ - **Page props** (from `page.server.hook.ts`) are specific to each page and override layout props if there's a conflict
339
+ - **Combined props** are available to both layouts and pages
340
+
341
+ **File Convention:**
342
+ - Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
343
+ - Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
344
+
345
+ ### 📦 Route Groups
346
+
347
+ Route groups allow you to organize routes without affecting the URL structure. Directories wrapped in parentheses like `(dashboard)` or `(landing)` are treated as route groups and don't appear in the URL.
348
+
349
+ **Key Features:**
350
+ - Route groups don't appear in URLs
351
+ - Each route group can have its own layout
352
+ - Route groups help organize large applications
353
+ - Layouts are applied in order: root → route group → nested → page
354
+
355
+ **Example Structure:**
356
+
357
+ ```
358
+ app/
359
+ ├── layout.tsx # Root layout (applies to all routes)
360
+ ├── layout.server.hook.ts # Root layout server hook
361
+ ├── (dashboard)/
362
+ │ ├── layout.tsx # Dashboard layout (applies to /settings, /profile)
363
+ │ ├── layout.server.hook.ts # Dashboard layout server hook
364
+ │ ├── settings/
365
+ │ │ └── page.tsx # /settings (NOT /dashboard/settings)
366
+ │ └── profile/
367
+ │ └── page.tsx # → /profile (NOT /dashboard/profile)
368
+ ├── (landing)/
369
+ │ ├── layout.tsx # Landing layout (applies to /about, /contact)
370
+ │ ├── layout.server.hook.ts # Landing layout server hook
371
+ │ ├── about/
372
+ │ │ └── page.tsx # → /about (NOT /landing/about)
373
+ │ └── contact/
374
+ │ └── page.tsx # → /contact (NOT /landing/contact)
375
+ └── page.tsx # / (root page)
376
+ ```
377
+
378
+ **Layout Order:**
379
+
380
+ For a page at `app/(dashboard)/settings/page.tsx`, layouts are applied in this order:
381
+
382
+ 1. `app/layout.tsx` (root layout)
383
+ 2. `app/(dashboard)/layout.tsx` (route group layout)
384
+ 3. `app/(dashboard)/settings/layout.tsx` (if exists, nested layout)
385
+
386
+ **Server Hooks:**
387
+
388
+ Server hooks work the same way with route groups. The execution order is:
389
+
390
+ 1. Root layout hook (`app/layout.server.hook.ts`)
391
+ 2. Route group layout hook (`app/(dashboard)/layout.server.hook.ts`)
392
+ 3. Nested layout hooks (if any)
393
+ 4. Page hook (`app/(dashboard)/settings/page.server.hook.ts`)
394
+
395
+ **Example:**
396
+
397
+ ```tsx
398
+ // app/(dashboard)/layout.tsx
399
+ export default function DashboardLayout({ children, user }) {
400
+ return (
401
+ <div className="dashboard">
402
+ <nav>Dashboard Navigation</nav>
403
+ {children}
404
+ </div>
405
+ );
406
+ }
407
+ ```
408
+
409
+ ```tsx
410
+ // app/(dashboard)/layout.server.hook.ts
411
+ import type { ServerLoader } from "@lolyjs/core";
412
+
413
+ export const getServerSideProps: ServerLoader = async (ctx) => {
414
+ const user = await getCurrentUser(ctx.req);
415
+ return {
416
+ props: {
417
+ user, // Available to all pages in (dashboard) group
418
+ },
419
+ };
420
+ };
421
+ ```
422
+
423
+ ```tsx
424
+ // app/(dashboard)/settings/page.tsx
425
+ export default function SettingsPage({ user, settings }) {
426
+ // user comes from (dashboard)/layout.server.hook.ts
427
+ // settings comes from page.server.hook.ts
428
+ return <div>Settings for {user.name}</div>;
429
+ }
430
+ ```
431
+
432
+ **Important Notes:**
433
+ - Route groups are purely organizational - they don't affect URLs
434
+ - You cannot have duplicate routes that would result from ignoring route groups
435
+ - Invalid: `app/(dashboard)/settings/page.tsx` and `app/settings/page.tsx` (both map to `/settings`)
436
+ - ✅ Valid: `app/(dashboard)/settings/page.tsx` and `app/(landing)/settings/page.tsx` (both map to `/settings` - conflict!)
437
+ - Route groups work seamlessly with SPA navigation and preloading
438
+
439
+ **Future: Parallel Routes**
440
+
441
+ The architecture is prepared for future parallel routes support (e.g., `(modal)`). Route groups can be extended to support special types that render in parallel slots.
442
+
443
+ ### 🔄 URL Rewrites
444
+
445
+ URL rewrites allow you to rewrite incoming request paths to different destination paths internally, without changing the URL visible in the browser. This is especially useful for multitenancy, API proxying, and other advanced routing scenarios.
446
+
447
+ **Key Features:**
448
+ - Rewrites happen internally - the URL in the browser doesn't change
449
+ - Support for dynamic parameters (`:param`, `*` catch-all)
450
+ - Conditional rewrites based on host, headers, cookies, or query parameters
451
+ - Async destination functions for dynamic rewrites
452
+ - High performance with pre-compiled regex patterns and caching
453
+
454
+ **Configuration:**
455
+
456
+ Create `rewrites.config.ts` in your project root:
457
+
458
+ ```typescript
459
+ import type { RewriteConfig } from "@lolyjs/core";
460
+
461
+ export default async function rewrites(): Promise<RewriteConfig> {
462
+ return [
463
+ // Static rewrite
464
+ {
465
+ source: "/old-path",
466
+ destination: "/new-path",
467
+ },
468
+
469
+ // Rewrite with parameters
470
+ {
471
+ source: "/tenant/:tenant*",
472
+ destination: "/app/:tenant*",
473
+ },
474
+
475
+ // Rewrite with async function (for dynamic logic)
476
+ {
477
+ source: "/api/proxy/:path*",
478
+ destination: async (params, req) => {
479
+ const tenant = extractTenantFromRequest(req);
480
+ return `/api/${tenant}/:path*`;
481
+ },
482
+ },
483
+
484
+ // Conditional rewrite based on host (multitenant by subdomain)
485
+ {
486
+ source: "/:path*",
487
+ has: [
488
+ { type: "host", value: ":tenant.example.com" },
489
+ ],
490
+ destination: "/project/:tenant/:path*",
491
+ },
492
+ ];
493
+ }
494
+ ```
495
+
496
+ **Multitenant by Subdomain (Main Use Case):**
497
+
498
+ The most common use case is multitenancy where each tenant has its own subdomain:
499
+
500
+ ```typescript
501
+ // rewrites.config.ts
502
+ import type { RewriteConfig } from "@lolyjs/core";
503
+
504
+ export default async function rewrites(): Promise<RewriteConfig> {
505
+ return [
506
+ // Multitenant by subdomain - catch-all pattern
507
+ // tenant1.example.com/* → /project/tenant1/*
508
+ // tenant2.example.com/* → /project/tenant2/*
509
+ // All routes under the tenant subdomain will be rewritten
510
+ // If a route doesn't exist in /project/[tenantId]/*, it will return 404
511
+ {
512
+ source: "/:path*",
513
+ has: [
514
+ {
515
+ type: "host",
516
+ value: ":tenant.example.com" // Captures tenant from subdomain
517
+ }
518
+ ],
519
+ destination: "/project/:tenant/:path*",
520
+ },
521
+ ];
522
+ }
523
+ ```
524
+
525
+ **How It Works:**
526
+ - User visits: `tenant1.example.com/dashboard`
527
+ - Internally rewrites to: `/project/tenant1/dashboard`
528
+ - URL visible in browser: `tenant1.example.com/dashboard` (unchanged)
529
+ - Route `/project/[tenantId]/dashboard` receives `params.tenantId = "tenant1"`
530
+
531
+ **Multitenant by Path:**
532
+
533
+ Alternatively, you can use path-based multitenancy:
534
+
535
+ ```typescript
536
+ // rewrites.config.ts
537
+ export default async function rewrites(): Promise<RewriteConfig> {
538
+ return [
539
+ // /tenant1/dashboard → /project/tenant1/dashboard
540
+ {
541
+ source: "/:tenant/:path*",
542
+ destination: "/project/:tenant/:path*",
543
+ },
544
+ ];
545
+ }
546
+ ```
547
+
548
+ **API Proxy Example:**
549
+
550
+ ```typescript
551
+ export default async function rewrites(): Promise<RewriteConfig> {
552
+ return [
553
+ // Proxy all /api/proxy/* requests to external API
554
+ {
555
+ source: "/api/proxy/:path*",
556
+ destination: async (params, req) => {
557
+ const externalApi = process.env.EXTERNAL_API_URL;
558
+ return `${externalApi}/${params.path}`;
559
+ },
560
+ },
561
+ ];
562
+ }
563
+ ```
564
+
565
+ **Conditional Rewrites:**
566
+
567
+ Rewrites can be conditional based on request properties:
568
+
569
+ ```typescript
570
+ export default async function rewrites(): Promise<RewriteConfig> {
571
+ return [
572
+ // Rewrite based on host
573
+ {
574
+ source: "/:path*",
575
+ has: [
576
+ { type: "host", value: "api.example.com" },
577
+ ],
578
+ destination: "/api/:path*",
579
+ },
580
+
581
+ // Rewrite based on header
582
+ {
583
+ source: "/admin/:path*",
584
+ has: [
585
+ { type: "header", key: "X-Admin-Key", value: "secret" },
586
+ ],
587
+ destination: "/admin-panel/:path*",
588
+ },
589
+
590
+ // Rewrite based on cookie
591
+ {
592
+ source: "/premium/:path*",
593
+ has: [
594
+ { type: "cookie", key: "premium", value: "true" },
595
+ ],
596
+ destination: "/premium-content/:path*",
597
+ },
598
+
599
+ // Rewrite based on query parameter
600
+ {
601
+ source: "/:path*",
602
+ has: [
603
+ { type: "query", key: "version", value: "v2" },
604
+ ],
605
+ destination: "/v2/:path*",
606
+ },
607
+ ];
608
+ }
609
+ ```
610
+
611
+ **Pattern Syntax:**
612
+
613
+ - `:param` - Named parameter (matches one segment)
614
+ - `:param*` - Named catch-all (matches remaining path)
615
+ - `*` - Anonymous catch-all (matches remaining path)
616
+
617
+ **Accessing Extracted Parameters:**
618
+
619
+ Parameters extracted from rewrites (including from host conditions) are automatically available in:
620
+ - `req.query` - Query parameters
621
+ - `req.locals` - Request locals (for server hooks)
622
+ - `ctx.params` - Route parameters (if the rewritten path matches a dynamic route)
623
+
624
+ ```typescript
625
+ // app/project/[tenantId]/dashboard/page.server.hook.ts
626
+ export const getServerSideProps: ServerLoader = async (ctx) => {
627
+ // tenantId comes from the rewrite: /project/:tenant/:path*
628
+ const tenantId = ctx.params.tenantId;
629
+
630
+ // Also available in req.query and req.locals
631
+ const tenantFromQuery = ctx.req.query.tenant;
632
+ const tenantFromLocals = ctx.req.locals?.tenant;
633
+
634
+ return { props: { tenantId } };
635
+ };
636
+ ```
637
+
638
+ **Performance & Caching:**
639
+
640
+ - Rewrites config is loaded once and cached
641
+ - Regex patterns are pre-compiled for performance
642
+ - In development: File tracking invalidates cache only when `rewrites.config.ts` changes
643
+ - In production: Rewrites are loaded from manifest (faster, no async functions)
644
+
645
+ **Important Notes:**
646
+
647
+ - Rewrites are applied **before** route matching
648
+ - The original URL is preserved in the browser (not a redirect)
649
+ - Query parameters are preserved and can be extended
650
+ - Rewrites work for both pages and API routes
651
+ - Functions in rewrite destinations cannot be serialized in production builds (only static rewrites are included in manifest)
652
+ - Rewrites are evaluated in order - the first match wins
653
+ - **Behavior**: Rewrites are applied ALWAYS if the source pattern matches, regardless of whether the destination route exists
654
+ - If a rewritten route doesn't exist, a 404 will be returned (strict behavior, no fallback to original route)
655
+ - Catch-all patterns (`/:path*`) are fully supported and recommended for multitenancy scenarios
656
+ - **API Routes**: Can be rewritten. If rewritten route starts with `/api/`, it's handled as API route. Otherwise, it's handled as a page route
657
+ - **WSS Routes**: Automatically excluded from rewrites (WebSocket handled separately by Socket.IO)
658
+ - System routes (`/static/*`, `/__fw/*`, `/favicon.ico`) are automatically excluded from rewrites
659
+
660
+ **Validation:**
661
+
662
+ The framework automatically validates rewrites to prevent:
663
+ - Infinite loops (warns if source and destination are identical)
664
+ - Duplicate source patterns (warns if multiple rewrites have the same source)
665
+
666
+ ### 🚀 Hybrid Rendering
667
+
668
+ Choose the best rendering strategy for each page:
669
+
670
+ **SSR (Server-Side Rendering):**
671
+
672
+ ```tsx
673
+ // app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
674
+ export const dynamic = "force-dynamic" as const;
675
+
676
+ export const getServerSideProps: ServerLoader = async (ctx) => {
677
+ const posts = await fetchFreshPosts();
678
+ return { props: { posts } };
679
+ };
680
+ ```
681
+
682
+ **SSG (Static Site Generation):**
683
+
684
+ ```tsx
685
+ // app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
686
+ export const dynamic = "force-static" as const;
687
+
688
+ export const generateStaticParams: GenerateStaticParams = async () => {
689
+ const posts = await getAllPosts();
690
+ return posts.map((post) => ({ slug: post.slug }));
691
+ };
692
+
693
+ export const getServerSideProps: ServerLoader = async (ctx) => {
694
+ const post = await getPost(ctx.params.slug);
695
+ return { props: { post } };
696
+ };
697
+ ```
698
+
699
+ **CSR (Client-Side Rendering):**
700
+
701
+ ```tsx
702
+ // app/dashboard/page.tsx (No page.server.hook.ts)
703
+ import { useState, useEffect } from "react";
704
+
705
+ export default function Dashboard() {
706
+ const [data, setData] = useState(null);
707
+
708
+ useEffect(() => {
709
+ fetchData().then(setData);
710
+ }, []);
711
+
712
+ return <div>{data}</div>;
713
+ }
714
+ ```
715
+
716
+ ### 📄 Static Files & Assets
717
+
718
+ Loly serves static files from the `public/` directory at the root of your application. This is perfect for SEO files like `sitemap.xml`, `robots.txt`, favicons, and other static assets.
719
+
720
+ **How it works:**
721
+ - Files in `public/` are served at the root URL (e.g., `public/sitemap.xml` → `/sitemap.xml`)
722
+ - Static files have **priority over dynamic routes** - if a file exists in `public/`, it will be served instead of matching a route
723
+ - Perfect for SEO: Google automatically finds `sitemap.xml` and `robots.txt` at the root
724
+ - Works in both development and production environments
725
+ - Subdirectories are supported: `public/assets/logo.png` → `/assets/logo.png`
726
+
727
+ **Directory Structure:**
728
+ ```
729
+ public/
730
+ ├── sitemap.xml # Available at /sitemap.xml
731
+ ├── robots.txt # Available at /robots.txt
732
+ ├── favicon.ico # Available at /favicon.ico (or favicon.png)
733
+ ├── favicon.png # Available at /favicon.png (alternative to .ico)
734
+ └── assets/
735
+ ├── logo.png # Available at /assets/logo.png
736
+ └── images/ # Available at /assets/images/*
737
+ └── hero.jpg
738
+ ```
739
+
740
+ **Favicon:**
741
+ Place your favicon in the `public/` directory as either `favicon.ico` or `favicon.png`. The framework automatically detects and includes it in the HTML head with the correct MIME type:
742
+ - `public/favicon.ico` → `/favicon.ico` (type: `image/x-icon`)
743
+ - `public/favicon.png` → `/favicon.png` (type: `image/png`)
744
+
745
+ If both exist, `favicon.ico` takes priority (checked first).
746
+
747
+ **SEO Example:**
748
+
749
+ Create `public/sitemap.xml`:
750
+ ```xml
751
+ <?xml version="1.0" encoding="UTF-8"?>
752
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
753
+ <url>
754
+ <loc>https://example.com/</loc>
755
+ <lastmod>2024-01-01</lastmod>
756
+ <changefreq>daily</changefreq>
757
+ <priority>1.0</priority>
758
+ </url>
759
+ </urlset>
760
+ ```
761
+
762
+ Create `public/robots.txt`:
763
+ ```
764
+ User-agent: *
765
+ Allow: /
766
+
767
+ Sitemap: https://example.com/sitemap.xml
768
+ ```
769
+
770
+ Both files will be automatically available at `/sitemap.xml` and `/robots.txt` respectively, and search engines will find them at the standard locations.
771
+
772
+ **Important Notes:**
773
+ - **All static files** (including favicons) must be placed in the `public/` directory
774
+ - The framework **only** looks for favicons in `public/` (not in the root or `app/` directory)
775
+ - Favicons are automatically detected and included in the HTML `<head>` with the correct MIME type
776
+ - Static files have **priority over dynamic routes** - perfect for SEO files
777
+
778
+ **Configuration:**
779
+ The static directory can be customized in `loly.config.ts`:
780
+ ```tsx
781
+ import type { FrameworkConfig } from "@lolyjs/core";
782
+
783
+ export default {
784
+ directories: {
785
+ static: "public", // Default: "public"
786
+ },
787
+ } satisfies Partial<FrameworkConfig>;
788
+ ```
789
+
790
+ ### 🔌 API Routes
791
+
792
+ Create RESTful APIs with flexible middleware support:
793
+
794
+ ```tsx
795
+ // app/api/posts/route.ts
796
+ import type { ApiContext } from "@lolyjs/core";
797
+ import { validate } from "@lolyjs/core";
798
+ import { z } from "zod";
799
+
800
+ const postSchema = z.object({
801
+ title: z.string().min(1),
802
+ content: z.string().min(1),
803
+ });
804
+
805
+ export async function GET(ctx: ApiContext) {
806
+ const posts = await getPosts();
807
+ return ctx.Response({ posts });
808
+ }
809
+
810
+ export async function POST(ctx: ApiContext) {
811
+ const data = validate(postSchema, ctx.req.body);
812
+ const post = await createPost(data);
813
+ return ctx.Response({ post }, 201);
814
+ }
815
+ ```
816
+
817
+ ### 📊 SEO & Metadata
818
+
819
+ Loly provides comprehensive metadata support for SEO and social sharing. Metadata can be defined at both layout and page levels, with intelligent merging:
820
+
821
+ **Layout Metadata (Base/Defaults):**
822
+
823
+ ```tsx
824
+ // app/layout.server.hook.ts
825
+ import type { ServerLoader } from "@lolyjs/core";
826
+
827
+ export const getServerSideProps: ServerLoader = async () => {
828
+ return {
829
+ props: { /* ... */ },
830
+ metadata: {
831
+ // Site-wide defaults
832
+ description: "My awesome site",
833
+ lang: "en",
834
+ robots: "index, follow",
835
+ themeColor: "#000000",
836
+
837
+ // Open Graph defaults
838
+ openGraph: {
839
+ type: "website",
840
+ siteName: "My Site",
841
+ locale: "en_US",
842
+ },
843
+
844
+ // Twitter Card defaults
845
+ twitter: {
846
+ card: "summary_large_image",
847
+ },
848
+
849
+ // Custom meta tags
850
+ metaTags: [
851
+ { name: "author", content: "My Name" },
852
+ ],
853
+
854
+ // Custom link tags (preconnect, etc.)
855
+ links: [
856
+ { rel: "preconnect", href: "https://api.example.com" },
857
+ ],
858
+ },
859
+ };
860
+ };
861
+ ```
862
+
863
+ **Page Metadata (Overrides Layout):**
864
+
865
+ ```tsx
866
+ // app/page.server.hook.ts
867
+ import type { ServerLoader } from "@lolyjs/core";
868
+
869
+ export const getServerSideProps: ServerLoader = async (ctx) => {
870
+ const post = await getPost(ctx.params.slug);
871
+
872
+ return {
873
+ props: { post },
874
+ metadata: {
875
+ // Page-specific (overrides layout)
876
+ title: `${post.title} | My Site`,
877
+ description: post.excerpt,
878
+ canonical: `https://mysite.com/blog/${post.slug}`,
879
+
880
+ // Open Graph (inherits type, siteName from layout)
881
+ openGraph: {
882
+ title: post.title,
883
+ description: post.excerpt,
884
+ url: `https://mysite.com/blog/${post.slug}`,
885
+ image: {
886
+ url: post.imageUrl,
887
+ width: 1200,
888
+ height: 630,
889
+ alt: post.title,
890
+ },
891
+ },
892
+
893
+ // Twitter Card (inherits card type from layout)
894
+ twitter: {
895
+ title: post.title,
896
+ description: post.excerpt,
897
+ image: post.imageUrl,
898
+ imageAlt: post.title,
899
+ },
900
+ },
901
+ };
902
+ };
903
+ ```
904
+
905
+ **Full Metadata API:**
906
+
907
+ ```tsx
908
+ interface PageMetadata {
909
+ // Basic fields
910
+ title?: string;
911
+ description?: string;
912
+ lang?: string;
913
+ canonical?: string;
914
+ robots?: string;
915
+ themeColor?: string;
916
+ viewport?: string;
917
+
918
+ // Open Graph
919
+ openGraph?: {
920
+ title?: string;
921
+ description?: string;
922
+ type?: string;
923
+ url?: string;
924
+ image?: string | {
925
+ url: string;
926
+ width?: number;
927
+ height?: number;
928
+ alt?: string;
929
+ };
930
+ siteName?: string;
931
+ locale?: string;
932
+ };
933
+
934
+ // Twitter Cards
935
+ twitter?: {
936
+ card?: "summary" | "summary_large_image" | "app" | "player";
937
+ title?: string;
938
+ description?: string;
939
+ image?: string;
940
+ imageAlt?: string;
941
+ site?: string;
942
+ creator?: string;
943
+ };
944
+
945
+ // Custom meta tags
946
+ metaTags?: Array<{
947
+ name?: string;
948
+ property?: string;
949
+ httpEquiv?: string;
950
+ content: string;
951
+ }>;
952
+
953
+ // Custom link tags
954
+ links?: Array<{
955
+ rel: string;
956
+ href: string;
957
+ as?: string;
958
+ crossorigin?: string;
959
+ type?: string;
960
+ }>;
961
+ }
962
+ ```
963
+
964
+ **Key Features:**
965
+
966
+ - **Layout + Page Merging**: Layout metadata provides defaults, page metadata overrides specific fields
967
+ - **Automatic Updates**: Metadata updates automatically during SPA navigation
968
+ - **SSR & SSG Support**: Works in both server-side rendering and static generation
969
+ - **Type-Safe**: Full TypeScript support with `PageMetadata` type
970
+
971
+ ### 🛡️ Built-in Security
972
+
973
+ **Rate Limiting:**
974
+
975
+ ```tsx
976
+ // loly.config.ts
977
+ import { ServerConfig } from "@lolyjs/core";
978
+
979
+ export const config = (env: string): ServerConfig => {
980
+ return {
981
+ rateLimit: {
982
+ windowMs: 15 * 60 * 1000, // 15 minutes
983
+ max: 1000,
984
+ strictMax: 5,
985
+ strictPatterns: ["/api/auth/**"],
986
+ },
987
+ };
988
+ };
989
+ ```
990
+
991
+ **Validation with Zod:**
992
+
993
+ ```tsx
994
+ import { validate, ValidationError } from "@lolyjs/core";
995
+ import { z } from "zod";
996
+
997
+ const schema = z.object({
998
+ email: z.string().email(),
999
+ age: z.number().int().min(0).max(150),
1000
+ });
1001
+
1002
+ try {
1003
+ const data = validate(schema, req.body);
1004
+ } catch (error) {
1005
+ if (error instanceof ValidationError) {
1006
+ return Response({ errors: error.format() }, 400);
1007
+ }
1008
+ }
1009
+ ```
1010
+
1011
+ **Automatic Sanitization:**
1012
+
1013
+ Route parameters and query strings are automatically sanitized to prevent XSS attacks.
1014
+
1015
+ **Security Headers:**
1016
+
1017
+ Helmet is configured by default with CSP (Content Security Policy) and nonce support.
1018
+
1019
+ ### 📝 Structured Logging
1020
+
1021
+ ```tsx
1022
+ import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
1023
+
1024
+ // Request logger (automatic request ID)
1025
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1026
+ const logger = getRequestLogger(ctx.req);
1027
+ logger.info("Processing request", { userId: ctx.locals.user?.id });
1028
+ return { props: {} };
1029
+ };
1030
+
1031
+ // Module logger
1032
+ const logger = createModuleLogger("my-module");
1033
+ logger.info("Module initialized");
1034
+ logger.error("Error occurred", error);
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ ## Project Structure
1040
+
1041
+ ```
1042
+ your-app/
1043
+ ├── app/
1044
+ │ ├── layout.tsx # Root layout
1045
+ │ ├── layout.server.hook.ts # Root layout server hook (stable props)
1046
+ │ ├── page.tsx # Home page (/)
1047
+ │ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
1048
+ │ ├── _not-found.tsx # Custom 404
1049
+ │ ├── _error.tsx # Custom error page
1050
+ │ ├── blog/
1051
+ │ │ ├── layout.tsx # Blog layout
1052
+ │ │ ├── layout.server.hook.ts # Blog layout server hook
1053
+ │ │ ├── page.tsx # /blog
1054
+ │ │ └── [slug]/
1055
+ │ │ ├── page.tsx # /blog/:slug
1056
+ │ │ └── page.server.hook.ts # Page server hook
1057
+ │ ├── api/
1058
+ │ │ └── posts/
1059
+ │ │ └── route.ts # /api/posts
1060
+ │ └── wss/
1061
+ │ └── chat/
1062
+ │ └── events.ts # WebSocket namespace /chat
1063
+ ├── components/ # React components
1064
+ ├── lib/ # Utilities
1065
+ ├── public/ # Static files (served at root: /sitemap.xml, /robots.txt, etc.)
1066
+ ├── loly.config.ts # Framework configuration
1067
+ ├── init.server.ts # Server initialization (DB, services, etc.)
1068
+ └── package.json
1069
+ ```
1070
+
1071
+ ---
1072
+
1073
+ ## API Reference
1074
+
1075
+ ### Server Loader
1076
+
1077
+ **Page Server Hook:**
1078
+
1079
+ ```tsx
1080
+ // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
1081
+ import type { ServerLoader } from "@lolyjs/core";
1082
+
1083
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1084
+ const { req, res, params, pathname, locals } = ctx;
1085
+
1086
+ // Fetch data
1087
+ const data = await fetchData();
1088
+
1089
+ // Redirect
1090
+ return {
1091
+ redirect: {
1092
+ destination: "/new-path",
1093
+ permanent: true,
1094
+ },
1095
+ };
1096
+
1097
+ // Not found
1098
+ return { notFound: true };
1099
+
1100
+ // Return props
1101
+ return {
1102
+ props: { data },
1103
+ metadata: {
1104
+ title: "Page Title",
1105
+ description: "Page description",
1106
+ // See "SEO & Metadata" section above for full metadata options
1107
+ // including Open Graph, Twitter Cards, canonical URLs, etc.
1108
+ },
1109
+ };
1110
+ };
1111
+ ```
1112
+
1113
+ **Layout Server Hook:**
1114
+
1115
+ ```tsx
1116
+ // app/layout.server.hook.ts (same directory as layout.tsx)
1117
+ import type { ServerLoader } from "@lolyjs/core";
1118
+
1119
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1120
+ // Fetch stable data that persists across all pages
1121
+ const user = await getCurrentUser();
1122
+ const navigation = await getNavigation();
1123
+
1124
+ return {
1125
+ props: {
1126
+ user, // Available to layout and all pages
1127
+ navigation, // Available to layout and all pages
1128
+ },
1129
+ };
1130
+ };
1131
+ ```
1132
+
1133
+ **Props Merging:**
1134
+
1135
+ - Layout props (from `layout.server.hook.ts`) are merged first
1136
+ - Page props (from `page.server.hook.ts`) are merged second and override layout props
1137
+ - Both layouts and pages receive the combined props
1138
+
1139
+ ```tsx
1140
+ // app/layout.tsx
1141
+ export default function Layout({ user, navigation, children }) {
1142
+ // Receives: user, navigation (from layout.server.hook.ts)
1143
+ // Also receives: any props from page.server.hook.ts
1144
+ return <div>{/* ... */}</div>;
1145
+ }
1146
+
1147
+ // app/page.tsx
1148
+ export default function Page({ user, navigation, posts }) {
1149
+ // Receives: user, navigation (from layout.server.hook.ts)
1150
+ // Receives: posts (from page.server.hook.ts)
1151
+ return <div>{/* ... */}</div>;
1152
+ }
1153
+ ```
1154
+
1155
+ ### API Route Handler
1156
+
1157
+ ```tsx
1158
+ import type { ApiContext } from "@lolyjs/core";
1159
+
1160
+ export async function GET(ctx: ApiContext) {
1161
+ return ctx.Response({ data: "value" });
1162
+ }
1163
+
1164
+ export async function POST(ctx: ApiContext) {
1165
+ return ctx.Response({ created: true }, 201);
1166
+ }
1167
+
1168
+ export async function DELETE(ctx: ApiContext) {
1169
+ return ctx.Response({ deleted: true }, 204);
1170
+ }
1171
+ ```
1172
+
1173
+ ### WebSocket Event Handler (New API - Realtime v1)
1174
+
1175
+ ```tsx
1176
+ import { defineWssRoute } from "@lolyjs/core";
1177
+ import { z } from "zod";
1178
+
1179
+ export default defineWssRoute({
1180
+ auth: async (ctx) => {
1181
+ // Authenticate user
1182
+ return await getUserFromToken(ctx.req.headers.authorization);
1183
+ },
1184
+
1185
+ onConnect: (ctx) => {
1186
+ console.log("User connected:", ctx.user?.id);
1187
+ },
1188
+
1189
+ events: {
1190
+ "custom-event": {
1191
+ schema: z.object({ message: z.string() }),
1192
+ guard: ({ user }) => !!user,
1193
+ handler: (ctx) => {
1194
+ // Emit to all clients
1195
+ ctx.actions.emit("response", { message: "Hello" });
1196
+
1197
+ // Broadcast to all except sender
1198
+ ctx.actions.broadcast("notification", ctx.data);
1199
+
1200
+ // Send to specific user
1201
+ ctx.actions.toUser(userId).emit("private", ctx.data);
1202
+
1203
+ // Send to room
1204
+ ctx.actions.toRoom("room-name").emit("room-message", ctx.data);
1205
+ },
1206
+ },
1207
+ },
1208
+ });
1209
+ ```
1210
+ ```
1211
+
1212
+ ### Client Cache
1213
+
1214
+ ```tsx
1215
+ import { revalidate } from "@lolyjs/core/client-cache";
1216
+
1217
+ export default function Page({ props }) {
1218
+ const handleRefresh = async () => {
1219
+ await revalidate(); // Refresh current page data
1220
+ };
1221
+
1222
+ return <div>{/* Your UI */}</div>;
1223
+ }
1224
+ ```
1225
+
1226
+ ### Components
1227
+
1228
+ ```tsx
1229
+ import { Link } from "@lolyjs/core/components";
1230
+
1231
+ export default function Navigation() {
1232
+ return (
1233
+ <nav>
1234
+ <Link href="/">Home</Link>
1235
+ <Link href="/about">About</Link>
1236
+ <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
1237
+ My Post
1238
+ </Link>
1239
+ </nav>
1240
+ );
1241
+ }
1242
+ ```
1243
+
1244
+ ---
1245
+
1246
+ ## Configuration
1247
+
1248
+ ### Framework Configuration
1249
+
1250
+ Create `loly.config.ts` in your project root to configure the framework:
1251
+
1252
+ ```tsx
1253
+ import type { FrameworkConfig } from "@lolyjs/core";
1254
+
1255
+ // Option 1: Partial config (only specify what you want to change)
1256
+ export default {
1257
+ directories: {
1258
+ static: "public",
1259
+ },
1260
+ } satisfies Partial<FrameworkConfig>;
1261
+
1262
+ // Option 2: Full config (for strict validation)
1263
+ // export default {
1264
+ // directories: { app: "app", build: ".loly", static: "public" },
1265
+ // conventions: { /* ... */ },
1266
+ // routing: { /* ... */ },
1267
+ // build: { /* ... */ },
1268
+ // server: { adapter: "express", port: 3000, host: "localhost" },
1269
+ // rendering: { framework: "react", streaming: true, ssr: true, ssg: true },
1270
+ // } satisfies FrameworkConfig;
1271
+ ```
1272
+
1273
+ ### Server Configuration
1274
+
1275
+ Configure server settings (CORS, rate limiting, WebSocket, etc.) in `loly.config.ts` by exporting a `config` function:
1276
+
1277
+ ```tsx
1278
+ // loly.config.ts
1279
+ import { ServerConfig } from "@lolyjs/core";
1280
+
1281
+ export const config = (env: string): ServerConfig => {
1282
+ const isDev = env === "development";
1283
+
1284
+ return {
1285
+ bodyLimit: "1mb",
1286
+ corsOrigin: isDev ? "*" : ["https://yourdomain.com"],
1287
+ rateLimit: {
1288
+ windowMs: 15 * 60 * 1000,
1289
+ max: 1000,
1290
+ strictMax: 5,
1291
+ strictPatterns: ["/api/auth/**"],
1292
+ },
1293
+ // Realtime (WebSocket) configuration
1294
+ realtime: {
1295
+ enabled: true,
1296
+ // For production, configure allowed origins
1297
+ // For development, localhost is auto-allowed
1298
+ allowedOrigins: isDev ? undefined : ["https://yourdomain.com"],
1299
+ // Optional: Configure Redis for multi-instance scaling
1300
+ // scale: {
1301
+ // mode: "cluster",
1302
+ // adapter: { url: "redis://localhost:6379" },
1303
+ // stateStore: { name: "redis", url: "redis://localhost:6379" },
1304
+ // },
1305
+ },
1306
+ };
1307
+ };
1308
+ ```
1309
+
1310
+ **Note:** For local development, Realtime works out of the box without any configuration. The framework automatically allows `localhost` connections. Only configure `allowedOrigins` when deploying to production.
1311
+
1312
+ ### Server Initialization
1313
+
1314
+ Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
1315
+
1316
+ ```tsx
1317
+ // init.server.ts
1318
+ import { InitServerData } from "@lolyjs/core";
1319
+
1320
+ export async function init({
1321
+ serverContext,
1322
+ }: {
1323
+ serverContext: InitServerData;
1324
+ }) {
1325
+ // Initialize database connection
1326
+ await connectToDatabase();
1327
+
1328
+ // Setup external services
1329
+ await setupExternalServices();
1330
+
1331
+ // Any other initialization logic
1332
+ console.log("Server initialized successfully");
1333
+ }
1334
+ ```
1335
+
1336
+ **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
1337
+
1338
+ ---
1339
+
1340
+ ## CLI Commands
1341
+
1342
+ ```bash
1343
+ # Development server
1344
+ npx loly dev
1345
+
1346
+ # Build for production
1347
+ npx loly build
1348
+
1349
+ # Start production server
1350
+ npx loly start
1351
+ ```
1352
+
1353
+ ---
1354
+
1355
+ ## TypeScript Support
1356
+
1357
+ Loly is built with TypeScript and provides full type safety:
1358
+
1359
+ ```tsx
1360
+ import type {
1361
+ ServerContext,
1362
+ ServerLoader,
1363
+ ApiContext,
1364
+ WssContext,
1365
+ RouteMiddleware,
1366
+ ApiMiddleware,
1367
+ GenerateStaticParams,
1368
+ } from "@lolyjs/core";
1369
+ ```
1370
+
1371
+ ---
1372
+
1373
+ ## Production
1374
+
1375
+ ### Build
1376
+
1377
+ ```bash
1378
+ npm run build
1379
+ ```
1380
+
1381
+ This generates:
1382
+
1383
+ - Client bundle (`.loly/client`)
1384
+ - Static pages if using SSG (`.loly/ssg`)
1385
+ - Server code (`.loly/server`)
1386
+
1387
+ ### Environment Variables
1388
+
1389
+ ```bash
1390
+ PORT=3000
1391
+ HOST=0.0.0.0
1392
+ NODE_ENV=production
1393
+ # PUBLIC_WS_BASE_URL is optional - defaults to window.location.origin
1394
+ # Only set if WebSocket server is on a different domain
1395
+ PUBLIC_WS_BASE_URL=http://localhost:3000
1396
+ ```
1397
+
1398
+ **Note:** For WebSocket connections, `PUBLIC_WS_BASE_URL` is optional. By default, `lolySocket` uses `window.location.origin`, so you only need to set it if your WebSocket server is on a different domain than your web app.
1399
+
1400
+ ---
1401
+
1402
+ ## Exports
1403
+
1404
+ ```tsx
1405
+ // Server
1406
+ import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
1407
+
1408
+ // Types
1409
+ import type {
1410
+ ServerContext,
1411
+ ServerLoader,
1412
+ ApiContext,
1413
+ WssContext,
1414
+ RouteMiddleware,
1415
+ ApiMiddleware,
1416
+ GenerateStaticParams,
1417
+ } from "@lolyjs/core";
1418
+
1419
+ // Validation
1420
+ import { validate, safeValidate, ValidationError } from "@lolyjs/core";
1421
+
1422
+ // Security
1423
+ import { sanitizeString, sanitizeObject } from "@lolyjs/core";
1424
+ import {
1425
+ createRateLimiter,
1426
+ defaultRateLimiter,
1427
+ strictRateLimiter,
1428
+ } from "@lolyjs/core";
1429
+
1430
+ // Logging
1431
+ import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
1432
+
1433
+ // Client
1434
+ import { Link } from "@lolyjs/core/components";
1435
+ import { lolySocket } from "@lolyjs/core/sockets";
1436
+ import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
1437
+ ```
1438
+
1439
+ ---
1440
+
1441
+ ## License
1442
+
1443
+ ISC
1444
+
1445
+ ---
1446
+
1447
+ ## Built With
1448
+
1449
+ - [React](https://react.dev/) - UI library
1450
+ - [Express](https://expressjs.com/) - Web framework
1451
+ - [Rspack](https://rspack.dev/) - Fast bundler
1452
+ - [Socket.IO](https://socket.io/) - WebSocket library
1453
+ - [Pino](https://getpino.io/) - Fast logger
1454
+ - [Zod](https://zod.dev/) - Schema validation
1455
+ - [Helmet](https://helmetjs.github.io/) - Security headers
1456
+
1457
+ ---
1458
+
1459
+ <div align="center">
1460
+
1461
+ **Made with ❤️ by the Loly team**
1462
+
1463
+ </div>