@alepha/react 0.14.3 → 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 (45) 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 +959 -194
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/head/index.browser.js +59 -19
  6. package/dist/head/index.browser.js.map +1 -1
  7. package/dist/head/index.d.ts +99 -560
  8. package/dist/head/index.d.ts.map +1 -1
  9. package/dist/head/index.js +91 -87
  10. package/dist/head/index.js.map +1 -1
  11. package/dist/router/index.browser.js +30 -15
  12. package/dist/router/index.browser.js.map +1 -1
  13. package/dist/router/index.d.ts +616 -192
  14. package/dist/router/index.d.ts.map +1 -1
  15. package/dist/router/index.js +960 -195
  16. package/dist/router/index.js.map +1 -1
  17. package/package.json +4 -4
  18. package/src/core/__tests__/Router.spec.tsx +4 -4
  19. package/src/head/{__tests__/expandSeo.spec.ts → helpers/SeoExpander.spec.ts} +1 -1
  20. package/src/head/index.ts +10 -28
  21. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +1 -76
  22. package/src/head/providers/BrowserHeadProvider.ts +25 -19
  23. package/src/head/providers/HeadProvider.ts +76 -10
  24. package/src/head/providers/ServerHeadProvider.ts +22 -138
  25. package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
  26. package/src/router/__tests__/page-head.spec.ts +44 -0
  27. package/src/{head → router}/__tests__/seo-head.spec.ts +2 -2
  28. package/src/router/atoms/ssrManifestAtom.ts +60 -0
  29. package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  30. package/src/router/errors/Redirection.ts +1 -1
  31. package/src/router/index.shared.ts +1 -0
  32. package/src/router/index.ts +16 -2
  33. package/src/router/primitives/$page.browser.spec.tsx +15 -15
  34. package/src/router/primitives/$page.spec.tsx +18 -18
  35. package/src/router/primitives/$page.ts +46 -10
  36. package/src/router/providers/ReactBrowserProvider.ts +14 -29
  37. package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
  38. package/src/router/providers/ReactPageProvider.ts +11 -4
  39. package/src/router/providers/ReactServerProvider.ts +331 -316
  40. package/src/router/providers/ReactServerTemplateProvider.ts +775 -0
  41. package/src/router/providers/SSRManifestProvider.ts +365 -0
  42. package/src/router/services/ReactPageServerService.ts +5 -3
  43. package/src/router/services/ReactRouter.ts +3 -3
  44. package/src/head/__tests__/page-head.spec.ts +0 -39
  45. package/src/head/providers/ServerHeadProvider.spec.ts +0 -163
@@ -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
+ });
@@ -1,7 +1,7 @@
1
- import { $page } from "@alepha/react/router";
2
1
  import { Alepha } from "alepha";
3
2
  import { describe, it } from "vitest";
4
- import { $head, AlephaReactHead } from "../index.ts";
3
+ import { $head, AlephaReactHead } from "@alepha/react/head";
4
+ import { $page } from "../index.ts";
5
5
 
