@alepha/react 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/auth/index.browser.js +29 -14
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.js +960 -195
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/core/index.d.ts +4 -0
  6. package/dist/core/index.d.ts.map +1 -1
  7. package/dist/core/index.js +7 -4
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/head/index.browser.js +59 -19
  10. package/dist/head/index.browser.js.map +1 -1
  11. package/dist/head/index.d.ts +99 -560
  12. package/dist/head/index.d.ts.map +1 -1
  13. package/dist/head/index.js +92 -87
  14. package/dist/head/index.js.map +1 -1
  15. package/dist/router/index.browser.js +30 -15
  16. package/dist/router/index.browser.js.map +1 -1
  17. package/dist/router/index.d.ts +616 -192
  18. package/dist/router/index.d.ts.map +1 -1
  19. package/dist/router/index.js +961 -196
  20. package/dist/router/index.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/auth/__tests__/$auth.spec.ts +188 -0
  23. package/src/core/__tests__/Router.spec.tsx +169 -0
  24. package/src/core/hooks/useAction.browser.spec.tsx +569 -0
  25. package/src/core/hooks/useAction.ts +11 -0
  26. package/src/form/hooks/useForm.browser.spec.tsx +366 -0
  27. package/src/head/helpers/SeoExpander.spec.ts +203 -0
  28. package/src/head/hooks/useHead.spec.tsx +288 -0
  29. package/src/head/index.ts +11 -28
  30. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  31. package/src/head/providers/BrowserHeadProvider.ts +25 -19
  32. package/src/head/providers/HeadProvider.ts +76 -10
  33. package/src/head/providers/ServerHeadProvider.ts +22 -138
  34. package/src/i18n/__tests__/integration.spec.tsx +239 -0
  35. package/src/i18n/components/Localize.spec.tsx +357 -0
  36. package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  37. package/src/i18n/providers/I18nProvider.spec.ts +389 -0
  38. package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
  39. package/src/router/__tests__/page-head.spec.ts +44 -0
  40. package/src/router/__tests__/seo-head.spec.ts +121 -0
  41. package/src/router/atoms/ssrManifestAtom.ts +60 -0
  42. package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  43. package/src/router/errors/Redirection.ts +1 -1
  44. package/src/router/index.shared.ts +1 -0
  45. package/src/router/index.ts +16 -2
  46. package/src/router/primitives/$page.browser.spec.tsx +702 -0
  47. package/src/router/primitives/$page.spec.tsx +702 -0
  48. package/src/router/primitives/$page.ts +46 -10
  49. package/src/router/providers/ReactBrowserProvider.ts +14 -29
  50. package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
  51. package/src/router/providers/ReactPageProvider.ts +11 -4
  52. package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
  53. package/src/router/providers/ReactServerProvider.ts +331 -315
  54. package/src/router/providers/ReactServerTemplateProvider.ts +775 -0
  55. package/src/router/providers/SSRManifestProvider.ts +365 -0
  56. package/src/router/services/ReactPageServerService.ts +5 -3
  57. package/src/router/services/ReactRouter.ts +3 -3
