@merkle-open/magnolia-headless-frontend-nextjs 0.0.1

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.
Files changed (70) hide show
  1. package/README.md +76 -0
  2. package/index.ts +1 -0
  3. package/package.json +61 -0
  4. package/src/Constants.ts +1 -0
  5. package/src/Dependencies.ts +14 -0
  6. package/src/Index.ts +8 -0
  7. package/src/api/ErrorPageApi.ts +30 -0
  8. package/src/api/RobotsTxtApi.ts +33 -0
  9. package/src/api/SitemapApi.ts +33 -0
  10. package/src/api/_Dependencies.ts +12 -0
  11. package/src/api/_Index.ts +3 -0
  12. package/src/api/_README.md +73 -0
  13. package/src/config/ComponentMappingsProvider.ts +5 -0
  14. package/src/config/ConfigProvider.ts +35 -0
  15. package/src/config/Logger.ts +10 -0
  16. package/src/config/StylesheetProvider.ts +3 -0
  17. package/src/config/_Index.ts +4 -0
  18. package/src/config/_README.md +115 -0
  19. package/src/dynamic-page/AbstractDynamicErrorPage.tsx +45 -0
  20. package/src/dynamic-page/AbstractDynamicPage.tsx +53 -0
  21. package/src/dynamic-page/DynamicErrorPage.tsx +53 -0
  22. package/src/dynamic-page/DynamicErrorPageGlobal.tsx +66 -0
  23. package/src/dynamic-page/DynamicPage.tsx +50 -0
  24. package/src/dynamic-page/DynamicPageLayout.tsx +35 -0
  25. package/src/dynamic-page/PageProps.ts +40 -0
  26. package/src/dynamic-page/_Dependencies.ts +16 -0
  27. package/src/dynamic-page/_Index.ts +5 -0
  28. package/src/helper/BasicAuth.ts +3 -0
  29. package/src/helper/BrowserLanguageProvider.ts +31 -0
  30. package/src/helper/Logger.ts +22 -0
  31. package/src/helper/MagnoliaContextProvider.ts +60 -0
  32. package/src/helper/MagnoliaPageRestClient.ts +80 -0
  33. package/src/helper/MetaProps.ts +23 -0
  34. package/src/helper/MetadataProvider.ts +54 -0
  35. package/src/helper/RestClient.ts +49 -0
  36. package/src/helper/_Dependencies.ts +18 -0
  37. package/src/helper/_Index.ts +2 -0
  38. package/src/middleware/Middleware.ts +61 -0
  39. package/src/middleware/_Dependencies.ts +14 -0
  40. package/src/middleware/_Index.ts +1 -0
  41. package/src/middleware/_README.md +72 -0
  42. package/src/middleware/impl/DynamicResponseHeaderMiddleware.ts +45 -0
  43. package/src/middleware/impl/I18nRedirectMiddleware.ts +29 -0
  44. package/src/middleware/impl/VanityMiddleware.ts +103 -0
  45. package/src/scripts/generate-magnolia-config.ts +55 -0
  46. package/src/templates/ComponentMappingsProvider.ts +17 -0
  47. package/src/templates/_Dependencies.ts +8 -0
  48. package/src/templates/_Index.ts +5 -0
  49. package/src/templates/_README.md +86 -0
  50. package/src/templates/areas/BaseProps.ts +5 -0
  51. package/src/templates/areas/__magnolia-editable-area/EditableArea.tsx +27 -0
  52. package/src/templates/areas/__magnolia-editable-area/EditableAreaProps.ts +12 -0
  53. package/src/templates/components/BaseProps.ts +3 -0
  54. package/src/templates/components/__magnolia-editable-component/EditableComponent.tsx +23 -0
  55. package/src/templates/components/__magnolia-editable-component/EditableComponentProps.ts +5 -0
  56. package/src/templates/components/_error/ErrorComponent.tsx +7 -0
  57. package/src/templates/components/_error/ErrorComponentProps.ts +4 -0
  58. package/src/templates/components/_error/template.json +4 -0
  59. package/src/templates/components/_redirect/RedirectComponent.tsx +17 -0
  60. package/src/templates/components/_redirect/RedirectComponentProps.ts +11 -0
  61. package/src/templates/components/_redirect/template.json +4 -0
  62. package/src/templates/elements/_error/ErrorBoundary.tsx +33 -0
  63. package/src/templates/elements/_error/ErrorElement.tsx +22 -0
  64. package/src/templates/elements/_error/ErrorElementProps.ts +6 -0
  65. package/src/templates/magnolia.config.ts +10 -0
  66. package/src/templates/pages/BaseProps.ts +4 -0
  67. package/src/templates/pages/__magnolia-editable-page/EditablePage.tsx +16 -0
  68. package/src/templates/pages/__magnolia-editable-page/EditablePageProps.ts +11 -0
  69. package/src/templates/pages/_error-static/ErrorStatic.tsx +26 -0
  70. package/src/templates/pages/_error-static/ErrorStaticProps.ts +6 -0
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Magnolia NextJs Headless Frontend
2
+ - [Templates](./src/templates/_README.md)
3
+ - [Proxy/Middleware](./src/middleware/_README.md)
4
+ - [Config](./src/config/_README.md)
5
+ - [API](./src/api/_README.md)
6
+ - [Dynamic-Page](./README_dynamic-page.md)
7
+
8
+ ## setup
9
+ - Add the following dependencies to your package.json
10
+ - `@merkle-open/magnolia-headless-frontend-nextjs`
11
+ - [`tsyringe`](https://github.com/Microsoft/tsyringe)
12
+ - `reflect-metadata`
13
+ - `next`
14
+ - `react`
15
+ - `react-dom`
16
+ - Configure tsconfig.json to include the following settings (`./tsconfig.json`)
17
+ ```json
18
+ {
19
+ "compilerOptions": {
20
+ "experimentalDecorators": true,
21
+ "emitDecoratorMetadata": true
22
+ },
23
+ "include": [
24
+ "node_modules/@merkle-open/magnolia-headless-frontend-nextjs"
25
+ ]
26
+ }
27
+ ```
28
+ - Configure the [middleware composer](./src/middleware/_README.md#configure-middleware-composer)
29
+ - Implement and bind the interfaces specified [here](./src/config/_README.md)
30
+ - Implement API endpoints specified [here](./src/api/_README.md)
31
+ - Configure [instrumentation.ts](https://nextjs.org/docs/app/guides/instrumentation#importing-files-with-side-effects) (`./instrumentation.ts`)
32
+ ```typescript
33
+ export async function register() {
34
+ await import('reflect-metadata');
35
+ }
36
+ ```
37
+ - Setup [dynamic page](./README_dynamic-page.md)
38
+ - Setup Dependencies (`./Dependencies.ts`)
39
+ ```typescript
40
+ import 'reflect-metadata';
41
+ import { container } from 'tsyringe';
42
+ import { register as registerOss } from '@merkle-open/magnolia-headless-frontend-nextjs';
43
+ import registerConfigs from './config/_Dependencies.ts';
44
+
45
+ registerConfigs(container);
46
+ registerOss(container);
47
+
48
+ export { container };
49
+ ```
50
+ - Setup next.config.ts (`./next.config.ts`)
51
+ ```typescript
52
+ import type { NextConfig } from 'next';
53
+ import type { Rewrite } from 'next/dist/lib/load-custom-routes';
54
+
55
+ const nextConfig: NextConfig = {
56
+ transpilePackages: ['@merkle-open/magnolia-headless-frontend-nextjs'],
57
+ async rewrites(): Promise<Rewrite[]> {
58
+ return [
59
+ {
60
+ source: '/robots.txt',
61
+ destination: '/api/robots.txt',
62
+ },
63
+ {
64
+ source: '/:language([a-z]{2})/sitemap/:type',
65
+ destination: '/api/:language/sitemap/:type',
66
+ },
67
+ {
68
+ source: '/:language([a-z]{2})/:path*',
69
+ destination: '/dynamic/:language/:path*',
70
+ },
71
+ ];
72
+ },
73
+ ...
74
+ };
75
+ export default nextConfig;
76
+ ```
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './src/Index.ts';
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@merkle-open/magnolia-headless-frontend-nextjs",
3
+ "version": "0.0.1",
4
+ "description": "Magnolia headless frontend nextJs",
5
+ "license": "MIT",
6
+ "author": "Merkle Inc.",
7
+ "private": false,
8
+ "main": "index.ts",
9
+ "module": "index.ts",
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=24"
13
+ },
14
+ "files": [
15
+ "README.md",
16
+ "index.ts",
17
+ "src"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/merkle-open/magnolia-headless.git"
22
+ },
23
+ "scripts": {
24
+ "generate-oss-magnoliaConfig": "npx tsx src/scripts/generate-magnolia-config.ts src/templates src/templates",
25
+ "build": "tsc --build",
26
+ "clean": "tsc --build --clean",
27
+ "format": "prettier --write src/**",
28
+ "lint": "eslint src/**",
29
+ "prepare": "cd .. && husky magnolia-headless-frontend-nextjs/.husky"
30
+ },
31
+ "dependencies": {
32
+ "@magnolia/react-editor": "2.1.1"
33
+ },
34
+ "peerDependencies": {
35
+ "next": "^16.1",
36
+ "react": "^19.2",
37
+ "react-dom": "^19.2",
38
+ "reflect-metadata": "^0.2",
39
+ "tsyringe": "^4.4"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^9.39.3",
43
+ "@types/node": "^25.0.10",
44
+ "@types/react": "19.2.10",
45
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
46
+ "@typescript-eslint/parser": "^8.56.1",
47
+ "eslint": "^9.39.3",
48
+ "eslint-config-prettier": "^10.1.8",
49
+ "eslint-plugin-prettier": "^5.5.5",
50
+ "eslint-plugin-react": "^7.37.5",
51
+ "husky": "^9.1.7",
52
+ "lint-staged": "^16.2.7",
53
+ "prettier": "^3.8.1",
54
+ "tsx": "^4.21.0",
55
+ "typescript-eslint": "^8.56.1",
56
+ "jiti": "^2.6.1"
57
+ },
58
+ "lint-staged": {
59
+ "*.{js,jsx,ts,tsx,md,mdx,html,css}": "npm run format"
60
+ }
61
+ }
@@ -0,0 +1 @@
1
+ export const TOKEN_PREFIX = 'com.merkle.oss.magnolia.headless.';
@@ -0,0 +1,14 @@
1
+ import { DependencyContainer } from 'tsyringe';
2
+ import registerApis from './api/_Dependencies.ts';
3
+ import registerDynamicPages from './dynamic-page/_Dependencies.ts';
4
+ import registerHelpers from './helper/_Dependencies.ts';
5
+ import registerMiddlewares from './middleware/_Dependencies.ts';
6
+ import registerTemplates from './templates/_Dependencies.ts';
7
+
8
+ export function register(container: DependencyContainer) {
9
+ registerApis(container);
10
+ registerDynamicPages(container);
11
+ registerHelpers(container);
12
+ registerMiddlewares(container);
13
+ registerTemplates(container);
14
+ }
package/src/Index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './api/_Index.ts';
2
+ export * from './dynamic-page/_Index.ts';
3
+ export * from './config/_Index.ts';
4
+ export * from './helper/_Index.ts';
5
+ export * from './middleware/_Index.ts';
6
+ export * from './templates/_Index.ts';
7
+ export * from './Constants.ts';
8
+ export * from './Dependencies.ts';
@@ -0,0 +1,30 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { inject, injectable } from 'tsyringe';
3
+ import { ErrorType, MagnoliaPageRestClient } from '../helper/MagnoliaPageRestClient.ts';
4
+
5
+ import { TOKEN_PREFIX } from '../Constants.ts';
6
+
7
+ @injectable()
8
+ export class ErrorPageApi {
9
+ constructor(@inject(TOKEN_PREFIX + 'MagnoliaPageRestClient') private magnoliaPageRestClient: MagnoliaPageRestClient) {}
10
+
11
+ public async get(req: NextRequest, language: string): Promise<Response> {
12
+ const queryParams = req.nextUrl.searchParams;
13
+ const url = new URL(`https://${req.headers.get('host')}/${language}`);
14
+ const errorType = this.getErrorTypeFromString(queryParams.get('errorType'));
15
+ const headers = new Headers();
16
+ headers.set('Content-Type', 'application/json');
17
+ return this.magnoliaPageRestClient
18
+ .getErrorPageContent(url, errorType)
19
+ .then((content) => JSON.stringify(content))
20
+ .then((body) => new Response(body, { headers: headers }));
21
+ }
22
+
23
+ private getErrorTypeFromString(value: string): ErrorType | undefined {
24
+ const directions = Object.values(ErrorType);
25
+ if (directions.includes(value as ErrorType)) {
26
+ return value as ErrorType;
27
+ }
28
+ return undefined;
29
+ }
30
+ }
@@ -0,0 +1,33 @@
1
+ import { RestClient } from '../helper/RestClient.ts';
2
+ import { NextRequest } from 'next/server';
3
+ import { inject, injectable } from 'tsyringe';
4
+ import type { HeadlessConfigProviderI, MagnoliaApiEndpointsProvider } from '../config/ConfigProvider.ts';
5
+
6
+ import { TOKEN_PREFIX } from '../Constants.ts';
7
+
8
+ @injectable()
9
+ export class RobotsTxtApi {
10
+ private readonly apisProvider: MagnoliaApiEndpointsProvider;
11
+
12
+ constructor(
13
+ @inject(TOKEN_PREFIX + 'HeadlessConfigProviderI') configProvider: HeadlessConfigProviderI,
14
+ @inject(TOKEN_PREFIX + 'RestClient') private restClient: RestClient,
15
+ ) {
16
+ this.apisProvider = configProvider.get().magnoliaApisProvider;
17
+ }
18
+
19
+ public async get(req: NextRequest, sitemapUrlTemplate: string): Promise<Response> {
20
+ const domain: string = new URL('https://' + req.headers.get('host')).hostname; //strip port
21
+ const headers = new Headers();
22
+ headers.set('Content-Type', 'text/plain');
23
+ return this.getRobots(domain, sitemapUrlTemplate).then((robots) => new Response(robots, { headers: headers }));
24
+ }
25
+
26
+ private async getRobots(domain: string, sitemapUrlTemplate: string): Promise<string> {
27
+ const queryParams = new URLSearchParams();
28
+ queryParams.set('domain', domain);
29
+ queryParams.set('sitemapUrlTemplate', sitemapUrlTemplate);
30
+ const url = this.apisProvider.robots() + '?' + queryParams.toString();
31
+ return this.restClient.fetchMagnoliaBasicAuth(url).then((response) => this.restClient.getText(url, response));
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ import { RestClient } from '../helper/RestClient.ts';
2
+ import { NextRequest } from 'next/server';
3
+ import { inject, injectable } from 'tsyringe';
4
+ import type { HeadlessConfigProviderI, MagnoliaApiEndpointsProvider } from '../config/ConfigProvider.ts';
5
+
6
+ import { TOKEN_PREFIX } from '../Constants.ts';
7
+
8
+ @injectable()
9
+ export class SitemapApi {
10
+ private readonly apisProvider: MagnoliaApiEndpointsProvider;
11
+
12
+ constructor(
13
+ @inject(TOKEN_PREFIX + 'HeadlessConfigProviderI') configProvider: HeadlessConfigProviderI,
14
+ @inject(TOKEN_PREFIX + 'RestClient') private restClient: RestClient,
15
+ ) {
16
+ this.apisProvider = configProvider.get().magnoliaApisProvider;
17
+ }
18
+
19
+ public async get(req: NextRequest, language: string, type: string): Promise<Response> {
20
+ const domain: string = new URL('https://' + req.headers.get('host')).hostname; //strip port
21
+ const headers = new Headers();
22
+ headers.set('Content-Type', 'application/xml');
23
+ return this.getSitemap(domain, language, type).then((sitemap) => new Response(sitemap, { headers: headers }));
24
+ }
25
+
26
+ private async getSitemap(domain: string, language: string, type: string): Promise<string> {
27
+ const queryParams = new URLSearchParams();
28
+ queryParams.set('domain', domain);
29
+ queryParams.set('type', type);
30
+ const url = this.apisProvider.sitemap(language) + '?' + queryParams.toString();
31
+ return this.restClient.fetchMagnoliaBasicAuth(url).then((response) => this.restClient.getText(url, response, 'application/xml'));
32
+ }
33
+ }
@@ -0,0 +1,12 @@
1
+ import { DependencyContainer } from 'tsyringe';
2
+ import { ErrorPageApi } from './ErrorPageApi.ts';
3
+ import { RobotsTxtApi } from './RobotsTxtApi.ts';
4
+ import { SitemapApi } from './SitemapApi.ts';
5
+
6
+ import { TOKEN_PREFIX } from '../Constants.ts';
7
+
8
+ export default function register(container: DependencyContainer) {
9
+ container.register(TOKEN_PREFIX + 'ErrorPageApi', { useClass: ErrorPageApi });
10
+ container.register(TOKEN_PREFIX + 'RobotsTxtApi', { useClass: RobotsTxtApi });
11
+ container.register(TOKEN_PREFIX + 'SitemapApi', { useClass: SitemapApi });
12
+ }
@@ -0,0 +1,3 @@
1
+ export * from './ErrorPageApi.ts';
2
+ export * from './RobotsTxtApi.ts';
3
+ export * from './SitemapApi.ts';
@@ -0,0 +1,73 @@
1
+ # API
2
+
3
+ ## [Robots.txt](./RobotsTxtApi.ts)
4
+
5
+ Consumes content provided by the [Magnolia Robots Endpoint](../../../magnolia-headless-spring/src/main/java/com/merkle/oss/magnolia/headless/api/robots/README.md).
6
+
7
+ `/app/api/robots.txt/route.ts`
8
+
9
+ ```typescript
10
+ import { NextRequest } from 'next/server';
11
+ import { container } from '../../Dependencies.ts';
12
+ import { RobotsTxtApi } from '@merkle-open/magnolia-headless-frontend-nextjs';
13
+ import { TOKEN_PREFIX } from '@merkle-open/magnolia-headless-frontend-nextjs';
14
+
15
+ const robotsApi = container.resolve<RobotsTxtApi>(TOKEN_PREFIX + 'RobotsTxtApi');
16
+
17
+ export async function GET(req: NextRequest): Promise<Response> {
18
+ return robotsApi.get(req, '/{language}/sitemap/{type}.xml');
19
+ }
20
+ ```
21
+
22
+ ## [Sitemap](./SitemapApi.ts)
23
+
24
+ Consumes content provided by the [Magnolia Sitemap Endpoint](../../../magnolia-headless-spring/src/main/java/com/merkle/oss/magnolia/headless/api/sitemap/README.md).
25
+
26
+ `/app/api/[language]/sitemap/[type]/route.ts`
27
+
28
+ ```typescript
29
+ import { NextRequest } from 'next/server';
30
+ import { container } from '../../../../Dependencies.ts';
31
+ import { SitemapApi } from '@merkle-open/magnolia-headless-frontend-nextjs';
32
+ import { TOKEN_PREFIX } from '@merkle-open/magnolia-headless-frontend-nextjs';
33
+
34
+ const sitemapApi = container.resolve<SitemapApi>(TOKEN_PREFIX + 'SitemapApi');
35
+
36
+ export async function GET(req: NextRequest): Promise<Response> {
37
+ const language: string = getLanguage(req.url);
38
+ const type: string = getType(req.url);
39
+ return sitemapApi.get(req, language, type);
40
+ }
41
+
42
+ function getLanguage(req: NextRequest): String {
43
+ //TODO implement
44
+ }
45
+ function getType(req: NextRequest): String {
46
+ //TODO implement
47
+ }
48
+ ```
49
+
50
+ ## [ErrorPage](./ErrorPageApi.ts)
51
+
52
+ Consumes content provided by the [Magnolia Page Endpoint](../../../magnolia-headless-spring/src/main/java/com/merkle/oss/magnolia/headless/api/page/README.md).
53
+ Frontend API path must be provided in the [HeadlessConfigProviderI](../config/_README.md#headlessconfigprovideri)!
54
+
55
+ `/app/api/[language]/error/route.ts`
56
+
57
+ ```typescript
58
+ import { NextRequest } from 'next/server';
59
+ import { container } from '../../../Dependencies.ts';
60
+ import { ErrorPageApi } from '@merkle-open/magnolia-headless-frontend-nextjs';
61
+ import { TOKEN_PREFIX } from '@merkle-open/magnolia-headless-frontend-nextjs';
62
+
63
+ const errorPageApi = container.resolve<ErrorPageApi>(TOKEN_PREFIX + 'ErrorPageApi');
64
+
65
+ export async function GET(req: NextRequest): Promise<Response> {
66
+ const language: string = getLanguage(req.url);
67
+ return errorPageApi.get(req, language);
68
+ }
69
+
70
+ function getLanguage(req: NextRequest): String {
71
+ //TODO implement
72
+ }
73
+ ```
@@ -0,0 +1,5 @@
1
+ import { ComponentMappings } from '@magnolia/react-editor';
2
+
3
+ export interface ComponentMappingsProviderI {
4
+ getComponentMappings(): ComponentMappings;
5
+ }
@@ -0,0 +1,35 @@
1
+ export interface MagnoliaApiEndpointsProvider {
2
+ annotationTemplates(): string;
3
+ pageContent(language: string): string;
4
+ errorPageContent(language: string): string;
5
+ dynamicResponseHeader(): string;
6
+ vanity(language: string): string;
7
+ robots(): string;
8
+ sitemap(language: string): string;
9
+ }
10
+
11
+ export interface FrontendApiEndpointsProvider {
12
+ errorPage(language: string): string;
13
+ }
14
+
15
+ export interface ThemesProvider {
16
+ getFallback(): string;
17
+ getAll(): string[];
18
+ }
19
+
20
+ export type Credentials = {
21
+ username: string;
22
+ password: string;
23
+ };
24
+
25
+ export type Config = {
26
+ magnoliaCredentials: Credentials;
27
+ multiTree: boolean;
28
+ magnoliaApisProvider: MagnoliaApiEndpointsProvider;
29
+ frontendApisProvider: FrontendApiEndpointsProvider;
30
+ themesProvider: ThemesProvider;
31
+ };
32
+
33
+ export interface HeadlessConfigProviderI {
34
+ get(): Config;
35
+ }
@@ -0,0 +1,10 @@
1
+ export enum Level {
2
+ DEBUG = 'DEBUG',
3
+ INFO = 'INFO',
4
+ WARN = 'WARN',
5
+ ERROR = 'ERROR',
6
+ }
7
+
8
+ export interface LoggerI {
9
+ log(level: Level, message: string): void;
10
+ }
@@ -0,0 +1,3 @@
1
+ export interface StylesheetProviderI {
2
+ get(theme: string): string;
3
+ }
@@ -0,0 +1,4 @@
1
+ export * from './Logger.ts';
2
+ export * from './ConfigProvider.ts';
3
+ export * from './ComponentMappingsProvider.ts';
4
+ export { type StylesheetProviderI } from './StylesheetProvider.ts';
@@ -0,0 +1,115 @@
1
+ # Config
2
+
3
+ ## Interfaces
4
+
5
+ ### [HeadlessConfigProviderI](./ConfigProvider.ts)
6
+
7
+ `/app/config/SampleHeadlessConfigProvider.ts`
8
+
9
+ ```typescript
10
+ import { Config, HeadlessConfigProviderI } from '@merkle-open/magnolia-headless-frontend-nextjs';
11
+ import { injectable } from 'tsyringe';
12
+
13
+ export enum Themes {
14
+ Default = 'default',
15
+ Theme1 = 'theme1',
16
+ }
17
+
18
+ @injectable()
19
+ export class SampleHeadlessConfigProvider implements HeadlessConfigProviderI {
20
+ private readonly config: Config = {
21
+ magnoliaApisProvider: {
22
+ pageContent: (language: string) => process.env.MGNL_API_PAGES.replace('{language}', language),
23
+ ...
24
+ },
25
+ frontendApisProvider: {
26
+ errorPage: (language: string) => `/api/${language}/error`,
27
+ },
28
+ themesProvider: {
29
+ getFallback: () => Themes.Default,
30
+ getAll: () => Object.values(Themes),
31
+ },
32
+ ...
33
+ };
34
+
35
+ public get(): Config {
36
+ return this.config;
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### [LoggerI](./Logger.ts)
42
+
43
+ `/app/config/SampleLogger.ts`
44
+
45
+ ```typescript
46
+ import { Level, LoggerI } from '@merkle-open/magnolia-headless-frontend-nextjs';
47
+ import { injectable } from 'tsyringe';
48
+
49
+ @injectable()
50
+ export class SampleLogger implements LoggerI {
51
+ public log(level: Level, message: string): void {
52
+ //TODO implement
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### [ComponentMappingsProviderI](./ComponentMappingsProvider.ts)
58
+
59
+ `/app/config/SampleComponentMappingsProvider.ts`
60
+
61
+ ```typescript
62
+ import { injectable } from 'tsyringe';
63
+ import { ComponentMappingsProviderI } from '@merkle-open/magnolia-headless-frontend-nextjs';
64
+ import { ComponentMappings } from '@magnolia/react-editor';
65
+ import someComponent from './components/someComponent/SomeComponent.tsx';
66
+
67
+ @injectable()
68
+ export class SampleComponentMappingsProvider implements ComponentMappingsProviderI {
69
+ getComponentMappings(): ComponentMappings {
70
+ return {
71
+ "<MAGNOLIA_TEMPLATE_ID_SOME_COMPONENT>": someComponent,
72
+ ...
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### [StylesheetProviderI](./StylesheetProvider.ts)
79
+
80
+ `/app/config/SampleStylesheetProvider.ts`
81
+
82
+ ```typescript
83
+ import { injectable } from 'tsyringe';
84
+ import { StylesheetProviderI } from '@merkle-open/magnolia-headless-frontend-nextjs';
85
+
86
+ @injectable()
87
+ export class SampleStylesheetProvider implements StylesheetProviderI {
88
+ public get(theme: string): string {
89
+ return `/css/theme/${theme}.min.css`;
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Binding
95
+
96
+ `/app/config/_Dependencies.ts`
97
+
98
+ ```typescript
99
+ import { DependencyContainer } from 'tsyringe';
100
+ import { TOKEN_PREFIX } from '@merkle-open/magnolia-headless-frontend-nextjs';
101
+ import { Logger } from '@merkle-open/magnolia-headless-frontend-nextjs';
102
+
103
+ export default function register(container: DependencyContainer) {
104
+ container.register(TOKEN_PREFIX + 'HeadlessConfigProviderI', {
105
+ useClass: SampleHeadlessConfigProvider,
106
+ });
107
+ container.register(TOKEN_PREFIX + 'ComponentMappingsProviderI', {
108
+ useClass: SampleComponentMappingsProvider,
109
+ });
110
+ container.register(TOKEN_PREFIX + 'StylesheetProviderI', {
111
+ useClass: SampleStylesheetProvider,
112
+ });
113
+ container.register(TOKEN_PREFIX + 'LoggerI', { useClass: SampleLogger });
114
+ }
115
+ ```
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { ReactNode } from 'react';
4
+ import { ErrorType } from '../helper/MagnoliaPageRestClient.ts';
5
+ import { ErrorStatic } from '../templates/pages/_error-static/ErrorStatic.tsx';
6
+ import { AbstractDynamicPage } from './AbstractDynamicPage.tsx';
7
+ import { Logger } from '../helper/Logger.ts';
8
+ import { RestClient } from '../helper/RestClient.ts';
9
+ import { MagnoliaContextProvider } from '../helper/MagnoliaContextProvider.ts';
10
+ import type { FrontendApiEndpointsProvider, HeadlessConfigProviderI } from '../config/ConfigProvider.ts';
11
+ import { type StylesheetProviderI } from '../config/StylesheetProvider.ts';
12
+ import { type ComponentMappingsProviderI } from '../config/ComponentMappingsProvider.ts';
13
+
14
+ export abstract class AbstractDynamicErrorPage extends AbstractDynamicPage {
15
+ private readonly frontendApisProvider: FrontendApiEndpointsProvider;
16
+
17
+ protected constructor(
18
+ componentMappingsProvider: ComponentMappingsProviderI,
19
+ configProvider: HeadlessConfigProviderI,
20
+ StylesheetProviderI: StylesheetProviderI,
21
+ logger: Logger,
22
+ private readonly restClient: RestClient,
23
+ protected readonly magnoliaContextProvider: MagnoliaContextProvider,
24
+ ) {
25
+ super(componentMappingsProvider, configProvider, StylesheetProviderI, logger);
26
+ this.frontendApisProvider = configProvider.get().frontendApisProvider;
27
+ }
28
+
29
+ protected async renderDynamic(currentUrl: URL, errorType: ErrorType): Promise<ReactNode> {
30
+ const magnoliaContext = this.magnoliaContextProvider.getMagnoliaContext(currentUrl);
31
+ const content = await this.fetchErrorPageContent(magnoliaContext.currentLanguage, errorType);
32
+ return super.renderBase(magnoliaContext, content, {});
33
+ }
34
+
35
+ protected renderStatic(language: string, errorType: ErrorType): ReactNode {
36
+ return ErrorStatic({ language: language, errorType: errorType });
37
+ }
38
+
39
+ private async fetchErrorPageContent(language: string, errorType: ErrorType) {
40
+ const queryParams = new URLSearchParams();
41
+ queryParams.append('errorType', errorType);
42
+ const errorPageUrl = this.frontendApisProvider.errorPage(language) + '?' + queryParams.toString();
43
+ return fetch(errorPageUrl).then((response) => this.restClient.getJson(errorPageUrl, response));
44
+ }
45
+ }
@@ -0,0 +1,53 @@
1
+ import { ExtendedMagnoliaContext } from '../helper/MagnoliaContextProvider.ts';
2
+ import { Content } from '../helper/MagnoliaPageRestClient.ts';
3
+ import { MgnlTemplateAnnotations } from '@magnolia/frontend-helpers-base';
4
+ import React, { ReactNode } from 'react';
5
+ import { MagnoliaConfig, RefService } from '@magnolia/react-editor';
6
+ import { EditablePage } from '../templates/pages/__magnolia-editable-page/EditablePage.tsx';
7
+ import { Logger } from '../helper/Logger.ts';
8
+ import type { HeadlessConfigProviderI, ThemesProvider } from '../config/ConfigProvider.ts';
9
+ import { type StylesheetProviderI } from '../config/StylesheetProvider.ts';
10
+ import { type ComponentMappingsProviderI } from '../config/ComponentMappingsProvider.ts';
11
+
12
+ export abstract class AbstractDynamicPage {
13
+ private themesProvider: ThemesProvider;
14
+
15
+ protected constructor(
16
+ private readonly componentMappingsProvider: ComponentMappingsProviderI,
17
+ readonly configProvider: HeadlessConfigProviderI,
18
+ private readonly StylesheetProviderI: StylesheetProviderI,
19
+ private readonly logger: Logger,
20
+ ) {
21
+ this.themesProvider = configProvider.get().themesProvider;
22
+ }
23
+
24
+ protected renderBase(magnoliaContext: ExtendedMagnoliaContext, content: Content, templateAnnotations: MgnlTemplateAnnotations): ReactNode {
25
+ const config: MagnoliaConfig = {
26
+ componentMappings: this.componentMappingsProvider.getComponentMappings(),
27
+ };
28
+
29
+ RefService.setMagnoliaContextRef(magnoliaContext);
30
+ global.mgnlInPageEditor = magnoliaContext.isMagnoliaEdit;
31
+
32
+ return (
33
+ <div>
34
+ {/* is moved to <head> see https://react.dev/blog/2024/12/05/react-19#support-for-stylesheets */}
35
+ <link rel="stylesheet" href={this.getStylesheet(content)} precedence="high" />
36
+ <EditablePage content={content} config={config} templateAnnotations={templateAnnotations} magnoliaContext={magnoliaContext} />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ private getStylesheet(content: Content) {
42
+ return this.StylesheetProviderI.get(this.getValidatedTheme(content.theme));
43
+ }
44
+ private getValidatedTheme(theme?: string): string {
45
+ if (!theme || !this.themesProvider.getAll().includes(theme)) {
46
+ if (theme) {
47
+ this.logger.error(`Got invalid theme '${theme}' from magnolia! falling back to ${this.themesProvider.getFallback()}`);
48
+ }
49
+ return this.themesProvider.getFallback();
50
+ }
51
+ return theme;
52
+ }
53
+ }