@duffcloudservices/cms-core 0.3.0 → 0.4.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.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Core types for DCS CMS configuration files.
3
+ * These mirror the schemas defined in contracts/spec/domains/site-configuration.yaml
4
+ */
5
+ /**
6
+ * Root structure of .dcs/content.yaml (version 1)
7
+ */
8
+ interface ContentConfiguration {
9
+ /** Schema version */
10
+ version: number;
11
+ /** ISO timestamp of last update */
12
+ lastUpdated?: string;
13
+ /** Email or identifier of who made the update */
14
+ updatedBy?: string;
15
+ /** Global text content shared across all pages */
16
+ global: Record<string, string>;
17
+ /** Page-specific text content keyed by page slug */
18
+ pages: Record<string, Record<string, string>>;
19
+ }
20
+ /** Alias for v1 content configuration */
21
+ type ContentConfigurationV1 = ContentConfiguration;
22
+ /**
23
+ * Root structure of .dcs/seo.yaml
24
+ */
25
+ interface SeoConfiguration {
26
+ /** Schema version */
27
+ version: number;
28
+ /** ISO timestamp of last update */
29
+ lastUpdated?: string;
30
+ /** Email or identifier of who made the update */
31
+ updatedBy?: string;
32
+ /** Global/site-wide SEO defaults */
33
+ global?: GlobalSeoConfig;
34
+ /** Page-specific SEO configurations keyed by page slug */
35
+ pages?: Record<string, PageSeoConfig>;
36
+ }
37
+ /**
38
+ * Global/site-wide SEO configuration
39
+ */
40
+ interface GlobalSeoConfig {
41
+ /** Site name used in titles and structured data */
42
+ siteName?: string;
43
+ /** Base URL of the site (e.g., https://example.com) */
44
+ siteUrl?: string;
45
+ /** Locale for Open Graph (e.g., en_US) */
46
+ locale?: string;
47
+ /** Default page title */
48
+ defaultTitle?: string;
49
+ /** Default meta description */
50
+ defaultDescription?: string;
51
+ /** Title template with %s placeholder (e.g., "%s | Site Name") */
52
+ titleTemplate?: string;
53
+ /** Author information for structured data */
54
+ author?: SeoAuthorConfig;
55
+ /** Social media handles */
56
+ social?: SeoSocialConfig;
57
+ /** Default images for social sharing */
58
+ images?: SeoImagesConfig;
59
+ /** Default robots directive (e.g., "index, follow") */
60
+ robots?: string;
61
+ /** Global JSON-LD schemas (Organization, WebSite, etc.) */
62
+ schemas?: SeoSchemaConfig[];
63
+ /** Search engine verification codes */
64
+ verification?: SeoVerificationConfig;
65
+ }
66
+ /**
67
+ * Page-specific SEO configuration
68
+ */
69
+ interface PageSeoConfig {
70
+ /** Page title */
71
+ title?: string;
72
+ /** Meta description */
73
+ description?: string;
74
+ /** Meta keywords (comma-separated) */
75
+ keywords?: string;
76
+ /** Canonical URL */
77
+ canonical?: string;
78
+ /** Page-specific robots directive */
79
+ robots?: string;
80
+ /** Open Graph configuration */
81
+ openGraph?: SeoOpenGraphConfig;
82
+ /** Twitter Card configuration */
83
+ twitter?: SeoTwitterConfig;
84
+ /** Page-specific JSON-LD schemas */
85
+ schemas?: SeoSchemaConfig[];
86
+ /** Alternate language links */
87
+ alternates?: SeoAlternateConfig[];
88
+ /** If true, don't apply titleTemplate to this page */
89
+ noTitleTemplate?: boolean;
90
+ }
91
+ /**
92
+ * Author information for structured data
93
+ */
94
+ interface SeoAuthorConfig {
95
+ /** Author name */
96
+ name?: string;
97
+ /** Author email */
98
+ email?: string;
99
+ /** Author image URL */
100
+ image?: string;
101
+ /** Job title */
102
+ jobTitle?: string;
103
+ /** Social profile URLs */
104
+ sameAs?: string[];
105
+ }
106
+ /**
107
+ * Social media handles
108
+ */
109
+ interface SeoSocialConfig {
110
+ /** Twitter handle (without @) */
111
+ twitter?: string;
112
+ /** LinkedIn company or profile slug */
113
+ linkedin?: string;
114
+ /** GitHub username */
115
+ github?: string;
116
+ /** Facebook page name */
117
+ facebook?: string;
118
+ /** Instagram username */
119
+ instagram?: string;
120
+ /** YouTube channel */
121
+ youtube?: string;
122
+ }
123
+ /**
124
+ * Default images for social sharing
125
+ */
126
+ interface SeoImagesConfig {
127
+ /** Logo image URL */
128
+ logo?: string;
129
+ /** Default Open Graph image */
130
+ ogDefault?: string;
131
+ /** Default Twitter Card image */
132
+ twitterDefault?: string;
133
+ /** Favicon URL */
134
+ favicon?: string;
135
+ }
136
+ /**
137
+ * Open Graph meta configuration
138
+ */
139
+ interface SeoOpenGraphConfig {
140
+ /** OG title (defaults to page title) */
141
+ title?: string;
142
+ /** OG description (defaults to page description) */
143
+ description?: string;
144
+ /** OG image URL */
145
+ image?: string;
146
+ /** Alt text for OG image */
147
+ imageAlt?: string;
148
+ /** OG image width in pixels */
149
+ imageWidth?: number;
150
+ /** OG image height in pixels */
151
+ imageHeight?: number;
152
+ /** OG type */
153
+ type?: 'website' | 'article' | 'profile' | 'book' | 'music.song' | 'music.album' | 'video.movie' | 'video.episode' | 'video.tv_show' | 'video.other';
154
+ /** OG URL (defaults to canonical) */
155
+ url?: string;
156
+ /** Article published time (ISO 8601) */
157
+ publishedTime?: string;
158
+ /** Article modified time (ISO 8601) */
159
+ modifiedTime?: string;
160
+ /** Article author */
161
+ author?: string;
162
+ /** Article section/category */
163
+ section?: string;
164
+ /** Article tags */
165
+ tags?: string[];
166
+ }
167
+ /**
168
+ * Twitter Card configuration
169
+ */
170
+ interface SeoTwitterConfig {
171
+ /** Card type */
172
+ card?: 'summary' | 'summary_large_image' | 'app' | 'player';
173
+ /** Twitter handle for the site */
174
+ site?: string;
175
+ /** Twitter handle for the content creator */
176
+ creator?: string;
177
+ /** Twitter title */
178
+ title?: string;
179
+ /** Twitter description */
180
+ description?: string;
181
+ /** Twitter card image */
182
+ image?: string;
183
+ /** Alt text for image */
184
+ imageAlt?: string;
185
+ }
186
+ /**
187
+ * JSON-LD schema configuration
188
+ */
189
+ interface SeoSchemaConfig {
190
+ /** Schema.org type (e.g., Organization, WebSite, Article) */
191
+ type: string;
192
+ /** Schema properties as key-value pairs */
193
+ properties?: Record<string, unknown>;
194
+ }
195
+ /**
196
+ * Alternate language link
197
+ */
198
+ interface SeoAlternateConfig {
199
+ /** Language code (e.g., en, es, fr) */
200
+ hrefLang: string;
201
+ /** URL for this language version */
202
+ href: string;
203
+ }
204
+ /**
205
+ * Search engine verification codes
206
+ */
207
+ interface SeoVerificationConfig {
208
+ /** Google Search Console verification */
209
+ google?: string;
210
+ /** Bing Webmaster Tools verification */
211
+ bing?: string;
212
+ /** DuckDuckGo verification (reserved for future use) */
213
+ duckduckgo?: string;
214
+ }
215
+ /**
216
+ * Resolved SEO for a specific page (global + page merged)
217
+ */
218
+ interface ResolvedSeo {
219
+ title: string;
220
+ description: string;
221
+ image?: string;
222
+ siteName?: string;
223
+ siteUrl?: string;
224
+ locale?: string;
225
+ canonical?: string;
226
+ robots?: string;
227
+ noIndex?: boolean;
228
+ openGraph?: SeoOpenGraphConfig;
229
+ twitter?: SeoTwitterConfig;
230
+ schemas?: SeoSchemaConfig[];
231
+ alternates?: SeoAlternateConfig[];
232
+ }
233
+ /**
234
+ * Root structure of .dcs/pages.yaml
235
+ */
236
+ interface PagesConfiguration {
237
+ /** Schema version */
238
+ version: number;
239
+ /** Site slug this configuration belongs to */
240
+ siteSlug: string;
241
+ /** List of page entries */
242
+ pages: PageEntry[];
243
+ }
244
+ /**
245
+ * Individual page entry
246
+ */
247
+ interface PageEntry {
248
+ /** URL-safe identifier */
249
+ slug: string;
250
+ /** The URL path for the page */
251
+ path: string;
252
+ /** Page type */
253
+ type: 'static' | 'index' | 'dynamic';
254
+ /** Human-readable title */
255
+ title: string;
256
+ /** Whether the page can be deleted */
257
+ deletable: boolean;
258
+ /** Text keys auto-discovered during snapshot */
259
+ textKeys: string[];
260
+ }
261
+ /**
262
+ * Unified .dcs/config.yaml format that merges site, content, SEO, and pages
263
+ * configurations into a single file. Falls back to split files when not present.
264
+ *
265
+ * @example
266
+ * ```yaml
267
+ * version: 2
268
+ * site:
269
+ * name: MI Handyman Services Web
270
+ * slug: mi-handyman
271
+ * productionUrl: https://mihandymanservices.com
272
+ * content:
273
+ * global:
274
+ * footer.copyright: "© 2026 MI Handyman Services"
275
+ * pages:
276
+ * home:
277
+ * hero.heading: "Your Trusted Michigan Handyman"
278
+ * seo:
279
+ * global:
280
+ * siteName: MI Handyman Services
281
+ * siteUrl: https://mihandymanservices.com
282
+ * pages:
283
+ * home:
284
+ * title: Home
285
+ * pages:
286
+ * - slug: home
287
+ * path: /
288
+ * type: static
289
+ * title: Home
290
+ * deletable: false
291
+ * textKeys: []
292
+ * ```
293
+ */
294
+ interface UnifiedConfiguration {
295
+ /** Schema version — must be 2 for unified format */
296
+ version: 2;
297
+ /** ISO timestamp of last update */
298
+ lastUpdated?: string;
299
+ /** Email or identifier of who made the update */
300
+ updatedBy?: string;
301
+ /** Site identity (replaces site.yaml) */
302
+ site?: UnifiedSiteConfig;
303
+ /** Text content (replaces content.yaml) */
304
+ content?: ContentConfiguration;
305
+ /** SEO configuration (replaces seo.yaml) */
306
+ seo?: SeoConfiguration;
307
+ /** Page registry (replaces pages.yaml) */
308
+ pages?: PageEntry[];
309
+ /** Site slug — convenience for pages.yaml siteSlug */
310
+ siteSlug?: string;
311
+ }
312
+ /**
313
+ * Site identity configuration (extracted from site.yaml)
314
+ */
315
+ interface UnifiedSiteConfig {
316
+ /** Human-readable site name */
317
+ name?: string;
318
+ /** URL-safe site slug */
319
+ slug?: string;
320
+ /** Production domain URL */
321
+ productionUrl?: string;
322
+ /** Preview URL pattern */
323
+ previewUrl?: string;
324
+ /** Preview base path */
325
+ previewBasePath?: string;
326
+ /** Azure SWA app location (build output) */
327
+ appLocation?: string;
328
+ /** Azure SWA output location */
329
+ outputLocation?: string;
330
+ }
331
+
332
+ /**
333
+ * Content resolution utilities
334
+ */
335
+
336
+ /**
337
+ * Resolve a text key for a specific page.
338
+ * Checks page-specific content first, then falls back to global content.
339
+ *
340
+ * @param content - The content configuration
341
+ * @param page - The page slug
342
+ * @param key - The text key to resolve
343
+ * @returns The resolved text value, or undefined if not found
344
+ */
345
+ declare function resolveTextKey(content: ContentConfiguration, page: string, key: string): string | undefined;
346
+ /**
347
+ * Get all content for a specific page, merging global and page-specific.
348
+ *
349
+ * @param content - The content configuration
350
+ * @param page - The page slug
351
+ * @returns Merged content object (global values overridden by page values)
352
+ */
353
+ declare function getPageContent(content: ContentConfiguration, page: string): Record<string, string>;
354
+ /**
355
+ * Get only the global content.
356
+ *
357
+ * @param content - The content configuration
358
+ * @returns Global content object
359
+ */
360
+ declare function getGlobalContent(content: ContentConfiguration): Record<string, string>;
361
+
362
+ /**
363
+ * SEO resolution utilities
364
+ */
365
+
366
+ /**
367
+ * Resolve SEO configuration for a specific page.
368
+ * Merges global defaults with page-specific overrides.
369
+ *
370
+ * @param seo - The SEO configuration
371
+ * @param page - The page slug
372
+ * @returns Resolved SEO object with all values filled in
373
+ */
374
+ declare function resolveSeoForPage(seo: SeoConfiguration, page: string): ResolvedSeo;
375
+ /**
376
+ * Meta tag representation for framework-agnostic usage
377
+ */
378
+ interface MetaTag {
379
+ name?: string;
380
+ property?: string;
381
+ content: string;
382
+ }
383
+ /**
384
+ * Build an array of meta tags from resolved SEO.
385
+ * Useful for frameworks that need to manually set meta tags.
386
+ *
387
+ * @param seo - Resolved SEO object
388
+ * @returns Array of meta tag objects
389
+ */
390
+ declare function buildMetaTags(seo: ResolvedSeo): MetaTag[];
391
+
392
+ /**
393
+ * Runtime content fetching for premium tier customers
394
+ */
395
+
396
+ /**
397
+ * Options for runtime fetch operations
398
+ */
399
+ interface FetchOptions {
400
+ /** Base URL for the DCS API */
401
+ apiBaseUrl?: string;
402
+ /** Timeout in milliseconds (default: 5000) */
403
+ timeout?: number;
404
+ /** Custom headers to include */
405
+ headers?: Record<string, string>;
406
+ }
407
+ /**
408
+ * Fetch runtime content from the DCS API.
409
+ * This is a premium tier feature - returns 403 for non-premium sites.
410
+ *
411
+ * @param siteSlug - The site's slug identifier
412
+ * @param options - Fetch options
413
+ * @returns Content configuration or null if fetch fails
414
+ */
415
+ declare function fetchRuntimeContent(siteSlug: string, options?: FetchOptions): Promise<ContentConfiguration | null>;
416
+ /**
417
+ * Fetch runtime SEO configuration from the DCS API.
418
+ * This is a premium tier feature - returns 403 for non-premium sites.
419
+ *
420
+ * @param siteSlug - The site's slug identifier
421
+ * @param options - Fetch options
422
+ * @returns SEO configuration or null if fetch fails
423
+ */
424
+ declare function fetchRuntimeSeo(siteSlug: string, options?: FetchOptions): Promise<SeoConfiguration | null>;
425
+
426
+ export { type ContentConfiguration as C, type FetchOptions as F, type GlobalSeoConfig as G, type MetaTag as M, type PagesConfiguration as P, type ResolvedSeo as R, type SeoConfiguration as S, type UnifiedConfiguration as U, type ContentConfigurationV1 as a, type PageSeoConfig as b, type SeoAuthorConfig as c, type SeoSocialConfig as d, type SeoImagesConfig as e, type SeoOpenGraphConfig as f, type SeoTwitterConfig as g, type SeoSchemaConfig as h, type SeoAlternateConfig as i, type SeoVerificationConfig as j, type PageEntry as k, type UnifiedSiteConfig as l, getPageContent as m, getGlobalContent as n, resolveSeoForPage as o, buildMetaTags as p, fetchRuntimeContent as q, resolveTextKey as r, fetchRuntimeSeo as s };
package/dist/browser.d.ts CHANGED
@@ -1,356 +1 @@
1
- /**
2
- * Core types for DCS CMS configuration files.
3
- * These mirror the schemas defined in contracts/spec/domains/site-configuration.yaml
4
- */
5
- /**
6
- * Root structure of .dcs/content.yaml (version 1)
7
- */
8
- interface ContentConfiguration {
9
- /** Schema version */
10
- version: number;
11
- /** ISO timestamp of last update */
12
- lastUpdated?: string;
13
- /** Email or identifier of who made the update */
14
- updatedBy?: string;
15
- /** Global text content shared across all pages */
16
- global: Record<string, string>;
17
- /** Page-specific text content keyed by page slug */
18
- pages: Record<string, Record<string, string>>;
19
- }
20
- /** Alias for v1 content configuration */
21
- type ContentConfigurationV1 = ContentConfiguration;
22
- /**
23
- * Root structure of .dcs/seo.yaml
24
- */
25
- interface SeoConfiguration {
26
- /** Schema version */
27
- version: number;
28
- /** ISO timestamp of last update */
29
- lastUpdated?: string;
30
- /** Email or identifier of who made the update */
31
- updatedBy?: string;
32
- /** Global/site-wide SEO defaults */
33
- global?: GlobalSeoConfig;
34
- /** Page-specific SEO configurations keyed by page slug */
35
- pages?: Record<string, PageSeoConfig>;
36
- }
37
- /**
38
- * Global/site-wide SEO configuration
39
- */
40
- interface GlobalSeoConfig {
41
- /** Site name used in titles and structured data */
42
- siteName?: string;
43
- /** Base URL of the site (e.g., https://example.com) */
44
- siteUrl?: string;
45
- /** Locale for Open Graph (e.g., en_US) */
46
- locale?: string;
47
- /** Default page title */
48
- defaultTitle?: string;
49
- /** Default meta description */
50
- defaultDescription?: string;
51
- /** Title template with %s placeholder (e.g., "%s | Site Name") */
52
- titleTemplate?: string;
53
- /** Author information for structured data */
54
- author?: SeoAuthorConfig;
55
- /** Social media handles */
56
- social?: SeoSocialConfig;
57
- /** Default images for social sharing */
58
- images?: SeoImagesConfig;
59
- /** Default robots directive (e.g., "index, follow") */
60
- robots?: string;
61
- /** Global JSON-LD schemas (Organization, WebSite, etc.) */
62
- schemas?: SeoSchemaConfig[];
63
- /** Search engine verification codes */
64
- verification?: SeoVerificationConfig;
65
- }
66
- /**
67
- * Page-specific SEO configuration
68
- */
69
- interface PageSeoConfig {
70
- /** Page title */
71
- title?: string;
72
- /** Meta description */
73
- description?: string;
74
- /** Meta keywords (comma-separated) */
75
- keywords?: string;
76
- /** Canonical URL */
77
- canonical?: string;
78
- /** Page-specific robots directive */
79
- robots?: string;
80
- /** Open Graph configuration */
81
- openGraph?: SeoOpenGraphConfig;
82
- /** Twitter Card configuration */
83
- twitter?: SeoTwitterConfig;
84
- /** Page-specific JSON-LD schemas */
85
- schemas?: SeoSchemaConfig[];
86
- /** Alternate language links */
87
- alternates?: SeoAlternateConfig[];
88
- /** If true, don't apply titleTemplate to this page */
89
- noTitleTemplate?: boolean;
90
- }
91
- /**
92
- * Author information for structured data
93
- */
94
- interface SeoAuthorConfig {
95
- /** Author name */
96
- name?: string;
97
- /** Author email */
98
- email?: string;
99
- /** Author image URL */
100
- image?: string;
101
- /** Job title */
102
- jobTitle?: string;
103
- /** Social profile URLs */
104
- sameAs?: string[];
105
- }
106
- /**
107
- * Social media handles
108
- */
109
- interface SeoSocialConfig {
110
- /** Twitter handle (without @) */
111
- twitter?: string;
112
- /** LinkedIn company or profile slug */
113
- linkedin?: string;
114
- /** GitHub username */
115
- github?: string;
116
- /** Facebook page name */
117
- facebook?: string;
118
- /** Instagram username */
119
- instagram?: string;
120
- /** YouTube channel */
121
- youtube?: string;
122
- }
123
- /**
124
- * Default images for social sharing
125
- */
126
- interface SeoImagesConfig {
127
- /** Logo image URL */
128
- logo?: string;
129
- /** Default Open Graph image */
130
- ogDefault?: string;
131
- /** Default Twitter Card image */
132
- twitterDefault?: string;
133
- /** Favicon URL */
134
- favicon?: string;
135
- }
136
- /**
137
- * Open Graph meta configuration
138
- */
139
- interface SeoOpenGraphConfig {
140
- /** OG title (defaults to page title) */
141
- title?: string;
142
- /** OG description (defaults to page description) */
143
- description?: string;
144
- /** OG image URL */
145
- image?: string;
146
- /** Alt text for OG image */
147
- imageAlt?: string;
148
- /** OG image width in pixels */
149
- imageWidth?: number;
150
- /** OG image height in pixels */
151
- imageHeight?: number;
152
- /** OG type */
153
- type?: 'website' | 'article' | 'profile' | 'book' | 'music.song' | 'music.album' | 'video.movie' | 'video.episode' | 'video.tv_show' | 'video.other';
154
- /** OG URL (defaults to canonical) */
155
- url?: string;
156
- /** Article published time (ISO 8601) */
157
- publishedTime?: string;
158
- /** Article modified time (ISO 8601) */
159
- modifiedTime?: string;
160
- /** Article author */
161
- author?: string;
162
- /** Article section/category */
163
- section?: string;
164
- /** Article tags */
165
- tags?: string[];
166
- }
167
- /**
168
- * Twitter Card configuration
169
- */
170
- interface SeoTwitterConfig {
171
- /** Card type */
172
- card?: 'summary' | 'summary_large_image' | 'app' | 'player';
173
- /** Twitter handle for the site */
174
- site?: string;
175
- /** Twitter handle for the content creator */
176
- creator?: string;
177
- /** Twitter title */
178
- title?: string;
179
- /** Twitter description */
180
- description?: string;
181
- /** Twitter card image */
182
- image?: string;
183
- /** Alt text for image */
184
- imageAlt?: string;
185
- }
186
- /**
187
- * JSON-LD schema configuration
188
- */
189
- interface SeoSchemaConfig {
190
- /** Schema.org type (e.g., Organization, WebSite, Article) */
191
- type: string;
192
- /** Schema properties as key-value pairs */
193
- properties?: Record<string, unknown>;
194
- }
195
- /**
196
- * Alternate language link
197
- */
198
- interface SeoAlternateConfig {
199
- /** Language code (e.g., en, es, fr) */
200
- hrefLang: string;
201
- /** URL for this language version */
202
- href: string;
203
- }
204
- /**
205
- * Search engine verification codes
206
- */
207
- interface SeoVerificationConfig {
208
- /** Google Search Console verification */
209
- google?: string;
210
- /** Bing Webmaster Tools verification */
211
- bing?: string;
212
- /** DuckDuckGo verification (reserved for future use) */
213
- duckduckgo?: string;
214
- }
215
- /**
216
- * Resolved SEO for a specific page (global + page merged)
217
- */
218
- interface ResolvedSeo {
219
- title: string;
220
- description: string;
221
- image?: string;
222
- siteName?: string;
223
- siteUrl?: string;
224
- locale?: string;
225
- canonical?: string;
226
- robots?: string;
227
- noIndex?: boolean;
228
- openGraph?: SeoOpenGraphConfig;
229
- twitter?: SeoTwitterConfig;
230
- schemas?: SeoSchemaConfig[];
231
- alternates?: SeoAlternateConfig[];
232
- }
233
- /**
234
- * Root structure of .dcs/pages.yaml
235
- */
236
- interface PagesConfiguration {
237
- /** Schema version */
238
- version: number;
239
- /** Site slug this configuration belongs to */
240
- siteSlug: string;
241
- /** List of page entries */
242
- pages: PageEntry[];
243
- }
244
- /**
245
- * Individual page entry
246
- */
247
- interface PageEntry {
248
- /** URL-safe identifier */
249
- slug: string;
250
- /** The URL path for the page */
251
- path: string;
252
- /** Page type */
253
- type: 'static' | 'index' | 'dynamic';
254
- /** Human-readable title */
255
- title: string;
256
- /** Whether the page can be deleted */
257
- deletable: boolean;
258
- /** Text keys auto-discovered during snapshot */
259
- textKeys: string[];
260
- }
261
-
262
- /**
263
- * Content resolution utilities
264
- */
265
-
266
- /**
267
- * Resolve a text key for a specific page.
268
- * Checks page-specific content first, then falls back to global content.
269
- *
270
- * @param content - The content configuration
271
- * @param page - The page slug
272
- * @param key - The text key to resolve
273
- * @returns The resolved text value, or undefined if not found
274
- */
275
- declare function resolveTextKey(content: ContentConfiguration, page: string, key: string): string | undefined;
276
- /**
277
- * Get all content for a specific page, merging global and page-specific.
278
- *
279
- * @param content - The content configuration
280
- * @param page - The page slug
281
- * @returns Merged content object (global values overridden by page values)
282
- */
283
- declare function getPageContent(content: ContentConfiguration, page: string): Record<string, string>;
284
- /**
285
- * Get only the global content.
286
- *
287
- * @param content - The content configuration
288
- * @returns Global content object
289
- */
290
- declare function getGlobalContent(content: ContentConfiguration): Record<string, string>;
291
-
292
- /**
293
- * SEO resolution utilities
294
- */
295
-
296
- /**
297
- * Resolve SEO configuration for a specific page.
298
- * Merges global defaults with page-specific overrides.
299
- *
300
- * @param seo - The SEO configuration
301
- * @param page - The page slug
302
- * @returns Resolved SEO object with all values filled in
303
- */
304
- declare function resolveSeoForPage(seo: SeoConfiguration, page: string): ResolvedSeo;
305
- /**
306
- * Meta tag representation for framework-agnostic usage
307
- */
308
- interface MetaTag {
309
- name?: string;
310
- property?: string;
311
- content: string;
312
- }
313
- /**
314
- * Build an array of meta tags from resolved SEO.
315
- * Useful for frameworks that need to manually set meta tags.
316
- *
317
- * @param seo - Resolved SEO object
318
- * @returns Array of meta tag objects
319
- */
320
- declare function buildMetaTags(seo: ResolvedSeo): MetaTag[];
321
-
322
- /**
323
- * Runtime content fetching for premium tier customers
324
- */
325
-
326
- /**
327
- * Options for runtime fetch operations
328
- */
329
- interface FetchOptions {
330
- /** Base URL for the DCS API */
331
- apiBaseUrl?: string;
332
- /** Timeout in milliseconds (default: 5000) */
333
- timeout?: number;
334
- /** Custom headers to include */
335
- headers?: Record<string, string>;
336
- }
337
- /**
338
- * Fetch runtime content from the DCS API.
339
- * This is a premium tier feature - returns 403 for non-premium sites.
340
- *
341
- * @param siteSlug - The site's slug identifier
342
- * @param options - Fetch options
343
- * @returns Content configuration or null if fetch fails
344
- */
345
- declare function fetchRuntimeContent(siteSlug: string, options?: FetchOptions): Promise<ContentConfiguration | null>;
346
- /**
347
- * Fetch runtime SEO configuration from the DCS API.
348
- * This is a premium tier feature - returns 403 for non-premium sites.
349
- *
350
- * @param siteSlug - The site's slug identifier
351
- * @param options - Fetch options
352
- * @returns SEO configuration or null if fetch fails
353
- */
354
- declare function fetchRuntimeSeo(siteSlug: string, options?: FetchOptions): Promise<SeoConfiguration | null>;
355
-
356
- export { type ContentConfiguration, type ContentConfigurationV1, type FetchOptions, type GlobalSeoConfig, type MetaTag, type PageEntry, type PageSeoConfig, type PagesConfiguration, type ResolvedSeo, type SeoAlternateConfig, type SeoAuthorConfig, type SeoConfiguration, type SeoImagesConfig, type SeoOpenGraphConfig, type SeoSchemaConfig, type SeoSocialConfig, type SeoTwitterConfig, type SeoVerificationConfig, buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, resolveSeoForPage, resolveTextKey };
1
+ export { C as ContentConfiguration, a as ContentConfigurationV1, F as FetchOptions, G as GlobalSeoConfig, M as MetaTag, k as PageEntry, b as PageSeoConfig, P as PagesConfiguration, R as ResolvedSeo, i as SeoAlternateConfig, c as SeoAuthorConfig, S as SeoConfiguration, e as SeoImagesConfig, f as SeoOpenGraphConfig, h as SeoSchemaConfig, d as SeoSocialConfig, g as SeoTwitterConfig, j as SeoVerificationConfig, p as buildMetaTags, q as fetchRuntimeContent, s as fetchRuntimeSeo, n as getGlobalContent, m as getPageContent, o as resolveSeoForPage, r as resolveTextKey } from './browser-BAFXkAvg.js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ContentConfiguration, SeoConfiguration, PagesConfiguration } from './browser.js';
2
- export { ContentConfigurationV1, FetchOptions, GlobalSeoConfig, MetaTag, PageEntry, PageSeoConfig, ResolvedSeo, SeoAlternateConfig, SeoAuthorConfig, SeoImagesConfig, SeoOpenGraphConfig, SeoSchemaConfig, SeoSocialConfig, SeoTwitterConfig, SeoVerificationConfig, buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, resolveSeoForPage, resolveTextKey } from './browser.js';
1
+ import { C as ContentConfiguration, S as SeoConfiguration, P as PagesConfiguration } from './browser-BAFXkAvg.js';
2
+ export { a as ContentConfigurationV1, F as FetchOptions, G as GlobalSeoConfig, M as MetaTag, k as PageEntry, b as PageSeoConfig, R as ResolvedSeo, i as SeoAlternateConfig, c as SeoAuthorConfig, e as SeoImagesConfig, f as SeoOpenGraphConfig, h as SeoSchemaConfig, d as SeoSocialConfig, g as SeoTwitterConfig, j as SeoVerificationConfig, U as UnifiedConfiguration, l as UnifiedSiteConfig, p as buildMetaTags, q as fetchRuntimeContent, s as fetchRuntimeSeo, n as getGlobalContent, m as getPageContent, o as resolveSeoForPage, r as resolveTextKey } from './browser-BAFXkAvg.js';
3
3
 
