rari 0.5.16 → 0.5.18

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/dist/client.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as fetchWithTimeout, S as createNavigationError, _ as LayoutErrorBoundary, a as LoadingSpinner, b as NavigationErrorOverlay, c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, l as createLoadingBoundary, m as extractServerPropsWithCache, n as DefaultLoading, o as NotFound, p as extractServerProps, r as ErrorBoundary, s as createErrorBoundary, t as DefaultError, u as clearPropsCache, v as ClientRouter, w as LayoutManager, x as NavigationErrorHandler, y as StatePreserver } from "./runtime-client-AtnJSL0q.mjs";
1
+ import { C as fetchWithTimeout, S as createNavigationError, _ as LayoutErrorBoundary, a as LoadingSpinner, b as NavigationErrorOverlay, c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, l as createLoadingBoundary, m as extractServerPropsWithCache, n as DefaultLoading, o as NotFound, p as extractServerProps, r as ErrorBoundary, s as createErrorBoundary, t as DefaultError, u as clearPropsCache, v as ClientRouter, w as LayoutManager, x as NavigationErrorHandler, y as StatePreserver } from "./runtime-client-DCkfDJpL.mjs";
2
2
 
3
3
  export { ClientRouter, DefaultError, DefaultLoading, ErrorBoundary, HttpRuntimeClient, LayoutErrorBoundary, LayoutManager, LoadingSpinner, NavigationErrorHandler, NavigationErrorOverlay, NotFound, StatePreserver, clearPropsCache, clearPropsCacheForComponent, createErrorBoundary, createHttpRuntimeClient, createLoadingBoundary, createNavigationError, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, fetchWithTimeout, hasServerSideDataFetching };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as headers, i as rariRouter, n as defineRariOptions, o as RariResponse, r as rari, t as defineRariConfig } from "./vite-B_QMp3M4.mjs";
2
2
  import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-DZjfJPdB.mjs";
3
- import { c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, m as extractServerPropsWithCache, p as extractServerProps, u as clearPropsCache } from "./runtime-client-AtnJSL0q.mjs";
3
+ import { c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, m as extractServerPropsWithCache, p as extractServerProps, u as clearPropsCache } from "./runtime-client-DCkfDJpL.mjs";
4
4
  import "./server-build-ChAPR3tl.mjs";
5
5
 
6
6
  export { AppRouteGenerator, HttpRuntimeClient, RariResponse, clearPropsCache, clearPropsCacheForComponent, createHttpRuntimeClient, defineRariConfig, defineRariOptions, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, generateAppRouteManifest, hasServerSideDataFetching, headers, loadManifest, rari, rariRouter, writeManifest };
