@duffcloudservices/cms-core 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.
- package/README.md +113 -0
- package/dist/browser.d.ts +356 -0
- package/dist/browser.js +214 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @duffcloudservices/cms-core
|
|
2
|
+
|
|
3
|
+
Shared types and utilities for DCS CMS framework packages.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides the foundation that all framework-specific CMS packages depend on:
|
|
8
|
+
|
|
9
|
+
- **Types**: TypeScript definitions for content.yaml, seo.yaml, and pages.yaml
|
|
10
|
+
- **Content utilities**: Functions to resolve text keys across global and page-specific content
|
|
11
|
+
- **SEO utilities**: Functions to resolve and merge SEO configuration
|
|
12
|
+
- **YAML loaders**: Functions to load and parse DCS configuration files
|
|
13
|
+
- **Runtime fetching**: Functions to fetch content from the DCS API (premium tier)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @duffcloudservices/cms-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Content Resolution
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { resolveTextKey, getPageContent } from '@duffcloudservices/cms-core'
|
|
27
|
+
import type { ContentConfiguration } from '@duffcloudservices/cms-core'
|
|
28
|
+
|
|
29
|
+
const content: ContentConfiguration = {
|
|
30
|
+
version: 1,
|
|
31
|
+
global: {
|
|
32
|
+
'nav.home': 'Home',
|
|
33
|
+
'footer.copyright': '© 2026 Company',
|
|
34
|
+
},
|
|
35
|
+
pages: {
|
|
36
|
+
home: {
|
|
37
|
+
'hero.title': 'Welcome',
|
|
38
|
+
'hero.subtitle': 'Build amazing things',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve a single key
|
|
44
|
+
const title = resolveTextKey(content, 'home', 'hero.title') // 'Welcome'
|
|
45
|
+
const nav = resolveTextKey(content, 'home', 'nav.home') // 'Home' (from global)
|
|
46
|
+
|
|
47
|
+
// Get all content for a page (merged)
|
|
48
|
+
const homeContent = getPageContent(content, 'home')
|
|
49
|
+
// { 'nav.home': 'Home', 'footer.copyright': '...', 'hero.title': '...', ... }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### SEO Resolution
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { resolveSeoForPage, buildMetaTags } from '@duffcloudservices/cms-core'
|
|
56
|
+
import type { SeoConfiguration } from '@duffcloudservices/cms-core'
|
|
57
|
+
|
|
58
|
+
const seo: SeoConfiguration = {
|
|
59
|
+
version: 1,
|
|
60
|
+
global: {
|
|
61
|
+
siteName: 'My Site',
|
|
62
|
+
defaultTitle: 'My Site',
|
|
63
|
+
titleTemplate: '%s | My Site',
|
|
64
|
+
},
|
|
65
|
+
pages: {
|
|
66
|
+
home: {
|
|
67
|
+
title: 'Welcome',
|
|
68
|
+
description: 'The homepage of My Site',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve SEO for a page
|
|
74
|
+
const resolved = resolveSeoForPage(seo, 'home')
|
|
75
|
+
// { title: 'Welcome | My Site', description: '...', ... }
|
|
76
|
+
|
|
77
|
+
// Build meta tags array
|
|
78
|
+
const tags = buildMetaTags(resolved)
|
|
79
|
+
// [{ name: 'description', content: '...' }, { property: 'og:title', ... }]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### YAML Loading (Build-time)
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { loadContentYaml, loadSeoYaml } from '@duffcloudservices/cms-core'
|
|
86
|
+
|
|
87
|
+
const content = await loadContentYaml('.dcs/content.yaml')
|
|
88
|
+
const seo = await loadSeoYaml('.dcs/seo.yaml')
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Runtime Fetching (Premium)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { fetchRuntimeContent } from '@duffcloudservices/cms-core'
|
|
95
|
+
|
|
96
|
+
// Premium tier only - returns null for non-premium sites
|
|
97
|
+
const content = await fetchRuntimeContent('my-site-slug', {
|
|
98
|
+
apiBaseUrl: 'https://api.duffcloudservices.com',
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Framework Packages
|
|
103
|
+
|
|
104
|
+
This core package is used by:
|
|
105
|
+
|
|
106
|
+
- `@duffcloudservices/cms-vue` - Vue 3 composables + Vite plugins
|
|
107
|
+
- `@duffcloudservices/cms-react` - React hooks + Vite plugins
|
|
108
|
+
- `@duffcloudservices/cms-angular` - Angular services + build scripts
|
|
109
|
+
- `@duffcloudservices/cms-astro` - Astro integration
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
/** Yandex Webmaster verification */
|
|
213
|
+
yandex?: 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 };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/content.ts
|
|
2
|
+
function resolveTextKey(content, page, key) {
|
|
3
|
+
const pageContent = content.pages?.[page];
|
|
4
|
+
if (pageContent && key in pageContent) {
|
|
5
|
+
return pageContent[key];
|
|
6
|
+
}
|
|
7
|
+
if (content.global && key in content.global) {
|
|
8
|
+
return content.global[key];
|
|
9
|
+
}
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
function getPageContent(content, page) {
|
|
13
|
+
const global = content.global ?? {};
|
|
14
|
+
const pageContent = content.pages?.[page] ?? {};
|
|
15
|
+
return { ...global, ...pageContent };
|
|
16
|
+
}
|
|
17
|
+
function getGlobalContent(content) {
|
|
18
|
+
return content.global ?? {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/seo.ts
|
|
22
|
+
function resolveSeoForPage(seo, page) {
|
|
23
|
+
const global = seo.global ?? {};
|
|
24
|
+
const pageSeo = seo.pages?.[page] ?? {};
|
|
25
|
+
let title = pageSeo.title ?? global.defaultTitle ?? "";
|
|
26
|
+
if (title && global.titleTemplate && !pageSeo.noTitleTemplate) {
|
|
27
|
+
title = global.titleTemplate.replace("%s", title);
|
|
28
|
+
}
|
|
29
|
+
const description = pageSeo.description ?? global.defaultDescription ?? "";
|
|
30
|
+
const ogImage = pageSeo.openGraph?.image ?? global.images?.ogDefault ?? void 0;
|
|
31
|
+
const openGraph = resolveOpenGraph(pageSeo.openGraph, ogImage, title, description);
|
|
32
|
+
const twitterImage = pageSeo.twitter?.image ?? global.images?.twitterDefault ?? ogImage;
|
|
33
|
+
const twitter = resolveTwitterCard(
|
|
34
|
+
pageSeo.twitter,
|
|
35
|
+
twitterImage,
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
global.social?.twitter
|
|
39
|
+
);
|
|
40
|
+
return {
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
image: ogImage,
|
|
44
|
+
siteName: global.siteName,
|
|
45
|
+
siteUrl: global.siteUrl,
|
|
46
|
+
locale: global.locale,
|
|
47
|
+
canonical: pageSeo.canonical,
|
|
48
|
+
robots: pageSeo.robots ?? global.robots,
|
|
49
|
+
noIndex: pageSeo.robots?.includes("noindex"),
|
|
50
|
+
openGraph,
|
|
51
|
+
twitter,
|
|
52
|
+
schemas: pageSeo.schemas ?? global.schemas,
|
|
53
|
+
alternates: pageSeo.alternates
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function resolveOpenGraph(pageOg, ogImage, title, description) {
|
|
57
|
+
if (pageOg) {
|
|
58
|
+
return {
|
|
59
|
+
...pageOg,
|
|
60
|
+
title: pageOg.title ?? title,
|
|
61
|
+
description: pageOg.description ?? description,
|
|
62
|
+
image: ogImage
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (ogImage) {
|
|
66
|
+
return {
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
image: ogImage,
|
|
70
|
+
type: "website"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function resolveTwitterCard(pageTwitter, twitterImage, title, description, globalTwitterHandle) {
|
|
76
|
+
if (pageTwitter) {
|
|
77
|
+
return {
|
|
78
|
+
card: pageTwitter.card ?? "summary_large_image",
|
|
79
|
+
site: pageTwitter.site ?? globalTwitterHandle,
|
|
80
|
+
...pageTwitter,
|
|
81
|
+
title: pageTwitter.title ?? title,
|
|
82
|
+
description: pageTwitter.description ?? description,
|
|
83
|
+
image: twitterImage
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (globalTwitterHandle || twitterImage) {
|
|
87
|
+
return {
|
|
88
|
+
card: "summary_large_image",
|
|
89
|
+
site: globalTwitterHandle,
|
|
90
|
+
title,
|
|
91
|
+
description,
|
|
92
|
+
image: twitterImage
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
function addTag(tags, content, attr) {
|
|
98
|
+
if (content) {
|
|
99
|
+
tags.push({ ...attr, content });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function buildOpenGraphTags(tags, seo) {
|
|
103
|
+
const og = seo.openGraph;
|
|
104
|
+
if (!og) return;
|
|
105
|
+
addTag(tags, og.title, { property: "og:title" });
|
|
106
|
+
addTag(tags, og.description, { property: "og:description" });
|
|
107
|
+
addTag(tags, og.image, { property: "og:image" });
|
|
108
|
+
addTag(tags, og.imageAlt, { property: "og:image:alt" });
|
|
109
|
+
addTag(tags, og.imageWidth?.toString(), { property: "og:image:width" });
|
|
110
|
+
addTag(tags, og.imageHeight?.toString(), { property: "og:image:height" });
|
|
111
|
+
addTag(tags, og.type, { property: "og:type" });
|
|
112
|
+
addTag(tags, og.url, { property: "og:url" });
|
|
113
|
+
addTag(tags, seo.siteName, { property: "og:site_name" });
|
|
114
|
+
addTag(tags, seo.locale, { property: "og:locale" });
|
|
115
|
+
}
|
|
116
|
+
function buildTwitterTags(tags, seo) {
|
|
117
|
+
const tw = seo.twitter;
|
|
118
|
+
if (!tw) return;
|
|
119
|
+
addTag(tags, tw.card, { name: "twitter:card" });
|
|
120
|
+
addTag(tags, tw.site ? `@${tw.site}` : void 0, { name: "twitter:site" });
|
|
121
|
+
addTag(tags, tw.creator ? `@${tw.creator}` : void 0, { name: "twitter:creator" });
|
|
122
|
+
addTag(tags, tw.title, { name: "twitter:title" });
|
|
123
|
+
addTag(tags, tw.description, { name: "twitter:description" });
|
|
124
|
+
addTag(tags, tw.image, { name: "twitter:image" });
|
|
125
|
+
addTag(tags, tw.imageAlt, { name: "twitter:image:alt" });
|
|
126
|
+
}
|
|
127
|
+
function buildMetaTags(seo) {
|
|
128
|
+
const tags = [];
|
|
129
|
+
addTag(tags, seo.description, { name: "description" });
|
|
130
|
+
addTag(tags, seo.robots, { name: "robots" });
|
|
131
|
+
buildOpenGraphTags(tags, seo);
|
|
132
|
+
buildTwitterTags(tags, seo);
|
|
133
|
+
return tags;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/fetch.ts
|
|
137
|
+
var DEFAULT_API_URL = "https://api.duffcloudservices.com";
|
|
138
|
+
var DEFAULT_TIMEOUT = 5e3;
|
|
139
|
+
async function fetchRuntimeContent(siteSlug, options = {}) {
|
|
140
|
+
const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options;
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(
|
|
145
|
+
`${apiBaseUrl}/portal/sites/${siteSlug}/content/runtime`,
|
|
146
|
+
{
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
...options.headers
|
|
151
|
+
},
|
|
152
|
+
signal: controller.signal
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
if (response.status === 403) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"[DCS] Runtime content requires premium tier. Using build-time content."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return await response.json();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
166
|
+
console.warn("[DCS] Runtime content fetch timed out");
|
|
167
|
+
} else {
|
|
168
|
+
console.warn("[DCS] Runtime content fetch failed:", error);
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function fetchRuntimeSeo(siteSlug, options = {}) {
|
|
176
|
+
const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options;
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(
|
|
181
|
+
`${apiBaseUrl}/portal/sites/${siteSlug}/seo/runtime`,
|
|
182
|
+
{
|
|
183
|
+
method: "GET",
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
...options.headers
|
|
187
|
+
},
|
|
188
|
+
signal: controller.signal
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
if (response.status === 403) {
|
|
193
|
+
console.warn(
|
|
194
|
+
"[DCS] Runtime SEO requires premium tier. Using build-time SEO."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return await response.json();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
202
|
+
console.warn("[DCS] Runtime SEO fetch timed out");
|
|
203
|
+
} else {
|
|
204
|
+
console.warn("[DCS] Runtime SEO fetch failed:", error);
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
} finally {
|
|
208
|
+
clearTimeout(timeoutId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, resolveSeoForPage, resolveTextKey };
|
|
213
|
+
//# sourceMappingURL=browser.js.map
|
|
214
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/content.ts","../src/seo.ts","../src/fetch.ts"],"names":[],"mappings":"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","file":"browser.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 * 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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* YAML file loaders for DCS configuration files
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load and parse .dcs/content.yaml
|
|
10
|
+
*
|
|
11
|
+
* @param filePath - Path to content.yaml (absolute or relative to cwd)
|
|
12
|
+
* @returns Parsed content configuration
|
|
13
|
+
* @throws Error if file not found or parse fails
|
|
14
|
+
*/
|
|
15
|
+
declare function loadContentYaml(filePath: string): Promise<ContentConfiguration>;
|
|
16
|
+
/**
|
|
17
|
+
* Load and parse .dcs/seo.yaml
|
|
18
|
+
*
|
|
19
|
+
* @param filePath - Path to seo.yaml (absolute or relative to cwd)
|
|
20
|
+
* @returns Parsed SEO configuration
|
|
21
|
+
* @throws Error if file not found or parse fails
|
|
22
|
+
*/
|
|
23
|
+
declare function loadSeoYaml(filePath: string): Promise<SeoConfiguration>;
|
|
24
|
+
/**
|
|
25
|
+
* Load and parse .dcs/pages.yaml
|
|
26
|
+
*
|
|
27
|
+
* @param filePath - Path to pages.yaml (absolute or relative to cwd)
|
|
28
|
+
* @returns Parsed pages configuration
|
|
29
|
+
* @throws Error if file not found or parse fails
|
|
30
|
+
*/
|
|
31
|
+
declare function loadPagesYaml(filePath: string): Promise<PagesConfiguration>;
|
|
32
|
+
|
|
33
|
+
export { ContentConfiguration, PagesConfiguration, SeoConfiguration, loadContentYaml, loadPagesYaml, loadSeoYaml };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
// src/content.ts
|
|
6
|
+
function resolveTextKey(content, page, key) {
|
|
7
|
+
const pageContent = content.pages?.[page];
|
|
8
|
+
if (pageContent && key in pageContent) {
|
|
9
|
+
return pageContent[key];
|
|
10
|
+
}
|
|
11
|
+
if (content.global && key in content.global) {
|
|
12
|
+
return content.global[key];
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
function getPageContent(content, page) {
|
|
17
|
+
const global = content.global ?? {};
|
|
18
|
+
const pageContent = content.pages?.[page] ?? {};
|
|
19
|
+
return { ...global, ...pageContent };
|
|
20
|
+
}
|
|
21
|
+
function getGlobalContent(content) {
|
|
22
|
+
return content.global ?? {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/seo.ts
|
|
26
|
+
function resolveSeoForPage(seo, page) {
|
|
27
|
+
const global = seo.global ?? {};
|
|
28
|
+
const pageSeo = seo.pages?.[page] ?? {};
|
|
29
|
+
let title = pageSeo.title ?? global.defaultTitle ?? "";
|
|
30
|
+
if (title && global.titleTemplate && !pageSeo.noTitleTemplate) {
|
|
31
|
+
title = global.titleTemplate.replace("%s", title);
|
|
32
|
+
}
|
|
33
|
+
const description = pageSeo.description ?? global.defaultDescription ?? "";
|
|
34
|
+
const ogImage = pageSeo.openGraph?.image ?? global.images?.ogDefault ?? void 0;
|
|
35
|
+
const openGraph = resolveOpenGraph(pageSeo.openGraph, ogImage, title, description);
|
|
36
|
+
const twitterImage = pageSeo.twitter?.image ?? global.images?.twitterDefault ?? ogImage;
|
|
37
|
+
const twitter = resolveTwitterCard(
|
|
38
|
+
pageSeo.twitter,
|
|
39
|
+
twitterImage,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
global.social?.twitter
|
|
43
|
+
);
|
|
44
|
+
return {
|
|
45
|
+
title,
|
|
46
|
+
description,
|
|
47
|
+
image: ogImage,
|
|
48
|
+
siteName: global.siteName,
|
|
49
|
+
siteUrl: global.siteUrl,
|
|
50
|
+
locale: global.locale,
|
|
51
|
+
canonical: pageSeo.canonical,
|
|
52
|
+
robots: pageSeo.robots ?? global.robots,
|
|
53
|
+
noIndex: pageSeo.robots?.includes("noindex"),
|
|
54
|
+
openGraph,
|
|
55
|
+
twitter,
|
|
56
|
+
schemas: pageSeo.schemas ?? global.schemas,
|
|
57
|
+
alternates: pageSeo.alternates
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function resolveOpenGraph(pageOg, ogImage, title, description) {
|
|
61
|
+
if (pageOg) {
|
|
62
|
+
return {
|
|
63
|
+
...pageOg,
|
|
64
|
+
title: pageOg.title ?? title,
|
|
65
|
+
description: pageOg.description ?? description,
|
|
66
|
+
image: ogImage
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (ogImage) {
|
|
70
|
+
return {
|
|
71
|
+
title,
|
|
72
|
+
description,
|
|
73
|
+
image: ogImage,
|
|
74
|
+
type: "website"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
function resolveTwitterCard(pageTwitter, twitterImage, title, description, globalTwitterHandle) {
|
|
80
|
+
if (pageTwitter) {
|
|
81
|
+
return {
|
|
82
|
+
card: pageTwitter.card ?? "summary_large_image",
|
|
83
|
+
site: pageTwitter.site ?? globalTwitterHandle,
|
|
84
|
+
...pageTwitter,
|
|
85
|
+
title: pageTwitter.title ?? title,
|
|
86
|
+
description: pageTwitter.description ?? description,
|
|
87
|
+
image: twitterImage
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (globalTwitterHandle || twitterImage) {
|
|
91
|
+
return {
|
|
92
|
+
card: "summary_large_image",
|
|
93
|
+
site: globalTwitterHandle,
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
image: twitterImage
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
function addTag(tags, content, attr) {
|
|
102
|
+
if (content) {
|
|
103
|
+
tags.push({ ...attr, content });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function buildOpenGraphTags(tags, seo) {
|
|
107
|
+
const og = seo.openGraph;
|
|
108
|
+
if (!og) return;
|
|
109
|
+
addTag(tags, og.title, { property: "og:title" });
|
|
110
|
+
addTag(tags, og.description, { property: "og:description" });
|
|
111
|
+
addTag(tags, og.image, { property: "og:image" });
|
|
112
|
+
addTag(tags, og.imageAlt, { property: "og:image:alt" });
|
|
113
|
+
addTag(tags, og.imageWidth?.toString(), { property: "og:image:width" });
|
|
114
|
+
addTag(tags, og.imageHeight?.toString(), { property: "og:image:height" });
|
|
115
|
+
addTag(tags, og.type, { property: "og:type" });
|
|
116
|
+
addTag(tags, og.url, { property: "og:url" });
|
|
117
|
+
addTag(tags, seo.siteName, { property: "og:site_name" });
|
|
118
|
+
addTag(tags, seo.locale, { property: "og:locale" });
|
|
119
|
+
}
|
|
120
|
+
function buildTwitterTags(tags, seo) {
|
|
121
|
+
const tw = seo.twitter;
|
|
122
|
+
if (!tw) return;
|
|
123
|
+
addTag(tags, tw.card, { name: "twitter:card" });
|
|
124
|
+
addTag(tags, tw.site ? `@${tw.site}` : void 0, { name: "twitter:site" });
|
|
125
|
+
addTag(tags, tw.creator ? `@${tw.creator}` : void 0, { name: "twitter:creator" });
|
|
126
|
+
addTag(tags, tw.title, { name: "twitter:title" });
|
|
127
|
+
addTag(tags, tw.description, { name: "twitter:description" });
|
|
128
|
+
addTag(tags, tw.image, { name: "twitter:image" });
|
|
129
|
+
addTag(tags, tw.imageAlt, { name: "twitter:image:alt" });
|
|
130
|
+
}
|
|
131
|
+
function buildMetaTags(seo) {
|
|
132
|
+
const tags = [];
|
|
133
|
+
addTag(tags, seo.description, { name: "description" });
|
|
134
|
+
addTag(tags, seo.robots, { name: "robots" });
|
|
135
|
+
buildOpenGraphTags(tags, seo);
|
|
136
|
+
buildTwitterTags(tags, seo);
|
|
137
|
+
return tags;
|
|
138
|
+
}
|
|
139
|
+
async function loadContentYaml(filePath) {
|
|
140
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
141
|
+
const fileContent = fs.readFileSync(absolutePath, "utf8");
|
|
142
|
+
const content = yaml.load(fileContent);
|
|
143
|
+
if (!content.version) {
|
|
144
|
+
content.version = 1;
|
|
145
|
+
}
|
|
146
|
+
if (!content.global) {
|
|
147
|
+
content.global = {};
|
|
148
|
+
}
|
|
149
|
+
if (!content.pages) {
|
|
150
|
+
content.pages = {};
|
|
151
|
+
}
|
|
152
|
+
return content;
|
|
153
|
+
}
|
|
154
|
+
async function loadSeoYaml(filePath) {
|
|
155
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
156
|
+
const fileContent = fs.readFileSync(absolutePath, "utf8");
|
|
157
|
+
const seo = yaml.load(fileContent);
|
|
158
|
+
if (!seo.version) {
|
|
159
|
+
seo.version = 1;
|
|
160
|
+
}
|
|
161
|
+
return seo;
|
|
162
|
+
}
|
|
163
|
+
async function loadPagesYaml(filePath) {
|
|
164
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
165
|
+
const fileContent = fs.readFileSync(absolutePath, "utf8");
|
|
166
|
+
const pages = yaml.load(fileContent);
|
|
167
|
+
if (!pages.version) {
|
|
168
|
+
pages.version = 3;
|
|
169
|
+
}
|
|
170
|
+
if (!pages.pages) {
|
|
171
|
+
pages.pages = [];
|
|
172
|
+
}
|
|
173
|
+
return pages;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/fetch.ts
|
|
177
|
+
var DEFAULT_API_URL = "https://api.duffcloudservices.com";
|
|
178
|
+
var DEFAULT_TIMEOUT = 5e3;
|
|
179
|
+
async function fetchRuntimeContent(siteSlug, options = {}) {
|
|
180
|
+
const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options;
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(
|
|
185
|
+
`${apiBaseUrl}/portal/sites/${siteSlug}/content/runtime`,
|
|
186
|
+
{
|
|
187
|
+
method: "GET",
|
|
188
|
+
headers: {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
...options.headers
|
|
191
|
+
},
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
if (response.status === 403) {
|
|
197
|
+
console.warn(
|
|
198
|
+
"[DCS] Runtime content requires premium tier. Using build-time content."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return await response.json();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
206
|
+
console.warn("[DCS] Runtime content fetch timed out");
|
|
207
|
+
} else {
|
|
208
|
+
console.warn("[DCS] Runtime content fetch failed:", error);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
} finally {
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function fetchRuntimeSeo(siteSlug, options = {}) {
|
|
216
|
+
const { apiBaseUrl = DEFAULT_API_URL, timeout = DEFAULT_TIMEOUT } = options;
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
219
|
+
try {
|
|
220
|
+
const response = await fetch(
|
|
221
|
+
`${apiBaseUrl}/portal/sites/${siteSlug}/seo/runtime`,
|
|
222
|
+
{
|
|
223
|
+
method: "GET",
|
|
224
|
+
headers: {
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
...options.headers
|
|
227
|
+
},
|
|
228
|
+
signal: controller.signal
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
if (response.status === 403) {
|
|
233
|
+
console.warn(
|
|
234
|
+
"[DCS] Runtime SEO requires premium tier. Using build-time SEO."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return await response.json();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
242
|
+
console.warn("[DCS] Runtime SEO fetch timed out");
|
|
243
|
+
} else {
|
|
244
|
+
console.warn("[DCS] Runtime SEO fetch failed:", error);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timeoutId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { buildMetaTags, fetchRuntimeContent, fetchRuntimeSeo, getGlobalContent, getPageContent, loadContentYaml, loadPagesYaml, loadSeoYaml, resolveSeoForPage, resolveTextKey };
|
|
253
|
+
//# sourceMappingURL=index.js.map
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/content.ts","../src/seo.ts","../src/loaders.ts","../src/fetch.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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duffcloudservices/cms-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared types and utilities for DCS CMS framework packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./browser": {
|
|
12
|
+
"types": "./dist/browser.d.ts",
|
|
13
|
+
"import": "./dist/browser.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "pnpm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"js-yaml": "^4.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/js-yaml": "^4.0.9",
|
|
34
|
+
"@types/node": "^20.11.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "~5.6.3",
|
|
37
|
+
"vitest": "^3.2.3"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"dcs",
|
|
41
|
+
"cms",
|
|
42
|
+
"content",
|
|
43
|
+
"seo",
|
|
44
|
+
"yaml"
|
|
45
|
+
],
|
|
46
|
+
"author": "Duff Cloud Services",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/duffn84/dcs-again",
|
|
51
|
+
"directory": "packages/cms-core"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|