@djangocfg/nextjs 1.0.6 → 2.1.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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Deep Merge Utility
3
+ *
4
+ * Recursively merges objects, replacing arrays instead of merging them.
5
+ */
6
+
7
+ export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
8
+ const output = { ...target };
9
+
10
+ for (const key in source) {
11
+ if (source[key] === undefined) continue;
12
+
13
+ // Arrays: replace (don't merge arrays)
14
+ if (Array.isArray(source[key])) {
15
+ output[key] = source[key] as any;
16
+ }
17
+ // Objects: deep merge
18
+ else if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
19
+ const targetValue = output[key];
20
+ if (targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
21
+ output[key] = deepMerge(targetValue, source[key] as any);
22
+ } else {
23
+ output[key] = source[key] as any;
24
+ }
25
+ }
26
+ // Primitives: replace
27
+ else {
28
+ output[key] = source[key] as any;
29
+ }
30
+ }
31
+
32
+ return output;
33
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Environment Variable Utilities
3
+ */
4
+
5
+ export const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
6
+ export const isDev = process.env.NODE_ENV === 'development';
7
+ export const isProduction = process.env.NODE_ENV === 'production';
8
+ export const isCI = process.env.CI === 'true';
9
+
10
+ /**
11
+ * Get base path for static builds
12
+ */
13
+ export function getBasePath(isDefaultCfgAdmin?: boolean): string {
14
+ if (!isStaticBuild) return '';
15
+ return isDefaultCfgAdmin ? '/cfg/admin' : '/cfg/nextjs-admin';
16
+ }
17
+
18
+ /**
19
+ * Get API URL (empty for static builds)
20
+ */
21
+ export function getApiUrl(): string {
22
+ return isStaticBuild ? '' : (process.env.NEXT_PUBLIC_API_URL || '');
23
+ }
24
+
25
+ /**
26
+ * Get Site URL (empty for static builds)
27
+ */
28
+ export function getSiteUrl(): string {
29
+ return isStaticBuild ? '' : (process.env.NEXT_PUBLIC_SITE_URL || '');
30
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Utilities exports
3
+ */
4
+
5
+ export { deepMerge } from './deepMerge';
6
+ export * from './env';
7
+ export * from './version';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Version Checking Utilities
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import semver from 'semver';
7
+ import consola from 'consola';
8
+ import Conf from 'conf';
9
+ import { PACKAGE_NAME, VERSION_CACHE_TTL_MS, DJANGOCFG_PACKAGES } from '../constants';
10
+
11
+ // Version cache using conf (stores in ~/.config/djangocfg-nextjs/)
12
+ const versionCache = new Conf<{
13
+ latestVersion?: string;
14
+ lastCheck?: number;
15
+ }>({
16
+ projectName: 'djangocfg-nextjs',
17
+ projectVersion: '1.0.0',
18
+ });
19
+
20
+ /**
21
+ * Get current package version from package.json
22
+ */
23
+ export function getCurrentVersion(): string | null {
24
+ try {
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const packageJson = require('../../../package.json');
27
+ return packageJson.version || null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Fetch latest version from npm registry (with 1 hour caching via conf)
35
+ */
36
+ export async function fetchLatestVersion(): Promise<string | null> {
37
+ // Check cache first
38
+ const lastCheck = versionCache.get('lastCheck') || 0;
39
+ const cachedVersion = versionCache.get('latestVersion');
40
+
41
+ if (cachedVersion && (Date.now() - lastCheck) < VERSION_CACHE_TTL_MS) {
42
+ return cachedVersion;
43
+ }
44
+
45
+ // Fetch from npm registry
46
+ try {
47
+ const https = await import('https');
48
+ return new Promise((resolve) => {
49
+ const req = https.get(
50
+ `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
51
+ { timeout: 5000 },
52
+ (res: any) => {
53
+ let data = '';
54
+ res.on('data', (chunk: string) => { data += chunk; });
55
+ res.on('end', () => {
56
+ try {
57
+ const json = JSON.parse(data);
58
+ const version = json.version || null;
59
+ if (version) {
60
+ versionCache.set('latestVersion', version);
61
+ versionCache.set('lastCheck', Date.now());
62
+ }
63
+ resolve(version);
64
+ } catch {
65
+ resolve(cachedVersion || null);
66
+ }
67
+ });
68
+ }
69
+ );
70
+ req.on('error', () => resolve(cachedVersion || null));
71
+ req.on('timeout', () => { req.destroy(); resolve(cachedVersion || null); });
72
+ });
73
+ } catch {
74
+ return cachedVersion || null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Check if update is available
80
+ */
81
+ export async function checkForUpdate(): Promise<{
82
+ hasUpdate: boolean;
83
+ currentVersion: string | null;
84
+ latestVersion: string | null;
85
+ }> {
86
+ const currentVersion = getCurrentVersion();
87
+ if (!currentVersion) {
88
+ return { hasUpdate: false, currentVersion: null, latestVersion: null };
89
+ }
90
+
91
+ const latestVersion = await fetchLatestVersion();
92
+ const hasUpdate = !!(latestVersion && semver.gt(latestVersion, currentVersion));
93
+
94
+ return { hasUpdate, currentVersion, latestVersion };
95
+ }
96
+
97
+ /**
98
+ * Get update command for all djangocfg packages
99
+ */
100
+ export function getUpdateCommand(): string {
101
+ return `pnpm add ${DJANGOCFG_PACKAGES.map(p => `${p}@latest`).join(' ')}`;
102
+ }
103
+
104
+ /**
105
+ * Print version info and update notification
106
+ */
107
+ export async function printVersionInfo(): Promise<void> {
108
+ const { hasUpdate, currentVersion, latestVersion } = await checkForUpdate();
109
+
110
+ if (!currentVersion) return;
111
+
112
+ // Print current version
113
+ consola.box(`📦 @djangocfg/nextjs v${currentVersion}`);
114
+
115
+ // Show update notification if available
116
+ if (hasUpdate && latestVersion) {
117
+ consola.warn(`Update Available! ${chalk.red(currentVersion)} → ${chalk.green(latestVersion)}`);
118
+ consola.info(`Run: ${chalk.cyan(getUpdateCommand())}`);
119
+ console.log('');
120
+ }
121
+ }
@@ -131,9 +131,8 @@ export function generateOgImageMetadata(
131
131
 
132
132
  // Generate relative OG image URL
133
133
  const relativeOgImageUrl = generateOgImageUrl(
134
- ogImageBaseUrl,
135
134
  finalOgImageParams,
136
- useBase64
135
+ { baseUrl: ogImageBaseUrl, useBase64 }
137
136
  );
138
137
 
139
138
  // CRITICAL: Use absolute URL to ensure query params are preserved
@@ -4,6 +4,9 @@
4
4
  * Utilities to generate OG image URLs with proper query parameters
5
5
  */
6
6
 
7
+ /** Default OG Image API base URL */
8
+ const DEFAULT_OG_IMAGE_BASE_URL = 'https://djangocfg.com/api/og';
9
+
7
10
  /**
8
11
  * Encode string to base64 with Unicode support
9
12
  * Works in both browser and Node.js environments
@@ -90,47 +93,53 @@ export interface OgImageUrlParams {
90
93
  [key: string]: string | number | boolean | undefined;
91
94
  }
92
95
 
96
+ /**
97
+ * Options for generating OG image URL
98
+ */
99
+ export interface GenerateOgImageUrlOptions {
100
+ /**
101
+ * Base URL of the OG image API route
102
+ * @default 'https://djangocfg.com/api/og'
103
+ */
104
+ baseUrl?: string;
105
+ /**
106
+ * If true, encode params as base64 for safer URLs
107
+ * @default true
108
+ */
109
+ useBase64?: boolean;
110
+ }
111
+
93
112
  /**
94
113
  * Generate OG image URL with query parameters or base64 encoding
95
114
  *
96
- * @param baseUrl - Base URL of the OG image API route (e.g., '/api/og' or 'https://example.com/api/og')
97
115
  * @param params - URL parameters for the OG image
98
- * @param useBase64 - If true, encode params as base64 for safer URLs (default: true)
116
+ * @param options - Generation options (baseUrl, useBase64)
99
117
  * @returns Complete OG image URL with encoded parameters
100
118
  *
101
119
  * @example
102
120
  * ```typescript
103
- * // Base64 encoding (safe, default) - all parameters can be encoded
104
- * const url = generateOgImageUrl('/api/og', {
121
+ * // Using default baseUrl (https://djangocfg.com/api/og)
122
+ * const url = generateOgImageUrl({
105
123
  * title: 'My Page Title',
106
124
  * description: 'Page description here',
107
- * siteName: 'My Site',
108
- * logo: '/logo.svg',
109
- * backgroundType: 'gradient',
110
- * gradientStart: '#0f172a',
111
- * gradientEnd: '#334155',
112
- * titleSize: 80,
113
- * titleWeight: 800,
114
- * titleColor: 'white',
115
- * descriptionSize: 36,
116
- * descriptionColor: 'rgba(226, 232, 240, 0.9)',
117
- * siteNameSize: 32,
118
- * siteNameColor: 'rgba(255, 255, 255, 0.95)',
119
- * padding: 80,
120
- * logoSize: 56,
121
125
  * });
122
- * // Result: /api/og/[base64-encoded-json]
123
126
  *
124
- * // Query params (legacy)
125
- * const url = generateOgImageUrl('/api/og', { title: 'Hello' }, false);
126
- * // Result: /api/og?title=Hello
127
+ * // With custom baseUrl
128
+ * const url = generateOgImageUrl(
129
+ * { title: 'My Page' },
130
+ * { baseUrl: '/api/og' }
131
+ * );
127
132
  * ```
128
133
  */
129
134
  export function generateOgImageUrl(
130
- baseUrl: string,
131
135
  params: OgImageUrlParams,
132
- useBase64: boolean = true
136
+ options: GenerateOgImageUrlOptions = {}
133
137
  ): string {
138
+ const {
139
+ baseUrl = DEFAULT_OG_IMAGE_BASE_URL,
140
+ useBase64 = true
141
+ } = options;
142
+
134
143
  if (useBase64) {
135
144
  // Clean params - remove undefined/null/empty values
136
145
  const cleanParams: Record<string, string | number | boolean> = {};
@@ -186,6 +195,11 @@ export function getAbsoluteOgImageUrl(
186
195
  relativePath: string,
187
196
  siteUrl: string
188
197
  ): string {
198
+ // If path is already an absolute URL, return as-is
199
+ if (relativePath.startsWith('http://') || relativePath.startsWith('https://')) {
200
+ return relativePath;
201
+ }
202
+
189
203
  // Remove trailing slash from site URL
190
204
  const cleanSiteUrl = siteUrl.replace(/\/$/, '');
191
205
 
@@ -202,30 +216,30 @@ export function getAbsoluteOgImageUrl(
202
216
  *
203
217
  * Useful when you want to reuse the same base URL and default parameters
204
218
  *
205
- * @param baseUrl - Base URL of the OG image API route
206
219
  * @param defaults - Default parameters to merge with each URL generation
220
+ * @param options - Default options (baseUrl, useBase64)
207
221
  * @returns URL builder function
208
222
  *
209
223
  * @example
210
224
  * ```typescript
211
- * const buildOgUrl = createOgImageUrlBuilder('/api/og', {
212
- * siteName: 'My Site',
213
- * logo: '/logo.png'
214
- * });
225
+ * const buildOgUrl = createOgImageUrlBuilder(
226
+ * { siteName: 'My Site', logo: '/logo.png' },
227
+ * { baseUrl: '/api/og' }
228
+ * );
215
229
  *
216
230
  * const url1 = buildOgUrl({ title: 'Page 1' });
217
231
  * const url2 = buildOgUrl({ title: 'Page 2', description: 'Custom desc' });
218
232
  * ```
219
233
  */
220
234
  export function createOgImageUrlBuilder(
221
- baseUrl: string,
222
- defaults: Partial<OgImageUrlParams> = {}
235
+ defaults: Partial<OgImageUrlParams> = {},
236
+ options: GenerateOgImageUrlOptions = {}
223
237
  ) {
224
238
  return (params: OgImageUrlParams): string => {
225
- return generateOgImageUrl(baseUrl, {
226
- ...defaults,
227
- ...params,
228
- });
239
+ return generateOgImageUrl(
240
+ { ...defaults, ...params },
241
+ options
242
+ );
229
243
  };
230
244
  }
231
245
 
@@ -287,7 +301,7 @@ export function parseOgImageData(
287
301
  try {
288
302
  // Handle URLSearchParams
289
303
  let params: Record<string, string | undefined>;
290
-
304
+
291
305
  if (searchParams instanceof URLSearchParams) {
292
306
  // Convert URLSearchParams to object
293
307
  params = {};
@@ -310,18 +324,18 @@ export function parseOgImageData(
310
324
  if (process.env.NODE_ENV === 'development') {
311
325
  console.log('[parseOgImageData] Found data param, length:', dataParam.length);
312
326
  }
313
-
327
+
314
328
  try {
315
329
  const decoded = decodeBase64(dataParam);
316
330
  if (process.env.NODE_ENV === 'development') {
317
331
  console.log('[parseOgImageData] Decoded string:', decoded.substring(0, 100));
318
332
  }
319
-
333
+
320
334
  const parsed = JSON.parse(decoded);
321
335
  if (process.env.NODE_ENV === 'development') {
322
336
  console.log('[parseOgImageData] Parsed JSON:', parsed);
323
337
  }
324
-
338
+
325
339
  // Ensure all values are strings
326
340
  const result: Record<string, string> = {};
327
341
  for (const [key, value] of Object.entries(parsed)) {
@@ -329,11 +343,11 @@ export function parseOgImageData(
329
343
  result[key] = String(value);
330
344
  }
331
345
  }
332
-
346
+
333
347
  if (process.env.NODE_ENV === 'development') {
334
348
  console.log('[parseOgImageData] Result:', result);
335
349
  }
336
-
350
+
337
351
  return result;
338
352
  } catch (decodeError) {
339
353
  console.error('[parseOgImageData] Error decoding/parsing data param:', decodeError);
@@ -355,11 +369,11 @@ export function parseOgImageData(
355
369
  result[key] = Array.isArray(value) ? value[0] : String(value);
356
370
  }
357
371
  }
358
-
372
+
359
373
  if (process.env.NODE_ENV === 'development') {
360
374
  console.log('[parseOgImageData] Fallback result:', result);
361
375
  }
362
-
376
+
363
377
  return result;
364
378
  } catch (error) {
365
379
  console.error('[parseOgImageData] Unexpected error:', error);
@@ -369,4 +383,3 @@ export function parseOgImageData(
369
383
 
370
384
  // Export base64 utilities for advanced use cases
371
385
  export { encodeBase64, decodeBase64 };
372
-