4
4
  /**
5
5
  * YAML file loaders for DCS configuration files
@@ -29,6 +29,42 @@ declare function loadSeoYaml(filePath: string): Promise<SeoConfiguration>;
29
29
  * @throws Error if file not found or parse fails
30
30
  */
31
31
  declare function loadPagesYaml(filePath: string): Promise<PagesConfiguration>;
32
+ /**
33
+ * Result of loading DCS configuration, whether from unified config.yaml
34
+ * or individual split files.
35
+ */
36
+ interface DcsConfigResult {
37
+ /** Resolved content configuration */
38
+ content: ContentConfiguration;
39
+ /** Resolved SEO configuration */
40
+ seo: SeoConfiguration;
41
+ /** Resolved pages configuration */
42
+ pages: PagesConfiguration;
43
+ /** Whether the configuration was loaded from a unified config.yaml */
44
+ isUnified: boolean;
45
+ /** The source file path(s) that were loaded */
46
+ sources: string[];
47
+ }
48
+ /**
49
+ * Load DCS configuration from either a unified `.dcs/config.yaml` (version 2)
50
+ * or fall back to individual split files (content.yaml, seo.yaml, pages.yaml).
51
+ *
52
+ * The unified format is preferred when `.dcs/config.yaml` exists and has `version: 2`.
53
+ * This provides a single source of truth for all site configuration.
54
+ *
55
+ * @param projectRoot - The project root directory containing `.dcs/`
56
+ * @returns Resolved configuration from all sources
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const config = await loadConfigYaml('/path/to/site')
61
+ * console.log(config.content.pages.home)
62
+ * console.log(config.seo.global?.siteName)
63
+ * console.log(config.pages.pages)
64
+ * console.log(config.isUnified) // true if loaded from config.yaml
65
+ * ```
66
+ */
67
+ declare function loadConfigYaml(projectRoot: string): Promise<DcsConfigResult>;
32
68
 
