@merkle-open/magnolia-headless-frontend-nextjs 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Magnolia NextJs Headless Frontend
2
+
2
3
  - [Templates](./src/templates/_README.md)
3
4
  - [Proxy/Middleware](./src/middleware/_README.md)
4
5
  - [Config](./src/config/_README.md)
@@ -6,71 +7,90 @@
6
7
  - [Dynamic-Page](./README_dynamic-page.md)
7
8
 
8
9
  ## setup
10
+
9
11
  - Add the following dependencies to your package.json
10
- - `@merkle-open/magnolia-headless-frontend-nextjs`
11
- - [`tsyringe`](https://github.com/Microsoft/tsyringe)
12
- - `reflect-metadata`
13
- - `next`
14
- - `react`
15
- - `react-dom`
12
+ - `@merkle-open/magnolia-headless-frontend-nextjs`
13
+ - [`tsyringe`](https://github.com/Microsoft/tsyringe)
14
+ - `reflect-metadata`
15
+ - `next`
16
+ - `react`
17
+ - `react-dom`
16
18
  - Configure tsconfig.json to include the following settings (`./tsconfig.json`)
17
- ```json
18
- {
19
- "compilerOptions": {
20
- "experimentalDecorators": true,
21
- "emitDecoratorMetadata": true
22
- },
23
- "include": [
24
- "node_modules/@merkle-open/magnolia-headless-frontend-nextjs"
25
- ]
26
- }
27
- ```
19
+ ```json
20
+ {
21
+ "compilerOptions": {
22
+ "experimentalDecorators": true,
23
+ "emitDecoratorMetadata": true
24
+ },
25
+ "include": [
26
+ "node_modules/@merkle-open/magnolia-headless-frontend-nextjs"
27
+ ]
28
+ }
29
+ ```
28
30
  - Configure the [middleware composer](./src/middleware/_README.md#configure-middleware-composer)
29
- - Implement and bind the interfaces specified [here](./src/config/_README.md)
31
+ - Implement and bind the interfaces specified [here](./src/config/_README.md)
30
32
  - Implement API endpoints specified [here](./src/api/_README.md)
31
33
  - Configure [instrumentation.ts](https://nextjs.org/docs/app/guides/instrumentation#importing-files-with-side-effects) (`./instrumentation.ts`)
32
- ```typescript
33
- export async function register() {
34
- await import('reflect-metadata');
35
- }
36
- ```
34
+ ```typescript
35
+ export async function register() {
36
+ await import('reflect-metadata');
37
+ }
38
+ ```
37
39
  - Setup [dynamic page](./README_dynamic-page.md)
38
40
  - Setup Dependencies (`./Dependencies.ts`)
39
- ```typescript
40
- import 'reflect-metadata';
41
- import { container } from 'tsyringe';
42
- import { register as registerOss } from '@merkle-open/magnolia-headless-frontend-nextjs';
43
- import registerConfigs from './config/_Dependencies.ts';
44
-
45
- registerConfigs(container);
46
- registerOss(container);
47
-
48
- export { container };
49
- ```
41
+
42
+ ```typescript
43
+ import 'reflect-metadata';
44
+ import { container } from 'tsyringe';
45
+ import { register as registerOss } from '@merkle-open/magnolia-headless-frontend-nextjs';
46
+ import registerConfigs from './config/_Dependencies.ts';
47
+
48
+ registerConfigs(container);
49
+ registerOss(container);
50
+
51
+ export { container };
52
+ ```
53
+
50
54
  - Setup next.config.ts (`./next.config.ts`)
51
- ```typescript
52
- import type { NextConfig } from 'next';
53
- import type { Rewrite } from 'next/dist/lib/load-custom-routes';
54
-
55
- const nextConfig: NextConfig = {
56
- transpilePackages: ['@merkle-open/magnolia-headless-frontend-nextjs'],
57
- async rewrites(): Promise<Rewrite[]> {
58
- return [
59
- {
60
- source: '/robots.txt',
61
- destination: '/api/robots.txt',
62
- },
63
- {
64
- source: '/:language([a-z]{2})/sitemap/:type',
65
- destination: '/api/:language/sitemap/:type',
66
- },
67
- {
68
- source: '/:language([a-z]{2})/:path*',
69
- destination: '/dynamic/:language/:path*',
70
- },
71
- ];
72
- },
73
- ...
74
- };
75
- export default nextConfig;
76
- ```
55
+
56
+ ```typescript
57
+ import type { NextConfig } from 'next';
58
+ import type { Rewrite } from 'next/dist/lib/load-custom-routes';
59
+
60
+ const nextConfig: NextConfig = {
61
+ transpilePackages: ['@merkle-open/magnolia-headless-frontend-nextjs'],
62
+ async rewrites(): Promise<Rewrite[]> {
63
+ return [
64
+ {
65
+ source: '/robots.txt',
66
+ destination: '/api/robots.txt',
67
+ },
68
+ {
69
+ source: '/:language([a-z]{2})/sitemap/:type',
70
+ destination: '/api/:language/sitemap/:type',
71
+ },
72
+ {
73
+ source: '/:language([a-z]{2})/:path*',
74
+ destination: '/dynamic/:language/:path*',
75
+ },
76
+ ];
77
+ },
78
+ ...
79
+ };
80
+ export default nextConfig;
81
+ ```
82
+
83
+ ## local development
84
+
85
+ npm link doesn't work with tsyringe, because the app and this lib's tsyringe modules have completely independent (global) variables (see [this comment](https://github.com/microsoft/tsyringe/pull/204#issuecomment-2401656829)).<br>
86
+ workaround using symbolic links:
87
+
88
+ ```json
89
+ {
90
+ ...
91
+ "scripts": {
92
+ "link-local-headlessOss": "npm link @merkle-open/magnolia-headless-frontend-nextjs && APP_DIR=`pwd` && cd node_modules/@merkle-open/magnolia-headless-frontend-nextjs && npm link ${APP_DIR}/node_modules/tsyringe"
93
+ },
94
+ ...
95
+ }
96
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkle-open/magnolia-headless-frontend-nextjs",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Magnolia headless frontend nextJs",
5
5
  "license": "MIT",
6
6
  "author": "Merkle Inc.",
@@ -37,7 +37,7 @@
37
37
  "react": "^19.2",
38
38
  "react-dom": "^19.2",
39
39
  "reflect-metadata": "^0.2",
40
- "tsyringe": "^4.4"
40
+ "tsyringe": "^4.10"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@eslint/js": "^9.39.3",
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import { ReactNode } from 'react';
4
2
  import { ErrorType } from '../helper/MagnoliaPageRestClient.ts';
5
3
  import { StaticErrorPage } from '../templates/pages/_error-static/ErrorStatic.tsx';
@@ -11,6 +9,7 @@ import { type StylesheetProviderI } from '../config/StylesheetProvider.ts';
11
9
  import { type ComponentMappingsProviderI } from '../config/ComponentMappingsProvider.ts';
12
10
  import { ThemeValidator } from '../helper/ThemeValidator.ts';
13
11
  import { EditablePage } from '../templates/pages/__magnolia-editable-page/EditablePage.tsx';
12
+ import { injectable } from 'tsyringe';
14
13
 
15
14
  export abstract class AbstractDynamicErrorPage extends AbstractDynamicPage {
16
15
  private readonly frontendApisProvider: FrontendApiEndpointsProvider;
@@ -29,11 +28,11 @@ export abstract class AbstractDynamicErrorPage extends AbstractDynamicPage {
29
28
  this.frontendApisProvider = configProvider.get().frontendApisProvider;
30
29
  }
31
30
 
32
- protected async renderDynamic(currentUrl: URL, errorType: ErrorType): Promise<ReactNode> {
31
+ protected async renderDynamic(currentUrl: URL, errorType: ErrorType, nonce?: string): Promise<ReactNode> {
33
32
  const magnoliaContext = this.magnoliaContextProvider.getMagnoliaContext(currentUrl);
34
33
  const content = await this.fetchErrorPageContent(magnoliaContext.currentLanguage, errorType);
35
34
  if (content) {
36
- return super.renderBase(magnoliaContext, content, {});
35
+ return super.renderBase(magnoliaContext, content, {}, nonce);
37
36
  }
38
37
  return Promise.reject(new Error('no dynamic error page maintained!'));
39
38
  }
@@ -49,3 +48,19 @@ export abstract class AbstractDynamicErrorPage extends AbstractDynamicPage {
49
48
  return fetch(errorPageUrl).then((response) => this.restClient.getJson(errorPageUrl, response));
50
49
  }
51
50
  }
51
+
52
+ @injectable()
53
+ export class ErrorPageLoader {
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
+ public render(errorType: ErrorType): ReactNode {
56
+ return <div>Loading...</div>;
57
+ }
58
+
59
+ public renderGlobal(errorType: ErrorType): ReactNode {
60
+ return (
61
+ <html>
62
+ <body>{this.render(errorType)}</body>
63
+ </html>
64
+ );
65
+ }
66
+ }
@@ -16,21 +16,20 @@ export abstract class AbstractDynamicPage {
16
16
  private readonly editablePage: EditablePage,
17
17
  ) {}
18
18
 
19
- protected async renderBase(magnoliaContext: ExtendedMagnoliaContext, content: PageProps, templateAnnotations: MgnlTemplateAnnotations): Promise<ReactNode> {
19
+ protected async renderBase(magnoliaContext: ExtendedMagnoliaContext, content: PageProps, templateAnnotations: MgnlTemplateAnnotations, nonce?: string): Promise<ReactNode> {
20
20
  const config: MagnoliaConfig = {
21
21
  componentMappings: this.componentMappingsProvider.getComponentMappings(),
22
22
  };
23
23
 
24
24
  RefService.setMagnoliaContextRef(magnoliaContext);
25
25
  global.mgnlInPageEditor = magnoliaContext.isMagnoliaEdit;
26
-
27
26
  return (
28
27
  <div>
29
28
  {/* is moved to <head> see https://react.dev/blog/2024/12/05/react-19#support-for-stylesheets */}
30
29
  {this.getStylesheet(magnoliaContext, content).map((stylesheet, index) => (
31
- <link rel="stylesheet" key={`AbstractDynamicPage-stylesheet-${index}`} href={stylesheet} precedence="high" />
30
+ <link rel="stylesheet" key={`AbstractDynamicPage-stylesheet-${index}`} href={stylesheet} precedence="high" nonce={nonce} />
32
31
  ))}
33
- {await this.editablePage.render({ content, config, templateAnnotations, magnoliaContext })}
32
+ {await this.editablePage.render({ content, config, templateAnnotations, magnoliaContext, nonce })}
34
33
  </div>
35
34
  );
36
35
  }
@@ -6,7 +6,7 @@ import { inject, injectable } from 'tsyringe';
6
6
  import { RestClient } from '../helper/RestClient.ts';
7
7
  import { MagnoliaContextProvider } from '../helper/MagnoliaContextProvider.ts';
8
8
  import { type HeadlessConfigProviderI, HEADLESS_CONFIG_PROVIDER_TOKEN } from '../config/ConfigProvider.ts';
9
- import { AbstractDynamicErrorPage } from './AbstractDynamicErrorPage.tsx';
9
+ import { AbstractDynamicErrorPage, ErrorPageLoader } from './AbstractDynamicErrorPage.tsx';
10
10
 
11
11
  import { type StylesheetProviderI, STYLESHEET_PROVIDER_TOKEN } from '../config/StylesheetProvider.ts';
12
12
  import { type ComponentMappingsProviderI } from '../config/ComponentMappingsProvider.ts';
@@ -14,6 +14,7 @@ import { StaticErrorPage } from '../templates/pages/_error-static/ErrorStatic.ts
14
14
  import { CombinedComponentMappingsProvider } from '../templates/ComponentMappingsProvider.ts';
15
15
  import { ThemeValidator } from '../helper/ThemeValidator.ts';
16
16
  import { EditablePage } from '../templates/pages/__magnolia-editable-page/EditablePage.tsx';
17
+ import { useCspHeaderNonce } from './provider/impl/CspHeaderNonceContext.tsx';
17
18
 
18
19
  @injectable()
19
20
  export class DynamicErrorPage extends AbstractDynamicErrorPage {
@@ -26,23 +27,24 @@ export class DynamicErrorPage extends AbstractDynamicErrorPage {
26
27
  @inject(RestClient) restClient: RestClient,
27
28
  @inject(StaticErrorPage) staticErrorPage: StaticErrorPage,
28
29
  @inject(MagnoliaContextProvider) magnoliaContextProvider: MagnoliaContextProvider,
30
+ @inject(ErrorPageLoader) private readonly errorPageLoader: ErrorPageLoader,
29
31
  ) {
30
32
  super(componentMappingsProvider, configProvider, StylesheetProviderI, themeValidator, editablePage, restClient, staticErrorPage, magnoliaContextProvider);
31
33
  }
32
34
 
33
35
  public render(errorType: ErrorType): ReactNode {
34
36
  const [errorPage, setErrorPage] = useState(null);
35
-
37
+ const nonce = useCspHeaderNonce();
36
38
  useEffect(() => {
37
39
  const currentUrl = new URL(window.location.href);
38
40
  super
39
- .renderDynamic(currentUrl, errorType)
40
- .then((content) => setErrorPage(content))
41
- .catch(() => setErrorPage(super.renderStatic(this.getLanguage(currentUrl), errorType)));
41
+ .renderDynamic(currentUrl, errorType, nonce)
42
+ .catch(() => super.renderStatic(this.getLanguage(currentUrl), errorType))
43
+ .then((content) => setErrorPage(content));
42
44
  }, []);
43
45
 
44
46
  if (!errorPage) {
45
- return <div>Loading...</div>;
47
+ return this.errorPageLoader.render(errorType);
46
48
  }
47
49
  return errorPage;
48
50
  }
@@ -4,7 +4,7 @@ import { ReactNode, useEffect, useState } from 'react';
4
4
  import { ErrorType } from '../helper/MagnoliaPageRestClient.ts';
5
5
  import { inject, injectable } from 'tsyringe';
6
6
  import { DynamicPageLayout } from './DynamicPageLayout.tsx';
7
- import { AbstractDynamicErrorPage } from './AbstractDynamicErrorPage.tsx';
7
+ import { AbstractDynamicErrorPage, ErrorPageLoader } from './AbstractDynamicErrorPage.tsx';
8
8
  import { type HeadlessConfigProviderI, HEADLESS_CONFIG_PROVIDER_TOKEN } from '../config/ConfigProvider.ts';
9
9
  import { RestClient } from '../helper/RestClient.ts';
10
10
  import { MagnoliaContextProvider } from '../helper/MagnoliaContextProvider.ts';
@@ -15,6 +15,7 @@ import { StaticErrorPage } from '../templates/pages/_error-static/ErrorStatic.ts
15
15
  import { CombinedComponentMappingsProvider } from '../templates/ComponentMappingsProvider.ts';
16
16
  import { ThemeValidator } from '../helper/ThemeValidator.ts';
17
17
  import { EditablePage } from '../templates/pages/__magnolia-editable-page/EditablePage.tsx';
18
+ import { useCspHeaderNonce } from './provider/impl/CspHeaderNonceContext.tsx';
18
19
 
19
20
  @injectable()
20
21
  export class DynamicErrorPageGlobal extends AbstractDynamicErrorPage {
@@ -28,42 +29,32 @@ export class DynamicErrorPageGlobal extends AbstractDynamicErrorPage {
28
29
  @inject(StaticErrorPage) staticErrorPage: StaticErrorPage,
29
30
  @inject(MagnoliaContextProvider) magnoliaContextProvider: MagnoliaContextProvider,
30
31
  @inject(DynamicPageLayout) private readonly dynamicPageLayout: DynamicPageLayout,
32
+ @inject(ErrorPageLoader) private readonly errorPageLoader: ErrorPageLoader,
31
33
  ) {
32
34
  super(componentMappingsProvider, configProvider, StylesheetProviderI, themeValidator, editablePage, restClient, staticErrorPage, magnoliaContextProvider);
33
35
  }
34
36
 
35
37
  public render(errorType: ErrorType): ReactNode {
36
38
  const [errorPage, setErrorPage] = useState(null);
37
-
39
+ const nonce = useCspHeaderNonce();
38
40
  useEffect(() => {
39
41
  const language = this.getLanguage();
40
- this.renderGlobalDynamicError(language, errorType)
42
+ this.renderGlobalDynamicError(language, errorType, nonce)
41
43
  .then((content) => setErrorPage(content))
42
44
  .catch(() => setErrorPage(this.renderGlobalStaticError(language, errorType)));
43
45
  }, []);
44
46
 
45
47
  if (!errorPage) {
46
- return this.renderLoader(errorType);
48
+ return this.errorPageLoader.renderGlobal(errorType);
47
49
  }
48
50
  return errorPage;
49
51
  }
50
52
 
51
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
52
- protected renderLoader(errorType: ErrorType): ReactNode {
53
- return (
54
- <html>
55
- <body>
56
- <div>Loading...</div>
57
- </body>
58
- </html>
59
- );
60
- }
61
-
62
- private async renderGlobalDynamicError(language: string, errorType: ErrorType): Promise<ReactNode> {
53
+ private async renderGlobalDynamicError(language: string, errorType: ErrorType, nonce: string): Promise<ReactNode> {
63
54
  try {
64
55
  const currentUrl = new URL(window.location.href);
65
56
  currentUrl.pathname = language;
66
- return super.renderDynamic(currentUrl, errorType).then((errorPage) => this.dynamicPageLayout.render(language, errorPage));
57
+ return super.renderDynamic(currentUrl, errorType, nonce).then((errorPage) => this.dynamicPageLayout.render(language, errorPage));
67
58
  } catch (e) {
68
59
  return Promise.reject(e);
69
60
  }
@@ -15,20 +15,22 @@ import { type ComponentMappingsProviderI } from '../config/ComponentMappingsProv
15
15
  import { CombinedComponentMappingsProvider } from '../templates/ComponentMappingsProvider.ts';
16
16
  import { ThemeValidator } from '../helper/ThemeValidator.ts';
17
17
  import { EditablePage } from '../templates/pages/__magnolia-editable-page/EditablePage.tsx';
18
+ import { ContentSecurityPolicyNonceProvider } from '../middleware/impl/ContentSecurityPolicyNonceMiddleware.ts';
18
19
 
19
20
  @injectable()
20
21
  export class DynamicPage extends AbstractDynamicPage {
21
22
  constructor(
22
23
  @inject(CombinedComponentMappingsProvider) componentMappingsProvider: ComponentMappingsProviderI,
23
- @inject(STYLESHEET_PROVIDER_TOKEN) StylesheetProviderI: StylesheetProviderI,
24
+ @inject(STYLESHEET_PROVIDER_TOKEN) stylesheetProviderI: StylesheetProviderI,
24
25
  @inject(ThemeValidator) themeValidator: ThemeValidator,
25
26
  @inject(EditablePage) editablePage: EditablePage,
27
+ @inject(ContentSecurityPolicyNonceProvider) private readonly cspNonceProvider: ContentSecurityPolicyNonceProvider,
26
28
  @inject(MagnoliaContextProvider) private readonly magnoliaContextProvider: MagnoliaContextProvider,
27
29
  @inject(MagnoliaPageRestClient) private readonly magnoliaPageRestClient: MagnoliaPageRestClient,
28
30
  @inject(MetadataProvider) private readonly metadataProvider: MetadataProvider,
29
31
  @inject(UrlProvider) private readonly urlProvider: UrlProvider,
30
32
  ) {
31
- super(componentMappingsProvider, StylesheetProviderI, themeValidator, editablePage);
33
+ super(componentMappingsProvider, stylesheetProviderI, themeValidator, editablePage);
32
34
  }
33
35
 
34
36
  public async render(pageProps: PageProps): Promise<ReactNode> {
@@ -39,7 +41,8 @@ export class DynamicPage extends AbstractDynamicPage {
39
41
  }
40
42
  const templateAnnotations: MgnlTemplateAnnotations = await this.magnoliaPageRestClient.getTemplateAnnotations(url, content['@path']);
41
43
  const magnoliaContext = this.magnoliaContextProvider.getMagnoliaContext(url);
42
- return super.renderBase(magnoliaContext, content, templateAnnotations);
44
+ const nonce = await this.cspNonceProvider.get();
45
+ return super.renderBase(magnoliaContext, content, templateAnnotations, nonce);
43
46
  }
44
47
 
45
48
  public async generateMetadata(pageProps: PageProps): Promise<Metadata> {
@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { inject, injectable } from 'tsyringe';
3
3
  import { MagnoliaContextProvider } from '../helper/MagnoliaContextProvider.ts';
4
4
  import { UrlProvider, Params } from './PageProps.ts';
5
+ import { ComposedContextProvider } from './provider/Provider.tsx';
5
6
 
6
7
  export interface Props {
7
8
  children: ReactNode;
@@ -13,6 +14,7 @@ export class DynamicPageLayout {
13
14
  constructor(
14
15
  @inject(MagnoliaContextProvider) private readonly magnoliaContextProvider: MagnoliaContextProvider,
15
16
  @inject(UrlProvider) private readonly urlProvider: UrlProvider,
17
+ @inject(ComposedContextProvider) private readonly composedProvider: ComposedContextProvider,
16
18
  ) {}
17
19
 
18
20
  public async renderUrl(props: Props): Promise<ReactNode> {
@@ -20,7 +22,7 @@ export class DynamicPageLayout {
20
22
  params: props.params,
21
23
  });
22
24
  const magnoliaContext = this.magnoliaContextProvider.getMagnoliaContext(url);
23
- return this.render(magnoliaContext.currentLanguage, props.children);
25
+ return this.composedProvider.render({ childrenProvider: () => this.render(magnoliaContext.currentLanguage, props.children) });
24
26
  }
25
27
 
26
28
  public render(language: string, children: ReactNode): ReactNode {
@@ -1,5 +1,3 @@
1
- // @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
2
- import { headers } from 'next/headers';
3
1
  import { injectable } from 'tsyringe';
4
2
 
5
3
  export type SearchParams = Record<string, string | string[]>;
@@ -14,9 +12,10 @@ export default interface PageProps {
14
12
  @injectable()
15
13
  export class UrlProvider {
16
14
  public async getUrl(props: PageProps): Promise<URL> {
17
- const headersList = await headers();
15
+ // @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
16
+ const headerList = await import('next/headers').then((headers) => headers.headers());
18
17
  const { pathname } = await props.params;
19
- const url = new URL(`https://${headersList.get('host')}/${pathname.join('/')}`);
18
+ const url = new URL(`https://${headerList.get('host')}/${(pathname || [])?.join('/')}`);
20
19
  const queryParams = props ? await props.searchParams : null;
21
20
  if (queryParams) {
22
21
  url.search = this.toUrlSearchParams(queryParams).toString();
@@ -4,6 +4,8 @@ import { DynamicPage } from './DynamicPage.tsx';
4
4
  import { DynamicErrorPageGlobal } from './DynamicErrorPageGlobal.tsx';
5
5
  import { DynamicPageLayout } from './DynamicPageLayout.tsx';
6
6
  import { UrlProvider } from './PageProps.ts';
7
+ import { ErrorPageLoader } from './AbstractDynamicErrorPage.tsx';
8
+ import registerProvider from './provider/_Dependencies.ts';
7
9
 
8
10
  export default function register(container: DependencyContainer) {
9
11
  container.register(DynamicErrorPage, { useClass: DynamicErrorPage });
@@ -11,4 +13,7 @@ export default function register(container: DependencyContainer) {
11
13
  container.register(DynamicPage, { useClass: DynamicPage });
12
14
  container.register(DynamicPageLayout, { useClass: DynamicPageLayout });
13
15
  container.register(UrlProvider, { useClass: UrlProvider });
16
+ container.register(ErrorPageLoader, { useClass: ErrorPageLoader });
17
+
18
+ registerProvider(container);
14
19
  }
@@ -1,5 +1,7 @@
1
1
  export * from './DynamicErrorPageGlobal.tsx';
2
2
  export * from './DynamicErrorPage.tsx';
3
+ export { ErrorPageLoader } from './AbstractDynamicErrorPage.tsx';
3
4
  export * from './DynamicPage.tsx';
4
5
  export * from './DynamicPageLayout.tsx';
5
- export type { default as PageProps } from './PageProps.ts';
6
+ export { UrlProvider, type default as PageProps } from './PageProps.ts';
7
+ export * from './provider/_Index.ts';
@@ -0,0 +1,56 @@
1
+ import { ReactNode } from 'react';
2
+ import { inject, injectable, injectAllWithTransform } from 'tsyringe';
3
+ import { token } from '../../Constants.ts';
4
+ import { Logger } from '../../helper/Logger.ts';
5
+ // @ts-expect-error: tsyringe missing exports prevents ESM resolution with 'nodenext'.
6
+ import { Transform } from 'tsyringe/dist/typings/types/index.d.ts';
7
+
8
+ export interface Props {
9
+ childrenProvider: () => ReactNode;
10
+ }
11
+ export interface ContextProvider {
12
+ render(props: Props): Promise<ReactNode>;
13
+ getOrder(): number;
14
+ getName(): string;
15
+ }
16
+
17
+ export const CONTEXT_PROVIDER_TOKEN = token('ContextProvider');
18
+
19
+ class ProviderTransform implements Transform<ContextProvider[], ContextProvider[]> {
20
+ public transform(providers: ContextProvider[]): ContextProvider[] {
21
+ return providers.sort((p1, p2) => p1.getOrder() - p2.getOrder());
22
+ }
23
+ }
24
+
25
+ @injectable()
26
+ export class ComposedContextProvider {
27
+ constructor(
28
+ @injectAllWithTransform(CONTEXT_PROVIDER_TOKEN, ProviderTransform) private readonly providers: ContextProvider[],
29
+ @inject(Logger) private readonly logger: Logger,
30
+ ) {}
31
+
32
+ public async render(props: Props): Promise<ReactNode> {
33
+ return this.renderProvider(this.providers, props);
34
+ }
35
+
36
+ private async renderProvider(providers: ContextProvider[], props: Props) {
37
+ if (providers.length > 0) {
38
+ const [provider, ...rest] = providers;
39
+ return this.renderProviderSafe(provider, props).then((rendered) =>
40
+ this.renderProvider(rest, {
41
+ childrenProvider: () => rendered,
42
+ }),
43
+ );
44
+ }
45
+ return Promise.resolve(props.childrenProvider());
46
+ }
47
+
48
+ private async renderProviderSafe(provider: ContextProvider, props: Props): Promise<ReactNode> {
49
+ try {
50
+ return provider.render(props);
51
+ } catch (e) {
52
+ this.logger.error(`failed to render provider ${provider.getName()} with order:${provider.getOrder()}, skipping... error: ${e}`);
53
+ return Promise.resolve(props.childrenProvider());
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,8 @@
1
+ import { ComposedContextProvider, CONTEXT_PROVIDER_TOKEN } from './Provider.tsx';
2
+ import { CspHeaderNonceProvider } from './impl/CspHeaderNonce.tsx';
3
+ import { DependencyContainer } from 'tsyringe';
4
+
5
+ export default function register(container: DependencyContainer) {
6
+ container.register(ComposedContextProvider, { useClass: ComposedContextProvider });
7
+ container.register(CONTEXT_PROVIDER_TOKEN, { useClass: CspHeaderNonceProvider });
8
+ }
@@ -0,0 +1 @@
1
+ export { ComposedContextProvider, type ContextProvider, type Props as ContextProviderProps, CONTEXT_PROVIDER_TOKEN } from './Provider.tsx';
@@ -0,0 +1,71 @@
1
+ # Context Provider
2
+
3
+ The provider composer executes all providers based on their getOrder() value. It is used in the dynamicPageLayout.
4
+
5
+ <b>Providers are not applied for global error and global not found pages (due to NextJs only allowing client components)!!<b>
6
+
7
+ ## Provided providers
8
+
9
+ | Provider | order | description |
10
+ | -------------- | ----- | ----------------------------- |
11
+ | CspHeaderNonce | 100 | Provides the csp header nonce |
12
+
13
+ ## Implement custom provider
14
+
15
+ `/app/dynamic/[..pathname]/provider/SomeContext.tsx`
16
+
17
+ ```typescript
18
+ 'use client';
19
+ import React, { createContext, ReactNode, useContext } from 'react';
20
+
21
+ const SomeContext = createContext<string | undefined>(undefined);
22
+
23
+ interface Props {
24
+ value?: string;
25
+ children: ReactNode;
26
+ }
27
+
28
+ export function Provider({ value, children }: Props) {
29
+ return <SomeContext value={value}>{children}</SomeContext>;
30
+ }
31
+
32
+ export const useSomeContext = () => useContext(SomeContext);
33
+
34
+ ```
35
+
36
+ `/app/dynamic/[..pathname]/provider/SomeProvider.tsx`
37
+
38
+ ```typescript
39
+ import React, {ReactNode} from 'react';
40
+ import {inject, injectable} from 'tsyringe';
41
+ import {Provider, Props} from '../Provider.tsx';
42
+ import {ContextProvider, ContextProviderProps} from '@merkle-open/magnolia-headless-frontend-nextjs';
43
+ import {Provider as Delegate} from './SomeContext.tsx';
44
+
45
+ @injectable()
46
+ export class SomeContextProvider implements ContextProvider {
47
+
48
+ public async render({childrenProvider}: ContextProviderProps): Promise<ReactNode> {
49
+ return <Delegate value = {"someContextValue"} > {childrenProvider()} < /Delegate>;
50
+ }
51
+
52
+ getOrder(): number {
53
+ return 200;
54
+ }
55
+
56
+ getName(): string {
57
+ return 'SomeProvider';
58
+ }
59
+ }
60
+ ```
61
+
62
+ Add binding:
63
+
64
+ ```typescript
65
+ import { CONTEXT_PROVIDER_TOKEN } from '@merkle-open/magnolia-headless-frontend-nextjs';
66
+ import { DependencyContainer } from 'tsyringe';
67
+
68
+ function bindSomeContextProvider(container: DependencyContainer): void {
69
+ container.register(CONTEXT_PROVIDER_TOKEN, {useClass: SomeContextProvider});
70
+ }
71
+ ```
@@ -0,0 +1,22 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { inject, injectable } from 'tsyringe';
3
+ import { ContentSecurityPolicyNonceProvider } from '../../../middleware/impl/ContentSecurityPolicyNonceMiddleware.ts';
4
+ import { ContextProvider, Props } from '../Provider.tsx';
5
+ import { Provider as Delegate } from './CspHeaderNonceContext.tsx';
6
+
7
+ @injectable()
8
+ export class CspHeaderNonceProvider implements ContextProvider {
9
+ constructor(@inject(ContentSecurityPolicyNonceProvider) private readonly cspNonceProvider: ContentSecurityPolicyNonceProvider) {}
10
+
11
+ public async render({ childrenProvider }: Props): Promise<ReactNode> {
12
+ const nonce = await this.cspNonceProvider.get();
13
+ return <Delegate value={nonce}>{childrenProvider()}</Delegate>;
14
+ }
15
+
16
+ getOrder(): number {
17
+ return 100;
18
+ }
19
+ getName(): string {
20
+ return 'CspHeaderNonce';
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, ReactNode, useContext } from 'react';
4
+
5
+ const CspHeaderNonceContext = createContext<string | undefined>(undefined);
6
+
7
+ interface Props {
8
+ value?: string;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function Provider({ value, children }: Props) {
13
+ return <CspHeaderNonceContext value={value}>{children}</CspHeaderNonceContext>;
14
+ }
15
+
16
+ export const useCspHeaderNonce = () => useContext(CspHeaderNonceContext);
@@ -18,8 +18,8 @@ export class MetadataProvider {
18
18
  keywords: meta.keywords,
19
19
  robots: meta.robots,
20
20
  applicationName: meta.openGraph?.siteName,
21
- openGraph: this.getOpenGraph(meta.openGraph),
22
- twitter: this.getTwitter(meta.openGraph),
21
+ openGraph: meta.openGraph ? this.getOpenGraph(meta.openGraph) : undefined,
22
+ twitter: meta.openGraph ? this.getTwitter(meta.openGraph) : undefined,
23
23
  alternates: {
24
24
  canonical: meta.canonical,
25
25
  languages: Object.fromEntries(meta.hrefLangLinks.map((link) => [link.language, link.href])),
@@ -1,5 +1,5 @@
1
1
  export { ErrorType } from './MagnoliaPageRestClient.ts';
2
- export { type ExtendedMagnoliaContext } from './MagnoliaContextProvider.ts';
2
+ export * from './MagnoliaContextProvider.ts';
3
3
  export * from './BasicAuth.ts';
4
4
  export * from './BrowserLanguageProvider.ts';
5
5
  export * from './Logger.ts';
@@ -23,7 +23,11 @@ export class EditableAreaClass {
23
23
  }
24
24
 
25
25
  protected renderComponent(content: any, index: number): ReactNode {
26
- return this.editableComponent.render({ key: ContentRendererService.buildKey(content), content: content, index: index });
26
+ return this.editableComponent.render({
27
+ key: ContentRendererService.buildKey(content),
28
+ content,
29
+ index,
30
+ });
27
31
  }
28
32
 
29
33
  protected merge(...content: any[]): any {
@@ -6,13 +6,14 @@ import ErrorBoundary from '../../elements/_error/ErrorBoundary.tsx';
6
6
 
7
7
  export class EditableComponent {
8
8
  public render(props: EditableComponentProps): ReactNode {
9
- const { key, content, additionalContent } = props;
9
+ const { key, content, additionalContent, index, ...rest } = props;
10
10
  const magnoliaContext: IMagnoliaContext = RefService.getMagnoliaContextRef<IMagnoliaContext>();
11
11
  const mergedContent: MgnlContent = this.merge(content, additionalContent);
12
+ const resolvedKey = key ?? content?.['@path'] ?? index;
12
13
 
13
14
  return (
14
- <ErrorBoundary isEditMode={magnoliaContext.isMagnoliaEdit} throwNotEditMode={false} path={content!['@path']} key={`error-${key}`}>
15
- <MagnoliaEditableComponent {...props} content={mergedContent} />
15
+ <ErrorBoundary isEditMode={magnoliaContext?.isMagnoliaEdit ?? false} throwNotEditMode={false} path={content!['@path']} key={`error-${resolvedKey}`}>
16
+ <MagnoliaEditableComponent {...rest} key={resolvedKey} content={mergedContent} />
16
17
  </ErrorBoundary>
17
18
  );
18
19
  }
@@ -19,7 +19,7 @@ export interface MetaProps {
19
19
  keywords: string[];
20
20
  robots?: string;
21
21
  canonical: string;
22
- openGraph: OpenGraph;
22
+ openGraph?: OpenGraph;
23
23
  hrefLangLinks: HrefLangLink[];
24
24
  favicons?: {
25
25
  favicon?: string;
@@ -9,4 +9,5 @@ export default interface EditablePageProps {
9
9
  templateAnnotations: MgnlTemplateAnnotations;
10
10
  config: MagnoliaConfig;
11
11
  magnoliaContext: IMagnoliaContext;
12
+ nonce?: string;
12
13
  }
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import React, { ReactNode } from 'react';
4
2
  import ErrorStaticProps from './ErrorStaticProps.ts';
5
3
  import { injectable } from 'tsyringe';