@@ -0,0 +1,389 @@
1
+ import { Alepha } from "alepha";
2
+ import { describe, test } from "vitest";
3
+ import { $dictionary } from "../primitives/$dictionary.ts";
4
+ import { AlephaReactI18n } from "../index.ts";
5
+ import { I18nProvider } from "./I18nProvider.ts";
6
+
7
+ describe("I18nProvider", () => {
8
+ test("should register dictionaries on initialization", async ({ expect }) => {
9
+ class App {
10
+ en = $dictionary({
11
+ lazy: async () => ({ default: { hello: "Hello" } }),
12
+ });
13
+
14
+ fr = $dictionary({
15
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
16
+ });
17
+ }
18
+
19
+ const alepha = Alepha.create().with(AlephaReactI18n);
20
+ const app = alepha.inject(App);
21
+ const i18n = alepha.inject(I18nProvider);
22
+
23
+ expect(i18n.registry).toHaveLength(2);
24
+ expect(i18n.registry[0].lang).toBe("en");
25
+ expect(i18n.registry[1].lang).toBe("fr");
26
+ });
27
+
28
+ test("should load all translations on server start", async ({ expect }) => {
29
+ class App {
30
+ en = $dictionary({
31
+ lazy: async () => ({ default: { hello: "Hello", world: "World" } }),
32
+ });
33
+
34
+ fr = $dictionary({
35
+ lazy: async () => ({ default: { hello: "Bonjour", world: "Monde" } }),
36
+ });
37
+ }
38
+
39
+ const alepha = Alepha.create().with(AlephaReactI18n);
40
+ const app = alepha.inject(App);
41
+ const i18n = alepha.inject(I18nProvider);
42
+
43
+ await alepha.start();
44
+
45
+ expect(i18n.registry[0].translations).toEqual({
46
+ hello: "Hello",
47
+ world: "World",
48
+ });
49
+ expect(i18n.registry[1].translations).toEqual({
50
+ hello: "Bonjour",
51
+ world: "Monde",
52
+ });
53
+ });
54
+
55
+ test("should default to fallback language", async ({ expect }) => {
56
+ class App {
57
+ en = $dictionary({
58
+ lazy: async () => ({ default: { hello: "Hello" } }),
59
+ });
60
+ }
61
+
62
+ const alepha = Alepha.create().with(AlephaReactI18n);
63
+ const app = alepha.inject(App);
64
+ const i18n = alepha.inject(I18nProvider);
65
+
66
+ expect(i18n.lang).toBe("en");
67
+ });
68
+
69
+ test("should translate keys in current language", async ({ expect }) => {
70
+ class App {
71
+ en = $dictionary({
72
+ lazy: async () => ({ default: { greeting: "Hello, World!" } }),
73
+ });
74
+
75
+ fr = $dictionary({
76
+ lazy: async () => ({ default: { greeting: "Bonjour, Monde!" } }),
77
+ });
78
+ }
79
+
80
+ const alepha = Alepha.create().with(AlephaReactI18n);
81
+ const app = alepha.inject(App);
82
+ const i18n = alepha.inject(I18nProvider);
83
+
84
+ await alepha.start();
85
+
86
+ expect(i18n.tr("greeting")).toBe("Hello, World!");
87
+
88
+ await i18n.setLang("fr");
89
+ expect(i18n.tr("greeting")).toBe("Bonjour, Monde!");
90
+ });
91
+
92
+ test("should fallback to fallback language when key missing", async ({
93
+ expect,
94
+ }) => {
95
+ class App {
96
+ en = $dictionary({
97
+ lazy: async () => ({
98
+ default: { hello: "Hello", world: "World" },
99
+ }),
100
+ });
101
+
102
+ fr = $dictionary({
103
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
104
+ });
105
+ }
106
+
107
+ const alepha = Alepha.create().with(AlephaReactI18n);
108
+ const app = alepha.inject(App);
109
+ const i18n = alepha.inject(I18nProvider);
110
+
111
+ await alepha.start();
112
+ await i18n.setLang("fr");
113
+
114
+ expect(i18n.tr("hello")).toBe("Bonjour");
115
+ expect(i18n.tr("world")).toBe("World"); // Falls back to English
116
+ });
117
+
118
+ test("should return key when translation not found", async ({ expect }) => {
119
+ class App {
120
+ en = $dictionary({
121
+ lazy: async () => ({ default: { hello: "Hello" } }),
122
+ });
123
+ }
124
+
125
+ const alepha = Alepha.create().with(AlephaReactI18n);
126
+ const app = alepha.inject(App);
127
+ const i18n = alepha.inject(I18nProvider);
128
+
129
+ await alepha.start();
130
+
131
+ expect(i18n.tr("missing.key")).toBe("missing.key");
132
+ });
133
+
134
+ test("should support default option when key missing", async ({ expect }) => {
135
+ class App {
136
+ en = $dictionary({
137
+ lazy: async () => ({ default: { hello: "Hello" } }),
138
+ });
139
+ }
140
+
141
+ const alepha = Alepha.create().with(AlephaReactI18n);
142
+ const app = alepha.inject(App);
143
+ const i18n = alepha.inject(I18nProvider);
144
+
145
+ await alepha.start();
146
+
147
+ expect(i18n.tr("missing", { default: "Default Text" })).toBe(
148
+ "Default Text",
149
+ );
150
+ });
151
+
152
+ test("should support variable interpolation", async ({ expect }) => {
153
+ class App {
154
+ en = $dictionary({
155
+ lazy: async () => ({
156
+ default: { greeting: "Hello, $1! You have $2 messages." },
157
+ }),
158
+ });
159
+ }
160
+
161
+ const alepha = Alepha.create().with(AlephaReactI18n);
162
+ const app = alepha.inject(App);
163
+ const i18n = alepha.inject(I18nProvider);
164
+
165
+ await alepha.start();
166
+
167
+ expect(i18n.tr("greeting", { args: ["John", "5"] })).toBe(
168
+ "Hello, John! You have 5 messages.",
169
+ );
170
+ });
171
+
172
+ test("should list all available languages", async ({ expect }) => {
173
+ class App {
174
+ en = $dictionary({
175
+ lazy: async () => ({ default: { hello: "Hello" } }),
176
+ });
177
+
178
+ fr = $dictionary({
179
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
180
+ });
181
+
182
+ de = $dictionary({
183
+ lazy: async () => ({ default: { hello: "Hallo" } }),
184
+ });
185
+ }
186
+
187
+ const alepha = Alepha.create().with(AlephaReactI18n);
188
+ const app = alepha.inject(App);
189
+ const i18n = alepha.inject(I18nProvider);
190
+
191
+ const languages = i18n.languages;
192
+ expect(languages).toContain("en");
193
+ expect(languages).toContain("fr");
194
+ expect(languages).toContain("de");
195
+ expect(languages).toHaveLength(3);
196
+ });
197
+
198
+ test("should support custom language codes", async ({ expect }) => {
199
+ class App {
200
+ enUS = $dictionary({
201
+ lang: "en-US",
202
+ lazy: async () => ({ default: { color: "color" } }),
203
+ });
204
+
205
+ enGB = $dictionary({
206
+ lang: "en-GB",
207
+ lazy: async () => ({ default: { color: "colour" } }),
208
+ });
209
+ }
210
+
211
+ const alepha = Alepha.create().with(AlephaReactI18n);
212
+ const app = alepha.inject(App);
213
+ const i18n = alepha.inject(I18nProvider);
214
+
215
+ await alepha.start();
216
+
217
+ expect(i18n.registry[0].lang).toBe("en-US");
218
+ expect(i18n.registry[1].lang).toBe("en-GB");
219
+ });
220
+
221
+ test("should support custom dictionary names", async ({ expect }) => {
222
+ class App {
223
+ english = $dictionary({
224
+ name: "custom-en",
225
+ lang: "en",
226
+ lazy: async () => ({ default: { hello: "Hello" } }),
227
+ });
228
+ }
229
+
230
+ const alepha = Alepha.create().with(AlephaReactI18n);
231
+ const app = alepha.inject(App);
232
+ const i18n = alepha.inject(I18nProvider);
233
+
234
+ expect(i18n.registry[0].name).toBe("custom-en");
235
+ });
236
+
237
+ test("should update state when language changes", async ({ expect }) => {
238
+ class App {
239
+ en = $dictionary({
240
+ lazy: async () => ({ default: { hello: "Hello" } }),
241
+ });
242
+
243
+ fr = $dictionary({
244
+ lazy: async () => ({ default: { hello: "Bonjour" } }),
245
+ });
246
+ }
247
+
248
+ const alepha = Alepha.create().with(AlephaReactI18n);
249
+ const app = alepha.inject(App);
250
+ const i18n = alepha.inject(I18nProvider);
251
+
252
+ await alepha.start();
253
+
254
+ // State is set lazily, so we should set a language first
255
+ await i18n.setLang("en");
256
+ expect(alepha.store.get("alepha.react.i18n.lang")).toBe("en");
257
+
258
+ await i18n.setLang("fr");
259
+ expect(alepha.store.get("alepha.react.i18n.lang")).toBe("fr");
260
+ });
261
+
262
+ test("should have number formatter for current language", async ({
263
+ expect,
264
+ }) => {
265
+ class App {
266
+ en = $dictionary({
267
+ lazy: async () => ({ default: {} }),
268
+ });
269
+ }
270
+
271
+ const alepha = Alepha.create().with(AlephaReactI18n);
272
+ const app = alepha.inject(App);
273
+ const i18n = alepha.inject(I18nProvider);
274
+
275
+ await alepha.start();
276
+
277
+ expect(i18n.numberFormat).toBeDefined();
278
+ expect(i18n.numberFormat.format).toBeTypeOf("function");
279
+ });
280
+
281
+ test("should handle multiple translations with same key across dictionaries", async ({
282
+ expect,
283
+ }) => {
284
+ class App {
285
+ en = $dictionary({
286
+ lazy: async () => ({
287
+ default: {
288
+ app: "Application",
289
+ settings: "Settings",
290
+ },
291
+ }),
292
+ });
293
+
294
+ fr = $dictionary({
295
+ lazy: async () => ({
296
+ default: {
297
+ app: "Programme",
298
+ settings: "Paramètres",
299
+ },
300
+ }),
301
+ });
302
+
303
+ es = $dictionary({
304
+ lazy: async () => ({
305
+ default: {
306
+ app: "Aplicación",
307
+ settings: "Configuración",
308
+ },
309
+ }),
310
+ });
311
+ }
312
+
313
+ const alepha = Alepha.create().with(AlephaReactI18n);
314
+ const app = alepha.inject(App);
315
+ const i18n = alepha.inject(I18nProvider);
316
+
317
+ await alepha.start();
318
+
319
+ expect(i18n.tr("app")).toBe("Application");
320
+ expect(i18n.tr("settings")).toBe("Settings");
321
+
322
+ await i18n.setLang("fr");
323
+ expect(i18n.tr("app")).toBe("Programme");
324
+ expect(i18n.tr("settings")).toBe("Paramètres");
325
+
326
+ await i18n.setLang("es");
327
+ expect(i18n.tr("app")).toBe("Aplicación");
328
+ expect(i18n.tr("settings")).toBe("Configuración");
329
+ });
330
+
331
+ test("should handle empty dictionaries", async ({ expect }) => {
332
+ class App {
333
+ en = $dictionary({
334
+ lazy: async () => ({ default: {} }),
335
+ });
336
+ }
337
+
338
+ const alepha = Alepha.create().with(AlephaReactI18n);
339
+ const app = alepha.inject(App);
340
+ const i18n = alepha.inject(I18nProvider);
341
+
342
+ await alepha.start();
343
+
344
+ expect(i18n.tr("anything")).toBe("anything");
345
+ });
346
+
347
+ test("should handle complex interpolation patterns", async ({ expect }) => {
348
+ class App {
349
+ en = $dictionary({
350
+ lazy: async () => ({
351
+ default: {
352
+ complex: "User $1 has $2 followers and follows $3 people",
353
+ },
354
+ }),
355
+ });
356
+ }
357
+
358
+ const alepha = Alepha.create().with(AlephaReactI18n);
359
+ const app = alepha.inject(App);
360
+ const i18n = alepha.inject(I18nProvider);
361
+
362
+ await alepha.start();
363
+
364
+ expect(i18n.tr("complex", { args: ["Alice", "1000", "500"] })).toBe(
365
+ "User Alice has 1000 followers and follows 500 people",
366
+ );
367
+ });
368
+
369
+ test("should handle partial interpolation args", async ({ expect }) => {
370
+ class App {
371
+ en = $dictionary({
372
+ lazy: async () => ({
373
+ default: { message: "Hello $1, you have $2 messages and $3 alerts" },
374
+ }),
375
+ });
376
+ }
377
+
378
+ const alepha = Alepha.create().with(AlephaReactI18n);
379
+ const app = alepha.inject(App);
380
+ const i18n = alepha.inject(I18nProvider);
381
+
382
+ await alepha.start();
383
+
384
+ // Provide fewer args than placeholders
385
+ expect(i18n.tr("message", { args: ["Bob"] })).toBe(
386
+ "Hello Bob, you have $2 messages and $3 alerts",
387
+ );
388
+ });
389
+ });
@@ -0,0 +1,91 @@
1
+ import { AlephaReactHead, BrowserHeadProvider, type Head } from "@alepha/react/head";
2
+ import { Alepha } from "alepha";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import { $page } from "../index.browser.ts";
5
+
6
+ describe("$page head integration (browser)", () => {
7
+ let provider: BrowserHeadProvider;
8
+
9
+ class TestApp {
10
+ simplePage = $page({
11
+ path: "/",
12
+ head: {
13
+ title: "Simple Page",
14
+ bodyAttributes: { class: "simple-page" },
15
+ },
16
+ component: () => "Simple content",
17
+ });
18
+
19
+ complexPage = $page({
20
+ path: "/complex",
21
+ head: {
22
+ title: "Complex Page",
23
+ htmlAttributes: {
24
+ lang: "en",
25
+ "data-theme": "dark",
26
+ },
27
+ bodyAttributes: {
28
+ class: "complex-page",
29
+ style: "background: black;",
30
+ },
31
+ meta: [
32
+ { name: "description", content: "Complex test page" },
33
+ {
34
+ name: "viewport",
35
+ content: "width=device-width, initial-scale=1",
36
+ },
37
+ ],
38
+ },
39
+ component: () => "Complex content",
40
+ });
41
+ }
42
+
43
+ beforeEach(() => {
44
+ // Reset document state
45
+ document.title = "";
46
+ document.head.innerHTML = "";
47
+ document.body.removeAttribute("class");
48
+ document.body.removeAttribute("style");
49
+ document.documentElement.removeAttribute("lang");
50
+ document.documentElement.removeAttribute("class");
51
+ document.documentElement.removeAttribute("data-theme");
52
+ });
53
+
54
+ afterEach(() => {
55
+ document.body.querySelector("#root")?.remove();
56
+ });
57
+
58
+ it("should render simple page head configuration", async () => {
59
+ const alepha = Alepha.create().with(AlephaReactHead).with(TestApp);
60
+ await alepha.start();
61
+
62
+ expect(document.title).toBe("Simple Page");
63
+ expect(document.body.getAttribute("class")).toBe("simple-page");
64
+ });
65
+
66
+ it("should get current head state and match page configuration", async () => {
67
+ const alepha = Alepha.create().with(AlephaReactHead);
68
+ provider = alepha.inject(BrowserHeadProvider);
69
+ const app = alepha.inject(TestApp);
70
+ await alepha.start();
71
+
72
+ // Apply complex page head
73
+ const headConfig = app.complexPage.options.head as Head;
74
+ provider.renderHead(document, headConfig);
75
+
76
+ // Get current head state
77
+ const currentHead = provider.getHead(document);
78
+
79
+ expect(currentHead.title).toBe(headConfig.title);
80
+ expect(currentHead.htmlAttributes?.lang).toBe(
81
+ headConfig.htmlAttributes?.lang,
82
+ );
83
+ expect(currentHead.bodyAttributes?.class).toBe(
84
+ headConfig.bodyAttributes?.class,
85
+ );
86
+ expect(currentHead.meta).toContainEqual({
87
+ name: "description",
88
+ content: "Complex test page",
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,44 @@
1
+ import { Alepha } from "alepha";
2
+ import { describe, it } from "vitest";
3
+ import { $head, AlephaReactHead } from "@alepha/react/head";
4
+ import { $page } from "../index.ts";
5
+
6
+ class App {
7
+ head = $head({
8
+ htmlAttributes: { lang: "fr", "x-data-custom": "ok" },
9
+ });
10
+
11
+ hello = $page({
12
+ head: {
13
+ title: "Hello World",
14
+ bodyAttributes: { class: "hello-world" },
15
+ meta: [
16
+ { name: "description", content: "This is a test page." },
17
+ { name: "keywords", content: "test, alepha, react" },
18
+ ],
19
+ },
20
+ component: () => "",
21
+ });
22
+ }
23
+
24
+ const alepha = Alepha.create().with(AlephaReactHead);
25
+ const a = alepha.inject(App);
26
+
27
+ describe("PageHead", () => {
28
+ it("should render page with custom head and body attributes", async ({
29
+ expect,
30
+ }) => {
31
+ const result = await a.hello.render({ html: true, hydration: false });
32
+
33
+ // Check key parts of the HTML output (streaming adds newlines between sections)
34
+ expect(result.html).toContain('<!DOCTYPE html>');
35
+ expect(result.html).toContain('<html lang="fr" x-data-custom="ok">');
36
+ expect(result.html).toContain('<title>Hello World</title>');
37
+ expect(result.html).toContain('<meta name="description" content="This is a test page.">');
38
+ expect(result.html).toContain('<meta name="keywords" content="test, alepha, react">');
39
+ expect(result.html).toContain('<body class="hello-world">');
40
+ expect(result.html).toContain('<div id="root">');
41
+ expect(result.html).toContain('</body>');
42
+ expect(result.html).toContain('</html>');
43
+ });
44
+ });
@@ -0,0 +1,121 @@
1
+ import { Alepha } from "alepha";
2
+ import { describe, it } from "vitest";
3
+ import { $head, AlephaReactHead } from "@alepha/react/head";
4
+ import { $page } from "../index.ts";
5
+
6
+ class App {
7
+ head = $head({
8
+ title: "Alepha Framework",
9
+ description: "TypeScript framework made easy",
10
+ image: "https://alepha.dev/og-image.png",
11
+ url: "https://alepha.dev/",
12
+ siteName: "Alepha",
13
+ locale: "en_US",
14
+ type: "website",
15
+ imageWidth: 1200,
16
+ imageHeight: 630,
17
+ twitter: {
18
+ card: "summary_large_image",
19
+ },
20
+ });
21
+
22
+ home = $page({
23
+ component: () => "Home",
24
+ });
25
+ }
26
+
27
+ const alepha = Alepha.create().with(AlephaReactHead);
28
+ const a = alepha.inject(App);
29
+
30
+ describe("SEO Head", () => {
31
+ it("should render page with SEO meta tags", async ({ expect }) => {
32
+ const result = await a.home.render({ html: true, hydration: false });
33
+
34
+ // Should include description
35
+ expect(result.html).toContain(
36
+ '<meta name="description" content="TypeScript framework made easy">',
37
+ );
38
+
39
+ // Should include OpenGraph tags with property attribute
40
+ expect(result.html).toContain(
41
+ '<meta property="og:title" content="Alepha Framework">',
42
+ );
43
+ expect(result.html).toContain(
44
+ '<meta property="og:description" content="TypeScript framework made easy">',
45
+ );
46
+ expect(result.html).toContain(
47
+ '<meta property="og:image" content="https://alepha.dev/og-image.png">',
48
+ );
49
+ expect(result.html).toContain(
50
+ '<meta property="og:url" content="https://alepha.dev/">',
51
+ );
52
+ expect(result.html).toContain('<meta property="og:type" content="website">');
53
+ expect(result.html).toContain(
54
+ '<meta property="og:site_name" content="Alepha">',
55
+ );
56
+ expect(result.html).toContain(
57
+ '<meta property="og:locale" content="en_US">',
58
+ );
59
+ expect(result.html).toContain(
60
+ '<meta property="og:image:width" content="1200">',
61
+ );
62
+ expect(result.html).toContain(
63
+ '<meta property="og:image:height" content="630">',
64
+ );
65
+
66
+ // Should include Twitter Card tags with name attribute
67
+ expect(result.html).toContain(
68
+ '<meta name="twitter:card" content="summary_large_image">',
69
+ );
70
+ expect(result.html).toContain(
71
+ '<meta name="twitter:title" content="Alepha Framework">',
72
+ );
73
+ expect(result.html).toContain(
74
+ '<meta name="twitter:description" content="TypeScript framework made easy">',
75
+ );
76
+ expect(result.html).toContain(
77
+ '<meta name="twitter:image" content="https://alepha.dev/og-image.png">',
78
+ );
79
+
80
+ // Should include canonical link
81
+ expect(result.html).toContain(
82
+ '<link rel="canonical" href="https://alepha.dev/">',
83
+ );
84
+ });
85
+ });
86
+
87
+ class AppWithPageSeo {
88
+ head = $head({
89
+ title: "My Site",
90
+ titleSeparator: " | ",
91
+ });
92
+
93
+ blog = $page({
94
+ path: "/blog",
95
+ head: {
96
+ title: "Blog",
97
+ description: "Read our latest articles",
98
+ image: "https://example.com/blog-og.png",
99
+ type: "article",
100
+ },
101
+ component: () => "Blog",
102
+ });
103
+ }
104
+
105
+ const alepha2 = Alepha.create().with(AlephaReactHead);
106
+ const app2 = alepha2.inject(AppWithPageSeo);
107
+
108
+ describe("SEO Head on Page", () => {
109
+ it("should render page-level SEO", async ({ expect }) => {
110
+ const result = await app2.blog.render({ html: true, hydration: false });
111
+
112
+ expect(result.html).toContain("<title>Blog | My Site</title>");
113
+ expect(result.html).toContain(
114
+ '<meta name="description" content="Read our latest articles">',
115
+ );
116
+ expect(result.html).toContain('<meta property="og:type" content="article">');
117
+ expect(result.html).toContain(
118
+ '<meta property="og:image" content="https://example.com/blog-og.png">',
119
+ );
120
+ });
121
+ });
@@ -0,0 +1,60 @@
1
+ import { $atom, t } from "alepha";
2
+
3
+ /**
4
+ * Schema for the SSR manifest atom.
5
+ */
6
+ export const ssrManifestAtomSchema = t.object({
7
+ /**
8
+ * Preload manifest mapping short keys to source paths.
9
+ * Generated by viteAlephaSsrPreload plugin at build time.
10
+ */
11
+ preload: t.optional(t.record(t.string(), t.string())),
12
+
13
+ /**
14
+ * SSR manifest mapping source files to their required chunks.
15
+ */
16
+ ssr: t.optional(t.record(t.string(), t.array(t.string()))),
17
+
18
+ /**
19
+ * Client manifest mapping source files to their output information.
20
+ */
21
+ client: t.optional(
22
+ t.record(
23
+ t.string(),
24
+ t.object({
25
+ file: t.string(),
26
+ src: t.optional(t.string()),
27
+ isEntry: t.optional(t.boolean()),
28
+ isDynamicEntry: t.optional(t.boolean()),
29
+ imports: t.optional(t.array(t.string())),
30
+ dynamicImports: t.optional(t.array(t.string())),
31
+ css: t.optional(t.array(t.string())),
32
+ assets: t.optional(t.array(t.string())),
33
+ }),
34
+ ),
35
+ ),
36
+ });
37
+
38
+ /**
39
+ * Type for the SSR manifest schema.
40
+ */
41
+ export type SsrManifestAtomSchema = typeof ssrManifestAtomSchema;
42
+
43
+ /**
44
+ * SSR Manifest atom containing all manifest data for SSR module preloading.
45
+ *
46
+ * This atom is populated at build time by embedding manifest data into the
47
+ * generated index.js. This approach is optimal for serverless deployments
48
+ * as it eliminates filesystem reads at runtime.
49
+ *
50
+ * The manifest includes:
51
+ * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
52
+ * - ssr: Maps source files to their required chunks
53
+ * - client: Maps source files to their output info including imports/css
54
+ */
55
+ export const ssrManifestAtom = $atom({
56
+ name: "alepha.react.ssr.manifest",
57
+ description: "SSR manifest for module preloading",
58
+ schema: ssrManifestAtomSchema,
59
+ default: {},
60
+ });