6
6
  class App {
7
7
  head = $head({
@@ -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
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Symbol key for SSR module preloading path.
3
+ * Using Symbol.for() allows the Vite plugin to inject this at build time.
4
+ * @internal
5
+ */
6
+ export const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
@@ -10,7 +10,7 @@ import { AlephaError } from "alepha";
10
10
  * import { Redirection } from "@alepha/react";
11
11
  *
12
12
  * const MyPage = $page({
13
- * resolve: async () => {
13
+ * loader: async () => {
14
14
  * if (needRedirect) {
15
15
  * throw new Redirection("/new-path");
16
16
  * }
@@ -6,6 +6,7 @@ export { default as NestedView } from "./components/NestedView.tsx";
6
6
  export type * from "./components/NestedView.tsx";
7
7
  export { default as NotFound } from "./components/NotFound.tsx";
8
8
  export type * from "./components/NotFound.tsx";
9
+ export * from "./constants/PAGE_PRELOAD_KEY.ts";
9
10
  export * from "./contexts/RouterLayerContext.ts";
10
11
  export * from "./primitives/$page.ts";
11
12
  export * from "./errors/Redirection.ts";
@@ -7,6 +7,8 @@ import { AlephaServer, type ServerRequest } from "alepha/server";
7
7
  import type { ReactNode } from "react";
8
8
  import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
9
9
  import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
10
+ import { ReactServerTemplateProvider } from "./providers/ReactServerTemplateProvider.ts";
11
+ import { SSRManifestProvider } from "./providers/SSRManifestProvider.ts";
10
12
  import { ReactPageServerService } from "./services/ReactPageServerService.ts";
11
13
  import { AlephaServerCache } from "alepha/server/cache";
12
14
  import { AlephaServerLinks } from "alepha/server/links";
@@ -19,6 +21,8 @@ export * from "./index.shared.ts";
19
21
  export * from "./providers/ReactPageProvider.ts";
20
22
  export * from "./providers/ReactBrowserProvider.ts";
21
23
  export * from "./providers/ReactServerProvider.ts";
24
+ export * from "./providers/ReactServerTemplateProvider.ts";
25
+ export * from "./providers/SSRManifestProvider.ts";
22
26
 
23
27
  // ---------------------------------------------------------------------------------------------------------------------
24
28
 
@@ -46,6 +50,9 @@ declare module "alepha" {
46
50
  // -----------------------------------------------------------------------------------------------------------------
47
51
  /**
48
52
  * Fires when the React application is being rendered on the browser.
53
+ *
54
+ * Note: this one is not really necessary, it's a hack because we need to isolate renderer from server code in order
55
+ * to avoid including react-dom/client in server bundles.
49
56
  */
50
57
  "react:browser:render": {
51
58
  root: HTMLElement;
@@ -94,7 +101,7 @@ declare module "alepha" {
94
101
  * - URL pattern matching with parameters (e.g., `/users/:id`)
95
102
  * - Nested routing with parent-child relationships
96
103
  * - Type-safe URL parameter and query string validation
97
- * - Server-side data fetching with the `resolve` function
104
+ * - Server-side data fetching with the `loader` function
98
105
  * - Lazy loading and code splitting
99
106
  * - Page animations and error handling
100
107
  *
@@ -107,7 +114,12 @@ export const AlephaReactRouter = $module({
107
114
  services: [
108
115
  ReactPageProvider,
109
116
  ReactPageService,
110
- ReactRouter, ReactServerProvider, ReactPageServerService],
117
+ ReactRouter,
118
+ ReactServerProvider,
119
+ ReactServerTemplateProvider,
120
+ SSRManifestProvider,
121
+ ReactPageServerService,
122
+ ],
111
123
  register: (alepha) =>
112
124
  alepha
113
125
  .with(AlephaReact)
@@ -119,6 +131,8 @@ export const AlephaReactRouter = $module({
119
131
  provide: ReactPageService,
120
132
  use: ReactPageServerService,
121
133
  })
134
+ .with(SSRManifestProvider)
135
+ .with(ReactServerTemplateProvider)
122
136
  .with(ReactServerProvider)
123
137
  .with(ReactPageProvider)
124
138
  .with(ReactRouter),
@@ -89,7 +89,7 @@ describe("$page browser tests", () => {
89
89
  id: t.text(),
90
90
  }),
91
91
  },
92
- resolve: ({ params }) => ({
92
+ loader: ({ params }) => ({
93
93
  userId: params.id,
94
94
  userName: `User ${params.id}`,
95
95
  }),
@@ -132,7 +132,7 @@ describe("$page browser tests", () => {
132
132
  class App {
133
133
  async = $page({
134
134
  path: "/async",
135
- resolve: async () => {
135
+ loader: async () => {
136
136
  await new Promise((resolve) => setTimeout(resolve, 10));
137
137
  return { message: "Loaded async data" };
138
138
  },
@@ -174,7 +174,7 @@ describe("$page browser tests", () => {
174
174
 
175
175
  protected = $page({
176
176
  path: "/protected",
177
- resolve: () => {
177
+ loader: () => {
178
178
  if (!isAuthenticated) {
179
179
  throw new Error("Unauthorized");
180
180
  }
@@ -227,7 +227,7 @@ describe("$page browser tests", () => {
227
227
  class App {
228
228
  errorPage = $page({
229
229
  path: "/error",
230
- resolve: () => {
230
+ loader: () => {
231
231
  throw new Error("Something went wrong");
232
232
  },
233
233
  errorHandler: (error) => (
@@ -259,7 +259,7 @@ describe("$page browser tests", () => {
259
259
  class App {
260
260
  layout = $page({
261
261
  path: "/",
262
- resolve: () => ({ appName: "My App" }),
262
+ loader: () => ({ appName: "My App" }),
263
263
  component: ({ appName }: { appName: string }) => (
264
264
  <div data-testid="layout">
265
265
  <header data-testid="header">{appName}</header>
@@ -316,7 +316,7 @@ describe("$page browser tests", () => {
316
316
  class App {
317
317
  layout = $page({
318
318
  path: "/",
319
- resolve: () => ({ theme: "dark" }),
319
+ loader: () => ({ theme: "dark" }),
320
320
  component: ({ theme }: { theme: string }) => (
321
321
  <div data-testid="layout" data-theme={theme}>
322
322
  <NestedView />
@@ -328,7 +328,7 @@ describe("$page browser tests", () => {
328
328
  page = $page({
329
329
  path: "/page",
330
330
  parent: this.layout,
331
- resolve: ({ theme }) => ({
331
+ loader: ({ theme }) => ({
332
332
  message: `Theme is ${theme}`,
333
333
  }),
334
334
  component: ({ message }: { message: string }) => (
@@ -362,7 +362,7 @@ describe("$page browser tests", () => {
362
362
  class App {
363
363
  root = $page({
364
364
  path: "/",
365
- resolve: () => ({ level: "root" }),
365
+ loader: () => ({ level: "root" }),
366
366
  component: ({ level }: { level: string }) => (
367
367
  <div data-testid="root">
368
368
  {level}
@@ -374,7 +374,7 @@ describe("$page browser tests", () => {
374
374
  section = $page({
375
375
  path: "/section",
376
376
  parent: this.root,
377
- resolve: ({ level }) => ({ level: `${level} > section` }),
377
+ loader: ({ level }) => ({ level: `${level} > section` }),
378
378
  component: ({ level }: { level: string }) => (
379
379
  <div data-testid="section">
380
380
  {level}
@@ -386,7 +386,7 @@ describe("$page browser tests", () => {
386
386
  page = $page({
387
387
  path: "/page",
388
388
  parent: this.section,
389
- resolve: ({ level }) => ({ level: `${level} > page` }),
389
+ loader: ({ level }) => ({ level: `${level} > page` }),
390
390
  component: ({ level }: { level: string }) => (
391
391
  <div data-testid="page">{level}</div>
392
392
  ),
@@ -469,7 +469,7 @@ describe("$page browser tests", () => {
469
469
  id: t.text(),
470
470
  }),
471
471
  },
472
- resolve: ({ params }) => ({
472
+ loader: ({ params }) => ({
473
473
  userId: params.id,
474
474
  }),
475
475
  component: ({ userId }: { userId: string }) => (
@@ -504,7 +504,7 @@ describe("$page browser tests", () => {
504
504
  page: t.number({ default: 1 }),
505
505
  }),
506
506
  },
507
- resolve: ({ query }) => ({
507
+ loader: ({ query }) => ({
508
508
  searchQuery: query.q,
509
509
  currentPage: query.page,
510
510
  }),
@@ -553,7 +553,7 @@ describe("$page browser tests", () => {
553
553
  page: t.number({ default: 1 }),
554
554
  }),
555
555
  },
556
- resolve: ({ query }) => ({
556
+ loader: ({ query }) => ({
557
557
  searchQuery: query.q,
558
558
  currentPage: query.page,
559
559
  }),
@@ -605,7 +605,7 @@ describe("$page browser tests", () => {
605
605
  limit: t.number({ default: 10 }),
606
606
  }),
607
607
  },
608
- resolve: ({ params, query }) => ({
608
+ loader: ({ params, query }) => ({
609
609
  userId: params.userId,
610
610
  sortBy: query.sort,
611
611
  limit: query.limit,
@@ -660,7 +660,7 @@ describe("$page browser tests", () => {
660
660
  page: t.number({ default: 1 }),
661
661
  }),
662
662
  },
663
- resolve: ({ query }) => ({
663
+ loader: ({ query }) => ({
664
664
  searchQuery: query.q,
665
665
  currentPage: query.page,
666
666
  }),
@@ -61,7 +61,7 @@ describe("$page primitive tests", () => {
61
61
  sort: t.optional(t.text()),
62
62
  }),
63
63
  },
64
- resolve: ({ params, query }) => ({ params, query }),
64
+ loader: ({ params, query }) => ({ params, query }),
65
65
  component: ({ params, query }) =>
66
66
  `User ${params.id} - Tab: ${query.tab}`,
67
67
  });
@@ -72,7 +72,7 @@ describe("$page primitive tests", () => {
72
72
 
73
73
  expect(app.user.options.schema?.params).toBeDefined();
74
74
  expect(app.user.options.schema?.query).toBeDefined();
75
- expect(app.user.options.resolve).toBeDefined();
75
+ expect(app.user.options.loader).toBeDefined();
76
76
  expect(app.user.options.component).toBeDefined();
77
77
 
78
78
  const rendered = await app.user.render({
@@ -89,7 +89,7 @@ describe("$page primitive tests", () => {
89
89
  class App {
90
90
  lazy = $page({
91
91
  path: "/lazy",
92
- resolve: () => ({ message: "loaded" }),
92
+ loader: () => ({ message: "loaded" }),
93
93
  lazy: async () => ({ default: LazyComponent }),
94
94
  });
95
95
  }
@@ -141,7 +141,7 @@ describe("$page primitive tests", () => {
141
141
  id: t.text(),
142
142
  }),
143
143
  },
144
- resolve: async ({ params }) => {
144
+ loader: async ({ params }) => {
145
145
  await new Promise((resolve) => setTimeout(resolve, 10));
146
146
  return { data: `Data for ${params.id}`, timestamp: Date.now() };
147
147
  },
@@ -152,8 +152,8 @@ describe("$page primitive tests", () => {
152
152
  const app = alepha.inject(App);
153
153
  await alepha.start();
154
154
 
155
- expect(app.async.options.resolve).toBeDefined();
156
- expect(typeof app.async.options.resolve).toBe("function");
155
+ expect(app.async.options.loader).toBeDefined();
156
+ expect(typeof app.async.options.loader).toBe("function");
157
157
 
158
158
  const mockContext = {
159
159
  params: { id: "test" },
@@ -161,7 +161,7 @@ describe("$page primitive tests", () => {
161
161
  pathname: "/async/test",
162
162
  search: "",
163
163
  };
164
- const result = await app.async.options.resolve!(mockContext as any);
164
+ const result = await app.async.options.loader!(mockContext as any);
165
165
  expect(result.data).toBe("Data for test");
166
166
  expect(typeof result.timestamp).toBe("number");
167
167
 
@@ -173,14 +173,14 @@ describe("$page primitive tests", () => {
173
173
  class App {
174
174
  parent = $page({
175
175
  path: "/parent",
176
- resolve: () => ({ parentData: "from parent" }),
176
+ loader: () => ({ parentData: "from parent" }),
177
177
  children: [],
178
178
  });
179
179
 
180
180
  child = $page({
181
181
  path: "/child",
182
182
  parent: this.parent,
183
- resolve: ({ parentData }) => ({
183
+ loader: ({ parentData }) => ({
184
184
  childData: `child with ${parentData}`,
185
185
  }),
186
186
  component: ({ childData }) => childData,
@@ -261,7 +261,7 @@ describe("$page primitive tests", () => {
261
261
  class App {
262
262
  errorPage = $page({
263
263
  path: "/error",
264
- resolve: () => {
264
+ loader: () => {
265
265
  throw new Error("Test error");
266
266
  },
267
267
  errorHandler: (error) => `Error: ${error.message}`,
@@ -289,7 +289,7 @@ describe("$page primitive tests", () => {
289
289
  class App {
290
290
  errorPage = $page({
291
291
  path: "/error",
292
- resolve: () => {
292
+ loader: () => {
293
293
  throw new Error("unauthorized");
294
294
  },
295
295
  errorHandler: (error) => {
@@ -342,7 +342,7 @@ describe("$page primitive tests", () => {
342
342
  static: {
343
343
  entries: [{ params: { id: "1" } }, { params: { id: "2" } }],
344
344
  },
345
- resolve: ({ params }) => ({ id: params.id }),
345
+ loader: ({ params }) => ({ id: params.id }),
346
346
  component: ({ id }) => `Static page ${id}`,
347
347
  });
348
348
  }
@@ -566,7 +566,7 @@ describe("$page primitive tests", () => {
566
566
  limit: t.number({ default: 10 }),
567
567
  }),
568
568
  },
569
- resolve: ({ params, query }) => ({
569
+ loader: ({ params, query }) => ({
570
570
  user: { id: params.userId },
571
571
  pagination: { page: query.page, limit: query.limit },
572
572
  filters: query.filters,
@@ -582,7 +582,7 @@ describe("$page primitive tests", () => {
582
582
 
583
583
  expect(app.complex.options.schema?.params).toBeDefined();
584
584
  expect(app.complex.options.schema?.query).toBeDefined();
585
- expect(app.complex.options.resolve).toBeDefined();
585
+ expect(app.complex.options.loader).toBeDefined();
586
586
 
587
587
  const rendered = await app.complex.render({
588
588
  params: { userId: "123" },
@@ -608,7 +608,7 @@ describe("$page primitive tests", () => {
608
608
 
609
609
  child = $page({
610
610
  path: "/child",
611
- resolve: () => {
611
+ loader: () => {
612
612
  throw new Error("Child error");
613
613
  },
614
614
  errorHandler: (error) => {
@@ -648,13 +648,13 @@ describe("$page primitive tests", () => {
648
648
  test("$page - resolve function receives parent props", async ({ expect }) => {
649
649
  class App {
650
650
  parent = $page({
651
- resolve: () => ({ parentValue: "from parent" }),
651
+ loader: () => ({ parentValue: "from parent" }),
652
652
  });
653
653
 
654
654
  child = $page({
655
655
  path: "/child",
656
656
  parent: this.parent,
657
- resolve: ({ parentValue }) => ({
657
+ loader: ({ parentValue }) => ({
658
658
  childData: `Child received: ${parentValue}`,
659
659
  }),
660
660
  component: ({ childData, parentValue }) =>
@@ -665,7 +665,7 @@ describe("$page primitive tests", () => {
665
665
  const app = alepha.inject(App);
666
666
  await alepha.start();
667
667
 
668
- expect(app.child.options.resolve).toBeDefined();
668
+ expect(app.child.options.loader).toBeDefined();
669
669
 
670
670
  const rendered = await app.child.fetch();
671
671
  expect(rendered.html).toBe("Child received: from parent and from parent");
@@ -14,6 +14,9 @@ import type { Redirection } from "../errors/Redirection.ts";
14
14
  import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
15
15
  import { ReactPageService } from "../services/ReactPageService.ts";
16
16
  import type { ClientOnlyProps } from "@alepha/react";
17
+ import type { Head } from "@alepha/react/head";
18
+ import { PAGE_PRELOAD_KEY } from "../constants/PAGE_PRELOAD_KEY.ts";
19
+
17
20
 
18
21
  /**
19
22
  * Main primitive for defining a React route in the application.
@@ -27,7 +30,7 @@ import type { ClientOnlyProps } from "@alepha/react";
27
30
  * - Type-safe URL parameter and query string validation
28
31
  *
29
32
  * **Data Loading**
30
- * - Server-side data fetching with the `resolve` function
33
+ * - Server-side data fetching with the `loader` function
31
34
  * - Automatic serialization and hydration for SSR
32
35
  * - Access to request context, URL params, and parent data
33
36
  *
@@ -64,7 +67,7 @@ import type { ClientOnlyProps } from "@alepha/react";
64
67
  * params: t.object({ id: t.integer() }),
65
68
  * query: t.object({ tab: t.optional(t.text()) })
66
69
  * },
67
- * resolve: async ({ params }) => {
70
+ * loader: async ({ params }) => {
68
71
  * const user = await userApi.getUser(params.id);
69
72
  * return { user };
70
73
  * },
@@ -77,7 +80,7 @@ import type { ClientOnlyProps } from "@alepha/react";
77
80
  * const projectSection = $page({
78
81
  * path: "/projects/:id",
79
82
  * children: () => [projectBoard, projectSettings],
80
- * resolve: async ({ params }) => {
83
+ * loader: async ({ params }) => {
81
84
  * const project = await projectApi.get(params.id);
82
85
  * return { project };
83
86
  * },
@@ -96,7 +99,7 @@ import type { ClientOnlyProps } from "@alepha/react";
96
99
  * static: {
97
100
  * entries: posts.map(p => ({ params: { slug: p.slug } }))
98
101
  * },
99
- * resolve: async ({ params }) => {
102
+ * loader: async ({ params }) => {
100
103
  * const post = await loadPost(params.slug);
101
104
  * return { post };
102
105
  * }
@@ -155,12 +158,12 @@ export interface PagePrimitiveOptions<
155
158
  *
156
159
  * > In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
157
160
  *
158
- * Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
161
+ * Loader can be stopped by throwing an error, which will be handled by the `errorHandler` function.
159
162
  * It's common to throw a `NotFoundError` to display a 404 page.
160
163
  *
161
164
  * RedirectError can be thrown to redirect the user to another page.
162
165
  */
163
- resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
166
+ loader?: (context: PageLoader<TConfig, TPropsParent>) => Async<TProps>;
164
167
 
165
168
  /**
166
169
  * Default props to pass to the component when rendering the page.
@@ -205,7 +208,7 @@ export interface PagePrimitiveOptions<
205
208
  can?: () => boolean;
206
209
 
207
210
  /**
208
- * Catch any error from the `resolve` function or during `rendering`.
211
+ * Catch any error from the `loader` function or during `rendering`.
209
212
  *
210
213
  * Expected to return one of the following:
211
214
  * - a ReactNode to render an error page
@@ -217,7 +220,7 @@ export interface PagePrimitiveOptions<
217
220
  *
218
221
  * @example Catch a 404 from API and render a custom not found component:
219
222
  * ```ts
220
- * resolve: async ({ params, query }) => {
223
+ * loader: async ({ params, query }) => {
221
224
  * api.fetch("/api/resource", { params, query });
222
225
  * },
223
226
  * errorHandler: (error, context) => {
@@ -229,7 +232,7 @@ export interface PagePrimitiveOptions<
229
232
  *
230
233
  * @example Catch an 401 error and redirect the user to the login page:
231
234
  * ```ts
232
- * resolve: async ({ params, query }) => {
235
+ * loader: async ({ params, query }) => {
233
236
  * // but the user is not authenticated
234
237
  * api.fetch("/api/resource", { params, query });
235
238
  * },
@@ -318,6 +321,39 @@ export interface PagePrimitiveOptions<
318
321
  * ```
319
322
  */
320
323
  animation?: PageAnimation;
324
+
325
+ /**
326
+ * Head configuration for the page (title, meta tags, etc.).
327
+ *
328
+ * Can be a static object or a function that receives resolved props.
329
+ *
330
+ * @example Static head
331
+ * ```ts
332
+ * head: {
333
+ * title: "My Page",
334
+ * description: "Page description",
335
+ * }
336
+ * ```
337
+ *
338
+ * @example Dynamic head based on props
339
+ * ```ts
340
+ * head: (props) => ({
341
+ * title: props.user.name,
342
+ * description: `Profile of ${props.user.name}`,
343
+ * })
344
+ * ```
345
+ */
346
+ head?: Head | ((props: TProps, previous?: Head) => Head);
347
+
348
+ /**
349
+ * Source path for SSR module preloading.
350
+ *
351
+ * This is automatically injected by the viteAlephaPreload plugin.
352
+ * It maps to the source file path used in Vite's SSR manifest.
353
+ *
354
+ * @internal
355
+ */
356
+ [PAGE_PRELOAD_KEY]?: string;
321
357
  }
322
358
 
323
359
  export type ErrorHandler = (
@@ -422,7 +458,7 @@ export interface PageRequestConfig<
422
458
  : Record<string, string>;
423
459
  }
424
460
 
425
- export type PageResolve<
461
+ export type PageLoader<
426
462
  TConfig extends PageConfigSchema = PageConfigSchema,
427
463
  TPropsParent extends object = TPropsParentDefault,
428
464
  > = PageRequestConfig<TConfig> &