33
69
  /**
34
70
  * Pure functions for resolving responsive image variants from DCS CDN URLs.
@@ -109,4 +145,4 @@ declare function resolveResponsiveImage(options: ResponsiveImageOptions): Respon
109
145
  */
110
146
  declare function isCdnAssetUrl(url: string): boolean;
111
147
 
112
- export { ContentConfiguration, type ImageContext, PagesConfiguration, type ResponsiveImageOptions, type ResponsiveImageResult, type ResponsiveSource, SeoConfiguration, isCdnAssetUrl, loadContentYaml, loadPagesYaml, loadSeoYaml, resolveResponsiveImage };
148
+ export { ContentConfiguration, type DcsConfigResult, type ImageContext, PagesConfiguration, type ResponsiveImageOptions, type ResponsiveImageResult, type ResponsiveSource, SeoConfiguration, isCdnAssetUrl, loadConfigYaml, loadContentYaml, loadPagesYaml, loadSeoYaml, resolveResponsiveImage };
package/dist/index.js CHANGED
@@ -172,6 +172,70 @@ async function loadPagesYaml(filePath) {
172
172
  }
173
173
  return pages;
174
174
  }
175
+ function findDcsConfigFile(filename, projectRoot) {
176
+ const possiblePaths = [
177
+ path.resolve(projectRoot, ".dcs", filename),
178
+ path.resolve(projectRoot, "..", ".dcs", filename),
179
+ // For VitePress docs folder
180
+ path.resolve(process.cwd(), ".dcs", filename)
181
+ ];
182
+ for (const testPath of possiblePaths) {
183
+ if (fs.existsSync(testPath)) {
184
+ return testPath;
185
+ }
186
+ }
187
+ return void 0;
188
+ }
189
+ async function loadConfigYaml(projectRoot) {
190
+ const unifiedPath = findDcsConfigFile("config.yaml", projectRoot);
191
+ if (unifiedPath) {
192
+ try {
193
+ const fileContent = fs.readFileSync(unifiedPath, "utf8");
194
+ const raw = yaml.load(fileContent);
195
+ if (raw && raw.version === 2) {
196
+ const unified = raw;
197
+ return {
198
+ content: unified.content ?? { version: 1, global: {}, pages: {} },
199
+ seo: unified.seo ?? { version: 1 },
200
+ pages: {
201
+ version: 3,
202
+ siteSlug: unified.siteSlug ?? unified.site?.slug ?? "",
203
+ pages: unified.pages ?? []
204
+ },
205
+ isUnified: true,
206
+ sources: [unifiedPath]
207
+ };
208
+ }
209
+ } catch {
210
+ }
211
+ }
212
+ const sources = [];
213
+ let content = { version: 1, global: {}, pages: {} };
214
+ const contentPath = findDcsConfigFile("content.yaml", projectRoot);
215
+ if (contentPath) {
216
+ content = await loadContentYaml(contentPath);
217
+ sources.push(contentPath);
218
+ }
219
+ let seo = { version: 1 };
220
+ const seoPath = findDcsConfigFile("seo.yaml", projectRoot);
221
+ if (seoPath) {
222
+ seo = await loadSeoYaml(seoPath);
223
+ sources.push(seoPath);
224
+ }
225
+ let pages = { version: 3, siteSlug: "", pages: [] };
226
+ const pagesPath = findDcsConfigFile("pages.yaml", projectRoot);
227
+ if (pagesPath) {
228
+ pages = await loadPagesYaml(pagesPath);
229
+ sources.push(pagesPath);
230
+ }
231
+ return {
232
+ content,
233
+ seo,
234
+ pages,
235
+ isUnified: false,
236
+ sources
237
+ };
238
+ }
175
239
 
