@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.
- package/README.md +1463 -761
- package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
- package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
- package/dist/cli.cjs +15701 -2448
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15704 -2441
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +17861 -4115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +323 -55
- package/dist/index.d.ts +323 -55
- package/dist/index.js +17982 -4227
- package/dist/index.js.map +1 -1
- package/dist/index.types-B9j4OQft.d.mts +222 -0
- package/dist/index.types-B9j4OQft.d.ts +222 -0
- package/dist/react/cache.cjs +107 -32
- package/dist/react/cache.cjs.map +1 -1
- package/dist/react/cache.d.mts +29 -21
- package/dist/react/cache.d.ts +29 -21
- package/dist/react/cache.js +107 -32
- package/dist/react/cache.js.map +1 -1
- package/dist/react/components.cjs +11 -12
- package/dist/react/components.cjs.map +1 -1
- package/dist/react/components.js +11 -12
- package/dist/react/components.js.map +1 -1
- package/dist/react/hooks.cjs +124 -74
- package/dist/react/hooks.cjs.map +1 -1
- package/dist/react/hooks.d.mts +6 -24
- package/dist/react/hooks.d.ts +6 -24
- package/dist/react/hooks.js +122 -71
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/sockets.cjs +5 -6
- package/dist/react/sockets.cjs.map +1 -1
- package/dist/react/sockets.js +5 -6
- package/dist/react/sockets.js.map +1 -1
- package/dist/react/themes.cjs +61 -18
- package/dist/react/themes.cjs.map +1 -1
- package/dist/react/themes.js +63 -20
- package/dist/react/themes.js.map +1 -1
- package/dist/runtime.cjs +531 -104
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +531 -104
- package/dist/runtime.js.map +1 -1
- 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
|
-
[](https://www.npmjs.com/package/@lolyjs/core)
|
|
8
|
-
[](https://opensource.org/licenses/ISC)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
export
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@lolyjs/core)
|
|
8
|
+
[](https://opensource.org/licenses/ISC)
|
|
9
|
+

|
|
10
|
+
<br>
|
|
11
|
+
[](https://github.com/MenvielleValen/loly-framework)
|
|
12
|
+
[](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>
|