@digitalygo/create-diggocms-app 0.1.2 → 0.1.3
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 +2 -0
- package/bin/cli.js +0 -6
- package/package.json +1 -1
- package/templates/full/.env.local.example +3 -2
- package/templates/full/README.md +6 -1
- package/templates/full/components/PageLayout.tsx +40 -0
- package/templates/full/lib/data-fetching.ts +55 -10
- package/templates/full/lib/diggo-config.ts +2 -2
- package/templates/full/package.json +0 -1
- package/templates/full/pages/[...slug].tsx +19 -8
- package/templates/full/styles/globals.css +34 -2
- package/templates/minimal/.env.local.example +3 -2
- package/templates/minimal/README.md +5 -1
- package/templates/minimal/lib/data-fetching.ts +46 -10
- package/templates/minimal/lib/diggo-config.ts +2 -2
- package/templates/minimal/package.json +0 -1
- package/templates/with-mock/.env.local.example +3 -2
- package/templates/with-mock/README.md +3 -0
- package/templates/with-mock/components/PageLayout.tsx +40 -0
- package/templates/with-mock/lib/data-fetching.ts +55 -10
- package/templates/with-mock/lib/diggo-config.ts +2 -2
- package/templates/with-mock/package.json +0 -1
- package/templates/with-mock/pages/[...slug].tsx +22 -8
- package/templates/with-mock/styles/globals.css +34 -2
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ MOCK=1
|
|
|
123
123
|
MOCK_API_URL=http://localhost:3001
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
Note: `BASE_URL` must be set to a valid URL when not using mock mode. Requests will timeout after 5 seconds if the server is unreachable.
|
|
127
|
+
|
|
126
128
|
## Package Manager Support
|
|
127
129
|
|
|
128
130
|
The CLI supports multiple package managers:
|
package/bin/cli.js
CHANGED
|
@@ -117,12 +117,6 @@ async function updatePackageJson(targetDir, projectName, packageManager) {
|
|
|
117
117
|
|
|
118
118
|
const scripts = packageJson.scripts;
|
|
119
119
|
|
|
120
|
-
if (packageManager === 'bun') {
|
|
121
|
-
scripts.dev = scripts.dev.replace('next dev', 'bunx next dev');
|
|
122
|
-
scripts.build = scripts.build.replace('next build', 'bunx next build');
|
|
123
|
-
scripts.start = scripts.start.replace('next start', 'bunx next start');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
120
|
if (scripts['dev:mock']) {
|
|
127
121
|
const mockCmd = {
|
|
128
122
|
bun: 'bun run mock:api',
|
package/package.json
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# Copy this file to .env.local and update the values
|
|
3
3
|
|
|
4
4
|
# CMS API Configuration
|
|
5
|
-
# Set your DiggoCMS API base URL
|
|
6
|
-
BASE_URL=https://
|
|
5
|
+
# Set your DiggoCMS API base URL (required when not using mock mode)
|
|
6
|
+
# Example: BASE_URL=https://cms.example.com
|
|
7
|
+
BASE_URL=
|
|
7
8
|
|
|
8
9
|
# Mock Mode
|
|
9
10
|
# Set to 1 to use local mock data instead of a real API
|
package/templates/full/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A complete DiggoCMS application built with Next.js Pages Router and the DiggoCMS
|
|
|
10
10
|
- Hot reload via `next dev`
|
|
11
11
|
- Extended components for all supported types: title, image, text, video, gallery, and card
|
|
12
12
|
- Navigation components with dropdown support
|
|
13
|
+
- Header renders the main menu from CMS or mock API data
|
|
13
14
|
- Error handling and mock indicators
|
|
14
15
|
|
|
15
16
|
## Getting Started
|
|
@@ -42,13 +43,15 @@ A complete DiggoCMS application built with Next.js Pages Router and the DiggoCMS
|
|
|
42
43
|
|
|
43
44
|
### Using a Real CMS API
|
|
44
45
|
|
|
45
|
-
Set your CMS API base URL
|
|
46
|
+
Set your CMS API base URL in `.env.local`:
|
|
46
47
|
|
|
47
48
|
```env
|
|
48
49
|
BASE_URL=https://your-cms-api.com
|
|
49
50
|
MOCK=0
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
Note: The template defaults to an empty `BASE_URL`. You must configure it before running in non-mock mode. Requests timeout after 5 seconds to prevent hanging.
|
|
54
|
+
|
|
52
55
|
### Using Mock Mode
|
|
53
56
|
|
|
54
57
|
Enable mock mode for development without a real API:
|
|
@@ -58,6 +61,8 @@ MOCK=1
|
|
|
58
61
|
MOCK_API_URL=http://localhost:3001
|
|
59
62
|
```
|
|
60
63
|
|
|
64
|
+
When `MOCK=1`, the `BASE_URL` setting is ignored.
|
|
65
|
+
|
|
61
66
|
## Project Structure
|
|
62
67
|
|
|
63
68
|
```text
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MenuPayload } from '@digitalygo/diggocms-sdk-core';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { renderMenu } from '@digitalygo/diggocms-sdk-core/server';
|
|
4
|
+
import { navigationRegistry } from '@/lib/diggo-config';
|
|
5
|
+
|
|
6
|
+
interface PageLayoutProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
menu: MenuPayload | null;
|
|
9
|
+
menuError: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PageLayout({ children, menu, menuError }: PageLayoutProps): ReactElement {
|
|
13
|
+
return (
|
|
14
|
+
<div className="page-shell">
|
|
15
|
+
<header className="site-header">
|
|
16
|
+
<div className="site-header__inner">
|
|
17
|
+
<a href="/home" className="site-brand">
|
|
18
|
+
DiggoCMS Demo
|
|
19
|
+
</a>
|
|
20
|
+
<div className="site-nav">
|
|
21
|
+
{menu ? (
|
|
22
|
+
renderMenu(menu, navigationRegistry)
|
|
23
|
+
) : (
|
|
24
|
+
<div className="menu-fallback" role="status">
|
|
25
|
+
Menu unavailable
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
{menuError && (
|
|
31
|
+
<div className="menu-alert" role="status">
|
|
32
|
+
{menuError}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<main className="page-content">{children}</main>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -12,15 +12,39 @@ interface PageIndexItem {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MOCK_API_URL = 'http://localhost:3001';
|
|
15
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
15
16
|
|
|
16
17
|
function getMockApiBaseUrl(): string {
|
|
17
18
|
return process.env.MOCK_API_URL ?? DEFAULT_MOCK_API_URL;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
async function fetchWithTimeout(
|
|
22
|
+
url: string,
|
|
23
|
+
timeoutMs: number
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
30
|
+
clearTimeout(timeoutId);
|
|
31
|
+
return response;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
35
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
async function fetchFromMockApi<T>(endpoint: string): Promise<T | null> {
|
|
21
42
|
try {
|
|
22
43
|
const baseUrl = getMockApiBaseUrl();
|
|
23
|
-
const response = await
|
|
44
|
+
const response = await fetchWithTimeout(
|
|
45
|
+
`${baseUrl}${endpoint}`,
|
|
46
|
+
FETCH_TIMEOUT_MS
|
|
47
|
+
);
|
|
24
48
|
|
|
25
49
|
if (!response.ok) {
|
|
26
50
|
return null;
|
|
@@ -99,7 +123,10 @@ export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
try {
|
|
102
|
-
const response = await
|
|
126
|
+
const response = await fetchWithTimeout(
|
|
127
|
+
`${sdkConfig.baseUrl}/pages/${slug}`,
|
|
128
|
+
FETCH_TIMEOUT_MS
|
|
129
|
+
);
|
|
103
130
|
|
|
104
131
|
if (!response.ok) {
|
|
105
132
|
return {
|
|
@@ -111,10 +138,14 @@ export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
|
111
138
|
|
|
112
139
|
const page = (await response.json()) as PagePayload;
|
|
113
140
|
return { page, error: null, usingMock: false };
|
|
114
|
-
} catch {
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const errorMessage =
|
|
143
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
115
144
|
return {
|
|
116
145
|
page: null,
|
|
117
|
-
error: '
|
|
146
|
+
error: errorMessage.includes('timeout')
|
|
147
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
148
|
+
: 'Error connecting to CMS',
|
|
118
149
|
usingMock: false,
|
|
119
150
|
};
|
|
120
151
|
}
|
|
@@ -143,7 +174,10 @@ export async function fetchCollectionData(
|
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
try {
|
|
146
|
-
const response = await
|
|
177
|
+
const response = await fetchWithTimeout(
|
|
178
|
+
`${sdkConfig.baseUrl}/collections/${type}`,
|
|
179
|
+
FETCH_TIMEOUT_MS
|
|
180
|
+
);
|
|
147
181
|
|
|
148
182
|
if (!response.ok) {
|
|
149
183
|
return {
|
|
@@ -155,10 +189,14 @@ export async function fetchCollectionData(
|
|
|
155
189
|
|
|
156
190
|
const collection = (await response.json()) as CollectionPayload;
|
|
157
191
|
return { collection, error: null, usingMock: false };
|
|
158
|
-
} catch {
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const errorMessage =
|
|
194
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
159
195
|
return {
|
|
160
196
|
collection: null,
|
|
161
|
-
error: '
|
|
197
|
+
error: errorMessage.includes('timeout')
|
|
198
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
199
|
+
: 'Error connecting to CMS',
|
|
162
200
|
usingMock: false,
|
|
163
201
|
};
|
|
164
202
|
}
|
|
@@ -183,7 +221,10 @@ export async function fetchMenuData(key: string): Promise<FetchMenuResult> {
|
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
try {
|
|
186
|
-
const response = await
|
|
224
|
+
const response = await fetchWithTimeout(
|
|
225
|
+
`${sdkConfig.baseUrl}/menus/${key}`,
|
|
226
|
+
FETCH_TIMEOUT_MS
|
|
227
|
+
);
|
|
187
228
|
|
|
188
229
|
if (!response.ok) {
|
|
189
230
|
return {
|
|
@@ -195,10 +236,14 @@ export async function fetchMenuData(key: string): Promise<FetchMenuResult> {
|
|
|
195
236
|
|
|
196
237
|
const menu = (await response.json()) as MenuPayload;
|
|
197
238
|
return { menu, error: null, usingMock: false };
|
|
198
|
-
} catch {
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const errorMessage =
|
|
241
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
199
242
|
return {
|
|
200
243
|
menu: null,
|
|
201
|
-
error: '
|
|
244
|
+
error: errorMessage.includes('timeout')
|
|
245
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
246
|
+
: 'Error connecting to CMS',
|
|
202
247
|
usingMock: false,
|
|
203
248
|
};
|
|
204
249
|
}
|
|
@@ -33,7 +33,7 @@ function getBaseUrl(): string {
|
|
|
33
33
|
return mockApiUrl ?? 'http://localhost:3001';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return baseUrl ?? '
|
|
36
|
+
return baseUrl ?? '';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export const sdkConfig: DiggoConfig = {
|
|
@@ -47,5 +47,5 @@ export function isMockMode(): boolean {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function hasBaseUrl(): boolean {
|
|
50
|
-
return !!baseUrl;
|
|
50
|
+
return !!baseUrl && baseUrl.length > 0;
|
|
51
51
|
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import type { GetStaticPaths, GetStaticProps } from 'next';
|
|
2
|
-
import type { PagePayload } from '@digitalygo/diggocms-sdk-core';
|
|
2
|
+
import type { MenuPayload, PagePayload } from '@digitalygo/diggocms-sdk-core';
|
|
3
3
|
import { useRouter } from 'next/router';
|
|
4
4
|
import Head from 'next/head';
|
|
5
5
|
import { renderPage } from '@digitalygo/diggocms-sdk-core/server';
|
|
6
6
|
import { componentsRegistry } from '@/lib/diggo-config';
|
|
7
|
-
import {
|
|
7
|
+
import { PageLayout } from '@/components/PageLayout';
|
|
8
|
+
import {
|
|
9
|
+
fetchMenuData,
|
|
10
|
+
fetchPageData,
|
|
11
|
+
FetchMenuResult,
|
|
12
|
+
FetchPageResult,
|
|
13
|
+
} from '@/lib/data-fetching';
|
|
8
14
|
|
|
9
15
|
interface CatchAllPageProps {
|
|
10
16
|
page: PagePayload | null;
|
|
11
17
|
error: string | null;
|
|
18
|
+
menu: MenuPayload | null;
|
|
19
|
+
menuError: string | null;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
export default function CatchAllPage({ page, error }: CatchAllPageProps) {
|
|
22
|
+
export default function CatchAllPage({ page, error, menu, menuError }: CatchAllPageProps) {
|
|
15
23
|
const router = useRouter();
|
|
16
24
|
|
|
17
25
|
if (router.isFallback) {
|
|
@@ -24,14 +32,14 @@ export default function CatchAllPage({ page, error }: CatchAllPageProps) {
|
|
|
24
32
|
|
|
25
33
|
if (!page) {
|
|
26
34
|
return (
|
|
27
|
-
<
|
|
35
|
+
<PageLayout menu={menu} menuError={menuError}>
|
|
28
36
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Page Not Found</h1>
|
|
29
37
|
{error && (
|
|
30
38
|
<div className="bg-red-50 border border-red-200 text-red-800 p-4 rounded">
|
|
31
39
|
{error}
|
|
32
40
|
</div>
|
|
33
41
|
)}
|
|
34
|
-
</
|
|
42
|
+
</PageLayout>
|
|
35
43
|
);
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -41,14 +49,14 @@ export default function CatchAllPage({ page, error }: CatchAllPageProps) {
|
|
|
41
49
|
<title>{page.title || 'My DiggoCMS App'}</title>
|
|
42
50
|
<meta name="description" content={page.title || 'DiggoCMS powered page'} />
|
|
43
51
|
</Head>
|
|
44
|
-
<
|
|
52
|
+
<PageLayout menu={menu} menuError={menuError}>
|
|
45
53
|
{error && (
|
|
46
54
|
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 mb-6 rounded">
|
|
47
55
|
<strong className="font-semibold">Warning:</strong> {error}
|
|
48
56
|
</div>
|
|
49
57
|
)}
|
|
50
58
|
{renderPage(page, componentsRegistry)}
|
|
51
|
-
</
|
|
59
|
+
</PageLayout>
|
|
52
60
|
</>
|
|
53
61
|
);
|
|
54
62
|
}
|
|
@@ -64,7 +72,8 @@ export const getStaticProps: GetStaticProps<CatchAllPageProps> = async ({ params
|
|
|
64
72
|
const slugArray = params?.slug as string[] | undefined;
|
|
65
73
|
const slug = Array.isArray(slugArray) ? slugArray.join('/') : 'home';
|
|
66
74
|
|
|
67
|
-
const { page, error }: FetchPageResult =
|
|
75
|
+
const [{ page, error }, { menu, error: menuError }]: [FetchPageResult, FetchMenuResult] =
|
|
76
|
+
await Promise.all([fetchPageData(slug), fetchMenuData('main')]);
|
|
68
77
|
|
|
69
78
|
if (!page) {
|
|
70
79
|
return {
|
|
@@ -76,6 +85,8 @@ export const getStaticProps: GetStaticProps<CatchAllPageProps> = async ({ params
|
|
|
76
85
|
props: {
|
|
77
86
|
page,
|
|
78
87
|
error,
|
|
88
|
+
menu,
|
|
89
|
+
menuError,
|
|
79
90
|
},
|
|
80
91
|
revalidate: 60,
|
|
81
92
|
};
|
|
@@ -14,6 +14,30 @@ body {
|
|
|
14
14
|
color: #333;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.page-shell {
|
|
18
|
+
@apply min-h-screen bg-gray-50;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.site-header {
|
|
22
|
+
@apply bg-white border-b border-gray-200 shadow-sm;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.site-header__inner {
|
|
26
|
+
@apply max-w-5xl mx-auto px-6 py-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.site-brand {
|
|
30
|
+
@apply text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.site-nav {
|
|
34
|
+
@apply w-full md:w-auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.page-content {
|
|
38
|
+
@apply max-w-5xl mx-auto px-6 py-10;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
/* DiggoCMS Component Styles */
|
|
18
42
|
.diggo-title {
|
|
19
43
|
@apply text-3xl font-bold mb-4 text-gray-900;
|
|
@@ -61,11 +85,11 @@ body {
|
|
|
61
85
|
|
|
62
86
|
/* Navigation Styles */
|
|
63
87
|
.menu {
|
|
64
|
-
@apply
|
|
88
|
+
@apply w-full;
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
.menu-list {
|
|
68
|
-
@apply flex flex-wrap gap-
|
|
92
|
+
@apply flex flex-wrap gap-4 items-center justify-start md:justify-end list-none;
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
.menu-item {
|
|
@@ -79,3 +103,11 @@ body {
|
|
|
79
103
|
.menu-link[aria-current="page"] {
|
|
80
104
|
@apply text-blue-600 font-semibold;
|
|
81
105
|
}
|
|
106
|
+
|
|
107
|
+
.menu-fallback {
|
|
108
|
+
@apply text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded px-3 py-2 inline-flex items-center;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.menu-alert {
|
|
112
|
+
@apply bg-yellow-50 text-yellow-800 border-t border-yellow-200 px-6 py-3 text-sm;
|
|
113
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# Copy this file to .env.local and update the values
|
|
3
3
|
|
|
4
4
|
# CMS API Configuration
|
|
5
|
-
# Set your DiggoCMS API base URL
|
|
6
|
-
BASE_URL=https://
|
|
5
|
+
# Set your DiggoCMS API base URL (required when not using mock mode)
|
|
6
|
+
# Example: BASE_URL=https://cms.example.com
|
|
7
|
+
BASE_URL=
|
|
7
8
|
|
|
8
9
|
# Mock Mode
|
|
9
10
|
# Set to 1 to use local mock data instead of a real API
|
|
@@ -40,13 +40,15 @@ A minimal DiggoCMS application built with Next.js Pages Router and the DiggoCMS
|
|
|
40
40
|
|
|
41
41
|
### Using a Real CMS API
|
|
42
42
|
|
|
43
|
-
Set your CMS API base URL
|
|
43
|
+
Set your CMS API base URL in `.env.local`:
|
|
44
44
|
|
|
45
45
|
```env
|
|
46
46
|
BASE_URL=https://your-cms-api.com
|
|
47
47
|
MOCK=0
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
Note: The template defaults to an empty `BASE_URL`. You must configure it before running in non-mock mode. Requests timeout after 5 seconds to prevent hanging.
|
|
51
|
+
|
|
50
52
|
### Using Mock Mode
|
|
51
53
|
|
|
52
54
|
Enable mock mode to use a local mock API:
|
|
@@ -56,6 +58,8 @@ MOCK=1
|
|
|
56
58
|
MOCK_API_URL=http://localhost:3001
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
When `MOCK=1`, the `BASE_URL` setting is ignored.
|
|
62
|
+
|
|
59
63
|
## Project Structure
|
|
60
64
|
|
|
61
65
|
```text
|
|
@@ -1,30 +1,59 @@
|
|
|
1
1
|
import type { PagePayload } from '@digitalygo/diggocms-sdk-core';
|
|
2
2
|
import { sdkConfig, isMockMode, hasBaseUrl } from './diggo-config';
|
|
3
3
|
|
|
4
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
5
|
+
|
|
4
6
|
export interface FetchPageResult {
|
|
5
7
|
page: PagePayload | null;
|
|
6
8
|
error: string | null;
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
async function fetchWithTimeout(
|
|
12
|
+
url: string,
|
|
13
|
+
timeoutMs: number
|
|
14
|
+
): Promise<Response> {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
20
|
+
clearTimeout(timeoutId);
|
|
21
|
+
return response;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
clearTimeout(timeoutId);
|
|
24
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
25
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
10
32
|
if (isMockMode()) {
|
|
11
33
|
try {
|
|
12
34
|
const mockApiUrl = process.env.MOCK_API_URL ?? 'http://localhost:3001';
|
|
13
|
-
const response = await
|
|
14
|
-
|
|
35
|
+
const response = await fetchWithTimeout(
|
|
36
|
+
`${mockApiUrl}/pages/${slug}`,
|
|
37
|
+
FETCH_TIMEOUT_MS
|
|
38
|
+
);
|
|
39
|
+
|
|
15
40
|
if (!response.ok) {
|
|
16
41
|
return {
|
|
17
42
|
page: null,
|
|
18
43
|
error: `Mock API returned ${response.status}`,
|
|
19
44
|
};
|
|
20
45
|
}
|
|
21
|
-
|
|
46
|
+
|
|
22
47
|
const page = (await response.json()) as PagePayload;
|
|
23
48
|
return { page, error: null };
|
|
24
|
-
} catch {
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const errorMessage =
|
|
51
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
25
52
|
return {
|
|
26
53
|
page: null,
|
|
27
|
-
error:
|
|
54
|
+
error: errorMessage.includes('timeout')
|
|
55
|
+
? 'Mock API request timed out. Is the server running?'
|
|
56
|
+
: 'Failed to connect to mock API. Is it running?',
|
|
28
57
|
};
|
|
29
58
|
}
|
|
30
59
|
}
|
|
@@ -37,21 +66,28 @@ export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
|
37
66
|
}
|
|
38
67
|
|
|
39
68
|
try {
|
|
40
|
-
const response = await
|
|
41
|
-
|
|
69
|
+
const response = await fetchWithTimeout(
|
|
70
|
+
`${sdkConfig.baseUrl}/pages/${slug}`,
|
|
71
|
+
FETCH_TIMEOUT_MS
|
|
72
|
+
);
|
|
73
|
+
|
|
42
74
|
if (!response.ok) {
|
|
43
75
|
return {
|
|
44
76
|
page: null,
|
|
45
77
|
error: `CMS returned ${response.status}`,
|
|
46
78
|
};
|
|
47
79
|
}
|
|
48
|
-
|
|
80
|
+
|
|
49
81
|
const page = (await response.json()) as PagePayload;
|
|
50
82
|
return { page, error: null };
|
|
51
|
-
} catch {
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const errorMessage =
|
|
85
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
52
86
|
return {
|
|
53
87
|
page: null,
|
|
54
|
-
error: '
|
|
88
|
+
error: errorMessage.includes('timeout')
|
|
89
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
90
|
+
: 'Error connecting to CMS',
|
|
55
91
|
};
|
|
56
92
|
}
|
|
57
93
|
}
|
|
@@ -14,7 +14,7 @@ function getBaseUrl(): string {
|
|
|
14
14
|
if (isMock) {
|
|
15
15
|
return mockApiUrl ?? 'http://localhost:3001';
|
|
16
16
|
}
|
|
17
|
-
return baseUrl ?? '
|
|
17
|
+
return baseUrl ?? '';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const sdkConfig: DiggoConfig = {
|
|
@@ -28,5 +28,5 @@ export function isMockMode(): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function hasBaseUrl(): boolean {
|
|
31
|
-
return !!baseUrl;
|
|
31
|
+
return !!baseUrl && baseUrl.length > 0;
|
|
32
32
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# Copy this file to .env.local and update the values
|
|
3
3
|
|
|
4
4
|
# CMS API Configuration
|
|
5
|
-
# Set your DiggoCMS API base URL (
|
|
6
|
-
BASE_URL=https://
|
|
5
|
+
# Set your DiggoCMS API base URL (only needed when not using mock mode)
|
|
6
|
+
# Example: BASE_URL=https://cms.example.com
|
|
7
|
+
BASE_URL=
|
|
7
8
|
|
|
8
9
|
# Mock Mode
|
|
9
10
|
# Set to 1 to use the local mock API server
|
|
@@ -10,6 +10,7 @@ A complete DiggoCMS application with mock API server for development, built with
|
|
|
10
10
|
- Hot reload via `next dev`
|
|
11
11
|
- Extended components for all supported types: title, image, text, video, gallery, and card
|
|
12
12
|
- Navigation components with dropdown support
|
|
13
|
+
- Header renders the main menu from mock API data (or CMS when configured)
|
|
13
14
|
- Built-in mock API server (runs with tsx, works with any package manager)
|
|
14
15
|
- Sample fixtures aligned with SDK payload model
|
|
15
16
|
- Mock mode indicator during development
|
|
@@ -118,6 +119,8 @@ To use a real CMS API instead of mock data:
|
|
|
118
119
|
MOCK=0
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
Note: `BASE_URL` defaults to empty. You must set a valid URL. Requests timeout after 5 seconds if the server is unreachable.
|
|
123
|
+
|
|
121
124
|
2. Restart the dev server:
|
|
122
125
|
|
|
123
126
|
```bash
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MenuPayload } from '@digitalygo/diggocms-sdk-core';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { renderMenu } from '@digitalygo/diggocms-sdk-core/server';
|
|
4
|
+
import { navigationRegistry } from '@/lib/diggo-config';
|
|
5
|
+
|
|
6
|
+
interface PageLayoutProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
menu: MenuPayload | null;
|
|
9
|
+
menuError: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PageLayout({ children, menu, menuError }: PageLayoutProps): ReactElement {
|
|
13
|
+
return (
|
|
14
|
+
<div className="page-shell">
|
|
15
|
+
<header className="site-header">
|
|
16
|
+
<div className="site-header__inner">
|
|
17
|
+
<a href="/home" className="site-brand">
|
|
18
|
+
DiggoCMS Demo
|
|
19
|
+
</a>
|
|
20
|
+
<div className="site-nav">
|
|
21
|
+
{menu ? (
|
|
22
|
+
renderMenu(menu, navigationRegistry)
|
|
23
|
+
) : (
|
|
24
|
+
<div className="menu-fallback" role="status">
|
|
25
|
+
Menu unavailable
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
{menuError && (
|
|
31
|
+
<div className="menu-alert" role="status">
|
|
32
|
+
{menuError}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<main className="page-content">{children}</main>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -12,15 +12,39 @@ interface PageIndexItem {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MOCK_API_URL = 'http://localhost:3001';
|
|
15
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
15
16
|
|
|
16
17
|
function getMockApiBaseUrl(): string {
|
|
17
18
|
return process.env.MOCK_API_URL ?? DEFAULT_MOCK_API_URL;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
async function fetchWithTimeout(
|
|
22
|
+
url: string,
|
|
23
|
+
timeoutMs: number
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
30
|
+
clearTimeout(timeoutId);
|
|
31
|
+
return response;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
35
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
async function fetchFromMockApi<T>(endpoint: string): Promise<T | null> {
|
|
21
42
|
try {
|
|
22
43
|
const baseUrl = getMockApiBaseUrl();
|
|
23
|
-
const response = await
|
|
44
|
+
const response = await fetchWithTimeout(
|
|
45
|
+
`${baseUrl}${endpoint}`,
|
|
46
|
+
FETCH_TIMEOUT_MS
|
|
47
|
+
);
|
|
24
48
|
|
|
25
49
|
if (!response.ok) {
|
|
26
50
|
return null;
|
|
@@ -99,7 +123,10 @@ export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
try {
|
|
102
|
-
const response = await
|
|
126
|
+
const response = await fetchWithTimeout(
|
|
127
|
+
`${sdkConfig.baseUrl}/pages/${slug}`,
|
|
128
|
+
FETCH_TIMEOUT_MS
|
|
129
|
+
);
|
|
103
130
|
|
|
104
131
|
if (!response.ok) {
|
|
105
132
|
return {
|
|
@@ -111,10 +138,14 @@ export async function fetchPageData(slug: string): Promise<FetchPageResult> {
|
|
|
111
138
|
|
|
112
139
|
const page = (await response.json()) as PagePayload;
|
|
113
140
|
return { page, error: null, usingMock: false };
|
|
114
|
-
} catch {
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const errorMessage =
|
|
143
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
115
144
|
return {
|
|
116
145
|
page: null,
|
|
117
|
-
error: '
|
|
146
|
+
error: errorMessage.includes('timeout')
|
|
147
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
148
|
+
: 'Error connecting to CMS',
|
|
118
149
|
usingMock: false,
|
|
119
150
|
};
|
|
120
151
|
}
|
|
@@ -143,7 +174,10 @@ export async function fetchCollectionData(
|
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
try {
|
|
146
|
-
const response = await
|
|
177
|
+
const response = await fetchWithTimeout(
|
|
178
|
+
`${sdkConfig.baseUrl}/collections/${type}`,
|
|
179
|
+
FETCH_TIMEOUT_MS
|
|
180
|
+
);
|
|
147
181
|
|
|
148
182
|
if (!response.ok) {
|
|
149
183
|
return {
|
|
@@ -155,10 +189,14 @@ export async function fetchCollectionData(
|
|
|
155
189
|
|
|
156
190
|
const collection = (await response.json()) as CollectionPayload;
|
|
157
191
|
return { collection, error: null, usingMock: false };
|
|
158
|
-
} catch {
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const errorMessage =
|
|
194
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
159
195
|
return {
|
|
160
196
|
collection: null,
|
|
161
|
-
error: '
|
|
197
|
+
error: errorMessage.includes('timeout')
|
|
198
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
199
|
+
: 'Error connecting to CMS',
|
|
162
200
|
usingMock: false,
|
|
163
201
|
};
|
|
164
202
|
}
|
|
@@ -183,7 +221,10 @@ export async function fetchMenuData(key: string): Promise<FetchMenuResult> {
|
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
try {
|
|
186
|
-
const response = await
|
|
224
|
+
const response = await fetchWithTimeout(
|
|
225
|
+
`${sdkConfig.baseUrl}/menus/${key}`,
|
|
226
|
+
FETCH_TIMEOUT_MS
|
|
227
|
+
);
|
|
187
228
|
|
|
188
229
|
if (!response.ok) {
|
|
189
230
|
return {
|
|
@@ -195,10 +236,14 @@ export async function fetchMenuData(key: string): Promise<FetchMenuResult> {
|
|
|
195
236
|
|
|
196
237
|
const menu = (await response.json()) as MenuPayload;
|
|
197
238
|
return { menu, error: null, usingMock: false };
|
|
198
|
-
} catch {
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const errorMessage =
|
|
241
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
199
242
|
return {
|
|
200
243
|
menu: null,
|
|
201
|
-
error: '
|
|
244
|
+
error: errorMessage.includes('timeout')
|
|
245
|
+
? 'Request to CMS timed out. Please check your BASE_URL and network connection.'
|
|
246
|
+
: 'Error connecting to CMS',
|
|
202
247
|
usingMock: false,
|
|
203
248
|
};
|
|
204
249
|
}
|
|
@@ -33,7 +33,7 @@ function getBaseUrl(): string {
|
|
|
33
33
|
return mockApiUrl ?? 'http://localhost:3001';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return baseUrl ?? '
|
|
36
|
+
return baseUrl ?? '';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export const sdkConfig: DiggoConfig = {
|
|
@@ -47,5 +47,5 @@ export function isMockMode(): boolean {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function hasBaseUrl(): boolean {
|
|
50
|
-
return !!baseUrl;
|
|
50
|
+
return !!baseUrl && baseUrl.length > 0;
|
|
51
51
|
}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import type { GetStaticPaths, GetStaticProps } from 'next';
|
|
2
|
-
import type { PagePayload } from '@digitalygo/diggocms-sdk-core';
|
|
2
|
+
import type { MenuPayload, PagePayload } from '@digitalygo/diggocms-sdk-core';
|
|
3
3
|
import { useRouter } from 'next/router';
|
|
4
4
|
import Head from 'next/head';
|
|
5
5
|
import { renderPage } from '@digitalygo/diggocms-sdk-core/server';
|
|
6
6
|
import { componentsRegistry, isMockMode } from '@/lib/diggo-config';
|
|
7
|
-
import {
|
|
7
|
+
import { PageLayout } from '@/components/PageLayout';
|
|
8
|
+
import {
|
|
9
|
+
fetchMenuData,
|
|
10
|
+
fetchPageData,
|
|
11
|
+
FetchMenuResult,
|
|
12
|
+
FetchPageResult,
|
|
13
|
+
loadPagesIndex,
|
|
14
|
+
} from '@/lib/data-fetching';
|
|
8
15
|
|
|
9
16
|
interface CatchAllPageProps {
|
|
10
17
|
page: PagePayload | null;
|
|
11
18
|
error: string | null;
|
|
12
19
|
usingMock: boolean;
|
|
20
|
+
menu: MenuPayload | null;
|
|
21
|
+
menuError: string | null;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
export default function CatchAllPage({ page, error, usingMock }: CatchAllPageProps) {
|
|
24
|
+
export default function CatchAllPage({ page, error, usingMock, menu, menuError }: CatchAllPageProps) {
|
|
16
25
|
const router = useRouter();
|
|
17
26
|
|
|
18
27
|
if (router.isFallback) {
|
|
@@ -25,14 +34,14 @@ export default function CatchAllPage({ page, error, usingMock }: CatchAllPagePro
|
|
|
25
34
|
|
|
26
35
|
if (!page) {
|
|
27
36
|
return (
|
|
28
|
-
<
|
|
37
|
+
<PageLayout menu={menu} menuError={menuError}>
|
|
29
38
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Page Not Found</h1>
|
|
30
39
|
{error && (
|
|
31
40
|
<div className="bg-red-50 border border-red-200 text-red-800 p-4 rounded">
|
|
32
41
|
{error}
|
|
33
42
|
</div>
|
|
34
43
|
)}
|
|
35
|
-
</
|
|
44
|
+
</PageLayout>
|
|
36
45
|
);
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -47,14 +56,14 @@ export default function CatchAllPage({ page, error, usingMock }: CatchAllPagePro
|
|
|
47
56
|
Mock Mode
|
|
48
57
|
</div>
|
|
49
58
|
)}
|
|
50
|
-
<
|
|
59
|
+
<PageLayout menu={menu} menuError={menuError}>
|
|
51
60
|
{error && (
|
|
52
61
|
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 mb-6 rounded">
|
|
53
62
|
<strong className="font-semibold">Warning:</strong> {error}
|
|
54
63
|
</div>
|
|
55
64
|
)}
|
|
56
65
|
{renderPage(page, componentsRegistry)}
|
|
57
|
-
</
|
|
66
|
+
</PageLayout>
|
|
58
67
|
</>
|
|
59
68
|
);
|
|
60
69
|
}
|
|
@@ -84,7 +93,10 @@ export const getStaticProps: GetStaticProps<CatchAllPageProps> = async ({ params
|
|
|
84
93
|
const slugArray = params?.slug as string[] | undefined;
|
|
85
94
|
const slug = Array.isArray(slugArray) ? slugArray.join('/') : 'home';
|
|
86
95
|
|
|
87
|
-
const { page, error, usingMock }:
|
|
96
|
+
const [{ page, error, usingMock }, { menu, error: menuError }]: [
|
|
97
|
+
FetchPageResult,
|
|
98
|
+
FetchMenuResult,
|
|
99
|
+
] = await Promise.all([fetchPageData(slug), fetchMenuData('main')]);
|
|
88
100
|
|
|
89
101
|
if (!page) {
|
|
90
102
|
return {
|
|
@@ -97,6 +109,8 @@ export const getStaticProps: GetStaticProps<CatchAllPageProps> = async ({ params
|
|
|
97
109
|
page,
|
|
98
110
|
error,
|
|
99
111
|
usingMock,
|
|
112
|
+
menu,
|
|
113
|
+
menuError,
|
|
100
114
|
},
|
|
101
115
|
revalidate: isMockMode() ? false : 60,
|
|
102
116
|
};
|
|
@@ -14,6 +14,30 @@ body {
|
|
|
14
14
|
color: #333;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.page-shell {
|
|
18
|
+
@apply min-h-screen bg-gray-50;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.site-header {
|
|
22
|
+
@apply bg-white border-b border-gray-200 shadow-sm;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.site-header__inner {
|
|
26
|
+
@apply max-w-5xl mx-auto px-6 py-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.site-brand {
|
|
30
|
+
@apply text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.site-nav {
|
|
34
|
+
@apply w-full md:w-auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.page-content {
|
|
38
|
+
@apply max-w-5xl mx-auto px-6 py-10;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
/* DiggoCMS Component Styles */
|
|
18
42
|
.diggo-title {
|
|
19
43
|
@apply text-3xl font-bold mb-4 text-gray-900;
|
|
@@ -61,11 +85,11 @@ body {
|
|
|
61
85
|
|
|
62
86
|
/* Navigation Styles */
|
|
63
87
|
.menu {
|
|
64
|
-
@apply
|
|
88
|
+
@apply w-full;
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
.menu-list {
|
|
68
|
-
@apply flex flex-wrap gap-
|
|
92
|
+
@apply flex flex-wrap gap-4 items-center justify-start md:justify-end list-none;
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
.menu-item {
|
|
@@ -80,6 +104,14 @@ body {
|
|
|
80
104
|
@apply text-blue-600 font-semibold;
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
.menu-fallback {
|
|
108
|
+
@apply text-sm text-gray-500 bg-gray-100 border border-gray-200 rounded px-3 py-2 inline-flex items-center;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.menu-alert {
|
|
112
|
+
@apply bg-yellow-50 text-yellow-800 border-t border-yellow-200 px-6 py-3 text-sm;
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
/* Mock Mode Indicator */
|
|
84
116
|
.mock-indicator {
|
|
85
117
|
@apply fixed bottom-4 right-4 bg-blue-600 text-white px-3 py-1 rounded text-sm font-medium shadow-lg z-50;
|