@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkle-open/magnolia-headless-frontend-nextjs",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Magnolia headless frontend nextJs",
5
5
  "license": "MIT",
6
6
  "author": "Merkle Inc.",
@@ -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
- const headers = new Headers();
15
- headers.set('Content-Type', 'application/json');
16
- return this.magnoliaPageRestClient
17
- .getErrorPageContent(url, errorType)
18
- .then((content) => JSON.stringify(content))
19
- .then((body) => new Response(body, { headers: headers }));
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
- dynamicResponseHeader(): string;
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
- return super.renderBase(magnoliaContext, content, {});
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
- const currentUrl = new URL(window.location.href);
64
- currentUrl.pathname = language;
65
- const errorPage = await super.renderDynamic(currentUrl, errorType);
66
- return this.dynamicPageLayout.render(language, errorPage);
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: NextResponse, event: NextFetchEvent): NextMiddlewareResult | Promise<NextMiddlewareResult>;
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: NextResponse, event: NextFetchEvent): NextMiddlewareResult | Promise<NextMiddlewareResult>;
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 middlewares = this.middlewares;
49
- const logger = this.logger;
50
- return async function middleware(request: NextRequest, event: NextFetchEvent): Promise<NextMiddlewareResult> {
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 { DynamicResponseHeaderMiddleware } from './impl/DynamicResponseHeaderMiddleware.ts';
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: DynamicResponseHeaderMiddleware });
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
  }
@@ -1 +1,2 @@
1
1
  export * from './Middleware.ts';
2
+ export { ContentSecurityPolicyNonceProvider } from './impl/ContentSecurityPolicyNonceMiddleware.ts';
@@ -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 response.
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/src/middleware/Middleware.ts';
14
+ import { MiddlewareComposer } from '@merkle-open/magnolia-headless-frontend-nextjs';
16
15
 
17
- const middleware: NextProxy = container.resolve<MiddlewareComposer>('MiddlewareComposer').composeMiddleware();
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 | order |
27
- | --------------------- | ----- |
28
- | I18nRedirect | 100 |
29
- | DynamicResponseHeader | 200 |
30
- | Vanity | 300 |
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 { NextRequest, NextResponse } from 'next/server';
38
- import { NextMiddlewareResult } from 'next/dist/server/web/types';
39
- import { AbstractMiddleware } from './Middleware.ts';
40
- import { inject, injectable } from 'tsyringe';
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<NextMiddlewareResult> {
56
+ public async apply(req: NextRequest, res: MiddlewareNextResponse): Promise<MiddlewareResult> {
55
57
  if (super.isPagePathRequest(req)) {
56
58
  // will break chain
57
- return NextResponse.redirect(new URL('someDestination'));
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
+ }
@@ -1,14 +1,22 @@
1
1
  // @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
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';
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 DynamicResponseHeaderMiddleware extends AbstractMiddleware {
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 'DynamicResponseHeader';
34
+ return 'DynamicHeader';
27
35
  }
28
36
 
29
- public async apply(req: NextRequest, res: NextResponse): Promise<NextMiddlewareResult> {
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
- res.headers.set(key, header[key]);
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<any> {
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.dynamicResponseHeader() + '?' + queryParams.toString();
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
- // @ts-expect-error: Next.js missing exports prevents ESM resolution with 'nodenext'.
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<NextMiddlewareResult> {
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 NextResponse.redirect(new URL(`/${language}${req.nextUrl.search}`, req.url));
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<NextMiddlewareResult> {
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
- if (vanity) {
52
- const headers = new Headers();
53
- if (vanity.disableCache) {
54
- headers.set('Cache-Control', 'no-cache');
55
- headers.set('Pragma', 'no-cache');
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