@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalygo/create-diggocms-app",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool to scaffold a new DiggoCMS project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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://your-cms-api.com
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
@@ -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 fetch(`${baseUrl}${endpoint}`);
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 fetch(`${sdkConfig.baseUrl}/pages/${slug}`);
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: 'Error connecting to CMS',
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 fetch(`${sdkConfig.baseUrl}/collections/${type}`);
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: 'Error connecting to CMS',
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 fetch(`${sdkConfig.baseUrl}/menus/${key}`);
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: 'Error connecting to CMS',
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 ?? 'http://placeholder.local';
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
  }
@@ -2,7 +2,6 @@
2
2
  "name": "my-diggocms-app",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "type": "module",
6
5
  "scripts": {
7
6
  "dev": "next dev",
8
7
  "build": "next build",
@@ -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 { fetchPageData, FetchPageResult } from '@/lib/data-fetching';
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
- <main className="max-w-5xl mx-auto p-8">
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
- </main>
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
- <main className="max-w-5xl mx-auto p-8">
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
- </main>
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 = await fetchPageData(slug);
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 bg-gray-100 py-4 mb-8;
88
+ @apply w-full;
65
89
  }
66
90
 
67
91
  .menu-list {
68
- @apply flex flex-wrap gap-6 max-w-5xl mx-auto px-8 list-none;
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://your-cms-api.com
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 fetch(`${mockApiUrl}/pages/${slug}`);
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: 'Failed to connect to mock API. Is it running?',
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 fetch(`${sdkConfig.baseUrl}/pages/${slug}`);
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: 'Error connecting to CMS',
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 ?? 'http://placeholder.local';
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,7 +2,6 @@
2
2
  "name": "my-diggocms-app",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "type": "module",
6
5
  "scripts": {
7
6
  "dev": "next dev",
8
7
  "build": "next build",
@@ -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 (not needed in mock mode)
6
- BASE_URL=https://your-cms-api.com
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 fetch(`${baseUrl}${endpoint}`);
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 fetch(`${sdkConfig.baseUrl}/pages/${slug}`);
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: 'Error connecting to CMS',
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 fetch(`${sdkConfig.baseUrl}/collections/${type}`);
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: 'Error connecting to CMS',
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 fetch(`${sdkConfig.baseUrl}/menus/${key}`);
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: 'Error connecting to CMS',
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 ?? 'http://placeholder.local';
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
  }
@@ -2,7 +2,6 @@
2
2
  "name": "my-diggocms-app",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "type": "module",
6
5
  "scripts": {
7
6
  "dev": "next dev",
8
7
  "build": "next build",
@@ -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 { fetchPageData, FetchPageResult, loadPagesIndex } from '@/lib/data-fetching';
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
- <main className="max-w-5xl mx-auto p-8">
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
- </main>
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
- <main className="max-w-5xl mx-auto p-8">
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
- </main>
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 }: FetchPageResult = await fetchPageData(slug);
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 bg-gray-100 py-4 mb-8;
88
+ @apply w-full;
65
89
  }
66
90
 
67
91
  .menu-list {
68
- @apply flex flex-wrap gap-6 max-w-5xl mx-auto px-8 list-none;
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;