create-eziwiki 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +46 -0
  2. package/bin/create-eziwiki.mjs +83 -0
  3. package/lib/scaffold.mjs +230 -0
  4. package/lib/scaffold.test.mjs +270 -0
  5. package/package.json +38 -0
  6. package/template/README.md +39 -0
  7. package/template/app/[...slug]/page.tsx +162 -0
  8. package/template/app/error.tsx +77 -0
  9. package/template/app/global-error.tsx +76 -0
  10. package/template/app/globals.css +44 -0
  11. package/template/app/graph/page.tsx +62 -0
  12. package/template/app/layout.tsx +164 -0
  13. package/template/app/not-found.tsx +48 -0
  14. package/template/app/page.tsx +34 -0
  15. package/template/app/robots.ts +20 -0
  16. package/template/app/sitemap.ts +48 -0
  17. package/template/components/ThemeToggle.tsx +77 -0
  18. package/template/components/graph/GraphView.tsx +156 -0
  19. package/template/components/layout/Backlinks.tsx +42 -0
  20. package/template/components/layout/Breadcrumb.tsx +88 -0
  21. package/template/components/layout/MobileMenu.tsx +299 -0
  22. package/template/components/layout/NavigationButtons.tsx +87 -0
  23. package/template/components/layout/PageLayout.tsx +89 -0
  24. package/template/components/layout/Sidebar.tsx +376 -0
  25. package/template/components/layout/TabBar.tsx +312 -0
  26. package/template/components/layout/TabBarSkeleton.tsx +12 -0
  27. package/template/components/layout/TabInitializer.tsx +99 -0
  28. package/template/components/layout/TableOfContents.tsx +138 -0
  29. package/template/components/markdown/CodeCopy.tsx +65 -0
  30. package/template/components/markdown/MarkdownContent.tsx +38 -0
  31. package/template/components/markdown/PageTransition.tsx +56 -0
  32. package/template/components/providers/UrlMapProvider.tsx +68 -0
  33. package/template/components/search/SearchDialog.tsx +286 -0
  34. package/template/components/search/SearchTrigger.tsx +41 -0
  35. package/template/content/guides/_meta.json +4 -0
  36. package/template/content/guides/writing.md +82 -0
  37. package/template/content/intro.md +29 -0
  38. package/template/eslintignore +7 -0
  39. package/template/eslintrc.js +40 -0
  40. package/template/gitignore +40 -0
  41. package/template/lib/basePath.test.ts +120 -0
  42. package/template/lib/basePath.ts +108 -0
  43. package/template/lib/cache.ts +36 -0
  44. package/template/lib/content/registry.ts +311 -0
  45. package/template/lib/content/resolver.ts +109 -0
  46. package/template/lib/graph/build.ts +214 -0
  47. package/template/lib/graph/layout.test.ts +189 -0
  48. package/template/lib/graph/layout.ts +247 -0
  49. package/template/lib/markdown/languages.test.ts +85 -0
  50. package/template/lib/markdown/languages.ts +103 -0
  51. package/template/lib/markdown/rehype-plugins.ts +240 -0
  52. package/template/lib/markdown/remark-wikilink.ts +141 -0
  53. package/template/lib/markdown/render.ts +175 -0
  54. package/template/lib/markdown/wikilink.test.ts +91 -0
  55. package/template/lib/markdown/wikilink.ts +85 -0
  56. package/template/lib/navigation/auto.ts +227 -0
  57. package/template/lib/navigation/builder.test.ts +129 -0
  58. package/template/lib/navigation/builder.ts +122 -0
  59. package/template/lib/navigation/hash.ts +32 -0
  60. package/template/lib/navigation/url.test.ts +88 -0
  61. package/template/lib/navigation/url.ts +108 -0
  62. package/template/lib/navigation/urlMap.ts +81 -0
  63. package/template/lib/payload/schema.ts +81 -0
  64. package/template/lib/payload/types.ts +105 -0
  65. package/template/lib/payload/validator.ts +56 -0
  66. package/template/lib/search/build.ts +204 -0
  67. package/template/lib/search/client.ts +190 -0
  68. package/template/lib/search/tokenizer.test.ts +60 -0
  69. package/template/lib/search/tokenizer.ts +83 -0
  70. package/template/lib/search/types.ts +40 -0
  71. package/template/lib/site.ts +86 -0
  72. package/template/lib/store/searchStore.ts +26 -0
  73. package/template/lib/store/tabStore.ts +313 -0
  74. package/template/next-env.d.ts +5 -0
  75. package/template/next.config.js +43 -0
  76. package/template/package-lock.json +9933 -0
  77. package/template/package.json +69 -0
  78. package/template/payload/config.ts +34 -0
  79. package/template/postcss.config.js +6 -0
  80. package/template/prettierignore +7 -0
  81. package/template/prettierrc +8 -0
  82. package/template/public/favicon.svg +10 -0
  83. package/template/public/fonts/Pretandard/Pretendard-Bold.woff2 +0 -0
  84. package/template/public/fonts/Pretandard/Pretendard-Regular.woff2 +0 -0
  85. package/template/public/fonts/Pretandard/Pretendard-SemiBold.woff2 +0 -0
  86. package/template/public/fonts/SUITE/SUITE-Bold.woff2 +0 -0
  87. package/template/public/fonts/SUITE/SUITE-Regular.woff2 +0 -0
  88. package/template/public/fonts/SUITE/SUITE-SemiBold.woff2 +0 -0
  89. package/template/public/images/.gitkeep +0 -0
  90. package/template/scripts/build-search-index.ts +36 -0
  91. package/template/scripts/check-links.ts +40 -0
  92. package/template/scripts/show-urls.ts +48 -0
  93. package/template/scripts/validate-payload.ts +30 -0
  94. package/template/styles/markdown.css +167 -0
  95. package/template/styles/theme.css +65 -0
  96. package/template/tailwind.config.ts +156 -0
  97. package/template/tsconfig.json +32 -0
  98. package/template/vitest.config.ts +30 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared shapes for the search index.
