@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.
- package/README.md +76 -0
- package/index.ts +1 -0
- package/package.json +61 -0
- package/src/Constants.ts +1 -0
- package/src/Dependencies.ts +14 -0
- package/src/Index.ts +8 -0
- package/src/api/ErrorPageApi.ts +30 -0
- package/src/api/RobotsTxtApi.ts +33 -0
- package/src/api/SitemapApi.ts +33 -0
- package/src/api/_Dependencies.ts +12 -0
- package/src/api/_Index.ts +3 -0
- package/src/api/_README.md +73 -0
- package/src/config/ComponentMappingsProvider.ts +5 -0
- package/src/config/ConfigProvider.ts +35 -0
- package/src/config/Logger.ts +10 -0
- package/src/config/StylesheetProvider.ts +3 -0
- package/src/config/_Index.ts +4 -0
- package/src/config/_README.md +115 -0
- package/src/dynamic-page/AbstractDynamicErrorPage.tsx +45 -0
- package/src/dynamic-page/AbstractDynamicPage.tsx +53 -0
- package/src/dynamic-page/DynamicErrorPage.tsx +53 -0
- package/src/dynamic-page/DynamicErrorPageGlobal.tsx +66 -0
- package/src/dynamic-page/DynamicPage.tsx +50 -0
- package/src/dynamic-page/DynamicPageLayout.tsx +35 -0
- package/src/dynamic-page/PageProps.ts +40 -0
- package/src/dynamic-page/_Dependencies.ts +16 -0
- package/src/dynamic-page/_Index.ts +5 -0
- package/src/helper/BasicAuth.ts +3 -0
- package/src/helper/BrowserLanguageProvider.ts +31 -0
- package/src/helper/Logger.ts +22 -0
- package/src/helper/MagnoliaContextProvider.ts +60 -0
- package/src/helper/MagnoliaPageRestClient.ts +80 -0
- package/src/helper/MetaProps.ts +23 -0
- package/src/helper/MetadataProvider.ts +54 -0
- package/src/helper/RestClient.ts +49 -0
- package/src/helper/_Dependencies.ts +18 -0
- package/src/helper/_Index.ts +2 -0
- package/src/middleware/Middleware.ts +61 -0
- package/src/middleware/_Dependencies.ts +14 -0
- package/src/middleware/_Index.ts +1 -0
- package/src/middleware/_README.md +72 -0
- package/src/middleware/impl/DynamicResponseHeaderMiddleware.ts +45 -0
- package/src/middleware/impl/I18nRedirectMiddleware.ts +29 -0
- package/src/middleware/impl/VanityMiddleware.ts +103 -0
- package/src/scripts/generate-magnolia-config.ts +55 -0
- package/src/templates/ComponentMappingsProvider.ts +17 -0
- package/src/templates/_Dependencies.ts +8 -0
- package/src/templates/_Index.ts +5 -0
- package/src/templates/_README.md +86 -0
- package/src/templates/areas/BaseProps.ts +5 -0
- package/src/templates/areas/__magnolia-editable-area/EditableArea.tsx +27 -0
- package/src/templates/areas/__magnolia-editable-area/EditableAreaProps.ts +12 -0
- package/src/templates/components/BaseProps.ts +3 -0
- package/src/templates/components/__magnolia-editable-component/EditableComponent.tsx +23 -0
- package/src/templates/components/__magnolia-editable-component/EditableComponentProps.ts +5 -0
- package/src/templates/components/_error/ErrorComponent.tsx +7 -0
- package/src/templates/components/_error/ErrorComponentProps.ts +4 -0
- package/src/templates/components/_error/template.json +4 -0
- package/src/templates/components/_redirect/RedirectComponent.tsx +17 -0
- package/src/templates/components/_redirect/RedirectComponentProps.ts +11 -0
- package/src/templates/components/_redirect/template.json +4 -0
- package/src/templates/elements/_error/ErrorBoundary.tsx +33 -0
- package/src/templates/elements/_error/ErrorElement.tsx +22 -0
- package/src/templates/elements/_error/ErrorElementProps.ts +6 -0
- package/src/templates/magnolia.config.ts +10 -0
- package/src/templates/pages/BaseProps.ts +4 -0
- package/src/templates/pages/__magnolia-editable-page/EditablePage.tsx +16 -0
- package/src/templates/pages/__magnolia-editable-page/EditablePageProps.ts +11 -0
- package/src/templates/pages/_error-static/ErrorStatic.tsx +26 -0
- 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
|
+
}
|
package/src/Constants.ts
ADDED
|
@@ -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,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,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,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
|
+
}
|