@lolyjs/core 0.2.0-alpha.15 → 0.2.0-alpha.17

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/README.md CHANGED
@@ -1,842 +1,1074 @@
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 create-loly-app mi-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
- },
81
- };
82
- };
83
- ```
84
-
85
- ```tsx
86
- // app/page.tsx
87
- export default function Home({ props }) {
88
- return <h1>{props.data}</h1>;
89
- }
90
- ```
91
-
92
- ### Start Development Server
93
-
94
- ```bash
95
- npx loly dev
96
- # Server runs on http://localhost:3000
97
- ```
98
-
99
- ---
100
-
101
- ## Key Features
102
-
103
- ### 🔌 Native WebSocket Support
104
-
105
- Loly includes built-in WebSocket support with automatic namespace routing. Define WebSocket events using the same file-based routing pattern as pages and APIs:
106
-
107
- ```tsx
108
- // app/wss/chat/events.ts
109
- import type { WssContext } from "@lolyjs/core";
110
-
111
- export const events = [
112
- {
113
- name: "connection",
114
- handler: (ctx: WssContext) => {
115
- console.log("Client connected:", ctx.socket.id);
116
- },
117
- },
118
- {
119
- name: "message",
120
- handler: (ctx: WssContext) => {
121
- const { data, actions } = ctx;
122
- // Broadcast to all clients
123
- actions.broadcast("message", {
124
- text: data.text,
125
- from: ctx.socket.id,
126
- });
127
- },
128
- },
129
- ];
130
- ```
131
-
132
- **Client-side:**
133
-
134
- ```tsx
135
- import { lolySocket } from "@lolyjs/core/sockets";
136
-
137
- const socket = lolySocket("/chat");
138
-
139
- socket.on("message", (data) => {
140
- console.log("Received:", data);
141
- });
142
-
143
- socket.emit("message", { text: "Hello!" });
144
- ```
145
-
146
- **Key Benefits:**
147
-
148
- - Automatic namespace creation from file structure
149
- - Same routing pattern as pages and APIs
150
- - Built-in broadcasting helpers (`emit`, `broadcast`, `emitTo`, `emitToClient`)
151
- - No manual configuration required
152
-
153
- ### 🎯 Route-Level Middlewares
154
-
155
- Define middlewares directly in your routes for fine-grained control:
156
-
157
- **For Pages:**
158
-
159
- ```tsx
160
- // app/dashboard/page.server.hook.ts (preferred) or app/dashboard/server.hook.ts (legacy)
161
- import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
162
-
163
- export const beforeServerData: RouteMiddleware[] = [
164
- async (ctx, next) => {
165
- // Authentication
166
- const token = ctx.req.headers.authorization;
167
- if (!token) {
168
- ctx.res.status(401).json({ error: "Unauthorized" });
169
- return;
170
- }
171
- ctx.locals.user = await verifyToken(token);
172
- await next();
173
- },
174
- ];
175
-
176
- export const getServerSideProps: ServerLoader = async (ctx) => {
177
- const user = ctx.locals.user; // Available from middleware
178
- return { props: { user } };
179
- };
180
- ```
181
-
182
- **For API Routes:**
183
-
184
- ```tsx
185
- // app/api/protected/route.ts
186
- import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
187
-
188
- // Global middleware for all methods
189
- export const beforeApi: ApiMiddleware[] = [
190
- async (ctx, next) => {
191
- // Authentication
192
- const user = await verifyUser(ctx.req);
193
- ctx.locals.user = user;
194
- await next();
195
- },
196
- ];
197
-
198
- // Method-specific middleware
199
- export const beforePOST: ApiMiddleware[] = [
200
- async (ctx, next) => {
201
- // Validation specific to POST
202
- await next();
203
- },
204
- ];
205
-
206
- export async function GET(ctx: ApiContext) {
207
- const user = ctx.locals.user;
208
- return ctx.Response({ user });
209
- }
210
- ```
211
-
212
- **Key Benefits:**
213
-
214
- - Middlewares execute before loaders/handlers
215
- - Share data via `ctx.locals`
216
- - Method-specific middlewares for APIs
217
- - Clean separation of concerns
218
-
219
- ### 📁 File-Based Routing
220
-
221
- Routes are automatically created from your file structure:
222
-
223
- | File Path | Route |
224
- | ----------------------------- | --------------------- |
225
- | `app/page.tsx` | `/` |
226
- | `app/about/page.tsx` | `/about` |
227
- | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
228
- | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
229
-
230
- **Nested Layouts:**
231
-
232
- **⚠️ 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.
233
-
234
- ```tsx
235
- // app/layout.tsx (Root layout)
236
- export default function RootLayout({ children, appName, navigation }) {
237
- return (
238
- <div>
239
- <nav>{navigation}</nav>
240
- {children}
241
- <footer>{appName}</footer>
242
- </div>
243
- );
244
- }
245
- ```
246
-
247
- ```tsx
248
- // app/layout.server.hook.ts (Root layout server hook - same directory as layout.tsx)
249
- import type { ServerLoader } from "@lolyjs/core";
250
-
251
- export const getServerSideProps: ServerLoader = async (ctx) => {
252
- return {
253
- props: {
254
- appName: "My App",
255
- navigation: ["Home", "About", "Blog"],
256
- },
257
- };
258
- };
259
- ```
260
-
261
- ```tsx
262
- // app/blog/layout.tsx (Nested layout)
263
- export default function BlogLayout({ children, sectionTitle }) {
264
- return (
265
- <div>
266
- <h1>{sectionTitle}</h1>
267
- <aside>Sidebar</aside>
268
- <main>{children}</main>
269
- </div>
270
- );
271
- }
272
- ```
273
-
274
- ```tsx
275
- // app/blog/layout.server.hook.ts (Nested layout server hook - same directory as layout.tsx)
276
- import type { ServerLoader } from "@lolyjs/core";
277
-
278
- export const getServerSideProps: ServerLoader = async (ctx) => {
279
- return {
280
- props: {
281
- sectionTitle: "Blog Section",
282
- },
283
- };
284
- };
285
- ```
286
-
287
- **Layout Server Hooks:**
288
-
289
- 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:
290
-
291
- - **Layout props** (from `layout.server.hook.ts`) are stable and available to both the layout and all pages
292
- - **Page props** (from `page.server.hook.ts`) are specific to each page and override layout props if there's a conflict
293
- - **Combined props** are available to both layouts and pages
294
-
295
- **File Convention:**
296
- - Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
297
- - Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
298
-
299
- ### 🚀 Hybrid Rendering
300
-
301
- Choose the best rendering strategy for each page:
302
-
303
- **SSR (Server-Side Rendering):**
304
-
305
- ```tsx
306
- // app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
307
- export const dynamic = "force-dynamic" as const;
308
-
309
- export const getServerSideProps: ServerLoader = async (ctx) => {
310
- const posts = await fetchFreshPosts();
311
- return { props: { posts } };
312
- };
313
- ```
314
-
315
- **SSG (Static Site Generation):**
316
-
317
- ```tsx
318
- // app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
319
- export const dynamic = "force-static" as const;
320
-
321
- export const generateStaticParams: GenerateStaticParams = async () => {
322
- const posts = await getAllPosts();
323
- return posts.map((post) => ({ slug: post.slug }));
324
- };
325
-
326
- export const getServerSideProps: ServerLoader = async (ctx) => {
327
- const post = await getPost(ctx.params.slug);
328
- return { props: { post } };
329
- };
330
- ```
331
-
332
- **CSR (Client-Side Rendering):**
333
-
334
- ```tsx
335
- // app/dashboard/page.tsx (No page.server.hook.ts)
336
- import { useState, useEffect } from "react";
337
-
338
- export default function Dashboard() {
339
- const [data, setData] = useState(null);
340
-
341
- useEffect(() => {
342
- fetchData().then(setData);
343
- }, []);
344
-
345
- return <div>{data}</div>;
346
- }
347
- ```
348
-
349
- ### 🔌 API Routes
350
-
351
- Create RESTful APIs with flexible middleware support:
352
-
353
- ```tsx
354
- // app/api/posts/route.ts
355
- import type { ApiContext } from "@lolyjs/core";
356
- import { validate } from "@lolyjs/core";
357
- import { z } from "zod";
358
-
359
- const postSchema = z.object({
360
- title: z.string().min(1),
361
- content: z.string().min(1),
362
- });
363
-
364
- export async function GET(ctx: ApiContext) {
365
- const posts = await getPosts();
366
- return ctx.Response({ posts });
367
- }
368
-
369
- export async function POST(ctx: ApiContext) {
370
- const data = validate(postSchema, ctx.req.body);
371
- const post = await createPost(data);
372
- return ctx.Response({ post }, 201);
373
- }
374
- ```
375
-
376
- ### 🛡️ Built-in Security
377
-
378
- **Rate Limiting:**
379
-
380
- ```tsx
381
- // loly.config.ts
382
- import { ServerConfig } from "@lolyjs/core";
383
-
384
- export const config = (env: string): ServerConfig => {
385
- return {
386
- rateLimit: {
387
- windowMs: 15 * 60 * 1000, // 15 minutes
388
- max: 1000,
389
- strictMax: 5,
390
- strictPatterns: ["/api/auth/**"],
391
- },
392
- };
393
- };
394
- ```
395
-
396
- **Validation with Zod:**
397
-
398
- ```tsx
399
- import { validate, ValidationError } from "@lolyjs/core";
400
- import { z } from "zod";
401
-
402
- const schema = z.object({
403
- email: z.string().email(),
404
- age: z.number().int().min(0).max(150),
405
- });
406
-
407
- try {
408
- const data = validate(schema, req.body);
409
- } catch (error) {
410
- if (error instanceof ValidationError) {
411
- return Response({ errors: error.format() }, 400);
412
- }
413
- }
414
- ```
415
-
416
- **Automatic Sanitization:**
417
-
418
- Route parameters and query strings are automatically sanitized to prevent XSS attacks.
419
-
420
- **Security Headers:**
421
-
422
- Helmet is configured by default with CSP (Content Security Policy) and nonce support.
423
-
424
- ### 📝 Structured Logging
425
-
426
- ```tsx
427
- import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
428
-
429
- // Request logger (automatic request ID)
430
- export const getServerSideProps: ServerLoader = async (ctx) => {
431
- const logger = getRequestLogger(ctx.req);
432
- logger.info("Processing request", { userId: ctx.locals.user?.id });
433
- return { props: {} };
434
- };
435
-
436
- // Module logger
437
- const logger = createModuleLogger("my-module");
438
- logger.info("Module initialized");
439
- logger.error("Error occurred", error);
440
- ```
441
-
442
- ---
443
-
444
- ## Project Structure
445
-
446
- ```
447
- your-app/
448
- ├── app/
449
- │ ├── layout.tsx # Root layout
450
- │ ├── layout.server.hook.ts # Root layout server hook (stable props)
451
- │ ├── page.tsx # Home page (/)
452
- │ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
453
- │ ├── _not-found.tsx # Custom 404
454
- │ ├── _error.tsx # Custom error page
455
- │ ├── blog/
456
- │ │ ├── layout.tsx # Blog layout
457
- │ │ ├── layout.server.hook.ts # Blog layout server hook
458
- │ │ ├── page.tsx # /blog
459
- │ │ └── [slug]/
460
- │ │ ├── page.tsx # /blog/:slug
461
- │ │ └── page.server.hook.ts # Page server hook
462
- │ ├── api/
463
- │ │ └── posts/
464
- │ │ └── route.ts # /api/posts
465
- │ └── wss/
466
- │ └── chat/
467
- │ └── events.ts # WebSocket namespace /chat
468
- ├── components/ # React components
469
- ├── lib/ # Utilities
470
- ├── public/ # Static files
471
- ├── loly.config.ts # Framework configuration
472
- ├── init.server.ts # Server initialization (DB, services, etc.)
473
- └── package.json
474
- ```
475
-
476
- ---
477
-
478
- ## API Reference
479
-
480
- ### Server Loader
481
-
482
- **Page Server Hook:**
483
-
484
- ```tsx
485
- // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
486
- import type { ServerLoader } from "@lolyjs/core";
487
-
488
- export const getServerSideProps: ServerLoader = async (ctx) => {
489
- const { req, res, params, pathname, locals } = ctx;
490
-
491
- // Fetch data
492
- const data = await fetchData();
493
-
494
- // Redirect
495
- return {
496
- redirect: {
497
- destination: "/new-path",
498
- permanent: true,
499
- },
500
- };
501
-
502
- // Not found
503
- return { notFound: true };
504
-
505
- // Return props
506
- return {
507
- props: { data },
508
- metadata: {
509
- title: "Page Title",
510
- description: "Page description",
511
- },
512
- };
513
- };
514
- ```
515
-
516
- **Layout Server Hook:**
517
-
518
- ```tsx
519
- // app/layout.server.hook.ts (same directory as layout.tsx)
520
- import type { ServerLoader } from "@lolyjs/core";
521
-
522
- export const getServerSideProps: ServerLoader = async (ctx) => {
523
- // Fetch stable data that persists across all pages
524
- const user = await getCurrentUser();
525
- const navigation = await getNavigation();
526
-
527
- return {
528
- props: {
529
- user, // Available to layout and all pages
530
- navigation, // Available to layout and all pages
531
- },
532
- };
533
- };
534
- ```
535
-
536
- **Props Merging:**
537
-
538
- - Layout props (from `layout.server.hook.ts`) are merged first
539
- - Page props (from `page.server.hook.ts`) are merged second and override layout props
540
- - Both layouts and pages receive the combined props
541
-
542
- ```tsx
543
- // app/layout.tsx
544
- export default function Layout({ user, navigation, children }) {
545
- // Receives: user, navigation (from layout.server.hook.ts)
546
- // Also receives: any props from page.server.hook.ts
547
- return <div>{/* ... */}</div>;
548
- }
549
-
550
- // app/page.tsx
551
- export default function Page({ user, navigation, posts }) {
552
- // Receives: user, navigation (from layout.server.hook.ts)
553
- // Receives: posts (from page.server.hook.ts)
554
- return <div>{/* ... */}</div>;
555
- }
556
- ```
557
-
558
- ### API Route Handler
559
-
560
- ```tsx
561
- import type { ApiContext } from "@lolyjs/core";
562
-
563
- export async function GET(ctx: ApiContext) {
564
- return ctx.Response({ data: "value" });
565
- }
566
-
567
- export async function POST(ctx: ApiContext) {
568
- return ctx.Response({ created: true }, 201);
569
- }
570
-
571
- export async function DELETE(ctx: ApiContext) {
572
- return ctx.Response({ deleted: true }, 204);
573
- }
574
- ```
575
-
576
- ### WebSocket Event Handler
577
-
578
- ```tsx
579
- import type { WssContext } from "@lolyjs/core";
580
-
581
- export const events = [
582
- {
583
- name: "connection",
584
- handler: (ctx: WssContext) => {
585
- // Handle connection
586
- },
587
- },
588
- {
589
- name: "custom-event",
590
- handler: (ctx: WssContext) => {
591
- const { socket, data, actions } = ctx;
592
-
593
- // Emit to all clients
594
- actions.emit("response", { message: "Hello" });
595
-
596
- // Broadcast to all except sender
597
- actions.broadcast("notification", data);
598
-
599
- // Emit to specific socket
600
- actions.emitTo(socketId, "private", data);
601
- },
602
- },
603
- ];
604
- ```
605
-
606
- ### Client Cache
607
-
608
- ```tsx
609
- import { revalidate } from "@lolyjs/core/client-cache";
610
-
611
- export default function Page({ props }) {
612
- const handleRefresh = async () => {
613
- await revalidate(); // Refresh current page data
614
- };
615
-
616
- return <div>{/* Your UI */}</div>;
617
- }
618
- ```
619
-
620
- ### Components
621
-
622
- ```tsx
623
- import { Link } from "@lolyjs/core/components";
624
-
625
- export default function Navigation() {
626
- return (
627
- <nav>
628
- <Link href="/">Home</Link>
629
- <Link href="/about">About</Link>
630
- <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
631
- My Post
632
- </Link>
633
- </nav>
634
- );
635
- }
636
- ```
637
-
638
- ---
639
-
640
- ## Configuration
641
-
642
- ### Framework Configuration
643
-
644
- Create `loly.config.ts` in your project root to configure the framework:
645
-
646
- ```tsx
647
- import { FrameworkConfig } from "@lolyjs/core";
648
-
649
- export default {
650
- directories: {
651
- app: "app",
652
- build: ".loly",
653
- static: "public",
654
- },
655
- server: {
656
- port: 3000,
657
- host: "localhost",
658
- },
659
- routing: {
660
- trailingSlash: "ignore",
661
- caseSensitive: false,
662
- basePath: "",
663
- },
664
- rendering: {
665
- framework: "react",
666
- streaming: true,
667
- ssr: true,
668
- ssg: true,
669
- },
670
- } satisfies FrameworkConfig;
671
- ```
672
-
673
- ### Server Configuration
674
-
675
- Configure server settings (CORS, rate limiting, etc.) in `loly.config.ts` by exporting a `config` function:
676
-
677
- ```tsx
678
- // loly.config.ts
679
- import { ServerConfig } from "@lolyjs/core";
680
-
681
- export const config = (env: string): ServerConfig => {
682
- return {
683
- bodyLimit: "1mb",
684
- corsOrigin: env === "production" ? ["https://yourdomain.com"] : "*",
685
- rateLimit: {
686
- windowMs: 15 * 60 * 1000,
687
- max: 1000,
688
- strictMax: 5,
689
- strictPatterns: ["/api/auth/**"],
690
- },
691
- };
692
- };
693
- ```
694
-
695
- ### Server Initialization
696
-
697
- Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
698
-
699
- ```tsx
700
- // init.server.ts
701
- import { InitServerData } from "@lolyjs/core";
702
-
703
- export async function init({
704
- serverContext,
705
- }: {
706
- serverContext: InitServerData;
707
- }) {
708
- // Initialize database connection
709
- await connectToDatabase();
710
-
711
- // Setup external services
712
- await setupExternalServices();
713
-
714
- // Any other initialization logic
715
- console.log("Server initialized successfully");
716
- }
717
- ```
718
-
719
- **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
720
-
721
- ---
722
-
723
- ## CLI Commands
724
-
725
- ```bash
726
- # Development server
727
- npx loly dev
728
-
729
- # Build for production
730
- npx loly build
731
-
732
- # Start production server
733
- npx loly start
734
- ```
735
-
736
- ---
737
-
738
- ## TypeScript Support
739
-
740
- Loly is built with TypeScript and provides full type safety:
741
-
742
- ```tsx
743
- import type {
744
- ServerContext,
745
- ServerLoader,
746
- ApiContext,
747
- WssContext,
748
- RouteMiddleware,
749
- ApiMiddleware,
750
- GenerateStaticParams,
751
- } from "@lolyjs/core";
752
- ```
753
-
754
- ---
755
-
756
- ## Production
757
-
758
- ### Build
759
-
760
- ```bash
761
- npm run build
762
- ```
763
-
764
- This generates:
765
-
766
- - Client bundle (`.loly/client`)
767
- - Static pages if using SSG (`.loly/ssg`)
768
- - Server code (`.loly/server`)
769
-
770
- ### Environment Variables
771
-
772
- ```bash
773
- PORT=3000
774
- HOST=0.0.0.0
775
- NODE_ENV=production
776
- PUBLIC_WS_BASE_URL=http://localhost:3000
777
- ```
778
-
779
- ---
780
-
781
- ## Exports
782
-
783
- ```tsx
784
- // Server
785
- import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
786
-
787
- // Types
788
- import type {
789
- ServerContext,
790
- ServerLoader,
791
- ApiContext,
792
- WssContext,
793
- RouteMiddleware,
794
- ApiMiddleware,
795
- GenerateStaticParams,
796
- } from "@lolyjs/core";
797
-
798
- // Validation
799
- import { validate, safeValidate, ValidationError } from "@lolyjs/core";
800
-
801
- // Security
802
- import { sanitizeString, sanitizeObject } from "@lolyjs/core";
803
- import {
804
- createRateLimiter,
805
- defaultRateLimiter,
806
- strictRateLimiter,
807
- } from "@lolyjs/core";
808
-
809
- // Logging
810
- import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
811
-
812
- // Client
813
- import { Link } from "@lolyjs/core/components";
814
- import { lolySocket } from "@lolyjs/core/sockets";
815
- import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
816
- ```
817
-
818
- ---
819
-
820
- ## License
821
-
822
- ISC
823
-
824
- ---
825
-
826
- ## Built With
827
-
828
- - [React](https://react.dev/) - UI library
829
- - [Express](https://expressjs.com/) - Web framework
830
- - [Rspack](https://rspack.dev/) - Fast bundler
831
- - [Socket.IO](https://socket.io/) - WebSocket library
832
- - [Pino](https://getpino.io/) - Fast logger
833
- - [Zod](https://zod.dev/) - Schema validation
834
- - [Helmet](https://helmetjs.github.io/) - Security headers
835
-
836
- ---
837
-
838
- <div align="center">
839
-
840
- **Made with ❤️ by the Loly team**
841
-
842
- </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 create-loly-app mi-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
+ ### 🚀 Hybrid Rendering
346
+
347
+ Choose the best rendering strategy for each page:
348
+
349
+ **SSR (Server-Side Rendering):**
350
+
351
+ ```tsx
352
+ // app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
353
+ export const dynamic = "force-dynamic" as const;
354
+
355
+ export const getServerSideProps: ServerLoader = async (ctx) => {
356
+ const posts = await fetchFreshPosts();
357
+ return { props: { posts } };
358
+ };
359
+ ```
360
+
361
+ **SSG (Static Site Generation):**
362
+
363
+ ```tsx
364
+ // app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
365
+ export const dynamic = "force-static" as const;
366
+
367
+ export const generateStaticParams: GenerateStaticParams = async () => {
368
+ const posts = await getAllPosts();
369
+ return posts.map((post) => ({ slug: post.slug }));
370
+ };
371
+
372
+ export const getServerSideProps: ServerLoader = async (ctx) => {
373
+ const post = await getPost(ctx.params.slug);
374
+ return { props: { post } };
375
+ };
376
+ ```
377
+
378
+ **CSR (Client-Side Rendering):**
379
+
380
+ ```tsx
381
+ // app/dashboard/page.tsx (No page.server.hook.ts)
382
+ import { useState, useEffect } from "react";
383
+
384
+ export default function Dashboard() {
385
+ const [data, setData] = useState(null);
386
+
387
+ useEffect(() => {
388
+ fetchData().then(setData);
389
+ }, []);
390
+
391
+ return <div>{data}</div>;
392
+ }
393
+ ```
394
+
395
+ ### 🔌 API Routes
396
+
397
+ Create RESTful APIs with flexible middleware support:
398
+
399
+ ```tsx
400
+ // app/api/posts/route.ts
401
+ import type { ApiContext } from "@lolyjs/core";
402
+ import { validate } from "@lolyjs/core";
403
+ import { z } from "zod";
404
+
405
+ const postSchema = z.object({
406
+ title: z.string().min(1),
407
+ content: z.string().min(1),
408
+ });
409
+
410
+ export async function GET(ctx: ApiContext) {
411
+ const posts = await getPosts();
412
+ return ctx.Response({ posts });
413
+ }
414
+
415
+ export async function POST(ctx: ApiContext) {
416
+ const data = validate(postSchema, ctx.req.body);
417
+ const post = await createPost(data);
418
+ return ctx.Response({ post }, 201);
419
+ }
420
+ ```
421
+
422
+ ### 📊 SEO & Metadata
423
+
424
+ Loly provides comprehensive metadata support for SEO and social sharing. Metadata can be defined at both layout and page levels, with intelligent merging:
425
+
426
+ **Layout Metadata (Base/Defaults):**
427
+
428
+ ```tsx
429
+ // app/layout.server.hook.ts
430
+ import type { ServerLoader } from "@lolyjs/core";
431
+
432
+ export const getServerSideProps: ServerLoader = async () => {
433
+ return {
434
+ props: { /* ... */ },
435
+ metadata: {
436
+ // Site-wide defaults
437
+ description: "My awesome site",
438
+ lang: "en",
439
+ robots: "index, follow",
440
+ themeColor: "#000000",
441
+
442
+ // Open Graph defaults
443
+ openGraph: {
444
+ type: "website",
445
+ siteName: "My Site",
446
+ locale: "en_US",
447
+ },
448
+
449
+ // Twitter Card defaults
450
+ twitter: {
451
+ card: "summary_large_image",
452
+ },
453
+
454
+ // Custom meta tags
455
+ metaTags: [
456
+ { name: "author", content: "My Name" },
457
+ ],
458
+
459
+ // Custom link tags (preconnect, etc.)
460
+ links: [
461
+ { rel: "preconnect", href: "https://api.example.com" },
462
+ ],
463
+ },
464
+ };
465
+ };
466
+ ```
467
+
468
+ **Page Metadata (Overrides Layout):**
469
+
470
+ ```tsx
471
+ // app/page.server.hook.ts
472
+ import type { ServerLoader } from "@lolyjs/core";
473
+
474
+ export const getServerSideProps: ServerLoader = async (ctx) => {
475
+ const post = await getPost(ctx.params.slug);
476
+
477
+ return {
478
+ props: { post },
479
+ metadata: {
480
+ // Page-specific (overrides layout)
481
+ title: `${post.title} | My Site`,
482
+ description: post.excerpt,
483
+ canonical: `https://mysite.com/blog/${post.slug}`,
484
+
485
+ // Open Graph (inherits type, siteName from layout)
486
+ openGraph: {
487
+ title: post.title,
488
+ description: post.excerpt,
489
+ url: `https://mysite.com/blog/${post.slug}`,
490
+ image: {
491
+ url: post.imageUrl,
492
+ width: 1200,
493
+ height: 630,
494
+ alt: post.title,
495
+ },
496
+ },
497
+
498
+ // Twitter Card (inherits card type from layout)
499
+ twitter: {
500
+ title: post.title,
501
+ description: post.excerpt,
502
+ image: post.imageUrl,
503
+ imageAlt: post.title,
504
+ },
505
+ },
506
+ };
507
+ };
508
+ ```
509
+
510
+ **Full Metadata API:**
511
+
512
+ ```tsx
513
+ interface PageMetadata {
514
+ // Basic fields
515
+ title?: string;
516
+ description?: string;
517
+ lang?: string;
518
+ canonical?: string;
519
+ robots?: string;
520
+ themeColor?: string;
521
+ viewport?: string;
522
+
523
+ // Open Graph
524
+ openGraph?: {
525
+ title?: string;
526
+ description?: string;
527
+ type?: string;
528
+ url?: string;
529
+ image?: string | {
530
+ url: string;
531
+ width?: number;
532
+ height?: number;
533
+ alt?: string;
534
+ };
535
+ siteName?: string;
536
+ locale?: string;
537
+ };
538
+
539
+ // Twitter Cards
540
+ twitter?: {
541
+ card?: "summary" | "summary_large_image" | "app" | "player";
542
+ title?: string;
543
+ description?: string;
544
+ image?: string;
545
+ imageAlt?: string;
546
+ site?: string;
547
+ creator?: string;
548
+ };
549
+
550
+ // Custom meta tags
551
+ metaTags?: Array<{
552
+ name?: string;
553
+ property?: string;
554
+ httpEquiv?: string;
555
+ content: string;
556
+ }>;
557
+
558
+ // Custom link tags
559
+ links?: Array<{
560
+ rel: string;
561
+ href: string;
562
+ as?: string;
563
+ crossorigin?: string;
564
+ type?: string;
565
+ }>;
566
+ }
567
+ ```
568
+
569
+ **Key Features:**
570
+
571
+ - **Layout + Page Merging**: Layout metadata provides defaults, page metadata overrides specific fields
572
+ - **Automatic Updates**: Metadata updates automatically during SPA navigation
573
+ - **SSR & SSG Support**: Works in both server-side rendering and static generation
574
+ - **Type-Safe**: Full TypeScript support with `PageMetadata` type
575
+
576
+ ### 🛡️ Built-in Security
577
+
578
+ **Rate Limiting:**
579
+
580
+ ```tsx
581
+ // loly.config.ts
582
+ import { ServerConfig } from "@lolyjs/core";
583
+
584
+ export const config = (env: string): ServerConfig => {
585
+ return {
586
+ rateLimit: {
587
+ windowMs: 15 * 60 * 1000, // 15 minutes
588
+ max: 1000,
589
+ strictMax: 5,
590
+ strictPatterns: ["/api/auth/**"],
591
+ },
592
+ };
593
+ };
594
+ ```
595
+
596
+ **Validation with Zod:**
597
+
598
+ ```tsx
599
+ import { validate, ValidationError } from "@lolyjs/core";
600
+ import { z } from "zod";
601
+
602
+ const schema = z.object({
603
+ email: z.string().email(),
604
+ age: z.number().int().min(0).max(150),
605
+ });
606
+
607
+ try {
608
+ const data = validate(schema, req.body);
609
+ } catch (error) {
610
+ if (error instanceof ValidationError) {
611
+ return Response({ errors: error.format() }, 400);
612
+ }
613
+ }
614
+ ```
615
+
616
+ **Automatic Sanitization:**
617
+
618
+ Route parameters and query strings are automatically sanitized to prevent XSS attacks.
619
+
620
+ **Security Headers:**
621
+
622
+ Helmet is configured by default with CSP (Content Security Policy) and nonce support.
623
+
624
+ ### 📝 Structured Logging
625
+
626
+ ```tsx
627
+ import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
628
+
629
+ // Request logger (automatic request ID)
630
+ export const getServerSideProps: ServerLoader = async (ctx) => {
631
+ const logger = getRequestLogger(ctx.req);
632
+ logger.info("Processing request", { userId: ctx.locals.user?.id });
633
+ return { props: {} };
634
+ };
635
+
636
+ // Module logger
637
+ const logger = createModuleLogger("my-module");
638
+ logger.info("Module initialized");
639
+ logger.error("Error occurred", error);
640
+ ```
641
+
642
+ ---
643
+
644
+ ## Project Structure
645
+
646
+ ```
647
+ your-app/
648
+ ├── app/
649
+ │ ├── layout.tsx # Root layout
650
+ │ ├── layout.server.hook.ts # Root layout server hook (stable props)
651
+ │ ├── page.tsx # Home page (/)
652
+ │ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
653
+ │ ├── _not-found.tsx # Custom 404
654
+ │ ├── _error.tsx # Custom error page
655
+ │ ├── blog/
656
+ │ │ ├── layout.tsx # Blog layout
657
+ │ │ ├── layout.server.hook.ts # Blog layout server hook
658
+ │ │ ├── page.tsx # /blog
659
+ │ │ └── [slug]/
660
+ │ │ ├── page.tsx # /blog/:slug
661
+ │ │ └── page.server.hook.ts # Page server hook
662
+ │ ├── api/
663
+ │ │ └── posts/
664
+ │ │ └── route.ts # /api/posts
665
+ │ └── wss/
666
+ │ └── chat/
667
+ │ └── events.ts # WebSocket namespace /chat
668
+ ├── components/ # React components
669
+ ├── lib/ # Utilities
670
+ ├── public/ # Static files
671
+ ├── loly.config.ts # Framework configuration
672
+ ├── init.server.ts # Server initialization (DB, services, etc.)
673
+ └── package.json
674
+ ```
675
+
676
+ ---
677
+
678
+ ## API Reference
679
+
680
+ ### Server Loader
681
+
682
+ **Page Server Hook:**
683
+
684
+ ```tsx
685
+ // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
686
+ import type { ServerLoader } from "@lolyjs/core";
687
+
688
+ export const getServerSideProps: ServerLoader = async (ctx) => {
689
+ const { req, res, params, pathname, locals } = ctx;
690
+
691
+ // Fetch data
692
+ const data = await fetchData();
693
+
694
+ // Redirect
695
+ return {
696
+ redirect: {
697
+ destination: "/new-path",
698
+ permanent: true,
699
+ },
700
+ };
701
+
702
+ // Not found
703
+ return { notFound: true };
704
+
705
+ // Return props
706
+ return {
707
+ props: { data },
708
+ metadata: {
709
+ title: "Page Title",
710
+ description: "Page description",
711
+ // See "SEO & Metadata" section above for full metadata options
712
+ // including Open Graph, Twitter Cards, canonical URLs, etc.
713
+ },
714
+ };
715
+ };
716
+ ```
717
+
718
+ **Layout Server Hook:**
719
+
720
+ ```tsx
721
+ // app/layout.server.hook.ts (same directory as layout.tsx)
722
+ import type { ServerLoader } from "@lolyjs/core";
723
+
724
+ export const getServerSideProps: ServerLoader = async (ctx) => {
725
+ // Fetch stable data that persists across all pages
726
+ const user = await getCurrentUser();
727
+ const navigation = await getNavigation();
728
+
729
+ return {
730
+ props: {
731
+ user, // Available to layout and all pages
732
+ navigation, // Available to layout and all pages
733
+ },
734
+ };
735
+ };
736
+ ```
737
+
738
+ **Props Merging:**
739
+
740
+ - Layout props (from `layout.server.hook.ts`) are merged first
741
+ - Page props (from `page.server.hook.ts`) are merged second and override layout props
742
+ - Both layouts and pages receive the combined props
743
+
744
+ ```tsx
745
+ // app/layout.tsx
746
+ export default function Layout({ user, navigation, children }) {
747
+ // Receives: user, navigation (from layout.server.hook.ts)
748
+ // Also receives: any props from page.server.hook.ts
749
+ return <div>{/* ... */}</div>;
750
+ }
751
+
752
+ // app/page.tsx
753
+ export default function Page({ user, navigation, posts }) {
754
+ // Receives: user, navigation (from layout.server.hook.ts)
755
+ // Receives: posts (from page.server.hook.ts)
756
+ return <div>{/* ... */}</div>;
757
+ }
758
+ ```
759
+
760
+ ### API Route Handler
761
+
762
+ ```tsx
763
+ import type { ApiContext } from "@lolyjs/core";
764
+
765
+ export async function GET(ctx: ApiContext) {
766
+ return ctx.Response({ data: "value" });
767
+ }
768
+
769
+ export async function POST(ctx: ApiContext) {
770
+ return ctx.Response({ created: true }, 201);
771
+ }
772
+
773
+ export async function DELETE(ctx: ApiContext) {
774
+ return ctx.Response({ deleted: true }, 204);
775
+ }
776
+ ```
777
+
778
+ ### WebSocket Event Handler (New API - Realtime v1)
779
+
780
+ ```tsx
781
+ import { defineWssRoute } from "@lolyjs/core";
782
+ import { z } from "zod";
783
+
784
+ export default defineWssRoute({
785
+ auth: async (ctx) => {
786
+ // Authenticate user
787
+ return await getUserFromToken(ctx.req.headers.authorization);
788
+ },
789
+
790
+ onConnect: (ctx) => {
791
+ console.log("User connected:", ctx.user?.id);
792
+ },
793
+
794
+ events: {
795
+ "custom-event": {
796
+ schema: z.object({ message: z.string() }),
797
+ guard: ({ user }) => !!user,
798
+ handler: (ctx) => {
799
+ // Emit to all clients
800
+ ctx.actions.emit("response", { message: "Hello" });
801
+
802
+ // Broadcast to all except sender
803
+ ctx.actions.broadcast("notification", ctx.data);
804
+
805
+ // Send to specific user
806
+ ctx.actions.toUser(userId).emit("private", ctx.data);
807
+
808
+ // Send to room
809
+ ctx.actions.toRoom("room-name").emit("room-message", ctx.data);
810
+ },
811
+ },
812
+ },
813
+ });
814
+ ```
815
+ ```
816
+
817
+ ### Client Cache
818
+
819
+ ```tsx
820
+ import { revalidate } from "@lolyjs/core/client-cache";
821
+
822
+ export default function Page({ props }) {
823
+ const handleRefresh = async () => {
824
+ await revalidate(); // Refresh current page data
825
+ };
826
+
827
+ return <div>{/* Your UI */}</div>;
828
+ }
829
+ ```
830
+
831
+ ### Components
832
+
833
+ ```tsx
834
+ import { Link } from "@lolyjs/core/components";
835
+
836
+ export default function Navigation() {
837
+ return (
838
+ <nav>
839
+ <Link href="/">Home</Link>
840
+ <Link href="/about">About</Link>
841
+ <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
842
+ My Post
843
+ </Link>
844
+ </nav>
845
+ );
846
+ }
847
+ ```
848
+
849
+ ---
850
+
851
+ ## Configuration
852
+
853
+ ### Framework Configuration
854
+
855
+ Create `loly.config.ts` in your project root to configure the framework:
856
+
857
+ ```tsx
858
+ import { FrameworkConfig } from "@lolyjs/core";
859
+
860
+ export default {
861
+ directories: {
862
+ app: "app",
863
+ build: ".loly",
864
+ static: "public",
865
+ },
866
+ server: {
867
+ port: 3000,
868
+ host: "localhost",
869
+ },
870
+ routing: {
871
+ trailingSlash: "ignore",
872
+ caseSensitive: false,
873
+ basePath: "",
874
+ },
875
+ rendering: {
876
+ framework: "react",
877
+ streaming: true,
878
+ ssr: true,
879
+ ssg: true,
880
+ },
881
+ } satisfies FrameworkConfig;
882
+ ```
883
+
884
+ ### Server Configuration
885
+
886
+ Configure server settings (CORS, rate limiting, WebSocket, etc.) in `loly.config.ts` by exporting a `config` function:
887
+
888
+ ```tsx
889
+ // loly.config.ts
890
+ import { ServerConfig } from "@lolyjs/core";
891
+
892
+ export const config = (env: string): ServerConfig => {
893
+ const isDev = env === "development";
894
+
895
+ return {
896
+ bodyLimit: "1mb",
897
+ corsOrigin: isDev ? "*" : ["https://yourdomain.com"],
898
+ rateLimit: {
899
+ windowMs: 15 * 60 * 1000,
900
+ max: 1000,
901
+ strictMax: 5,
902
+ strictPatterns: ["/api/auth/**"],
903
+ },
904
+ // Realtime (WebSocket) configuration
905
+ realtime: {
906
+ enabled: true,
907
+ // For production, configure allowed origins
908
+ // For development, localhost is auto-allowed
909
+ allowedOrigins: isDev ? undefined : ["https://yourdomain.com"],
910
+ // Optional: Configure Redis for multi-instance scaling
911
+ // scale: {
912
+ // mode: "cluster",
913
+ // adapter: { url: "redis://localhost:6379" },
914
+ // stateStore: { name: "redis", url: "redis://localhost:6379" },
915
+ // },
916
+ },
917
+ };
918
+ };
919
+ ```
920
+
921
+ **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.
922
+
923
+ ### Server Initialization
924
+
925
+ Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
926
+
927
+ ```tsx
928
+ // init.server.ts
929
+ import { InitServerData } from "@lolyjs/core";
930
+
931
+ export async function init({
932
+ serverContext,
933
+ }: {
934
+ serverContext: InitServerData;
935
+ }) {
936
+ // Initialize database connection
937
+ await connectToDatabase();
938
+
939
+ // Setup external services
940
+ await setupExternalServices();
941
+
942
+ // Any other initialization logic
943
+ console.log("Server initialized successfully");
944
+ }
945
+ ```
946
+
947
+ **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
948
+
949
+ ---
950
+
951
+ ## CLI Commands
952
+
953
+ ```bash
954
+ # Development server
955
+ npx loly dev
956
+
957
+ # Build for production
958
+ npx loly build
959
+
960
+ # Start production server
961
+ npx loly start
962
+ ```
963
+
964
+ ---
965
+
966
+ ## TypeScript Support
967
+
968
+ Loly is built with TypeScript and provides full type safety:
969
+
970
+ ```tsx
971
+ import type {
972
+ ServerContext,
973
+ ServerLoader,
974
+ ApiContext,
975
+ WssContext,
976
+ RouteMiddleware,
977
+ ApiMiddleware,
978
+ GenerateStaticParams,
979
+ } from "@lolyjs/core";
980
+ ```
981
+
982
+ ---
983
+
984
+ ## Production
985
+
986
+ ### Build
987
+
988
+ ```bash
989
+ npm run build
990
+ ```
991
+
992
+ This generates:
993
+
994
+ - Client bundle (`.loly/client`)
995
+ - Static pages if using SSG (`.loly/ssg`)
996
+ - Server code (`.loly/server`)
997
+
998
+ ### Environment Variables
999
+
1000
+ ```bash
1001
+ PORT=3000
1002
+ HOST=0.0.0.0
1003
+ NODE_ENV=production
1004
+ # PUBLIC_WS_BASE_URL is optional - defaults to window.location.origin
1005
+ # Only set if WebSocket server is on a different domain
1006
+ PUBLIC_WS_BASE_URL=http://localhost:3000
1007
+ ```
1008
+
1009
+ **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.
1010
+
1011
+ ---
1012
+
1013
+ ## Exports
1014
+
1015
+ ```tsx
1016
+ // Server
1017
+ import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
1018
+
1019
+ // Types
1020
+ import type {
1021
+ ServerContext,
1022
+ ServerLoader,
1023
+ ApiContext,
1024
+ WssContext,
1025
+ RouteMiddleware,
1026
+ ApiMiddleware,
1027
+ GenerateStaticParams,
1028
+ } from "@lolyjs/core";
1029
+
1030
+ // Validation
1031
+ import { validate, safeValidate, ValidationError } from "@lolyjs/core";
1032
+
1033
+ // Security
1034
+ import { sanitizeString, sanitizeObject } from "@lolyjs/core";
1035
+ import {
1036
+ createRateLimiter,
1037
+ defaultRateLimiter,
1038
+ strictRateLimiter,
1039
+ } from "@lolyjs/core";
1040
+
1041
+ // Logging
1042
+ import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
1043
+
1044
+ // Client
1045
+ import { Link } from "@lolyjs/core/components";
1046
+ import { lolySocket } from "@lolyjs/core/sockets";
1047
+ import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
1048
+ ```
1049
+
1050
+ ---
1051
+
1052
+ ## License
1053
+
1054
+ ISC
1055
+
1056
+ ---
1057
+
1058
+ ## Built With
1059
+
1060
+ - [React](https://react.dev/) - UI library
1061
+ - [Express](https://expressjs.com/) - Web framework
1062
+ - [Rspack](https://rspack.dev/) - Fast bundler
1063
+ - [Socket.IO](https://socket.io/) - WebSocket library
1064
+ - [Pino](https://getpino.io/) - Fast logger
1065
+ - [Zod](https://zod.dev/) - Schema validation
1066
+ - [Helmet](https://helmetjs.github.io/) - Security headers
1067
+
1068
+ ---
1069
+
1070
+ <div align="center">
1071
+
1072
+ **Made with ❤️ by the Loly team**
1073
+
1074
+ </div>