176
240
  // src/fetch.ts
177
241
  var DEFAULT_API_URL = "https://api.duffcloudservices.com";
@@ -323,6 +387,6 @@ function isCdnAssetUrl(url) {
323
387
  return CDN_PATTERN.test(url);
324
388
  }
325
389
 
326
- export { buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, isCdnAssetUrl, loadContentYaml, loadPagesYaml, loadSeoYaml, resolveResponsiveImage, resolveSeoForPage, resolveTextKey };
390
+ export { buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, isCdnAssetUrl, loadConfigYaml, loadContentYaml, loadPagesYaml, loadSeoYaml, resolveResponsiveImage, resolveSeoForPage, resolveTextKey };
327
391
  //# sourceMappingURL=index.js.map
328
392
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/content.ts","../src/seo.ts","../src/loaders.ts","../src/fetch.ts","../src/responsive-image.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * Content resolution utilities\r\n */\r\n\r\nimport type { ContentConfiguration } from './types'\r\n\r\n/**\r\n * Resolve a text key for a specific page.\r\n * Checks page-specific content first, then falls back to global content.\r\n *\r\n * @param content - The content configuration\r\n * @param page - The page slug\r\n * @param key - The text key to resolve\r\n * @returns The resolved text value, or undefined if not found\r\n */\r\nexport function resolveTextKey(\r\n content: ContentConfiguration,\r\n page: string,\r\n key: string\r\n): string | undefined {\r\n // Check page-specific content first\r\n const pageContent = content.pages?.[page]\r\n if (pageContent && key in pageContent) {\r\n return pageContent[key]\r\n }\r\n\r\n // Fall back to global content\r\n if (content.global && key in content.global) {\r\n return content.global[key]\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Get all content for a specific page, merging global and page-specific.\r\n *\r\n * @param content - The content configuration\r\n * @param page - The page slug\r\n * @returns Merged content object (global values overridden by page values)\r\n */\r\nexport function getPageContent(\r\n content: ContentConfiguration,\r\n page: string\r\n): Record<string, string> {\r\n const global = content.global ?? {}\r\n const pageContent = content.pages?.[page] ?? {}\r\n return { ...global, ...pageContent }\r\n}\r\n\r\n/**\r\n * Get only the global content.\r\n *\r\n * @param content - The content configuration\r\n * @returns Global content object\r\n */\r\nexport function getGlobalContent(\r\n content: ContentConfiguration\r\n): Record<string, string> {\r\n return content.global ?? {}\r\n}\r\n","/**\r\n * SEO resolution utilities\r\n */\r\n\r\nimport type {\r\n SeoConfiguration,\r\n ResolvedSeo,\r\n SeoOpenGraphConfig,\r\n SeoTwitterConfig,\r\n} from './types'\r\n\r\n/**\r\n * Resolve SEO configuration for a specific page.\r\n * Merges global defaults with page-specific overrides.\r\n *\r\n * @param seo - The SEO configuration\r\n * @param page - The page slug\r\n * @returns Resolved SEO object with all values filled in\r\n */\r\nexport function resolveSeoForPage(\r\n seo: SeoConfiguration,\r\n page: string\r\n): ResolvedSeo {\r\n const global = seo.global ?? {}\r\n const pageSeo = seo.pages?.[page] ?? {}\r\n\r\n // Resolve title with template\r\n let title = pageSeo.title ?? global.defaultTitle ?? ''\r\n if (title && global.titleTemplate && !pageSeo.noTitleTemplate) {\r\n title = global.titleTemplate.replace('%s', title)\r\n }\r\n\r\n const description = pageSeo.description ?? global.defaultDescription ?? ''\r\n\r\n // Resolve Open Graph\r\n const ogImage =\r\n pageSeo.openGraph?.image ?? global.images?.ogDefault ?? undefined\r\n const openGraph = resolveOpenGraph(pageSeo.openGraph, ogImage, title, description)\r\n\r\n // Resolve Twitter Card\r\n const twitterImage =\r\n pageSeo.twitter?.image ?? global.images?.twitterDefault ?? ogImage\r\n const twitter = resolveTwitterCard(\r\n pageSeo.twitter,\r\n twitterImage,\r\n title,\r\n description,\r\n global.social?.twitter\r\n )\r\n\r\n return {\r\n title,\r\n description,\r\n image: ogImage,\r\n siteName: global.siteName,\r\n siteUrl: global.siteUrl,\r\n locale: global.locale,\r\n canonical: pageSeo.canonical,\r\n robots: pageSeo.robots ?? global.robots,\r\n noIndex: pageSeo.robots?.includes('noindex'),\r\n openGraph,\r\n twitter,\r\n schemas: pageSeo.schemas ?? global.schemas,\r\n alternates: pageSeo.alternates,\r\n }\r\n}\r\n\r\n/**\r\n * Resolve Open Graph configuration\r\n */\r\nfunction resolveOpenGraph(\r\n pageOg: SeoOpenGraphConfig | undefined,\r\n ogImage: string | undefined,\r\n title: string,\r\n description: string\r\n): SeoOpenGraphConfig | undefined {\r\n if (pageOg) {\r\n return {\r\n ...pageOg,\r\n title: pageOg.title ?? title,\r\n description: pageOg.description ?? description,\r\n image: ogImage,\r\n }\r\n }\r\n\r\n if (ogImage) {\r\n return {\r\n title,\r\n description,\r\n image: ogImage,\r\n type: 'website',\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Resolve Twitter Card configuration\r\n */\r\nfunction resolveTwitterCard(\r\n pageTwitter: SeoTwitterConfig | undefined,\r\n twitterImage: string | undefined,\r\n title: string,\r\n description: string,\r\n globalTwitterHandle: string | undefined\r\n): SeoTwitterConfig | undefined {\r\n if (pageTwitter) {\r\n return {\r\n card: pageTwitter.card ?? 'summary_large_image',\r\n site: pageTwitter.site ?? globalTwitterHandle,\r\n ...pageTwitter,\r\n title: pageTwitter.title ?? title,\r\n description: pageTwitter.description ?? description,\r\n image: twitterImage,\r\n }\r\n }\r\n\r\n if (globalTwitterHandle || twitterImage) {\r\n return {\r\n card: 'summary_large_image',\r\n site: globalTwitterHandle,\r\n title,\r\n description,\r\n image: twitterImage,\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Meta tag representation for framework-agnostic usage\r\n */\r\nexport interface MetaTag {\r\n name?: string\r\n property?: string\r\n content: string\r\n}\r\n\r\n/**\r\n * Helper to add a meta tag if content exists\r\n */\r\nfunction addTag(\r\n tags: MetaTag[],\r\n content: string | undefined,\r\n attr: { name?: string; property?: string }\r\n): void {\r\n if (content) {\r\n tags.push({ ...attr, content })\r\n }\r\n}\r\n\r\n/**\r\n * Build Open Graph meta tags\r\n */\r\nfunction buildOpenGraphTags(\r\n tags: MetaTag[],\r\n seo: ResolvedSeo\r\n): void {\r\n const og = seo.openGraph\r\n if (!og) return\r\n\r\n addTag(tags, og.title, { property: 'og:title' })\r\n addTag(tags, og.description, { property: 'og:description' })\r\n addTag(tags, og.image, { property: 'og:image' })\r\n addTag(tags, og.imageAlt, { property: 'og:image:alt' })\r\n addTag(tags, og.imageWidth?.toString(), { property: 'og:image:width' })\r\n addTag(tags, og.imageHeight?.toString(), { property: 'og:image:height' })\r\n addTag(tags, og.type, { property: 'og:type' })\r\n addTag(tags, og.url, { property: 'og:url' })\r\n addTag(tags, seo.siteName, { property: 'og:site_name' })\r\n addTag(tags, seo.locale, { property: 'og:locale' })\r\n}\r\n\r\n/**\r\n * Build Twitter Card meta tags\r\n */\r\nfunction buildTwitterTags(\r\n tags: MetaTag[],\r\n seo: ResolvedSeo\r\n): void {\r\n const tw = seo.twitter\r\n if (!tw) return\r\n\r\n addTag(tags, tw.card, { name: 'twitter:card' })\r\n addTag(tags, tw.site ? `@${tw.site}` : undefined, { name: 'twitter:site' })\r\n addTag(tags, tw.creator ? `@${tw.creator}` : undefined, { name: 'twitter:creator' })\r\n addTag(tags, tw.title, { name: 'twitter:title' })\r\n addTag(tags, tw.description, { name: 'twitter:description' })\r\n addTag(tags, tw.image, { name: 'twitter:image' })\r\n addTag(tags, tw.imageAlt, { name: 'twitter:image:alt' })\r\n}\r\n\r\n/**\r\n * Build an array of meta tags from resolved SEO.\r\n * Useful for frameworks that need to manually set meta tags.\r\n *\r\n * @param seo - Resolved SEO object\r\n * @returns Array of meta tag objects\r\n */\r\nexport function buildMetaTags(seo: ResolvedSeo): MetaTag[] {\r\n const tags: MetaTag[] = []\r\n\r\n // Basic meta\r\n addTag(tags, seo.description, { name: 'description' })\r\n addTag(tags, seo.robots, { name: 'robots' })\r\n\r\n // Open Graph\r\n buildOpenGraphTags(tags, seo)\r\n\r\n // Twitter Card\r\n buildTwitterTags(tags, seo)\r\n\r\n return tags\r\n}\r\n","/**\r\n * YAML file loaders for DCS configuration files\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type {\r\n ContentConfiguration,\r\n SeoConfiguration,\r\n PagesConfiguration,\r\n} from './types'\r\n\r\n/**\r\n * Load and parse .dcs/content.yaml\r\n *\r\n * @param filePath - Path to content.yaml (absolute or relative to cwd)\r\n * @returns Parsed content configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadContentYaml(\r\n filePath: string\r\n): Promise<ContentConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const content = yaml.load(fileContent) as ContentConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!content.version) {\r\n content.version = 1\r\n }\r\n if (!content.global) {\r\n content.global = {}\r\n }\r\n if (!content.pages) {\r\n content.pages = {}\r\n }\r\n\r\n return content\r\n}\r\n\r\n/**\r\n * Load and parse .dcs/seo.yaml\r\n *\r\n * @param filePath - Path to seo.yaml (absolute or relative to cwd)\r\n * @returns Parsed SEO configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadSeoYaml(filePath: string): Promise<SeoConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const seo = yaml.load(fileContent) as SeoConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!seo.version) {\r\n seo.version = 1\r\n }\r\n\r\n return seo\r\n}\r\n\r\n/**\r\n * Load and parse .dcs/pages.yaml\r\n *\r\n * @param filePath - Path to pages.yaml (absolute or relative to cwd)\r\n * @returns Parsed pages configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadPagesYaml(\r\n filePath: string\r\n): Promise<PagesConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const pages = yaml.load(fileContent) as PagesConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!pages.version) {\r\n pages.version = 3\r\n }\r\n if (!pages.pages) {\r\n pages.pages = []\r\n }\r\n\r\n return pages\r\n}\r\n\r\n/**\r\n * Try to find a DCS config file in common locations\r\n *\r\n * @param filename - The config file name (e.g., 'content.yaml')\r\n * @param projectRoot - The project root directory\r\n * @returns The found path, or undefined if not found\r\n */\r\nexport function findDcsConfigFile(\r\n filename: string,\r\n projectRoot: string\r\n): string | undefined {\r\n const possiblePaths = [\r\n path.resolve(projectRoot, '.dcs', filename),\r\n path.resolve(projectRoot, '..', '.dcs', filename), // For VitePress docs folder\r\n path.resolve(process.cwd(), '.dcs', filename),\r\n ]\r\n\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n return testPath\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n","/**\r\n * Runtime content fetching for premium tier customers\r\n */\r\n\r\nimport type { ContentConfiguration, SeoConfiguration } from './types'\r\n\r\n/**\r\n * Options for runtime fetch operations\r\n */\r\nexport interface FetchOptions {\r\n /** Base URL for the DCS API */\r\n apiBaseUrl?: string\r\n /** Timeout in milliseconds (default: 5000) */\r\n timeout?: number\r\n /** Custom headers to include */\r\n headers?: Record<string, string>\r\n}\r\n\r\nconst DEFAULT_API_URL = 'https://api.duffcloudservices.com'\r\nconst DEFAULT_TIMEOUT = 5000\r\n\r\n/**\r\n * Fetch runtime content from the DCS API.\r\n * This is a premium tier feature - returns 403 for non-premium sites.\r\n *\r\n * @param siteSlug - The site's slug identifier\r\n * @param options - Fetch options\r\n * @returns Content configuration or null if fetch fails\r\n */\r\nexport async function fetchRuntimeContent(\r\n siteSlug: string,\r\n options: FetchOptions = {}\r\n): Promise<ContentConfiguration | null> {\r\n const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options\r\n\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n\r\n try {\r\n const response = await fetch(\r\n `${apiBaseUrl}/portal/sites/${siteSlug}/content/runtime`,\r\n {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...options.headers,\r\n },\r\n signal: controller.signal,\r\n }\r\n )\r\n\r\n if (!response.ok) {\r\n if (response.status === 403) {\r\n console.warn(\r\n '[DCS] Runtime content requires premium tier. Using build-time content.'\r\n )\r\n }\r\n return null\r\n }\r\n\r\n return (await response.json()) as ContentConfiguration\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'AbortError') {\r\n console.warn('[DCS] Runtime content fetch timed out')\r\n } else {\r\n console.warn('[DCS] Runtime content fetch failed:', error)\r\n }\r\n return null\r\n } finally {\r\n clearTimeout(timeoutId)\r\n }\r\n}\r\n\r\n/**\r\n * Fetch runtime SEO configuration from the DCS API.\r\n * This is a premium tier feature - returns 403 for non-premium sites.\r\n *\r\n * @param siteSlug - The site's slug identifier\r\n * @param options - Fetch options\r\n * @returns SEO configuration or null if fetch fails\r\n */\r\nexport async function fetchRuntimeSeo(\r\n siteSlug: string,\r\n options: FetchOptions = {}\r\n): Promise<SeoConfiguration | null> {\r\n const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options\r\n\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n\r\n try {\r\n const response = await fetch(\r\n `${apiBaseUrl}/portal/sites/${siteSlug}/seo/runtime`,\r\n {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...options.headers,\r\n },\r\n signal: controller.signal,\r\n }\r\n )\r\n\r\n if (!response.ok) {\r\n if (response.status === 403) {\r\n console.warn(\r\n '[DCS] Runtime SEO requires premium tier. Using build-time SEO.'\r\n )\r\n }\r\n return null\r\n }\r\n\r\n return (await response.json()) as SeoConfiguration\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'AbortError') {\r\n console.warn('[DCS] Runtime SEO fetch timed out')\r\n } else {\r\n console.warn('[DCS] Runtime SEO fetch failed:', error)\r\n }\r\n return null\r\n } finally {\r\n clearTimeout(timeoutId)\r\n }\r\n}\r\n","/**\r\n * Pure functions for resolving responsive image variants from DCS CDN URLs.\r\n *\r\n * The DCS imaging pipeline generates WebP variants during upload:\r\n * - thumb (150px) — grid thumbnails\r\n * - sm (640px) — mobile displays\r\n * - md (1024px) — tablet / blog body\r\n * - lg (1920px) — desktop / hero sections\r\n * - og (1200×630) — social sharing (fixed aspect)\r\n *\r\n * This module provides framework-agnostic resolution logic that\r\n * framework packages (@duffcloudservices/cms, cms-react, etc.) wrap\r\n * in their respective APIs.\r\n *\r\n * @module responsive-image\r\n */\r\n\r\n/** Sizing context that controls which variants are included in the srcset. */\r\nexport type ImageContext = 'hero' | 'card' | 'thumbnail' | 'content' | 'og'\r\n\r\n/** Options for resolving a responsive image. */\r\nexport interface ResponsiveImageOptions {\r\n /** Source URL — original CDN URL or local path. */\r\n src: string\r\n /** Alt text for accessibility. */\r\n alt: string\r\n /** Sizing context — determines which variants to include. */\r\n context?: ImageContext\r\n /** Optional explicit width hints for the browser `sizes` attribute. */\r\n sizes?: string\r\n /** Skip variant resolution and return the original URL only. */\r\n original?: boolean\r\n}\r\n\r\n/** A single `<source>` entry for a `<picture>` element. */\r\nexport interface ResponsiveSource {\r\n srcset: string\r\n type: string\r\n sizes?: string\r\n}\r\n\r\n/** Resolved image properties for rendering. */\r\nexport interface ResponsiveImageResult {\r\n /** Props spreadable onto an `<img>` element. */\r\n imgProps: {\r\n src: string\r\n alt: string\r\n loading: 'lazy' | 'eager'\r\n decoding: 'async' | 'auto'\r\n }\r\n /** WebP `<source>` entries for a `<picture>` element. */\r\n sources: ResponsiveSource[]\r\n /** Whether responsive variants were detected. */\r\n hasVariants: boolean\r\n}\r\n\r\n/**\r\n * Regex that matches DCS CDN asset URLs and captures the base path, UUID, and extension.\r\n *\r\n * Group 1: base path including trailing slash (e.g. `https://files.duffcloudservices.com/kept/assets/hero/`)\r\n * Group 2: UUID (e.g. `abc-123`)\r\n * Group 3: file extension (e.g. `jpg`)\r\n */\r\nconst CDN_PATTERN = /^(https?:\\/\\/files\\.[^/]+\\/[^/]+\\/assets\\/(?:[^/]+\\/)*)([a-f0-9-]+)\\.(\\w+)$/\r\n\r\n/** Variant definitions mapping suffix → pixel width. */\r\nconst VARIANT_MAP: Record<string, { suffix: string; width: number }> = {\r\n thumb: { suffix: '-thumb', width: 150 },\r\n sm: { suffix: '-sm', width: 640 },\r\n md: { suffix: '-md', width: 1024 },\r\n lg: { suffix: '-lg', width: 1920 },\r\n}\r\n\r\n/** Which variants each context includes (smallest → largest). */\r\nconst CONTEXT_VARIANTS: Record<ImageContext, string[]> = {\r\n hero: ['sm', 'md', 'lg'],\r\n card: ['thumb', 'sm', 'md'],\r\n thumbnail: ['thumb', 'sm'],\r\n content: ['sm', 'md', 'lg'],\r\n og: [], // OG uses the dedicated -og variant, not srcset\r\n}\r\n\r\n/** Default `sizes` attribute per context. */\r\nconst DEFAULT_SIZES: Record<ImageContext, string> = {\r\n hero: '100vw',\r\n card: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',\r\n thumbnail: '150px',\r\n content: '(max-width: 1024px) 100vw, 1024px',\r\n og: '',\r\n}\r\n\r\n/**\r\n * Resolves responsive image metadata from a DCS CDN URL.\r\n *\r\n * If the URL matches the CDN asset pattern, a srcset of WebP variants\r\n * is generated based on the sizing `context`. Non-CDN URLs are returned\r\n * as-is with no variants.\r\n *\r\n * @example\r\n * ```ts\r\n * const result = resolveResponsiveImage({\r\n * src: 'https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg',\r\n * alt: 'Hero image',\r\n * context: 'hero',\r\n * })\r\n * // result.hasVariants === true\r\n * // result.sources[0].srcset contains sm, md, lg WebP sizes\r\n * // result.imgProps.loading === 'eager' (hero context)\r\n * ```\r\n */\r\nexport function resolveResponsiveImage(options: ResponsiveImageOptions): ResponsiveImageResult {\r\n const { src, alt, context = 'content', sizes, original } = options\r\n\r\n const fallback: ResponsiveImageResult = {\r\n imgProps: {\r\n src,\r\n alt,\r\n loading: context === 'hero' ? 'eager' : 'lazy',\r\n decoding: context === 'hero' ? 'auto' : 'async',\r\n },\r\n sources: [],\r\n hasVariants: false,\r\n }\r\n\r\n if (original) {\r\n return fallback\r\n }\r\n\r\n const match = src.match(CDN_PATTERN)\r\n if (!match) {\r\n return fallback\r\n }\r\n\r\n const [, basePath, uuid] = match\r\n\r\n const variants = CONTEXT_VARIANTS[context] ?? CONTEXT_VARIANTS.content\r\n if (variants.length === 0) {\r\n return fallback\r\n }\r\n\r\n const srcsetParts = variants.map((key) => {\r\n const v = VARIANT_MAP[key]\r\n return `${basePath}${uuid}${v.suffix}.webp ${v.width}w`\r\n })\r\n\r\n const resolvedSizes = sizes ?? DEFAULT_SIZES[context] ?? DEFAULT_SIZES.content\r\n\r\n return {\r\n imgProps: {\r\n src: `${basePath}${uuid}-md.webp`, // Fallback to md variant\r\n alt,\r\n loading: context === 'hero' ? 'eager' : 'lazy',\r\n decoding: context === 'hero' ? 'auto' : 'async',\r\n },\r\n sources: [\r\n {\r\n srcset: srcsetParts.join(', '),\r\n type: 'image/webp',\r\n ...(resolvedSizes ? { sizes: resolvedSizes } : {}),\r\n },\r\n ],\r\n hasVariants: true,\r\n }\r\n}\r\n\r\n/**\r\n * Tests whether a URL matches the DCS CDN asset pattern.\r\n *\r\n * Useful when rendering a mix of CDN-hosted and local images\r\n * to decide whether responsive treatment is applicable.\r\n */\r\nexport function isCdnAssetUrl(url: string): boolean {\r\n return CDN_PATTERN.test(url)\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/content.ts","../src/seo.ts","../src/loaders.ts","../src/fetch.ts","../src/responsive-image.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * Content resolution utilities\r\n */\r\n\r\nimport type { ContentConfiguration } from './types'\r\n\r\n/**\r\n * Resolve a text key for a specific page.\r\n * Checks page-specific content first, then falls back to global content.\r\n *\r\n * @param content - The content configuration\r\n * @param page - The page slug\r\n * @param key - The text key to resolve\r\n * @returns The resolved text value, or undefined if not found\r\n */\r\nexport function resolveTextKey(\r\n content: ContentConfiguration,\r\n page: string,\r\n key: string\r\n): string | undefined {\r\n // Check page-specific content first\r\n const pageContent = content.pages?.[page]\r\n if (pageContent && key in pageContent) {\r\n return pageContent[key]\r\n }\r\n\r\n // Fall back to global content\r\n if (content.global && key in content.global) {\r\n return content.global[key]\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Get all content for a specific page, merging global and page-specific.\r\n *\r\n * @param content - The content configuration\r\n * @param page - The page slug\r\n * @returns Merged content object (global values overridden by page values)\r\n */\r\nexport function getPageContent(\r\n content: ContentConfiguration,\r\n page: string\r\n): Record<string, string> {\r\n const global = content.global ?? {}\r\n const pageContent = content.pages?.[page] ?? {}\r\n return { ...global, ...pageContent }\r\n}\r\n\r\n/**\r\n * Get only the global content.\r\n *\r\n * @param content - The content configuration\r\n * @returns Global content object\r\n */\r\nexport function getGlobalContent(\r\n content: ContentConfiguration\r\n): Record<string, string> {\r\n return content.global ?? {}\r\n}\r\n","/**\r\n * SEO resolution utilities\r\n */\r\n\r\nimport type {\r\n SeoConfiguration,\r\n ResolvedSeo,\r\n SeoOpenGraphConfig,\r\n SeoTwitterConfig,\r\n} from './types'\r\n\r\n/**\r\n * Resolve SEO configuration for a specific page.\r\n * Merges global defaults with page-specific overrides.\r\n *\r\n * @param seo - The SEO configuration\r\n * @param page - The page slug\r\n * @returns Resolved SEO object with all values filled in\r\n */\r\nexport function resolveSeoForPage(\r\n seo: SeoConfiguration,\r\n page: string\r\n): ResolvedSeo {\r\n const global = seo.global ?? {}\r\n const pageSeo = seo.pages?.[page] ?? {}\r\n\r\n // Resolve title with template\r\n let title = pageSeo.title ?? global.defaultTitle ?? ''\r\n if (title && global.titleTemplate && !pageSeo.noTitleTemplate) {\r\n title = global.titleTemplate.replace('%s', title)\r\n }\r\n\r\n const description = pageSeo.description ?? global.defaultDescription ?? ''\r\n\r\n // Resolve Open Graph\r\n const ogImage =\r\n pageSeo.openGraph?.image ?? global.images?.ogDefault ?? undefined\r\n const openGraph = resolveOpenGraph(pageSeo.openGraph, ogImage, title, description)\r\n\r\n // Resolve Twitter Card\r\n const twitterImage =\r\n pageSeo.twitter?.image ?? global.images?.twitterDefault ?? ogImage\r\n const twitter = resolveTwitterCard(\r\n pageSeo.twitter,\r\n twitterImage,\r\n title,\r\n description,\r\n global.social?.twitter\r\n )\r\n\r\n return {\r\n title,\r\n description,\r\n image: ogImage,\r\n siteName: global.siteName,\r\n siteUrl: global.siteUrl,\r\n locale: global.locale,\r\n canonical: pageSeo.canonical,\r\n robots: pageSeo.robots ?? global.robots,\r\n noIndex: pageSeo.robots?.includes('noindex'),\r\n openGraph,\r\n twitter,\r\n schemas: pageSeo.schemas ?? global.schemas,\r\n alternates: pageSeo.alternates,\r\n }\r\n}\r\n\r\n/**\r\n * Resolve Open Graph configuration\r\n */\r\nfunction resolveOpenGraph(\r\n pageOg: SeoOpenGraphConfig | undefined,\r\n ogImage: string | undefined,\r\n title: string,\r\n description: string\r\n): SeoOpenGraphConfig | undefined {\r\n if (pageOg) {\r\n return {\r\n ...pageOg,\r\n title: pageOg.title ?? title,\r\n description: pageOg.description ?? description,\r\n image: ogImage,\r\n }\r\n }\r\n\r\n if (ogImage) {\r\n return {\r\n title,\r\n description,\r\n image: ogImage,\r\n type: 'website',\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Resolve Twitter Card configuration\r\n */\r\nfunction resolveTwitterCard(\r\n pageTwitter: SeoTwitterConfig | undefined,\r\n twitterImage: string | undefined,\r\n title: string,\r\n description: string,\r\n globalTwitterHandle: string | undefined\r\n): SeoTwitterConfig | undefined {\r\n if (pageTwitter) {\r\n return {\r\n card: pageTwitter.card ?? 'summary_large_image',\r\n site: pageTwitter.site ?? globalTwitterHandle,\r\n ...pageTwitter,\r\n title: pageTwitter.title ?? title,\r\n description: pageTwitter.description ?? description,\r\n image: twitterImage,\r\n }\r\n }\r\n\r\n if (globalTwitterHandle || twitterImage) {\r\n return {\r\n card: 'summary_large_image',\r\n site: globalTwitterHandle,\r\n title,\r\n description,\r\n image: twitterImage,\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n/**\r\n * Meta tag representation for framework-agnostic usage\r\n */\r\nexport interface MetaTag {\r\n name?: string\r\n property?: string\r\n content: string\r\n}\r\n\r\n/**\r\n * Helper to add a meta tag if content exists\r\n */\r\nfunction addTag(\r\n tags: MetaTag[],\r\n content: string | undefined,\r\n attr: { name?: string; property?: string }\r\n): void {\r\n if (content) {\r\n tags.push({ ...attr, content })\r\n }\r\n}\r\n\r\n/**\r\n * Build Open Graph meta tags\r\n */\r\nfunction buildOpenGraphTags(\r\n tags: MetaTag[],\r\n seo: ResolvedSeo\r\n): void {\r\n const og = seo.openGraph\r\n if (!og) return\r\n\r\n addTag(tags, og.title, { property: 'og:title' })\r\n addTag(tags, og.description, { property: 'og:description' })\r\n addTag(tags, og.image, { property: 'og:image' })\r\n addTag(tags, og.imageAlt, { property: 'og:image:alt' })\r\n addTag(tags, og.imageWidth?.toString(), { property: 'og:image:width' })\r\n addTag(tags, og.imageHeight?.toString(), { property: 'og:image:height' })\r\n addTag(tags, og.type, { property: 'og:type' })\r\n addTag(tags, og.url, { property: 'og:url' })\r\n addTag(tags, seo.siteName, { property: 'og:site_name' })\r\n addTag(tags, seo.locale, { property: 'og:locale' })\r\n}\r\n\r\n/**\r\n * Build Twitter Card meta tags\r\n */\r\nfunction buildTwitterTags(\r\n tags: MetaTag[],\r\n seo: ResolvedSeo\r\n): void {\r\n const tw = seo.twitter\r\n if (!tw) return\r\n\r\n addTag(tags, tw.card, { name: 'twitter:card' })\r\n addTag(tags, tw.site ? `@${tw.site}` : undefined, { name: 'twitter:site' })\r\n addTag(tags, tw.creator ? `@${tw.creator}` : undefined, { name: 'twitter:creator' })\r\n addTag(tags, tw.title, { name: 'twitter:title' })\r\n addTag(tags, tw.description, { name: 'twitter:description' })\r\n addTag(tags, tw.image, { name: 'twitter:image' })\r\n addTag(tags, tw.imageAlt, { name: 'twitter:image:alt' })\r\n}\r\n\r\n/**\r\n * Build an array of meta tags from resolved SEO.\r\n * Useful for frameworks that need to manually set meta tags.\r\n *\r\n * @param seo - Resolved SEO object\r\n * @returns Array of meta tag objects\r\n */\r\nexport function buildMetaTags(seo: ResolvedSeo): MetaTag[] {\r\n const tags: MetaTag[] = []\r\n\r\n // Basic meta\r\n addTag(tags, seo.description, { name: 'description' })\r\n addTag(tags, seo.robots, { name: 'robots' })\r\n\r\n // Open Graph\r\n buildOpenGraphTags(tags, seo)\r\n\r\n // Twitter Card\r\n buildTwitterTags(tags, seo)\r\n\r\n return tags\r\n}\r\n","/**\r\n * YAML file loaders for DCS configuration files\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type {\r\n ContentConfiguration,\r\n SeoConfiguration,\r\n PagesConfiguration,\r\n UnifiedConfiguration,\r\n} from './types'\r\n\r\n/**\r\n * Load and parse .dcs/content.yaml\r\n *\r\n * @param filePath - Path to content.yaml (absolute or relative to cwd)\r\n * @returns Parsed content configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadContentYaml(\r\n filePath: string\r\n): Promise<ContentConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const content = yaml.load(fileContent) as ContentConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!content.version) {\r\n content.version = 1\r\n }\r\n if (!content.global) {\r\n content.global = {}\r\n }\r\n if (!content.pages) {\r\n content.pages = {}\r\n }\r\n\r\n return content\r\n}\r\n\r\n/**\r\n * Load and parse .dcs/seo.yaml\r\n *\r\n * @param filePath - Path to seo.yaml (absolute or relative to cwd)\r\n * @returns Parsed SEO configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadSeoYaml(filePath: string): Promise<SeoConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const seo = yaml.load(fileContent) as SeoConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!seo.version) {\r\n seo.version = 1\r\n }\r\n\r\n return seo\r\n}\r\n\r\n/**\r\n * Load and parse .dcs/pages.yaml\r\n *\r\n * @param filePath - Path to pages.yaml (absolute or relative to cwd)\r\n * @returns Parsed pages configuration\r\n * @throws Error if file not found or parse fails\r\n */\r\nexport async function loadPagesYaml(\r\n filePath: string\r\n): Promise<PagesConfiguration> {\r\n const absolutePath = path.isAbsolute(filePath)\r\n ? filePath\r\n : path.resolve(process.cwd(), filePath)\r\n\r\n const fileContent = fs.readFileSync(absolutePath, 'utf8')\r\n const pages = yaml.load(fileContent) as PagesConfiguration\r\n\r\n // Ensure required fields exist\r\n if (!pages.version) {\r\n pages.version = 3\r\n }\r\n if (!pages.pages) {\r\n pages.pages = []\r\n }\r\n\r\n return pages\r\n}\r\n\r\n/**\r\n * Try to find a DCS config file in common locations\r\n *\r\n * @param filename - The config file name (e.g., 'content.yaml')\r\n * @param projectRoot - The project root directory\r\n * @returns The found path, or undefined if not found\r\n */\r\nexport function findDcsConfigFile(\r\n filename: string,\r\n projectRoot: string\r\n): string | undefined {\r\n const possiblePaths = [\r\n path.resolve(projectRoot, '.dcs', filename),\r\n path.resolve(projectRoot, '..', '.dcs', filename), // For VitePress docs folder\r\n path.resolve(process.cwd(), '.dcs', filename),\r\n ]\r\n\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n return testPath\r\n }\r\n }\r\n\r\n return undefined\r\n}\r\n\r\n// ─── Unified Config Loader ──────────────────────────────────────\r\n\r\n/**\r\n * Result of loading DCS configuration, whether from unified config.yaml\r\n * or individual split files.\r\n */\r\nexport interface DcsConfigResult {\r\n /** Resolved content configuration */\r\n content: ContentConfiguration\r\n /** Resolved SEO configuration */\r\n seo: SeoConfiguration\r\n /** Resolved pages configuration */\r\n pages: PagesConfiguration\r\n /** Whether the configuration was loaded from a unified config.yaml */\r\n isUnified: boolean\r\n /** The source file path(s) that were loaded */\r\n sources: string[]\r\n}\r\n\r\n/**\r\n * Load DCS configuration from either a unified `.dcs/config.yaml` (version 2)\r\n * or fall back to individual split files (content.yaml, seo.yaml, pages.yaml).\r\n *\r\n * The unified format is preferred when `.dcs/config.yaml` exists and has `version: 2`.\r\n * This provides a single source of truth for all site configuration.\r\n *\r\n * @param projectRoot - The project root directory containing `.dcs/`\r\n * @returns Resolved configuration from all sources\r\n *\r\n * @example\r\n * ```typescript\r\n * const config = await loadConfigYaml('/path/to/site')\r\n * console.log(config.content.pages.home)\r\n * console.log(config.seo.global?.siteName)\r\n * console.log(config.pages.pages)\r\n * console.log(config.isUnified) // true if loaded from config.yaml\r\n * ```\r\n */\r\nexport async function loadConfigYaml(projectRoot: string): Promise<DcsConfigResult> {\r\n // Try unified config.yaml first\r\n const unifiedPath = findDcsConfigFile('config.yaml', projectRoot)\r\n if (unifiedPath) {\r\n try {\r\n const fileContent = fs.readFileSync(unifiedPath, 'utf8')\r\n const raw = yaml.load(fileContent) as Record<string, unknown>\r\n\r\n // Only treat as unified if version === 2\r\n if (raw && raw.version === 2) {\r\n const unified = raw as unknown as UnifiedConfiguration\r\n return {\r\n content: unified.content ?? { version: 1, global: {}, pages: {} },\r\n seo: unified.seo ?? { version: 1 },\r\n pages: {\r\n version: 3,\r\n siteSlug: unified.siteSlug ?? unified.site?.slug ?? '',\r\n pages: unified.pages ?? [],\r\n },\r\n isUnified: true,\r\n sources: [unifiedPath],\r\n }\r\n }\r\n } catch {\r\n // config.yaml exists but failed to parse — fall through to split files\r\n }\r\n }\r\n\r\n // Fall back to individual split files\r\n const sources: string[] = []\r\n\r\n let content: ContentConfiguration = { version: 1, global: {}, pages: {} }\r\n const contentPath = findDcsConfigFile('content.yaml', projectRoot)\r\n if (contentPath) {\r\n content = await loadContentYaml(contentPath)\r\n sources.push(contentPath)\r\n }\r\n\r\n let seo: SeoConfiguration = { version: 1 }\r\n const seoPath = findDcsConfigFile('seo.yaml', projectRoot)\r\n if (seoPath) {\r\n seo = await loadSeoYaml(seoPath)\r\n sources.push(seoPath)\r\n }\r\n\r\n let pages: PagesConfiguration = { version: 3, siteSlug: '', pages: [] }\r\n const pagesPath = findDcsConfigFile('pages.yaml', projectRoot)\r\n if (pagesPath) {\r\n pages = await loadPagesYaml(pagesPath)\r\n sources.push(pagesPath)\r\n }\r\n\r\n return {\r\n content,\r\n seo,\r\n pages,\r\n isUnified: false,\r\n sources,\r\n }\r\n}\r\n","/**\r\n * Runtime content fetching for premium tier customers\r\n */\r\n\r\nimport type { ContentConfiguration, SeoConfiguration } from './types'\r\n\r\n/**\r\n * Options for runtime fetch operations\r\n */\r\nexport interface FetchOptions {\r\n /** Base URL for the DCS API */\r\n apiBaseUrl?: string\r\n /** Timeout in milliseconds (default: 5000) */\r\n timeout?: number\r\n /** Custom headers to include */\r\n headers?: Record<string, string>\r\n}\r\n\r\nconst DEFAULT_API_URL = 'https://api.duffcloudservices.com'\r\nconst DEFAULT_TIMEOUT = 5000\r\n\r\n/**\r\n * Fetch runtime content from the DCS API.\r\n * This is a premium tier feature - returns 403 for non-premium sites.\r\n *\r\n * @param siteSlug - The site's slug identifier\r\n * @param options - Fetch options\r\n * @returns Content configuration or null if fetch fails\r\n */\r\nexport async function fetchRuntimeContent(\r\n siteSlug: string,\r\n options: FetchOptions = {}\r\n): Promise<ContentConfiguration | null> {\r\n const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options\r\n\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n\r\n try {\r\n const response = await fetch(\r\n `${apiBaseUrl}/portal/sites/${siteSlug}/content/runtime`,\r\n {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...options.headers,\r\n },\r\n signal: controller.signal,\r\n }\r\n )\r\n\r\n if (!response.ok) {\r\n if (response.status === 403) {\r\n console.warn(\r\n '[DCS] Runtime content requires premium tier. Using build-time content.'\r\n )\r\n }\r\n return null\r\n }\r\n\r\n return (await response.json()) as ContentConfiguration\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'AbortError') {\r\n console.warn('[DCS] Runtime content fetch timed out')\r\n } else {\r\n console.warn('[DCS] Runtime content fetch failed:', error)\r\n }\r\n return null\r\n } finally {\r\n clearTimeout(timeoutId)\r\n }\r\n}\r\n\r\n/**\r\n * Fetch runtime SEO configuration from the DCS API.\r\n * This is a premium tier feature - returns 403 for non-premium sites.\r\n *\r\n * @param siteSlug - The site's slug identifier\r\n * @param options - Fetch options\r\n * @returns SEO configuration or null if fetch fails\r\n */\r\nexport async function fetchRuntimeSeo(\r\n siteSlug: string,\r\n options: FetchOptions = {}\r\n): Promise<SeoConfiguration | null> {\r\n const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options\r\n\r\n const controller = new AbortController()\r\n const timeoutId = setTimeout(() => controller.abort(), timeout)\r\n\r\n try {\r\n const response = await fetch(\r\n `${apiBaseUrl}/portal/sites/${siteSlug}/seo/runtime`,\r\n {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...options.headers,\r\n },\r\n signal: controller.signal,\r\n }\r\n )\r\n\r\n if (!response.ok) {\r\n if (response.status === 403) {\r\n console.warn(\r\n '[DCS] Runtime SEO requires premium tier. Using build-time SEO.'\r\n )\r\n }\r\n return null\r\n }\r\n\r\n return (await response.json()) as SeoConfiguration\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'AbortError') {\r\n console.warn('[DCS] Runtime SEO fetch timed out')\r\n } else {\r\n console.warn('[DCS] Runtime SEO fetch failed:', error)\r\n }\r\n return null\r\n } finally {\r\n clearTimeout(timeoutId)\r\n }\r\n}\r\n","/**\r\n * Pure functions for resolving responsive image variants from DCS CDN URLs.\r\n *\r\n * The DCS imaging pipeline generates WebP variants during upload:\r\n * - thumb (150px) — grid thumbnails\r\n * - sm (640px) — mobile displays\r\n * - md (1024px) — tablet / blog body\r\n * - lg (1920px) — desktop / hero sections\r\n * - og (1200×630) — social sharing (fixed aspect)\r\n *\r\n * This module provides framework-agnostic resolution logic that\r\n * framework packages (@duffcloudservices/cms, cms-react, etc.) wrap\r\n * in their respective APIs.\r\n *\r\n * @module responsive-image\r\n */\r\n\r\n/** Sizing context that controls which variants are included in the srcset. */\r\nexport type ImageContext = 'hero' | 'card' | 'thumbnail' | 'content' | 'og'\r\n\r\n/** Options for resolving a responsive image. */\r\nexport interface ResponsiveImageOptions {\r\n /** Source URL — original CDN URL or local path. */\r\n src: string\r\n /** Alt text for accessibility. */\r\n alt: string\r\n /** Sizing context — determines which variants to include. */\r\n context?: ImageContext\r\n /** Optional explicit width hints for the browser `sizes` attribute. */\r\n sizes?: string\r\n /** Skip variant resolution and return the original URL only. */\r\n original?: boolean\r\n}\r\n\r\n/** A single `<source>` entry for a `<picture>` element. */\r\nexport interface ResponsiveSource {\r\n srcset: string\r\n type: string\r\n sizes?: string\r\n}\r\n\r\n/** Resolved image properties for rendering. */\r\nexport interface ResponsiveImageResult {\r\n /** Props spreadable onto an `<img>` element. */\r\n imgProps: {\r\n src: string\r\n alt: string\r\n loading: 'lazy' | 'eager'\r\n decoding: 'async' | 'auto'\r\n }\r\n /** WebP `<source>` entries for a `<picture>` element. */\r\n sources: ResponsiveSource[]\r\n /** Whether responsive variants were detected. */\r\n hasVariants: boolean\r\n}\r\n\r\n/**\r\n * Regex that matches DCS CDN asset URLs and captures the base path, UUID, and extension.\r\n *\r\n * Group 1: base path including trailing slash (e.g. `https://files.duffcloudservices.com/kept/assets/hero/`)\r\n * Group 2: UUID (e.g. `abc-123`)\r\n * Group 3: file extension (e.g. `jpg`)\r\n */\r\nconst CDN_PATTERN = /^(https?:\\/\\/files\\.[^/]+\\/[^/]+\\/assets\\/(?:[^/]+\\/)*)([a-f0-9-]+)\\.(\\w+)$/\r\n\r\n/** Variant definitions mapping suffix → pixel width. */\r\nconst VARIANT_MAP: Record<string, { suffix: string; width: number }> = {\r\n thumb: { suffix: '-thumb', width: 150 },\r\n sm: { suffix: '-sm', width: 640 },\r\n md: { suffix: '-md', width: 1024 },\r\n lg: { suffix: '-lg', width: 1920 },\r\n}\r\n\r\n/** Which variants each context includes (smallest → largest). */\r\nconst CONTEXT_VARIANTS: Record<ImageContext, string[]> = {\r\n hero: ['sm', 'md', 'lg'],\r\n card: ['thumb', 'sm', 'md'],\r\n thumbnail: ['thumb', 'sm'],\r\n content: ['sm', 'md', 'lg'],\r\n og: [], // OG uses the dedicated -og variant, not srcset\r\n}\r\n\r\n/** Default `sizes` attribute per context. */\r\nconst DEFAULT_SIZES: Record<ImageContext, string> = {\r\n hero: '100vw',\r\n card: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',\r\n thumbnail: '150px',\r\n content: '(max-width: 1024px) 100vw, 1024px',\r\n og: '',\r\n}\r\n\r\n/**\r\n * Resolves responsive image metadata from a DCS CDN URL.\r\n *\r\n * If the URL matches the CDN asset pattern, a srcset of WebP variants\r\n * is generated based on the sizing `context`. Non-CDN URLs are returned\r\n * as-is with no variants.\r\n *\r\n * @example\r\n * ```ts\r\n * const result = resolveResponsiveImage({\r\n * src: 'https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg',\r\n * alt: 'Hero image',\r\n * context: 'hero',\r\n * })\r\n * // result.hasVariants === true\r\n * // result.sources[0].srcset contains sm, md, lg WebP sizes\r\n * // result.imgProps.loading === 'eager' (hero context)\r\n * ```\r\n */\r\nexport function resolveResponsiveImage(options: ResponsiveImageOptions): ResponsiveImageResult {\r\n const { src, alt, context = 'content', sizes, original } = options\r\n\r\n const fallback: ResponsiveImageResult = {\r\n imgProps: {\r\n src,\r\n alt,\r\n loading: context === 'hero' ? 'eager' : 'lazy',\r\n decoding: context === 'hero' ? 'auto' : 'async',\r\n },\r\n sources: [],\r\n hasVariants: false,\r\n }\r\n\r\n if (original) {\r\n return fallback\r\n }\r\n\r\n const match = src.match(CDN_PATTERN)\r\n if (!match) {\r\n return fallback\r\n }\r\n\r\n const [, basePath, uuid] = match\r\n\r\n const variants = CONTEXT_VARIANTS[context] ?? CONTEXT_VARIANTS.content\r\n if (variants.length === 0) {\r\n return fallback\r\n }\r\n\r\n const srcsetParts = variants.map((key) => {\r\n const v = VARIANT_MAP[key]\r\n return `${basePath}${uuid}${v.suffix}.webp ${v.width}w`\r\n })\r\n\r\n const resolvedSizes = sizes ?? DEFAULT_SIZES[context] ?? DEFAULT_SIZES.content\r\n\r\n return {\r\n imgProps: {\r\n src: `${basePath}${uuid}-md.webp`, // Fallback to md variant\r\n alt,\r\n loading: context === 'hero' ? 'eager' : 'lazy',\r\n decoding: context === 'hero' ? 'auto' : 'async',\r\n },\r\n sources: [\r\n {\r\n srcset: srcsetParts.join(', '),\r\n type: 'image/webp',\r\n ...(resolvedSizes ? { sizes: resolvedSizes } : {}),\r\n },\r\n ],\r\n hasVariants: true,\r\n }\r\n}\r\n\r\n/**\r\n * Tests whether a URL matches the DCS CDN asset pattern.\r\n *\r\n * Useful when rendering a mix of CDN-hosted and local images\r\n * to decide whether responsive treatment is applicable.\r\n */\r\nexport function isCdnAssetUrl(url: string): boolean {\r\n return CDN_PATTERN.test(url)\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duffcloudservices/cms-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared types and utilities for DCS CMS framework packages",
5
5
  "type": "module",
6
6
  "exports": {