@merkle-open/magnolia-headless-frontend-nextjs 0.0.6 → 0.0.7
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/package.json +1 -1
- package/src/api/ErrorPageApi.ts +9 -6
- package/src/config/ConfigProvider.ts +1 -1
- package/src/dynamic-page/AbstractDynamicErrorPage.tsx +4 -1
- package/src/dynamic-page/AbstractDynamicPage.tsx +4 -4
- package/src/dynamic-page/DynamicErrorPageGlobal.tsx +7 -4
- package/src/helper/MetadataProvider.ts +9 -0
- package/src/middleware/Middleware.ts +61 -21
- package/src/middleware/_Dependencies.ts +5 -2
- package/src/middleware/_Index.ts +1 -0
- package/src/middleware/_README.md +21 -15
- package/src/middleware/impl/ContentSecurityPolicyNonceMiddleware.ts +56 -0
- package/src/middleware/impl/{DynamicResponseHeaderMiddleware.ts → DynamicHeaderMiddleware.ts} +25 -10
- package/src/middleware/impl/I18nRedirectMiddleware.ts +7 -5
- package/src/middleware/impl/VanityMiddleware.ts +63 -45
- package/src/templates/components/__magnolia-editable-component/EditableComponent.tsx +2 -2
- package/src/templates/pages/BaseProps.ts +5 -0
- package/src/templates/pages/__magnolia-editable-page/EditablePage.tsx +1 -1
package/package.json
CHANGED
package/src/api/ErrorPageApi.ts
CHANGED
|
@@ -11,12 +11,15 @@ export class ErrorPageApi {
|
|
|
11
11
|
const queryParams = req.nextUrl.searchParams;
|
|
12
12
|
const url = new URL(`https://${req.headers.get('host')}/${language}`);
|
|
13
13
|
const errorType = this.getErrorTypeFromString(queryParams.get('errorType'));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
|
|
15
|
+
return this.magnoliaPageRestClient.getErrorPageContent(url, errorType).then((content) => {
|
|
16
|
+
if (content) {
|
|
17
|
+
const headers = new Headers();
|
|
18
|
+
headers.set('Content-Type', 'application/json');
|
|
19
|
+
return new Response(JSON.stringify(content), { headers: headers });
|
|
20
|
+
}
|
|
21
|
+
return new Response(null, { status: 404 });
|
|
22
|
+
});
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
private getErrorTypeFromString(value: string): ErrorType | undefined {
|
|
@@ -4,7 +4,7 @@ export interface MagnoliaApiEndpointsProvider {
|
|
|
4
4
|
annotationTemplates(): string;
|
|
5
5
|
pageContent(language: string): string;
|
|
6
6
|
errorPageContent(language: string): string;
|
|
7
|
-
|
|
7
|
+
dynamicHeader(): string;
|
|
8
8
|
vanity(language: string): string;
|
|
9
9
|
robots(): string;
|
|
10
10
|
sitemap(language: string): string;
|
|
@@ -32,7 +32,10 @@ export abstract class AbstractDynamicErrorPage extends AbstractDynamicPage {
|
|
|
32
32
|
protected async renderDynamic(currentUrl: URL, errorType: ErrorType): Promise<ReactNode> {
|
|
33
33
|
const magnoliaContext = this.magnoliaContextProvider.getMagnoliaContext(currentUrl);
|
|
34
34
|
const content = await this.fetchErrorPageContent(magnoliaContext.currentLanguage, errorType);
|
|
35
|
-
|
|
35
|
+
if (content) {
|
|
36
|
+
return super.renderBase(magnoliaContext, content, {});
|
|
37
|
+
}
|
|
38
|
+
return Promise.reject(new Error('no dynamic error page maintained!'));
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
protected renderStatic(language: string, errorType: ErrorType): ReactNode {
|
|
@@ -16,7 +16,7 @@ export abstract class AbstractDynamicPage {
|
|
|
16
16
|
private readonly editablePage: EditablePage,
|
|
17
17
|
) {}
|
|
18
18
|
|
|
19
|
-
protected renderBase(magnoliaContext: ExtendedMagnoliaContext, content: PageProps, templateAnnotations: MgnlTemplateAnnotations): ReactNode {
|
|
19
|
+
protected async renderBase(magnoliaContext: ExtendedMagnoliaContext, content: PageProps, templateAnnotations: MgnlTemplateAnnotations): Promise<ReactNode> {
|
|
20
20
|
const config: MagnoliaConfig = {
|
|
21
21
|
componentMappings: this.componentMappingsProvider.getComponentMappings(),
|
|
22
22
|
};
|
|
@@ -27,10 +27,10 @@ export abstract class AbstractDynamicPage {
|
|
|
27
27
|
return (
|
|
28
28
|
<div>
|
|
29
29
|
{/* is moved to <head> see https://react.dev/blog/2024/12/05/react-19#support-for-stylesheets */}
|
|
30
|
-
{this.getStylesheet(magnoliaContext, content).map((stylesheet) => (
|
|
31
|
-
<link rel="stylesheet" href={stylesheet} precedence="high" />
|
|
30
|
+
{this.getStylesheet(magnoliaContext, content).map((stylesheet, index) => (
|
|
31
|
+
<link rel="stylesheet" key={`AbstractDynamicPage-stylesheet-${index}`} href={stylesheet} precedence="high" />
|
|
32
32
|
))}
|
|
33
|
-
{this.editablePage.render({ content, config, templateAnnotations, magnoliaContext })}
|
|
33
|
+
{await this.editablePage.render({ content, config, templateAnnotations, magnoliaContext })}
|
|
34
34
|
</div>
|
|
35
35
|
);
|
|
36
36
|
}
|
|
@@ -60,10 +60,13 @@ export class DynamicErrorPageGlobal extends AbstractDynamicErrorPage {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
private async renderGlobalDynamicError(language: string, errorType: ErrorType): Promise<ReactNode> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
try {
|
|
64
|
+
const currentUrl = new URL(window.location.href);
|
|
65
|
+
currentUrl.pathname = language;
|
|
66
|
+
return super.renderDynamic(currentUrl, errorType).then((errorPage) => this.dynamicPageLayout.render(language, errorPage));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return Promise.reject(e);
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
private renderGlobalStaticError(language: string, errorType: ErrorType): ReactNode {
|
|
@@ -24,6 +24,15 @@ export class MetadataProvider {
|
|
|
24
24
|
canonical: meta.canonical,
|
|
25
25
|
languages: Object.fromEntries(meta.hrefLangLinks.map((link) => [link.language, link.href])),
|
|
26
26
|
},
|
|
27
|
+
...(meta.favicons && {
|
|
28
|
+
icons: {
|
|
29
|
+
icon: [
|
|
30
|
+
meta.favicons.faviconLegacy && { url: meta.favicons.faviconLegacy, sizes: '32x32' },
|
|
31
|
+
meta.favicons.favicon && { url: meta.favicons.favicon, type: 'image/svg+xml' },
|
|
32
|
+
].filter(Boolean),
|
|
33
|
+
apple: [meta.favicons.appleTouchIcon && { url: meta.favicons.appleTouchIcon, sizes: '180x180' }].filter(Boolean),
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
27
36
|
};
|
|
28
37
|
}
|
|
29
38
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
2
2
|
import { NextFetchEvent, NextProxy, NextRequest, NextResponse } from 'next/server';
|
|
3
|
-
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
4
|
-
import { NextMiddlewareResult } from 'next/dist/server/web/types';
|
|
5
3
|
import { Logger } from '../helper/Logger.ts';
|
|
6
4
|
import { inject, injectable, injectAllWithTransform } from 'tsyringe';
|
|
7
5
|
// @ts-expect-error: tsyringe missing exports prevents ESM resolution with 'nodenext'.
|
|
@@ -10,15 +8,23 @@ import { Transform } from 'tsyringe/dist/typings/types';
|
|
|
10
8
|
import { token } from '../Constants.ts';
|
|
11
9
|
|
|
12
10
|
export interface Middleware {
|
|
13
|
-
apply(request: NextRequest, response:
|
|
11
|
+
apply(request: NextRequest, response: MiddlewareNextResponse, event: NextFetchEvent): Promise<MiddlewareResult>;
|
|
14
12
|
|
|
15
13
|
getOrder(): number;
|
|
16
14
|
getName(): string;
|
|
17
15
|
}
|
|
16
|
+
export interface MiddlewareResult {
|
|
17
|
+
response: MiddlewareNextResponse;
|
|
18
|
+
break?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MiddlewareNextResponse extends NextResponse, Response {
|
|
21
|
+
requestHeaders: Headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export const MIDDLEWARE_TOKEN = token('Middleware');
|
|
19
25
|
|
|
20
26
|
export abstract class AbstractMiddleware implements Middleware {
|
|
21
|
-
public abstract apply(request: NextRequest, response:
|
|
27
|
+
public abstract apply(request: NextRequest, response: MiddlewareNextResponse, event: NextFetchEvent): Promise<MiddlewareResult>;
|
|
22
28
|
|
|
23
29
|
public abstract getOrder(): number;
|
|
24
30
|
public abstract getName(): string;
|
|
@@ -40,26 +46,60 @@ class MiddlewareTransform implements Transform<Middleware[], Middleware[]> {
|
|
|
40
46
|
@injectable()
|
|
41
47
|
export class MiddlewareComposer {
|
|
42
48
|
constructor(
|
|
43
|
-
@injectAllWithTransform(MIDDLEWARE_TOKEN, MiddlewareTransform) private middlewares: Middleware[],
|
|
44
|
-
@inject(Logger) private logger: Logger,
|
|
49
|
+
@injectAllWithTransform(MIDDLEWARE_TOKEN, MiddlewareTransform) private readonly middlewares: Middleware[],
|
|
50
|
+
@inject(Logger) private readonly logger: Logger,
|
|
45
51
|
) {}
|
|
46
52
|
|
|
47
53
|
public composeMiddleware(): NextProxy {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const response = NextResponse.next();
|
|
52
|
-
for (const middleware of middlewares) {
|
|
53
|
-
try {
|
|
54
|
-
const result = await middleware.apply(request, response, event);
|
|
55
|
-
if (result instanceof NextResponse || result instanceof Response) {
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
} catch (e) {
|
|
59
|
-
logger.error(`failed to execute middleware ${middleware.getName()} with order:${middleware.getOrder()}, skipping... error: ${e}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return response;
|
|
54
|
+
const composedMiddleware = new ComposedMiddleware(this.middlewares, this.logger);
|
|
55
|
+
return async function middleware(request: NextRequest, event: NextFetchEvent): Promise<NextResponse> {
|
|
56
|
+
return composedMiddleware.apply(request, event);
|
|
63
57
|
};
|
|
64
58
|
}
|
|
65
59
|
}
|
|
60
|
+
|
|
61
|
+
class ComposedMiddleware {
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly middlewares: Middleware[],
|
|
64
|
+
private readonly logger: Logger,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
public async apply(request: NextRequest, event: NextFetchEvent): Promise<NextResponse> {
|
|
68
|
+
const requestHeaders = new Headers(request.headers);
|
|
69
|
+
const response: MiddlewareNextResponse = NextResponse.next({
|
|
70
|
+
request: {
|
|
71
|
+
headers: requestHeaders,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
response.requestHeaders = requestHeaders;
|
|
75
|
+
return this.triggerMiddleware(this.middlewares, request, response, event).then((result) => result.response);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async triggerMiddleware(middlewares: Middleware[], request: NextRequest, response: MiddlewareNextResponse, event: NextFetchEvent): Promise<MiddlewareResult> {
|
|
79
|
+
if (middlewares.length > 0) {
|
|
80
|
+
const [middleware, ...rest] = middlewares;
|
|
81
|
+
return this.triggerMiddlewareSafe(middleware, request, response, event).then((result) => {
|
|
82
|
+
if (!result.break) {
|
|
83
|
+
return this.triggerMiddleware(rest, request, result.response, event);
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve(result);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return Promise.resolve({
|
|
89
|
+
response: response,
|
|
90
|
+
break: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async triggerMiddlewareSafe(middleware: Middleware, request: NextRequest, response: MiddlewareNextResponse, event: NextFetchEvent): Promise<MiddlewareResult> {
|
|
95
|
+
try {
|
|
96
|
+
return middleware.apply(request, response, event);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
this.logger.error(`failed to execute middleware ${middleware.getName()} with order:${middleware.getOrder()}, skipping... error: ${e}`);
|
|
99
|
+
return Promise.resolve({
|
|
100
|
+
response: response,
|
|
101
|
+
break: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { DependencyContainer } from 'tsyringe';
|
|
2
|
-
import {
|
|
2
|
+
import { DynamicHeaderMiddleware } from './impl/DynamicHeaderMiddleware.ts';
|
|
3
3
|
import { I18nRedirectMiddleware } from './impl/I18nRedirectMiddleware.ts';
|
|
4
4
|
import { VanityMiddleware } from './impl/VanityMiddleware.ts';
|
|
5
|
+
import { ContentSecurityPolicyNonceMiddleware, ContentSecurityPolicyNonceProvider } from './impl/ContentSecurityPolicyNonceMiddleware.ts';
|
|
5
6
|
import { MiddlewareComposer, MIDDLEWARE_TOKEN } from './Middleware.ts';
|
|
6
7
|
|
|
7
8
|
export default function register(container: DependencyContainer) {
|
|
8
|
-
container.register(MIDDLEWARE_TOKEN, { useClass:
|
|
9
|
+
container.register(MIDDLEWARE_TOKEN, { useClass: DynamicHeaderMiddleware });
|
|
9
10
|
container.register(MIDDLEWARE_TOKEN, { useClass: I18nRedirectMiddleware });
|
|
10
11
|
container.register(MIDDLEWARE_TOKEN, { useClass: VanityMiddleware });
|
|
12
|
+
container.register(MIDDLEWARE_TOKEN, { useClass: ContentSecurityPolicyNonceMiddleware });
|
|
11
13
|
container.register(MiddlewareComposer, { useClass: MiddlewareComposer });
|
|
14
|
+
container.register(ContentSecurityPolicyNonceProvider, { useClass: ContentSecurityPolicyNonceProvider });
|
|
12
15
|
}
|
package/src/middleware/_Index.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# Proxy/Middleware
|
|
2
2
|
|
|
3
|
-
The middleware composer executes all middlewares based on their getOrder() value until the first one returns a
|
|
3
|
+
The middleware composer executes all middlewares based on their getOrder() value until the first one returns a result with break=true.
|
|
4
4
|
|
|
5
5
|
## Configure Middleware Composer
|
|
6
6
|
|
|
7
7
|
`/proxy.ts` [see nextJs docu](https://nextjs.org/docs/app/getting-started/proxy)
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
import 'reflect-metadata';
|
|
11
10
|
import { container } from './app/Dependencies';
|
|
12
11
|
import { NextProxy, NextRequest } from 'next/server';
|
|
13
12
|
import { NextMiddlewareResult } from 'next/dist/server/web/types';
|
|
14
13
|
import type { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event';
|
|
15
|
-
import { MiddlewareComposer } from '@merkle/magnolia-headless-frontend-nextjs
|
|
14
|
+
import { MiddlewareComposer } from '@merkle-open/magnolia-headless-frontend-nextjs';
|
|
16
15
|
|
|
17
|
-
const middleware: NextProxy = container.resolve<MiddlewareComposer>(
|
|
16
|
+
const middleware: NextProxy = container.resolve<MiddlewareComposer>(MiddlewareComposer).composeMiddleware();
|
|
18
17
|
|
|
19
18
|
export async function proxy(request: NextRequest, event: NextFetchEvent): Promise<NextMiddlewareResult> {
|
|
20
19
|
return middleware(request, event);
|
|
@@ -23,39 +22,46 @@ export async function proxy(request: NextRequest, event: NextFetchEvent): Promis
|
|
|
23
22
|
|
|
24
23
|
## Provided middlewares
|
|
25
24
|
|
|
26
|
-
| Middleware
|
|
27
|
-
|
|
|
28
|
-
| I18nRedirect
|
|
29
|
-
|
|
|
30
|
-
| Vanity
|
|
25
|
+
| Middleware | order | description |
|
|
26
|
+
| ------------- | ----- | -------------------------------------------------------------- |
|
|
27
|
+
| I18nRedirect | 100 | redirects to browser lang if url doesn't contain language |
|
|
28
|
+
| DynamicHeader | 200 | sets request/response headers provided by backend endpoint |
|
|
29
|
+
| Vanity | 300 | redirects/forwards based on backend endpoint |
|
|
30
|
+
| CSP-nonce | 1000 | generates nonce and replaces csp header '${nonce}' placeholder |
|
|
31
31
|
|
|
32
32
|
## Implement custom middleware
|
|
33
33
|
|
|
34
34
|
`/app/middlewares/SomeMiddleware.ts`
|
|
35
35
|
|
|
36
36
|
```typescript
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
37
|
+
import {NextRequest, NextResponse} from 'next/server';
|
|
38
|
+
import {NextMiddlewareResult} from 'next/dist/server/web/types';
|
|
39
|
+
import {inject, injectable} from 'tsyringe';
|
|
40
|
+
import { AbstractMiddleware, MiddlewareNextResponse, MiddlewareResult } from '@merkle-open/magnolia-headless-frontend-nextjs';
|
|
41
41
|
|
|
42
42
|
@injectable()
|
|
43
43
|
export class SomeMiddleware extends AbstractMiddleware {
|
|
44
44
|
constructor() {
|
|
45
45
|
super();
|
|
46
46
|
}
|
|
47
|
+
|
|
47
48
|
public getOrder(): number {
|
|
48
49
|
return 301;
|
|
49
50
|
}
|
|
51
|
+
|
|
50
52
|
public getName(): string {
|
|
51
53
|
return 'SomeMiddleware';
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
public async apply(req: NextRequest): Promise<
|
|
56
|
+
public async apply(req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
|
|
55
57
|
if (super.isPagePathRequest(req)) {
|
|
56
58
|
// will break chain
|
|
57
|
-
return
|
|
59
|
+
return Promise.resolve({
|
|
60
|
+
response: NextResponse.redirect(new URL('someDestination')),
|
|
61
|
+
break: true
|
|
62
|
+
});
|
|
58
63
|
}
|
|
64
|
+
return Promise.resolve({response: res});
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
3
|
+
import { AbstractMiddleware, MiddlewareNextResponse, MiddlewareResult } from '../Middleware.ts';
|
|
4
|
+
import { injectable } from 'tsyringe';
|
|
5
|
+
|
|
6
|
+
const CSP_HEADER_KEY = 'Content-Security-Policy';
|
|
7
|
+
const NONCE_HEADER_KEY = 'x-nonce';
|
|
8
|
+
|
|
9
|
+
// see https://nextjs.org/docs/app/guides/content-security-policy#how-nonces-work-in-nextjs
|
|
10
|
+
@injectable()
|
|
11
|
+
export class ContentSecurityPolicyNonceMiddleware extends AbstractMiddleware {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public getOrder(): number {
|
|
17
|
+
return 1000;
|
|
18
|
+
}
|
|
19
|
+
public getName(): string {
|
|
20
|
+
return 'CSP-nonce';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async apply(_req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
|
|
24
|
+
if (res.headers.has(CSP_HEADER_KEY)) {
|
|
25
|
+
const nonce = this.generateNonce();
|
|
26
|
+
const cspHeader = this.replacePlaceholder(res.headers.get(CSP_HEADER_KEY), nonce);
|
|
27
|
+
|
|
28
|
+
res.requestHeaders.set(NONCE_HEADER_KEY, nonce);
|
|
29
|
+
res.requestHeaders.set(CSP_HEADER_KEY, cspHeader);
|
|
30
|
+
res.headers.set(NONCE_HEADER_KEY, nonce);
|
|
31
|
+
res.headers.set(CSP_HEADER_KEY, cspHeader);
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve({ response: res });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected generateNonce(): string {
|
|
37
|
+
return Buffer.from(crypto.randomUUID()).toString('base64');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected replacePlaceholder(cspHeader: string, nonce: string): string {
|
|
41
|
+
return cspHeader.replaceAll('${nonce}', nonce);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@injectable()
|
|
46
|
+
export class ContentSecurityPolicyNonceProvider {
|
|
47
|
+
public async get(): Promise<string | undefined> {
|
|
48
|
+
try {
|
|
49
|
+
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
50
|
+
const headerList = await import('next/headers').then((headers) => headers.headers());
|
|
51
|
+
return headerList.get(NONCE_HEADER_KEY);
|
|
52
|
+
} catch {
|
|
53
|
+
return Promise.resolve(undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/middleware/impl/{DynamicResponseHeaderMiddleware.ts → DynamicHeaderMiddleware.ts}
RENAMED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
2
|
-
import { NextRequest
|
|
3
|
-
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
4
|
-
import { NextMiddlewareResult } from 'next/dist/server/web/types';
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
5
3
|
import { RestClient } from '../../helper/RestClient.ts';
|
|
6
|
-
import { AbstractMiddleware } from '../Middleware.ts';
|
|
4
|
+
import { AbstractMiddleware, MiddlewareNextResponse, MiddlewareResult } from '../Middleware.ts';
|
|
7
5
|
import { inject, injectable } from 'tsyringe';
|
|
8
6
|
import { type HeadlessConfigProviderI, MagnoliaApiEndpointsProvider, HEADLESS_CONFIG_PROVIDER_TOKEN } from '../../config/ConfigProvider.ts';
|
|
9
7
|
|
|
8
|
+
enum Target {
|
|
9
|
+
REQUEST = 'REQUEST',
|
|
10
|
+
RESPONSE = 'RESPONSE',
|
|
11
|
+
BOTH = 'BOTH',
|
|
12
|
+
}
|
|
13
|
+
interface HeaderValue {
|
|
14
|
+
value: string;
|
|
15
|
+
target: Target;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
@injectable()
|
|
11
|
-
export class
|
|
19
|
+
export class DynamicHeaderMiddleware extends AbstractMiddleware {
|
|
12
20
|
private apisProvider: MagnoliaApiEndpointsProvider;
|
|
13
21
|
|
|
14
22
|
constructor(
|
|
@@ -23,23 +31,30 @@ export class DynamicResponseHeaderMiddleware extends AbstractMiddleware {
|
|
|
23
31
|
return 300;
|
|
24
32
|
}
|
|
25
33
|
public getName(): string {
|
|
26
|
-
return '
|
|
34
|
+
return 'DynamicHeader';
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
public async apply(req: NextRequest, res:
|
|
37
|
+
public async apply(req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
|
|
30
38
|
if (super.isPagePathRequest(req)) {
|
|
31
39
|
const domain: string = new URL('https://' + req.headers.get('host')).hostname; //strip port
|
|
32
40
|
const header = await this.getHeader(domain);
|
|
33
41
|
for (const key in header) {
|
|
34
|
-
|
|
42
|
+
const { value, target }: HeaderValue = header[key];
|
|
43
|
+
if (target == Target.RESPONSE || target == Target.BOTH) {
|
|
44
|
+
res.headers.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
if (target == Target.REQUEST || target == Target.BOTH) {
|
|
47
|
+
res.requestHeaders.set(key, value);
|
|
48
|
+
}
|
|
35
49
|
}
|
|
36
50
|
}
|
|
51
|
+
return Promise.resolve({ response: res });
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
private async getHeader(domain: string): Promise<
|
|
54
|
+
private async getHeader(domain: string): Promise<Map<string, HeaderValue>> {
|
|
40
55
|
const queryParams = new URLSearchParams();
|
|
41
56
|
queryParams.set('domain', domain);
|
|
42
|
-
const url = this.apisProvider.
|
|
57
|
+
const url = this.apisProvider.dynamicHeader() + '?' + queryParams.toString();
|
|
43
58
|
return this.restClient.fetchMagnoliaBasicAuth(url).then((response) => this.restClient.getJson(url, response));
|
|
44
59
|
}
|
|
45
60
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
-
|
|
4
|
-
import { NextMiddlewareResult } from 'next/dist/server/web/types';
|
|
5
|
-
import { AbstractMiddleware } from '../Middleware.ts';
|
|
3
|
+
import { AbstractMiddleware, MiddlewareNextResponse, MiddlewareResult } from '../Middleware.ts';
|
|
6
4
|
import { BrowserLanguageProvider } from '../../helper/BrowserLanguageProvider.ts';
|
|
7
5
|
import { inject, injectable } from 'tsyringe';
|
|
8
6
|
|
|
@@ -19,11 +17,15 @@ export class I18nRedirectMiddleware extends AbstractMiddleware {
|
|
|
19
17
|
return 'i18nRedirect';
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
public async apply(req: NextRequest): Promise<
|
|
20
|
+
public async apply(req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
|
|
23
21
|
if (req.nextUrl.pathname === '/') {
|
|
24
22
|
// browser language redirect
|
|
25
23
|
const language = this.browserLanguageProvider.getBrowserLanguage(req);
|
|
26
|
-
return
|
|
24
|
+
return Promise.resolve({
|
|
25
|
+
response: NextResponse.redirect(new URL(`/${language}${req.nextUrl.search}`, req.url)),
|
|
26
|
+
break: true,
|
|
27
|
+
});
|
|
27
28
|
}
|
|
29
|
+
return Promise.resolve({ response: res });
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
-
// @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
|
|
4
|
-
import { NextMiddlewareResult } from 'next/dist/server/web/types';
|
|
5
3
|
import { RestClient } from '../../helper/RestClient.ts';
|
|
6
|
-
import { AbstractMiddleware } from '../Middleware.ts';
|
|
4
|
+
import { AbstractMiddleware, MiddlewareNextResponse, MiddlewareResult } from '../Middleware.ts';
|
|
7
5
|
import { BrowserLanguageProvider } from '../../helper/BrowserLanguageProvider.ts';
|
|
8
6
|
import { inject, injectable } from 'tsyringe';
|
|
9
7
|
import { type HeadlessConfigProviderI, MagnoliaApiEndpointsProvider, HEADLESS_CONFIG_PROVIDER_TOKEN } from '../../config/ConfigProvider.ts';
|
|
@@ -41,56 +39,22 @@ export class VanityMiddleware extends AbstractMiddleware {
|
|
|
41
39
|
return 'Vanity';
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
public async apply(req: NextRequest): Promise<
|
|
42
|
+
public async apply(req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
|
|
45
43
|
if (super.isPagePathRequest(req)) {
|
|
46
44
|
const url = new URL('https://' + req.headers.get('host'));
|
|
47
45
|
const domain: string = url.hostname; //strip port
|
|
48
46
|
const path = req.nextUrl.pathname;
|
|
49
47
|
const language: string = this.browserLanguageProvider.getBrowserLanguage(req);
|
|
50
48
|
const vanity = await this.getVanity(domain, path, language);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
const init: globalThis.ResponseInit = {
|
|
58
|
-
headers: headers,
|
|
59
|
-
};
|
|
60
|
-
const destination = this.getDestination(vanity, req);
|
|
61
|
-
switch (vanity.type) {
|
|
62
|
-
case RedirectType.FORWARD:
|
|
63
|
-
/*
|
|
64
|
-
* currently ignores NODE_TLS_REJECT_UNAUTHORIZED=0 flag, so external destinations (assets) don't work for local development with tomcat
|
|
65
|
-
* - https://github.com/vercel/next.js/discussions/49546
|
|
66
|
-
* - https://github.com/vercel/next.js/pull/78566
|
|
67
|
-
*/
|
|
68
|
-
return NextResponse.rewrite(destination, {
|
|
69
|
-
...init,
|
|
70
|
-
status: 303,
|
|
71
|
-
});
|
|
72
|
-
case RedirectType.TEMPORARY:
|
|
73
|
-
return NextResponse.redirect(destination, {
|
|
74
|
-
...init,
|
|
75
|
-
status: 307,
|
|
76
|
-
});
|
|
77
|
-
case RedirectType.PERMANENT:
|
|
78
|
-
return NextResponse.redirect(destination, {
|
|
79
|
-
...init,
|
|
80
|
-
status: 308,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
49
|
+
const response = vanity ? this.getResponse(req, vanity) : undefined;
|
|
50
|
+
if (response) {
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
response: response,
|
|
53
|
+
break: true,
|
|
54
|
+
});
|
|
83
55
|
}
|
|
84
56
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
private getDestination(vanity: Vanity, req: NextRequest): URL {
|
|
88
|
-
const isAbsolute = vanity.destination?.startsWith('https://');
|
|
89
|
-
const url = new URL(vanity.destination, isAbsolute ? undefined : req.nextUrl);
|
|
90
|
-
if (vanity.carryOverQueryParams) {
|
|
91
|
-
req.nextUrl.searchParams.forEach((value, key) => url.searchParams.set(key, value));
|
|
92
|
-
}
|
|
93
|
-
return url;
|
|
57
|
+
return Promise.resolve({ response: res });
|
|
94
58
|
}
|
|
95
59
|
|
|
96
60
|
private async getVanity(domain: string, path: string, language: string): Promise<Vanity> {
|
|
@@ -100,4 +64,58 @@ export class VanityMiddleware extends AbstractMiddleware {
|
|
|
100
64
|
const url = this.apisProvider.vanity(language) + '?' + queryParams.toString();
|
|
101
65
|
return this.restClient.fetchMagnoliaBasicAuth(url).then((response) => this.restClient.getJson(url, response));
|
|
102
66
|
}
|
|
67
|
+
|
|
68
|
+
private getResponse(req: NextRequest, vanity: Vanity): NextResponse | undefined {
|
|
69
|
+
const headers = new Headers();
|
|
70
|
+
if (vanity.disableCache) {
|
|
71
|
+
headers.set('Cache-Control', 'no-cache');
|
|
72
|
+
headers.set('Pragma', 'no-cache');
|
|
73
|
+
}
|
|
74
|
+
const init: globalThis.ResponseInit = {
|
|
75
|
+
headers: headers,
|
|
76
|
+
};
|
|
77
|
+
const destination = this.getDestination(req, vanity);
|
|
78
|
+
switch (vanity.type) {
|
|
79
|
+
case RedirectType.FORWARD:
|
|
80
|
+
/*
|
|
81
|
+
* currently ignores NODE_TLS_REJECT_UNAUTHORIZED=0 flag, so external destinations (assets) don't work for local development with tomcat
|
|
82
|
+
* - https://github.com/vercel/next.js/discussions/49546
|
|
83
|
+
* - https://github.com/vercel/next.js/pull/78566
|
|
84
|
+
*/
|
|
85
|
+
return Promise.resolve({
|
|
86
|
+
...NextResponse.rewrite(destination, {
|
|
87
|
+
...init,
|
|
88
|
+
status: 303,
|
|
89
|
+
}),
|
|
90
|
+
break: true,
|
|
91
|
+
});
|
|
92
|
+
case RedirectType.TEMPORARY:
|
|
93
|
+
return Promise.resolve({
|
|
94
|
+
...NextResponse.redirect(destination, {
|
|
95
|
+
...init,
|
|
96
|
+
status: 307,
|
|
97
|
+
}),
|
|
98
|
+
break: true,
|
|
99
|
+
});
|
|
100
|
+
case RedirectType.PERMANENT:
|
|
101
|
+
return Promise.resolve({
|
|
102
|
+
...NextResponse.redirect(destination, {
|
|
103
|
+
...init,
|
|
104
|
+
status: 308,
|
|
105
|
+
}),
|
|
106
|
+
break: true,
|
|
107
|
+
});
|
|
108
|
+
default:
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getDestination(req: NextRequest, vanity: Vanity): URL {
|
|
114
|
+
const isAbsolute = vanity.destination?.startsWith('https://');
|
|
115
|
+
const url = new URL(vanity.destination, isAbsolute ? undefined : req.nextUrl);
|
|
116
|
+
if (vanity.carryOverQueryParams) {
|
|
117
|
+
req.nextUrl.searchParams.forEach((value, key) => url.searchParams.set(key, value));
|
|
118
|
+
}
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
103
121
|
}
|
|
@@ -6,12 +6,12 @@ import ErrorBoundary from '../../elements/_error/ErrorBoundary.tsx';
|
|
|
6
6
|
|
|
7
7
|
export class EditableComponent {
|
|
8
8
|
public render(props: EditableComponentProps): ReactNode {
|
|
9
|
-
const { content, additionalContent } = props;
|
|
9
|
+
const { key, content, additionalContent } = props;
|
|
10
10
|
const magnoliaContext: IMagnoliaContext = RefService.getMagnoliaContextRef<IMagnoliaContext>();
|
|
11
11
|
const mergedContent: MgnlContent = this.merge(content, additionalContent);
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<ErrorBoundary isEditMode={magnoliaContext.isMagnoliaEdit} throwNotEditMode={false} path={content!['@path']}>
|
|
14
|
+
<ErrorBoundary isEditMode={magnoliaContext.isMagnoliaEdit} throwNotEditMode={false} path={content!['@path']} key={`error-${key}`}>
|
|
15
15
|
<MagnoliaEditableComponent {...props} content={mergedContent} />
|
|
16
16
|
</ErrorBoundary>
|
|
17
17
|
);
|
|
@@ -21,6 +21,11 @@ export interface MetaProps {
|
|
|
21
21
|
canonical: string;
|
|
22
22
|
openGraph: OpenGraph;
|
|
23
23
|
hrefLangLinks: HrefLangLink[];
|
|
24
|
+
favicons?: {
|
|
25
|
+
favicon?: string;
|
|
26
|
+
faviconLegacy?: string;
|
|
27
|
+
appleTouchIcon?: string;
|
|
28
|
+
};
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export default interface Props extends MgnlContent {
|
|
@@ -7,7 +7,7 @@ import { injectable } from 'tsyringe';
|
|
|
7
7
|
|
|
8
8
|
@injectable()
|
|
9
9
|
export class EditablePage {
|
|
10
|
-
public render(props: EditablePageProps): ReactNode {
|
|
10
|
+
public async render(props: EditablePageProps): Promise<ReactNode> {
|
|
11
11
|
const { content } = props;
|
|
12
12
|
const magnoliaContext: IMagnoliaContext = RefService.getMagnoliaContextRef<IMagnoliaContext>();
|
|
13
13
|
|