3
+ *
4
+ * This module is isomorphic on purpose: the build script writes the index and
5
+ * the browser reads it, and both need to agree on the format.
6
+ */
7
+
8
+ /** One searchable unit — a whole document, or a section within one. */
9
+ export interface SearchDoc {
10
+ /** Stable identifier, `path` for a page and `path#anchor` for a section */
11
+ id: string;
12
+ /** Content-relative path of the source document */
13
+ path: string;
14
+ /** Href to navigate to, resolved under the site's URL strategy */
15
+ url: string;
16
+ /** Title of the page this entry belongs to */
17
+ title: string;
18
+ /** Heading text when this entry is a section, otherwise undefined */
19
+ section?: string;
20
+ /** Anchor id of the section, when applicable */
21
+ anchor?: string;
22
+ /** Page description from frontmatter */
23
+ description?: string;
24
+ /** Plain-text body of the page or section, used for matching and previews */
25
+ body: string;
26
+ }
27
+
28
+ /** The generated index file, as served to the browser. */
29
+ export interface SearchIndex {
30
+ /** Format version, so a stale cached index can be detected and ignored */
31
+ version: number;
32
+ /** Every searchable entry */
33
+ docs: SearchDoc[];
34
+ }
35
+
36
+ /** Current index format version. Bump when the SearchDoc shape changes. */
37
+ export const SEARCH_INDEX_VERSION = 1;
38
+
39
+ /** Public path the generated index is served from. */
40
+ export const SEARCH_INDEX_PATH = '/search-index.json';
@@ -0,0 +1,86 @@
1
+ import { payload } from '@/payload/config';
2
+ import { getNavigation } from './navigation/auto';
3
+ import { getUrlMap } from './navigation/urlMap';
4
+ import { getAllDocPaths, getContentRegistry } from './content/registry';
5
+ import type { NavigationItem, GlobalConfig, ThemeConfig } from './payload/types';
6
+ import type { UrlMap } from './navigation/url';
7
+
8
+ /**
9
+ * The resolved site model.
10
+ *
11
+ * Pages and layouts read from here rather than from `payload/config.ts`
12
+ * directly, because navigation and URLs are only fully determined once the
13
+ * content directory has been scanned. Server-only.
14
+ */
15
+ export interface Site {
16
+ /** Global configuration, verbatim from the payload */
17
+ global: GlobalConfig;
18
+ /** Theme overrides, if any */
19
+ theme?: Partial<ThemeConfig>;
20
+ /** Navigation tree, with discovered documents merged in */
21
+ navigation: NavigationItem[];
22
+ /** Bidirectional mapping between content paths and URL segments */
23
+ urlMap: UrlMap;
24
+ /** Every buildable document path, including ones hidden from navigation */
25
+ docPaths: string[];
26
+ /** Paths that should be built but never listed or indexed */
27
+ hiddenPaths: Set<string>;
28
+ }
29
+
30
+ /**
31
+ * Collects every path that must stay unlisted.
32
+ *
33
+ * A page can be marked hidden in two independent places — `hidden: true` in its
34
+ * frontmatter, or on its entry in the curated navigation — and consulting only
35
+ * one of them is how a page ends up hidden from the sidebar yet still announced
36
+ * in the sitemap and in search. Hiding a section hides everything beneath it.
37
+ *
38
+ * @param navigation - Resolved navigation tree
39
+ * @returns Content paths to exclude from listings
40
+ */
41
+ export function collectHiddenPaths(navigation: NavigationItem[]): Set<string> {
42
+ const hidden = new Set<string>();
43
+
44
+ for (const doc of getContentRegistry().docs) {
45
+ if (doc.hidden) hidden.add(doc.path);
46
+ }
47
+
48
+ function visit(items: NavigationItem[], inherited: boolean) {
49
+ for (const item of items) {
50
+ const isHidden = inherited || item.hidden === true;
51
+ if (isHidden && item.path) hidden.add(item.path);
52
+ if (item.children) visit(item.children, isHidden);
53
+ }
54
+ }
55
+
56
+ visit(navigation, false);
57
+ return hidden;
58
+ }
59
+
60
+ /**
61
+ * Resolves the site model from the payload and the content directory.
62
+ *
63
+ * Each underlying piece memoises its own work, so calling this per page is
64
+ * cheap.
65
+ *
66
+ * @returns The resolved site
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const site = getSite();
71
+ * site.navigation; // curated entries plus discovered documents
72
+ * site.urlMap.strategy; // 'path' | 'hash'
73
+ * ```
74
+ */
75
+ export function getSite(): Site {
76
+ const navigation = getNavigation(payload.navigation, payload.global.autoNavigation ?? true);
77
+
78
+ return {
79
+ global: payload.global,
80
+ theme: payload.theme,
81
+ navigation,
82
+ urlMap: getUrlMap(),
83
+ docPaths: getAllDocPaths(),
84
+ hiddenPaths: collectHiddenPaths(navigation),
85
+ };
86
+ }
@@ -0,0 +1,26 @@
1
+ import { create } from 'zustand';
2
+
3
+ /**
4
+ * Open/closed state for the search dialog.
5
+ *
6
+ * Kept in a store rather than in component state because the dialog is opened
7
+ * from several places — the sidebar, the mobile header, and a global keyboard
8
+ * shortcut — that share no common ancestor short of the layout.
9
+ */
10
+ interface SearchState {
11
+ /** Whether the dialog is showing */
12
+ isOpen: boolean;
13
+ /** Opens the dialog */
14
+ open: () => void;
15
+ /** Closes the dialog */
16
+ close: () => void;
17
+ /** Toggles the dialog */
18
+ toggle: () => void;
19
+ }
20
+
21
+ export const useSearchStore = create<SearchState>((set) => ({
22
+ isOpen: false,
23
+ open: () => set({ isOpen: true }),
24
+ close: () => set({ isOpen: false }),
25
+ toggle: () => set((state) => ({ isOpen: !state.isOpen })),
26
+ }));
@@ -0,0 +1,313 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+
4
+ export interface HistoryEntry {
5
+ path: string;
6
+ title: string;
7
+ }
8
+
9
+ export interface Tab {
10
+ id: string;
11
+ title: string;
12
+ path: string;
13
+ scrollPosition?: number;
14
+ history: HistoryEntry[]; // History stack for this tab
15
+ historyIndex: number; // Current position in history
16
+ }
17
+
18
+ interface TabStore {
19
+ tabs: Tab[];
20
+ activeTabId: string | null;
21
+ sidebarWidth: number;
22
+ sidebarCollapsed: boolean;
23
+ hasHydrated: boolean;
24
+ setHasHydrated: (hydrated: boolean) => void;
25
+ addTab: (tab?: Omit<Tab, 'id' | 'history' | 'historyIndex'>) => string;
26
+ removeTab: (id: string) => void;
27
+ setActiveTab: (id: string) => void;
28
+ updateTabPath: (id: string, path: string, title: string) => void;
29
+ navigateInHistory: (id: string, path: string, title: string) => void;
30
+ goBack: (id: string) => { path: string; title: string } | null;
31
+ goForward: (id: string) => { path: string; title: string } | null;
32
+ canGoBack: (id: string) => boolean;
33
+ canGoForward: (id: string) => boolean;
34
+ updateTabHistoryIndex: (id: string, index: number) => void;
35
+ updateTabScroll: (id: string, scrollPosition: number) => void;
36
+ closeOtherTabs: (id: string) => void;
37
+ closeTabsToRight: (id: string) => void;
38
+ reorderTabs: (fromIndex: number, toIndex: number) => void;
39
+ setSidebarWidth: (width: number) => void;
40
+ setSidebarCollapsed: (collapsed: boolean) => void;
41
+ }
42
+
43
+ export const useTabStore = create<TabStore>()(
44
+ persist(
45
+ (set, get) => ({
46
+ tabs: [],
47
+ activeTabId: null,
48
+ sidebarWidth: 256, // Default 256px (w-64)
49
+ sidebarCollapsed: false,
50
+ hasHydrated: false,
51
+
52
+ setHasHydrated: (hydrated) => {
53
+ set({ hasHydrated: hydrated });
54
+
55
+ // After hydration, ensure at least one tab exists
56
+ if (hydrated) {
57
+ const { tabs } = get();
58
+ if (tabs.length === 0) {
59
+ // No tabs in storage, will be created by PageLayout based on URL
60
+ return;
61
+ }
62
+ }
63
+ },
64
+
65
+ addTab: (tab) => {
66
+ const { tabs } = get();
67
+
68
+ // Create new tab with default title "New Tab" and initial history
69
+ const initialPath = tab?.path || '';
70
+ const initialTitle = tab?.title || 'New Tab';
71
+ const newTab: Tab = {
72
+ title: initialTitle,
73
+ path: initialPath,
74
+ id: `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
75
+ history: [{ path: initialPath, title: initialTitle }],
76
+ historyIndex: 0,
77
+ };
78
+
79
+ set({
80
+ tabs: [...tabs, newTab],
81
+ activeTabId: newTab.id,
82
+ });
83
+
84
+ return newTab.id;
85
+ },
86
+
87
+ removeTab: (id) => {
88
+ const { tabs, activeTabId } = get();
89
+ const index = tabs.findIndex((t) => t.id === id);
90
+
91
+ if (index === -1) return;
92
+
93
+ const newTabs = tabs.filter((t) => t.id !== id);
94
+
95
+ // If closed tab was active, activate adjacent tab
96
+ let newActiveTabId = activeTabId;
97
+ if (activeTabId === id) {
98
+ if (newTabs.length > 0) {
99
+ // Activate right tab if exists, otherwise left
100
+ newActiveTabId = newTabs[Math.min(index, newTabs.length - 1)]?.id || null;
101
+ } else {
102
+ newActiveTabId = null;
103
+ }
104
+ }
105
+
106
+ set({
107
+ tabs: newTabs,
108
+ activeTabId: newActiveTabId,
109
+ });
110
+ },
111
+
112
+ setActiveTab: (id) => {
113
+ const { tabs } = get();
114
+ const tab = tabs.find((t) => t.id === id);
115
+
116
+ // Only update if the tab exists
117
+ if (tab) {
118
+ set({ activeTabId: id });
119
+ }
120
+ },
121
+
122
+ updateTabPath: (id, path, title) => {
123
+ set((state) => ({
124
+ tabs: state.tabs.map((tab) => (tab.id === id ? { ...tab, path, title } : tab)),
125
+ }));
126
+ },
127
+
128
+ navigateInHistory: (id, path, title) => {
129
+ set((state) => ({
130
+ tabs: state.tabs.map((tab) => {
131
+ if (tab.id !== id) return tab;
132
+
133
+ // If navigating to a new page (not back/forward), add to history
134
+ const currentEntry = tab.history[tab.historyIndex];
135
+ if (currentEntry.path === path) {
136
+ // Same page, just update title if needed
137
+ const updatedHistory = [...tab.history];
138
+ updatedHistory[tab.historyIndex] = { path, title };
139
+ return { ...tab, title, history: updatedHistory };
140
+ }
141
+
142
+ // Remove any forward history and add new entry
143
+ const newHistory = tab.history.slice(0, tab.historyIndex + 1);
144
+ newHistory.push({ path, title });
145
+
146
+ return {
147
+ ...tab,
148
+ path,
149
+ title,
150
+ history: newHistory,
151
+ historyIndex: newHistory.length - 1,
152
+ };
153
+ }),
154
+ }));
155
+ },
156
+
157
+ goBack: (id) => {
158
+ const { tabs } = get();
159
+ const tab = tabs.find((t) => t.id === id);
160
+
161
+ if (!tab || !tab.history || tab.historyIndex <= 0) return null;
162
+
163
+ const newIndex = tab.historyIndex - 1;
164
+ const newEntry = tab.history[newIndex];
165
+
166
+ if (!newEntry) return null;
167
+
168
+ set((state) => ({
169
+ tabs: state.tabs.map((t) =>
170
+ t.id === id
171
+ ? { ...t, path: newEntry.path, title: newEntry.title, historyIndex: newIndex }
172
+ : t,
173
+ ),
174
+ }));
175
+
176
+ return { path: newEntry.path, title: newEntry.title };
177
+ },
178
+
179
+ goForward: (id) => {
180
+ const { tabs } = get();
181
+ const tab = tabs.find((t) => t.id === id);
182
+
183
+ if (!tab || !tab.history || tab.historyIndex >= tab.history.length - 1) return null;
184
+
185
+ const newIndex = tab.historyIndex + 1;
186
+ const newEntry = tab.history[newIndex];
187
+
188
+ if (!newEntry) return null;
189
+
190
+ set((state) => ({
191
+ tabs: state.tabs.map((t) =>
192
+ t.id === id
193
+ ? { ...t, path: newEntry.path, title: newEntry.title, historyIndex: newIndex }
194
+ : t,
195
+ ),
196
+ }));
197
+
198
+ return { path: newEntry.path, title: newEntry.title };
199
+ },
200
+
201
+ canGoBack: (id) => {
202
+ const { tabs } = get();
203
+ const tab = tabs.find((t) => t.id === id);
204
+ return tab && tab.history ? tab.historyIndex > 0 : false;
205
+ },
206
+
207
+ canGoForward: (id) => {
208
+ const { tabs } = get();
209
+ const tab = tabs.find((t) => t.id === id);
210
+ return tab && tab.history ? tab.historyIndex < tab.history.length - 1 : false;
211
+ },
212
+
213
+ updateTabHistoryIndex: (id, index) => {
214
+ const { tabs } = get();
215
+ const tab = tabs.find((t) => t.id === id);
216
+
217
+ if (!tab || !tab.history || index < 0 || index >= tab.history.length) return;
218
+
219
+ const entry = tab.history[index];
220
+
221
+ if (!entry) return;
222
+
223
+ set((state) => ({
224
+ tabs: state.tabs.map((t) =>
225
+ t.id === id ? { ...t, path: entry.path, title: entry.title, historyIndex: index } : t,
226
+ ),
227
+ }));
228
+ },
229
+
230
+ updateTabScroll: (id, scrollPosition) => {
231
+ set((state) => ({
232
+ tabs: state.tabs.map((tab) => (tab.id === id ? { ...tab, scrollPosition } : tab)),
233
+ }));
234
+ },
235
+
236
+ closeOtherTabs: (id) => {
237
+ set((state) => ({
238
+ tabs: state.tabs.filter((tab) => tab.id === id),
239
+ activeTabId: id,
240
+ }));
241
+ },
242
+
243
+ closeTabsToRight: (id) => {
244
+ set((state) => {
245
+ const index = state.tabs.findIndex((tab) => tab.id === id);
246
+ return {
247
+ tabs: state.tabs.slice(0, index + 1),
248
+ };
249
+ });
250
+ },
251
+
252
+ reorderTabs: (fromIndex, toIndex) => {
253
+ set((state) => {
254
+ const newTabs = [...state.tabs];
255
+ const [movedTab] = newTabs.splice(fromIndex, 1);
256
+ newTabs.splice(toIndex, 0, movedTab);
257
+ return { tabs: newTabs };
258
+ });
259
+ },
260
+
261
+ setSidebarWidth: (width) => {
262
+ set({ sidebarWidth: width });
263
+ },
264
+
265
+ setSidebarCollapsed: (collapsed) => {
266
+ set({ sidebarCollapsed: collapsed });
267
+ },
268
+ }),
269
+ {
270
+ name: 'tab-storage',
271
+ // Exclude scroll position when saving to localStorage
272
+ partialize: (state) => ({
273
+ tabs: state.tabs.map((tab) => {
274
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
275
+ const { scrollPosition, ...rest } = tab;
276
+ return rest;
277
+ }),
278
+ activeTabId: state.activeTabId,
279
+ sidebarWidth: state.sidebarWidth,
280
+ sidebarCollapsed: state.sidebarCollapsed,
281
+ }),
282
+ // Migrate old tabs without history
283
+ migrate: (persistedState: unknown) => {
284
+ const state = persistedState as Record<string, unknown>;
285
+ if (state?.tabs && Array.isArray(state.tabs)) {
286
+ state.tabs = state.tabs.map((tab: Record<string, unknown>) => {
287
+ // Handle old format (string array) or missing history
288
+ let history = tab.history;
289
+ if (!history) {
290
+ history = [{ path: tab.path || '', title: tab.title || 'New Tab' }];
291
+ } else if (Array.isArray(history) && typeof history[0] === 'string') {
292
+ // Migrate from old string array format to new object format
293
+ history = history.map((path: string) => ({
294
+ path,
295
+ title: tab.title || 'New Tab',
296
+ }));
297
+ }
298
+
299
+ return {
300
+ ...tab,
301
+ history,
302
+ historyIndex: tab.historyIndex ?? 0,
303
+ };
304
+ });
305
+ }
306
+ return state;
307
+ },
308
+ onRehydrateStorage: () => (state) => {
309
+ state?.setHasHydrated(true);
310
+ },
311
+ },
312
+ ),
313
+ );
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Deployment base path.
3
+ *
4
+ * GitHub Pages project sites are served from a subdirectory; everywhere else
5
+ * the site sits at the root. Next rewrites its own links and assets for this,
6
+ * but code that fetches a file itself — the search index, for one — has to
7
+ * prefix the path, so it is exposed to the client too.
8
+ *
9
+ * Read from the environment rather than inferred from CI: `GITHUB_ACTIONS` is
10
+ * set for every job in a workflow, so inferring from it applied the prefix
11
+ * during tests and on forks as well as during the deploy it was meant for. The
12
+ * deploy workflow sets this explicitly.
13
+ */
14
+ const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
15
+
16
+ /** @type {import('next').NextConfig} */
17
+ const nextConfig = {
18
+ // Only use static export in production builds
19
+ ...(process.env.NODE_ENV === 'production' && { output: 'export' }),
20
+ images: {
21
+ unoptimized: true, // Required for static export
22
+ },
23
+ reactStrictMode: true,
24
+ ...(basePath && { basePath, assetPrefix: basePath }),
25
+ env: {
26
+ NEXT_PUBLIC_BASE_PATH: basePath,
27
+ },
28
+ trailingSlash: true, // Ensures proper routing for static hosting
29
+
30
+ // Disable source maps in production to prevent viewing original source
31
+ productionBrowserSourceMaps: false,
32
+
33
+ // Additional optimization for production
34
+ ...(process.env.NODE_ENV === 'production' && {
35
+ compiler: {
36
+ removeConsole: {
37
+ exclude: ['error', 'warn'], // Remove console.log but keep error/warn
38
+ },
39
+ },
40
+ }),
41
+ };
42
+
43
+ module.exports = nextConfig;