@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 +80 -60
- package/package.json +2 -2
- package/src/dynamic-page/AbstractDynamicErrorPage.tsx +19 -4
- package/src/dynamic-page/AbstractDynamicPage.tsx +3 -4
- package/src/dynamic-page/DynamicErrorPage.tsx +8 -6
- package/src/dynamic-page/DynamicErrorPageGlobal.tsx +8 -17
- package/src/dynamic-page/DynamicPage.tsx +6 -3
- package/src/dynamic-page/DynamicPageLayout.tsx +3 -1
- package/src/dynamic-page/PageProps.ts +3 -4
- package/src/dynamic-page/_Dependencies.ts +5 -0
- package/src/dynamic-page/_Index.ts +3 -1
- package/src/dynamic-page/provider/Provider.tsx +56 -0
- package/src/dynamic-page/provider/_Dependencies.ts +8 -0
- package/src/dynamic-page/provider/_Index.ts +1 -0
- package/src/dynamic-page/provider/_README.md +71 -0
- package/src/dynamic-page/provider/impl/CspHeaderNonce.tsx +22 -0
- package/src/dynamic-page/provider/impl/CspHeaderNonceContext.tsx +16 -0
- package/src/helper/MetadataProvider.ts +2 -2
- package/src/helper/_Index.ts +1 -1
- package/src/templates/areas/__magnolia-editable-area/EditableArea.tsx +5 -1
- package/src/templates/components/__magnolia-editable-component/EditableComponent.tsx +4 -3
- package/src/templates/pages/BaseProps.ts +1 -1
- package/src/templates/pages/__magnolia-editable-page/EditablePageProps.ts +1 -0
- package/src/templates/pages/_error-static/ErrorStatic.tsx +0 -2
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
41
|
-
.
|
|
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
|
|
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.
|
|
48
|
+
return this.errorPageLoader.renderGlobal(errorType);
|
|
47
49
|
}
|
|
48
50
|
return errorPage;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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://${
|
|
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
|
|
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])),
|
package/src/helper/_Index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ErrorType } from './MagnoliaPageRestClient.ts';
|
|
2
|
-
export
|
|
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({
|
|
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
|
|
15
|
-
<MagnoliaEditableComponent {...
|
|
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
|
}
|