@@ -923,6 +923,112 @@ var StatePreserver = class {
923
923
 
924
924
  //#endregion
925
925
  //#region src/router/ClientRouter.tsx
926
+ function updateDocumentMetadata(metadata) {
927
+ if (metadata.title) document.title = metadata.title;
928
+ const updateOrCreateMetaTag = (selector, attributes) => {
929
+ let element = document.querySelector(selector);
930
+ if (!element) {
931
+ element = document.createElement("meta");
932
+ for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value);
933
+ document.head.appendChild(element);
934
+ } else if (attributes.content) element.setAttribute("content", attributes.content);
935
+ };
936
+ if (metadata.description) updateOrCreateMetaTag("meta[name=\"description\"]", {
937
+ name: "description",
938
+ content: metadata.description
939
+ });
940
+ if (metadata.keywords && metadata.keywords.length > 0) updateOrCreateMetaTag("meta[name=\"keywords\"]", {
941
+ name: "keywords",
942
+ content: metadata.keywords.join(", ")
943
+ });
944
+ if (metadata.viewport) updateOrCreateMetaTag("meta[name=\"viewport\"]", {
945
+ name: "viewport",
946
+ content: metadata.viewport
947
+ });
948
+ if (metadata.canonical) {
949
+ let canonical = document.querySelector("link[rel=\"canonical\"]");
950
+ if (!canonical) {
951
+ canonical = document.createElement("link");
952
+ canonical.setAttribute("rel", "canonical");
953
+ document.head.appendChild(canonical);
954
+ }
955
+ canonical.setAttribute("href", metadata.canonical);
956
+ }
957
+ if (metadata.robots) {
958
+ const robotsContent = [];
959
+ if (metadata.robots.index !== void 0) robotsContent.push(metadata.robots.index ? "index" : "noindex");
960
+ if (metadata.robots.follow !== void 0) robotsContent.push(metadata.robots.follow ? "follow" : "nofollow");
961
+ if (metadata.robots.nocache) robotsContent.push("nocache");
962
+ if (robotsContent.length > 0) updateOrCreateMetaTag("meta[name=\"robots\"]", {
963
+ name: "robots",
964
+ content: robotsContent.join(", ")
965
+ });
966
+ }
967
+ if (metadata.openGraph) {
968
+ const og = metadata.openGraph;
969
+ if (og.title) updateOrCreateMetaTag("meta[property=\"og:title\"]", {
970
+ property: "og:title",
971
+ content: og.title
972
+ });
973
+ if (og.description) updateOrCreateMetaTag("meta[property=\"og:description\"]", {
974
+ property: "og:description",
975
+ content: og.description
976
+ });
977
+ if (og.url) updateOrCreateMetaTag("meta[property=\"og:url\"]", {
978
+ property: "og:url",
979
+ content: og.url
980
+ });
981
+ if (og.siteName) updateOrCreateMetaTag("meta[property=\"og:site_name\"]", {
982
+ property: "og:site_name",
983
+ content: og.siteName
984
+ });
985
+ if (og.type) updateOrCreateMetaTag("meta[property=\"og:type\"]", {
986
+ property: "og:type",
987
+ content: og.type
988
+ });
989
+ if (og.images && og.images.length > 0) {
990
+ document.querySelectorAll("meta[property=\"og:image\"]").forEach((el) => el.remove());
991
+ for (const image of og.images) {
992
+ const meta = document.createElement("meta");
993
+ meta.setAttribute("property", "og:image");
994
+ meta.setAttribute("content", image);
995
+ document.head.appendChild(meta);
996
+ }
997
+ }
998
+ }
999
+ if (metadata.twitter) {
1000
+ const twitter = metadata.twitter;
1001
+ if (twitter.card) updateOrCreateMetaTag("meta[name=\"twitter:card\"]", {
1002
+ name: "twitter:card",
1003
+ content: twitter.card
1004
+ });
1005
+ if (twitter.site) updateOrCreateMetaTag("meta[name=\"twitter:site\"]", {
1006
+ name: "twitter:site",
1007
+ content: twitter.site
1008
+ });
1009
+ if (twitter.creator) updateOrCreateMetaTag("meta[name=\"twitter:creator\"]", {
1010
+ name: "twitter:creator",
1011
+ content: twitter.creator
1012
+ });
1013
+ if (twitter.title) updateOrCreateMetaTag("meta[name=\"twitter:title\"]", {
1014
+ name: "twitter:title",
1015
+ content: twitter.title
1016
+ });
1017
+ if (twitter.description) updateOrCreateMetaTag("meta[name=\"twitter:description\"]", {
1018
+ name: "twitter:description",
1019
+ content: twitter.description
1020
+ });
1021
+ if (twitter.images && twitter.images.length > 0) {
1022
+ document.querySelectorAll("meta[name=\"twitter:image\"]").forEach((el) => el.remove());
1023
+ for (const image of twitter.images) {
1024
+ const meta = document.createElement("meta");
1025
+ meta.setAttribute("name", "twitter:image");
1026
+ meta.setAttribute("content", image);
1027
+ document.head.appendChild(meta);
1028
+ }
1029
+ }
1030
+ }
1031
+ }
926
1032
  function ClientRouter({ children, manifest, initialRoute }) {
927
1033
  const [navigationState, setNavigationState] = useState(() => ({
928
1034
  currentRoute: normalizePath(initialRoute),
@@ -1083,6 +1189,15 @@ function ClientRouter({ children, manifest, initialRoute }) {
1083
1189
  cleanupAbortedNavigation(targetPath, navigationId);
1084
1190
  return;
1085
1191
  }
1192
+ try {
1193
+ const metadataHeader = response.headers.get("x-rari-metadata");
1194
+ if (metadataHeader) {
1195
+ const decodedMetadata = decodeURIComponent(metadataHeader);
1196
+ updateDocumentMetadata(JSON.parse(decodedMetadata));
1197
+ }
1198
+ } catch (metadataError) {
1199
+ console.warn("[ClientRouter] Failed to extract/apply metadata:", metadataError);
1200
+ }
1086
1201
  const rscWireFormat = await response.text();
1087
1202
  window.dispatchEvent(new CustomEvent("rari:navigate", { detail: {
1088
1203
  from: fromRoute,
@@ -1443,7 +1558,6 @@ async function extractMetadata(componentPath, params, searchParams) {
1443
1558
  /* @vite-ignore */
1444
1559
  componentPath
1445
1560
  );
1446
- if (module.metadata && typeof module.metadata === "object") return module.metadata;
1447
1561
  if (typeof module.generateMetadata === "function") {
1448
1562
  const metadata = await module.generateMetadata({
1449
1563
  params,
@@ -1451,6 +1565,7 @@ async function extractMetadata(componentPath, params, searchParams) {
1451
1565
  });
1452
1566
  if (metadata && typeof metadata === "object") return metadata;
1453
1567
  }
1568
+ if (module.metadata && typeof module.metadata === "object") return module.metadata;
1454
1569
  return {};
1455
1570
  } catch (error) {
1456
1571
  console.error(`Failed to extract metadata from ${componentPath}:`, error);
package/dist/vite.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as headers, i as rariRouter, n as defineRariOptions, o as RariResponse, r as rari, t as defineRariConfig } from "./vite-B_QMp3M4.mjs";
2
2
  import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-DZjfJPdB.mjs";
3
- import { c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, m as extractServerPropsWithCache, p as extractServerProps, u as clearPropsCache } from "./runtime-client-AtnJSL0q.mjs";
3
+ import { c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, m as extractServerPropsWithCache, p as extractServerProps, u as clearPropsCache } from "./runtime-client-DCkfDJpL.mjs";
4
4
  import "./server-build-ChAPR3tl.mjs";
5
5
 
6
6
  export { AppRouteGenerator, HttpRuntimeClient, RariResponse, clearPropsCache, clearPropsCacheForComponent, createHttpRuntimeClient, defineRariConfig, defineRariOptions, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, generateAppRouteManifest, hasServerSideDataFetching, headers, loadManifest, rari, rariRouter, writeManifest };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rari",
3
3
  "type": "module",
4
- "version": "0.5.16",
4
+ "version": "0.5.18",
5
5
  "description": "Runtime Accelerated Rendering Infrastructure (Rari)",
6
6
  "author": "Ryan Skinner",
7
7
  "license": "MIT",
@@ -89,11 +89,11 @@
89
89
  "picocolors": "^1.1.1"
90
90
  },
91
91
  "optionalDependencies": {
92
- "rari-darwin-arm64": "0.5.10",
93
- "rari-darwin-x64": "0.5.10",
94
- "rari-linux-arm64": "0.5.10",
95
- "rari-linux-x64": "0.5.10",
96
- "rari-win32-x64": "0.5.10"
92
+ "rari-darwin-arm64": "0.5.12",
93
+ "rari-darwin-x64": "0.5.12",
94
+ "rari-linux-arm64": "0.5.12",
95
+ "rari-linux-x64": "0.5.12",
96
+ "rari-win32-x64": "0.5.12"
97
97
  },
98
98
  "devDependencies": {
99
99
  "@types/node": "^25.0.1",
@@ -12,6 +12,193 @@ import { extractPathname, findLayoutChain, isExternalUrl, matchRouteParams, norm
12
12
  import { NavigationErrorOverlay } from './NavigationErrorOverlay'
13
13
  import { StatePreserver } from './StatePreserver'
14
14
 
15
+ interface PageMetadata {
16
+ title?: string
17
+ description?: string
18
+ keywords?: string[]
19
+ viewport?: string
20
+ canonical?: string
21
+ openGraph?: {
22
+ title?: string
23
+ description?: string
24
+ url?: string
25
+ siteName?: string
26
+ images?: string[]
27
+ type?: string
28
+ }
29
+ twitter?: {
30
+ card?: string
31
+ site?: string
32
+ creator?: string
33
+ title?: string
34
+ description?: string
35
+ images?: string[]
36
+ }
37
+ robots?: {
38
+ index?: boolean
39
+ follow?: boolean
40
+ nocache?: boolean
41
+ }
42
+ }
43
+
44
+ function updateDocumentMetadata(metadata: PageMetadata): void {
45
+ if (metadata.title) {
46
+ document.title = metadata.title
47
+ }
48
+
49
+ const updateOrCreateMetaTag = (selector: string, attributes: Record<string, string>) => {
50
+ let element = document.querySelector(selector) as HTMLMetaElement | null
51
+ if (!element) {
52
+ element = document.createElement('meta')
53
+ for (const [key, value] of Object.entries(attributes)) {
54
+ element.setAttribute(key, value)
55
+ }
56
+ document.head.appendChild(element)
57
+ }
58
+ else {
59
+ if (attributes.content) {
60
+ element.setAttribute('content', attributes.content)
61
+ }
62
+ }
63
+ }
64
+
65
+ if (metadata.description) {
66
+ updateOrCreateMetaTag('meta[name="description"]', {
67
+ name: 'description',
68
+ content: metadata.description,
69
+ })
70
+ }
71
+
72
+ if (metadata.keywords && metadata.keywords.length > 0) {
73
+ updateOrCreateMetaTag('meta[name="keywords"]', {
74
+ name: 'keywords',
75
+ content: metadata.keywords.join(', '),
76
+ })
77
+ }
78
+
79
+ if (metadata.viewport) {
80
+ updateOrCreateMetaTag('meta[name="viewport"]', {
81
+ name: 'viewport',
82
+ content: metadata.viewport,
83
+ })
84
+ }
85
+
86
+ if (metadata.canonical) {
87
+ let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null
88
+ if (!canonical) {
89
+ canonical = document.createElement('link')
90
+ canonical.setAttribute('rel', 'canonical')
91
+ document.head.appendChild(canonical)
92
+ }
93
+ canonical.setAttribute('href', metadata.canonical)
94
+ }
95
+
96
+ if (metadata.robots) {
97
+ const robotsContent: string[] = []
98
+ if (metadata.robots.index !== undefined) {
99
+ robotsContent.push(metadata.robots.index ? 'index' : 'noindex')
100
+ }
101
+ if (metadata.robots.follow !== undefined) {
102
+ robotsContent.push(metadata.robots.follow ? 'follow' : 'nofollow')
103
+ }
104
+ if (metadata.robots.nocache) {
105
+ robotsContent.push('nocache')
106
+ }
107
+ if (robotsContent.length > 0) {
108
+ updateOrCreateMetaTag('meta[name="robots"]', {
109
+ name: 'robots',
110
+ content: robotsContent.join(', '),
111
+ })
112
+ }
113
+ }
114
+
115
+ if (metadata.openGraph) {
116
+ const og = metadata.openGraph
117
+ if (og.title) {
118
+ updateOrCreateMetaTag('meta[property="og:title"]', {
119
+ property: 'og:title',
120
+ content: og.title,
121
+ })
122
+ }
123
+ if (og.description) {
124
+ updateOrCreateMetaTag('meta[property="og:description"]', {
125
+ property: 'og:description',
126
+ content: og.description,
127
+ })
128
+ }
129
+ if (og.url) {
130
+ updateOrCreateMetaTag('meta[property="og:url"]', {
131
+ property: 'og:url',
132
+ content: og.url,
133
+ })
134
+ }
135
+ if (og.siteName) {
136
+ updateOrCreateMetaTag('meta[property="og:site_name"]', {
137
+ property: 'og:site_name',
138
+ content: og.siteName,
139
+ })
140
+ }
141
+ if (og.type) {
142
+ updateOrCreateMetaTag('meta[property="og:type"]', {
143
+ property: 'og:type',
144
+ content: og.type,
145
+ })
146
+ }
147
+ if (og.images && og.images.length > 0) {
148
+ document.querySelectorAll('meta[property="og:image"]').forEach(el => el.remove())
149
+ for (const image of og.images) {
150
+ const meta = document.createElement('meta')
151
+ meta.setAttribute('property', 'og:image')
152
+ meta.setAttribute('content', image)
153
+ document.head.appendChild(meta)
154
+ }
155
+ }
156
+ }
157
+
158
+ if (metadata.twitter) {
159
+ const twitter = metadata.twitter
160
+ if (twitter.card) {
161
+ updateOrCreateMetaTag('meta[name="twitter:card"]', {
162
+ name: 'twitter:card',
163
+ content: twitter.card,
164
+ })
165
+ }
166
+ if (twitter.site) {
167
+ updateOrCreateMetaTag('meta[name="twitter:site"]', {
168
+ name: 'twitter:site',
169
+ content: twitter.site,
170
+ })
171
+ }
172
+ if (twitter.creator) {
173
+ updateOrCreateMetaTag('meta[name="twitter:creator"]', {
174
+ name: 'twitter:creator',
175
+ content: twitter.creator,
176
+ })
177
+ }
178
+ if (twitter.title) {
179
+ updateOrCreateMetaTag('meta[name="twitter:title"]', {
180
+ name: 'twitter:title',
181
+ content: twitter.title,
182
+ })
183
+ }
184
+ if (twitter.description) {
185
+ updateOrCreateMetaTag('meta[name="twitter:description"]', {
186
+ name: 'twitter:description',
187
+ content: twitter.description,
188
+ })
189
+ }
190
+ if (twitter.images && twitter.images.length > 0) {
191
+ document.querySelectorAll('meta[name="twitter:image"]').forEach(el => el.remove())
192
+ for (const image of twitter.images) {
193
+ const meta = document.createElement('meta')
194
+ meta.setAttribute('name', 'twitter:image')
195
+ meta.setAttribute('content', image)
196
+ document.head.appendChild(meta)
197
+ }
198
+ }
199
+ }
200
+ }
201
+
15
202
  export interface ClientRouterProps {
16
203
  children: React.ReactNode
17
204
  manifest: AppRouteManifest
@@ -305,6 +492,18 @@ export function ClientRouter({ children, manifest, initialRoute }: ClientRouterP
305
492
  return
306
493
  }
307
494
 
495
+ try {
496
+ const metadataHeader = response.headers.get('x-rari-metadata')
497
+ if (metadataHeader) {
498
+ const decodedMetadata = decodeURIComponent(metadataHeader)
499
+ const metadata = JSON.parse(decodedMetadata) as PageMetadata
500
+ updateDocumentMetadata(metadata)
501
+ }
502
+ }
503
+ catch (metadataError) {
504
+ console.warn('[ClientRouter] Failed to extract/apply metadata:', metadataError)
505
+ }
506
+
308
507
  const rscWireFormat = await response.text()
309
508
 
310
509
  window.dispatchEvent(new CustomEvent('rari:navigate', {
@@ -80,11 +80,13 @@ export type { NavigationErrorOverlayProps } from './NavigationErrorOverlay'
80
80
  export {
81
81
  clearPropsCache,
82
82
  clearPropsCacheForComponent,
83
+ collectMetadataFromChain,
83
84
  extractMetadata,
84
85
  extractServerProps,
85
86
  extractServerPropsWithCache,
86
87
  extractStaticParams,
87
88
  hasServerSideDataFetching,
89
+ mergeMetadata,
88
90
  } from './props-extractor'
89
91
 
90
92
  export type {
@@ -162,10 +162,6 @@ export async function extractMetadata(
162
162
  try {
163
163
  const module = await import(/* @vite-ignore */ componentPath)
164
164
 
165
- if (module.metadata && typeof module.metadata === 'object') {
166
- return module.metadata
167
- }
168
-
169
165
  if (typeof module.generateMetadata === 'function') {
170
166
  const metadata = await module.generateMetadata({ params, searchParams })
171
167
  if (metadata && typeof metadata === 'object') {
@@ -173,6 +169,10 @@ export async function extractMetadata(
173
169
  }
174
170
  }
175
171
 
172
+ if (module.metadata && typeof module.metadata === 'object') {
173
+ return module.metadata
174
+ }
175
+
176
176
  return {}
177
177
  }
178
178
  catch (error) {
@@ -181,6 +181,60 @@ export async function extractMetadata(
181
181
  }
182
182
  }
183
183
 
184
+ export function mergeMetadata(
185
+ parentMetadata: MetadataResult,
186
+ childMetadata: MetadataResult,
187
+ ): MetadataResult {
188
+ const merged: MetadataResult = { ...parentMetadata }
189
+
190
+ if (childMetadata.title !== undefined) {
191
+ if (typeof childMetadata.title === 'string') {
192
+ if (typeof parentMetadata.title === 'object' && parentMetadata.title?.template) {
193
+ merged.title = parentMetadata.title.template.replace('%s', childMetadata.title)
194
+ }
195
+ else {
196
+ merged.title = childMetadata.title
197
+ }
198
+ }
199
+ else {
200
+ merged.title = childMetadata.title
201
+ }
202
+ }
203
+
204
+ if (childMetadata.description !== undefined) {
205
+ merged.description = childMetadata.description
206
+ }
207
+ if (childMetadata.keywords !== undefined) {
208
+ merged.keywords = childMetadata.keywords
209
+ }
210
+ if (childMetadata.openGraph !== undefined) {
211
+ merged.openGraph = { ...parentMetadata.openGraph, ...childMetadata.openGraph }
212
+ }
213
+ if (childMetadata.twitter !== undefined) {
214
+ merged.twitter = { ...parentMetadata.twitter, ...childMetadata.twitter }
215
+ }
216
+ if (childMetadata.robots !== undefined) {
217
+ merged.robots = { ...parentMetadata.robots, ...childMetadata.robots }
218
+ }
219
+ if (childMetadata.icons !== undefined) {
220
+ merged.icons = { ...parentMetadata.icons, ...childMetadata.icons }
221
+ }
222
+ if (childMetadata.manifest !== undefined) {
223
+ merged.manifest = childMetadata.manifest
224
+ }
225
+ if (childMetadata.viewport !== undefined) {
226
+ merged.viewport = { ...parentMetadata.viewport, ...childMetadata.viewport }
227
+ }
228
+ if (childMetadata.verification !== undefined) {
229
+ merged.verification = { ...parentMetadata.verification, ...childMetadata.verification }
230
+ }
231
+ if (childMetadata.alternates !== undefined) {
232
+ merged.alternates = { ...parentMetadata.alternates, ...childMetadata.alternates }
233
+ }
234
+
235
+ return merged
236
+ }
237
+
184
238
  export async function extractStaticParams(
185
239
  componentPath: string,
186
240
  ): Promise<StaticParamsResult> {
@@ -260,3 +314,22 @@ export function clearPropsCacheForComponent(componentPath: string): void {
260
314
  }
261
315
  }
262
316
  }
317
+
318
+ export async function collectMetadataFromChain(
319
+ layoutPaths: string[],
320
+ pagePath: string,
321
+ params: Record<string, string>,
322
+ searchParams: Record<string, string>,
323
+ ): Promise<MetadataResult> {
324
+ let metadata: MetadataResult = {}
325
+
326
+ for (const layoutPath of layoutPaths) {
327
+ const layoutMetadata = await extractMetadata(layoutPath, params, searchParams)
328
+ metadata = mergeMetadata(metadata, layoutMetadata)
329
+ }
330
+
331
+ const pageMetadata = await extractMetadata(pagePath, params, searchParams)
332
+ metadata = mergeMetadata(metadata, pageMetadata)
333
+
334
+ return metadata
335
+ }