@discoverworthy/assets 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 ADDED
@@ -0,0 +1,210 @@
1
+ # @discoverworthy/assets
2
+
3
+ Framework-agnostic image references for DiscoverWorthy Managed Assets. Rotate, optimize, and manage your images from the dashboard — on any framework, without a rebuild.
4
+
5
+ ## The Three-Tier Model
6
+
7
+ **Tier 0 — Bare URL** (works everywhere)
8
+ ```
9
+ https://app.discoverworthy.com/img/{siteId}/{imageKey}
10
+ ```
11
+ Paste this into an `<img src>`, CSS `url()`, email, RSS, or anywhere. Rotation and transforms happen at the resolver — no SDK needed.
12
+
13
+ **Tier 1 — `urlFor()` helper** (any JS/TS framework)
14
+ ```js
15
+ import { urlFor, configure } from '@discoverworthy/assets';
16
+ configure({ siteId: 'my-site' });
17
+ const src = urlFor('hero-key', { width: 640 });
18
+ ```
19
+ Use in Astro frontmatter, Svelte templates, Vue bindings, Next.js, or anywhere you generate URLs.
20
+
21
+ **Tier 2 — Framework components** (optional polish)
22
+ ```tsx
23
+ import { DwImage } from '@discoverworthy/assets/react';
24
+ <DwImage asset="hero-key" alt="Team" />
25
+ ```
26
+ Per-framework components (React/Next first; others as demand appears) add responsive srcSet, lazy loading, blur-up, and fallback handling.
27
+
28
+ **Rule:** rotation and transforms are a property of **Tier 0** — the resolver, not the SDK. So even if you install nothing and just paste a URL, your images rotate from the dashboard. Tiers 1 and 2 only add developer ergonomics.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install @discoverworthy/assets
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### React / Next.js
39
+
40
+ ```tsx
41
+ import { configure, DwImage } from '@discoverworthy/assets';
42
+
43
+ // Configure once at app startup
44
+ configure({ siteId: 'my-site-123' });
45
+
46
+ export default function Hero() {
47
+ return (
48
+ <DwImage
49
+ asset="hero-key"
50
+ alt="Team in the workshop"
51
+ width={1024}
52
+ sizes="(max-width: 768px) 100vw, 50vw"
53
+ />
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Astro
59
+
60
+ ```astro
61
+ ---
62
+ import { configure, urlFor } from '@discoverworthy/assets';
63
+
64
+ configure({ siteId: 'my-site-123' });
65
+
66
+ const heroSrc = urlFor('hero-key', { width: 1024 });
67
+ ---
68
+
69
+ <img src={heroSrc} alt="Team in the workshop" />
70
+ ```
71
+
72
+ ### Svelte
73
+
74
+ ```svelte
75
+ <script>
76
+ import { configure, urlFor } from '@discoverworthy/assets';
77
+
78
+ configure({ siteId: 'my-site-123' });
79
+
80
+ const heroSrc = urlFor('hero-key', { width: 1024 });
81
+ </script>
82
+
83
+ <img src={heroSrc} alt="Team in the workshop" />
84
+ ```
85
+
86
+ ### Vue
87
+
88
+ ```vue
89
+ <template>
90
+ <img :src="heroSrc" alt="Team in the workshop" />
91
+ </template>
92
+
93
+ <script setup>
94
+ import { configure, urlFor } from '@discoverworthy/assets';
95
+
96
+ configure({ siteId: 'my-site-123' });
97
+ const heroSrc = urlFor('hero-key', { width: 1024 });
98
+ </script>
99
+ ```
100
+
101
+ ### Plain HTML / WordPress
102
+
103
+ No install needed. Just use the Tier 0 URL:
104
+
105
+ ```html
106
+ <img src="https://app.discoverworthy.com/img/my-site-123/hero-key" alt="Team in the workshop" />
107
+ ```
108
+
109
+ Image rotation, variant sizing, and all transforms work the same way. They're properties of the resolver, not the SDK.
110
+
111
+ ## API
112
+
113
+ ### `configure(opts)`
114
+
115
+ Set the default site ID and/or host once at startup. All subsequent `urlFor()` calls use these defaults unless overridden.
116
+
117
+ ```js
118
+ configure({ siteId: 'my-site-123' });
119
+ // Now urlFor('image-key') works without passing siteId.
120
+
121
+ configure({ siteId: 'my-site', host: 'https://dev.discoverworthy.com' });
122
+ // Override the host (useful for dev/staging).
123
+ ```
124
+
125
+ **Options:**
126
+ - `siteId` (string) — The site ID for this deployment. Required (either here or on every `urlFor()` call).
127
+ - `host` (string, default: `https://app.discoverworthy.com`) — Base URL for the image resolver.
128
+
129
+ ### `urlFor(assetId, opts)`
130
+
131
+ Generate a URL for a managed DiscoverWorthy asset.
132
+
133
+ ```js
134
+ urlFor('hero-key'); // Base URL
135
+ urlFor('hero-key', { width: 640 }); // With width variant
136
+ urlFor('hero-key', { width: 1024, siteId: 'other-site' }); // Override siteId
137
+ ```
138
+
139
+ **Parameters:**
140
+ - `assetId` (string, required) — The image key from `dw_adopt_image` or the dashboard.
141
+ - `opts.width` (number) — Requested width. The resolver produces WebP variants at `320`, `640`, `1024`, `1600`. Other values are passed through; the resolver will ignore unknown widths.
142
+ - `opts.siteId` (string) — Override the configured site ID.
143
+ - `opts.host` (string) — Override the configured host.
144
+
145
+ **Returns:** A string URL like `https://app.discoverworthy.com/img/my-site/hero-key?w=640`.
146
+
147
+ ### `ALLOWED_WIDTHS`
148
+
149
+ Exported array of allowed image widths: `[320, 640, 1024, 1600]`. Use this to build responsive srcSet or media-query logic.
150
+
151
+ ```js
152
+ import { ALLOWED_WIDTHS, urlFor } from '@discoverworthy/assets';
153
+
154
+ const srcSet = ALLOWED_WIDTHS.map(w =>
155
+ `${urlFor('hero-key', { width: w })} ${w}w`
156
+ ).join(', ');
157
+ ```
158
+
159
+ ### `<DwImage>` (React/Next)
160
+
161
+ A pre-built component that handles responsive images, lazy loading, and fallbacks.
162
+
163
+ ```tsx
164
+ import { DwImage } from '@discoverworthy/assets/react';
165
+
166
+ <DwImage
167
+ asset="hero-key"
168
+ alt="Team in the workshop"
169
+ width={1024}
170
+ sizes="(max-width: 768px) 100vw, 50vw"
171
+ fallback="/images/placeholder.jpg"
172
+ lazy={true}
173
+ />
174
+ ```
175
+
176
+ **Props:**
177
+ - `asset` (string, required) — Image key.
178
+ - `alt` (string, required) — Alt text for accessibility.
179
+ - `width` (number) — Requested width for the `src` attribute. The component auto-builds a srcSet with all allowed widths.
180
+ - `sizes` (string) — CSS media queries for responsive sizing.
181
+ - `fallback` (string) — URL to use if the managed asset fails to load.
182
+ - `lazy` (boolean, default: `true`) — Use lazy loading.
183
+ - `siteId` (string) — Override the configured site ID.
184
+ - `host` (string) — Override the configured host.
185
+ - All standard `<img>` attributes (`className`, `style`, `onLoad`, etc.) are passed through.
186
+
187
+ ## Transforms & Rotation
188
+
189
+ The resolver (`/img/{siteId}/{imageKey}`) handles all image transforms:
190
+
191
+ - **Width variants** — Add `?w=320|640|1024|1600` to produce responsive WebP variants
192
+ - **Rotation** — Configure a rotation policy in the dashboard (round-robin, weighted, scheduled) and images automatically cycle on the schedule you set
193
+ - **Fallback** — If no rotation is configured, the resolver uses the original source URL
194
+
195
+ All transforms work at **Tier 0** — even if you paste a bare URL with no SDK, your images rotate and resize.
196
+
197
+ ## Why This Matters
198
+
199
+ Managed Assets closes the gap between copy-pipeline sites (where the dashboard controls every image) and owned sites (where you build in your own repo). You get:
200
+
201
+ - **Zero-rebuild image swaps** — Change hero images from the dashboard without deploying new code
202
+ - **Rotation & A/B testing** — Cycle seasonal variants, A/B test images, or weight favored variants
203
+ - **Framework-agnostic** — Works on Next, Astro, Svelte, Vue, Hugo, plain HTML, WordPress, or anything else
204
+ - **Optional adoption** — Use Tier 0 (bare URL) for any site; Tier 1/2 are just ergonomics
205
+
206
+ ## More Information
207
+
208
+ - [DiscoverWorthy docs](https://discoverworthy.com)
209
+ - GitHub: [discover-worthy/saas](https://github.com/discover-worthy/saas)
210
+ - License: MIT
package/index.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Options for configuring the global asset resolver.
3
+ */
4
+ export interface ConfigureOptions {
5
+ /**
6
+ * Site ID for this host/deployment. Used by urlFor() if not overridden.
7
+ */
8
+ siteId?: string;
9
+ /**
10
+ * Base URL for the image resolver (default: https://app.discoverworthy.com).
11
+ * Usually not needed unless pointing at a different environment.
12
+ */
13
+ host?: string;
14
+ }
15
+
16
+ /**
17
+ * Options for building an asset URL.
18
+ */
19
+ export interface UrlForOptions {
20
+ /**
21
+ * Requested width (320, 640, 1024, or 1600).
22
+ * The resolver produces a WebP variant at this width.
23
+ * See ALLOWED_WIDTHS for accepted values.
24
+ */
25
+ width?: number;
26
+ /**
27
+ * Override the configured siteId for this call.
28
+ */
29
+ siteId?: string;
30
+ /**
31
+ * Override the configured host for this call.
32
+ */
33
+ host?: string;
34
+ }
35
+
36
+ /**
37
+ * Configure the default site ID and/or host for urlFor() calls.
38
+ * Call this once at startup to avoid passing siteId on every invocation.
39
+ *
40
+ * @example
41
+ * configure({ siteId: 'my-site-123' });
42
+ * // Then urlFor('image-key') works without passing siteId.
43
+ *
44
+ * @param opts - Configuration options
45
+ */
46
+ export function configure(opts?: ConfigureOptions): void;
47
+
48
+ /**
49
+ * Allowed image widths supported by the resolver.
50
+ * Use these to build srcset attributes or responsive image logic.
51
+ * The resolver produces a WebP variant for each width.
52
+ */
53
+ export const ALLOWED_WIDTHS: number[];
54
+
55
+ /**
56
+ * Generate a URL for a managed DiscoverWorthy asset.
57
+ *
58
+ * The URL points to the resolver at /img/{siteId}/{assetId}, which:
59
+ * - Rotates images based on policy (if configured on the dashboard)
60
+ * - Produces WebP variants at ?w=320|640|1024|1600
61
+ * - Falls back to replacement_url or source_url if no rotation is set
62
+ *
63
+ * Call configure({ siteId }) once at startup, or pass siteId in opts to override.
64
+ *
65
+ * @example
66
+ * // After configure({ siteId: 'my-site' }):
67
+ * urlFor('hero-key') // → https://app.discoverworthy.com/img/my-site/hero-key
68
+ * urlFor('hero-key', { width: 640 }) // → https://app.discoverworthy.com/img/my-site/hero-key?w=640
69
+ *
70
+ * @example
71
+ * // Or pass siteId directly:
72
+ * urlFor('hero-key', { siteId: 'other-site', width: 1024 })
73
+ *
74
+ * @param assetId - The image key returned by dw_adopt_image or manually assigned
75
+ * @param opts - Options
76
+ * @returns The resolver URL
77
+ * @throws If assetId is missing or siteId is not configured
78
+ */
79
+ export function urlFor(assetId: string, opts?: UrlForOptions): string;
package/index.js ADDED
@@ -0,0 +1,71 @@
1
+ const DEFAULT_HOST = 'https://app.discoverworthy.com';
2
+ let _config = { siteId: null, host: DEFAULT_HOST };
3
+
4
+ /**
5
+ * Configure the default site ID and/or host for urlFor() calls.
6
+ * Call this once at startup to avoid passing siteId on every invocation.
7
+ *
8
+ * @example
9
+ * configure({ siteId: 'my-site-123' });
10
+ * // Then urlFor('image-key') works without passing siteId.
11
+ *
12
+ * @param {Object} opts - Configuration options
13
+ * @param {string} [opts.siteId] - The site ID for this host/deployment
14
+ * @param {string} [opts.host] - Base URL for the image resolver (default: https://app.discoverworthy.com)
15
+ */
16
+ export function configure(opts = {}) {
17
+ if (opts.siteId !== undefined) _config.siteId = opts.siteId;
18
+ if (opts.host !== undefined) _config.host = opts.host || DEFAULT_HOST;
19
+ }
20
+
21
+ /**
22
+ * Allowed image widths supported by the resolver.
23
+ * Use these to build srcset attributes or responsive image logic.
24
+ * The resolver produces a WebP variant for each width.
25
+ *
26
+ * @type {number[]}
27
+ */
28
+ export const ALLOWED_WIDTHS = [320, 640, 1024, 1600];
29
+
30
+ /**
31
+ * Generate a URL for a managed DiscoverWorthy asset.
32
+ *
33
+ * The URL points to the resolver at /img/{siteId}/{assetId}, which:
34
+ * - Rotates images based on policy (if configured on the dashboard)
35
+ * - Produces WebP variants at ?w=320|640|1024|1600
36
+ * - Falls back to replacement_url or source_url if no rotation is set
37
+ *
38
+ * Call configure({ siteId }) once at startup, or pass siteId in opts to override.
39
+ *
40
+ * @example
41
+ * // After configure({ siteId: 'my-site' }):
42
+ * urlFor('hero-key') // → https://app.discoverworthy.com/img/my-site/hero-key
43
+ * urlFor('hero-key', { width: 640 }) // → https://app.discoverworthy.com/img/my-site/hero-key?w=640
44
+ *
45
+ * @example
46
+ * // Or pass siteId directly:
47
+ * urlFor('hero-key', { siteId: 'other-site', width: 1024 })
48
+ *
49
+ * @param {string} assetId - The image key returned by dw_adopt_image or manually assigned
50
+ * @param {Object} [opts] - Options
51
+ * @param {number} [opts.width] - Requested width (320, 640, 1024, or 1600) — resolver produces WebP
52
+ * @param {string} [opts.siteId] - Override the configured siteId for this call
53
+ * @param {string} [opts.host] - Override the configured host for this call
54
+ * @returns {string} The resolver URL
55
+ * @throws {Error} If assetId is missing or siteId is not configured
56
+ */
57
+ export function urlFor(assetId, opts = {}) {
58
+ if (!assetId || typeof assetId !== 'string') {
59
+ throw new Error('urlFor: assetId (the image key) is required');
60
+ }
61
+ const siteId = opts.siteId || _config.siteId;
62
+ if (!siteId) {
63
+ throw new Error('urlFor: no siteId — call configure({ siteId }) once at startup, or pass { siteId } here');
64
+ }
65
+ const host = (opts.host || _config.host || DEFAULT_HOST).replace(/\/+$/, '');
66
+ let url = `${host}/img/${encodeURIComponent(siteId)}/${encodeURIComponent(assetId)}`;
67
+ if (opts.width != null) {
68
+ url += `?w=${encodeURIComponent(String(opts.width))}`;
69
+ }
70
+ return url;
71
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@discoverworthy/assets",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic image references for DiscoverWorthy Managed Assets — urlFor() plus optional per-framework components.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./index.d.ts",
9
+ "default": "./index.js"
10
+ },
11
+ "./react": {
12
+ "types": "./react.d.ts",
13
+ "default": "./react.js"
14
+ },
15
+ "./server": {
16
+ "types": "./server.d.ts",
17
+ "default": "./server.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "index.d.ts",
23
+ "react.js",
24
+ "react.d.ts",
25
+ "server.js",
26
+ "server.d.ts",
27
+ "README.md"
28
+ ],
29
+ "keywords": [
30
+ "discoverworthy",
31
+ "assets",
32
+ "images",
33
+ "managed-assets",
34
+ "managed-copy",
35
+ "headless-cms",
36
+ "collections",
37
+ "image-optimization",
38
+ "next",
39
+ "react",
40
+ "astro",
41
+ "svelte",
42
+ "vue"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/discover-worthy/saas"
47
+ },
48
+ "license": "MIT",
49
+ "author": "DiscoverWorthy",
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "peerDependencies": {
54
+ "react": ">=17"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "react": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "sideEffects": false
62
+ }
package/react.d.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { FC, ImgHTMLAttributes, HTMLAttributes, ReactNode, ElementType } from 'react';
2
+
3
+ /**
4
+ * Props for the DwImage React component.
5
+ * Extends standard HTML img attributes but removes src/srcSet (managed by the component).
6
+ */
7
+ export interface DwImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet'> {
8
+ /**
9
+ * The image key from dw_adopt_image or manually assigned in the dashboard.
10
+ */
11
+ asset: string;
12
+
13
+ /**
14
+ * Alt text for accessibility. Required.
15
+ */
16
+ alt: string;
17
+
18
+ /**
19
+ * Requested width for the src attribute (320, 640, 1024, or 1600).
20
+ * The component automatically builds a srcSet with all allowed widths.
21
+ */
22
+ width?: number;
23
+
24
+ /**
25
+ * CSS media queries for responsive sizing.
26
+ * Passed directly to the <img> sizes attribute.
27
+ */
28
+ sizes?: string;
29
+
30
+ /**
31
+ * URL to use if the managed asset fails to load.
32
+ * The src and srcSet are replaced once on error (idempotent).
33
+ */
34
+ fallback?: string;
35
+
36
+ /**
37
+ * Whether to use lazy loading (default: true).
38
+ * Set to false for above-the-fold images.
39
+ */
40
+ lazy?: boolean;
41
+
42
+ /**
43
+ * Override the configured siteId for this component.
44
+ */
45
+ siteId?: string;
46
+
47
+ /**
48
+ * Override the configured host for this component.
49
+ */
50
+ host?: string;
51
+ }
52
+
53
+ /**
54
+ * A React component that renders an optimized <img> for a DiscoverWorthy managed asset.
55
+ *
56
+ * Automatically builds:
57
+ * - src: the resolver URL at the optionally specified width
58
+ * - srcSet: all allowed widths (320w, 640w, 1024w, 1600w) for responsive loading
59
+ * - loading: 'lazy' by default (can be overridden with the lazy prop or rest attributes)
60
+ * - decoding: 'async'
61
+ *
62
+ * Supports an optional fallback URL: if the image fails to load, the src switches
63
+ * to the fallback (once per render).
64
+ *
65
+ * @example
66
+ * // Configure once at app startup:
67
+ * import { configure } from '@discoverworthy/assets';
68
+ * configure({ siteId: 'my-site' });
69
+ *
70
+ * // Then use the component:
71
+ * <DwImage asset="hero-key" alt="Team in the workshop" />
72
+ *
73
+ * @example
74
+ * // With responsive widths and sizes:
75
+ * <DwImage
76
+ * asset="hero-key"
77
+ * alt="Team in the workshop"
78
+ * width={1024}
79
+ * sizes="(max-width: 768px) 100vw, 50vw"
80
+ * />
81
+ *
82
+ * @example
83
+ * // With a fallback for when the managed asset isn't ready yet:
84
+ * <DwImage
85
+ * asset="hero-key"
86
+ * alt="Team in the workshop"
87
+ * fallback="/images/hero.jpg"
88
+ * />
89
+ */
90
+ export const DwImage: FC<DwImageProps>;
91
+
92
+ /**
93
+ * Props for the DwText managed-copy component.
94
+ */
95
+ export interface DwTextProps extends HTMLAttributes<HTMLElement> {
96
+ /**
97
+ * The copy key from dw_adopt_copy. Rendered as the `data-dw-copy` attribute;
98
+ * the deploy-finalize pipeline overlays the managed value onto this element.
99
+ */
100
+ id: string;
101
+
102
+ /**
103
+ * The element/tag to render (e.g. 'h1', 'p', 'span', 'button'). Defaults to 'span'.
104
+ */
105
+ as?: ElementType;
106
+
107
+ /**
108
+ * The default text — kept inline as the SEO / no-JS fallback until a managed
109
+ * override exists.
110
+ */
111
+ children?: ReactNode;
112
+ }
113
+
114
+ /**
115
+ * A React component for a DiscoverWorthy managed-copy string — the text sibling
116
+ * of DwImage. Renders `<as data-dw-copy={id}>{children}</as>`; the managed value
117
+ * is injected server-side at deploy time, so the default stays in the markup.
118
+ *
119
+ * @example
120
+ * <DwText id="hero_headline" as="h1">Plumbing you can trust</DwText>
121
+ */
122
+ export const DwText: FC<DwTextProps>;
package/react.js ADDED
@@ -0,0 +1,108 @@
1
+ import { createElement } from 'react';
2
+ import { urlFor, ALLOWED_WIDTHS } from './index.js';
3
+
4
+ /**
5
+ * A React component that renders an optimized <img> for a DiscoverWorthy managed asset.
6
+ *
7
+ * Automatically builds:
8
+ * - src: the resolver URL at the optionally specified width
9
+ * - srcSet: all allowed widths (320w, 640w, 1024w, 1600w) for responsive loading
10
+ * - loading: 'lazy' by default (can be overridden with the lazy prop or rest attributes)
11
+ * - decoding: 'async'
12
+ *
13
+ * Supports an optional fallback URL: if the image fails to load, the src switches
14
+ * to the fallback (once per render).
15
+ *
16
+ * @param {Object} props - Component props
17
+ * @param {string} props.asset - The image key from dw_adopt_image
18
+ * @param {string} props.alt - Alt text for accessibility (required)
19
+ * @param {number} [props.width] - Requested width for the src attribute (320, 640, 1024, 1600)
20
+ * @param {string} [props.sizes] - CSS media queries for responsive sizing
21
+ * @param {string} [props.fallback] - URL to use if the image fails to load
22
+ * @param {boolean} [props.lazy=true] - Whether to use lazy loading (default: true)
23
+ * @param {string} [props.siteId] - Override the configured siteId
24
+ * @param {string} [props.host] - Override the configured host
25
+ * @param {...any} rest - Other <img> attributes (className, style, onLoad, etc.)
26
+ * @returns {JSX.Element} An <img> element
27
+ *
28
+ * @example
29
+ * // Configure once at app startup:
30
+ * configure({ siteId: 'my-site' });
31
+ *
32
+ * // Then use the component:
33
+ * <DwImage asset="hero-key" alt="Team in the workshop" />
34
+ *
35
+ * @example
36
+ * // With responsive widths and sizes:
37
+ * <DwImage
38
+ * asset="hero-key"
39
+ * alt="Team in the workshop"
40
+ * width={1024}
41
+ * sizes="(max-width: 768px) 100vw, 50vw"
42
+ * />
43
+ *
44
+ * @example
45
+ * // With a fallback for when the managed asset isn't ready yet:
46
+ * <DwImage
47
+ * asset="hero-key"
48
+ * alt="Team in the workshop"
49
+ * fallback="/images/hero.jpg"
50
+ * />
51
+ */
52
+ export function DwImage({
53
+ asset,
54
+ alt,
55
+ width,
56
+ sizes,
57
+ fallback,
58
+ lazy = true,
59
+ siteId,
60
+ host,
61
+ ...rest
62
+ }) {
63
+ const src = urlFor(asset, { width, siteId, host });
64
+
65
+ const srcSet = ALLOWED_WIDTHS.map((w) => {
66
+ const url = urlFor(asset, { width: w, siteId, host });
67
+ return `${url} ${w}w`;
68
+ }).join(', ');
69
+
70
+ const handleError = (event) => {
71
+ if (fallback && event.currentTarget.src !== fallback) {
72
+ event.currentTarget.src = fallback;
73
+ event.currentTarget.srcSet = '';
74
+ }
75
+ };
76
+
77
+ return createElement('img', {
78
+ src,
79
+ srcSet,
80
+ alt,
81
+ sizes,
82
+ loading: lazy ? 'lazy' : 'eager',
83
+ decoding: 'async',
84
+ onError: fallback ? handleError : undefined,
85
+ ...rest,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * A React component for a DiscoverWorthy managed-copy string (the text sibling
91
+ * of DwImage). Renders an element carrying `data-dw-copy="<id>"` with the given
92
+ * children as the default/fallback text. The deploy-finalize pipeline overlays
93
+ * the managed value onto this element server-side, so the default stays in the
94
+ * markup for SEO and no-JS — there's no client-side fetch.
95
+ *
96
+ * @param {Object} props
97
+ * @param {string} props.id - The copy key from dw_adopt_copy (goes in data-dw-copy)
98
+ * @param {string} [props.as='span'] - The element/tag to render (e.g. 'h1', 'p', 'button')
99
+ * @param {React.ReactNode} props.children - The default text, kept inline as the fallback
100
+ * @param {...any} rest - Other attributes (className, style, etc.)
101
+ * @returns {JSX.Element}
102
+ *
103
+ * @example
104
+ * <DwText id="hero_headline" as="h1">Plumbing you can trust</DwText>
105
+ */
106
+ export function DwText({ id, as = 'span', children, ...rest }) {
107
+ return createElement(as, { 'data-dw-copy': id, ...rest }, children);
108
+ }
package/server.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Server-side helpers for DiscoverWorthy Managed Collections.
3
+ * Build-time only — uses the workspace token (a secret). Never import in client code.
4
+ */
5
+
6
+ export interface CollectionFetchOptions {
7
+ /** Workspace token. Defaults to process.env.DISCOVERWORTHY_DEPLOY_TOKEN. */
8
+ token?: string;
9
+ /** Override the API host. Defaults to https://app.discoverworthy.com. */
10
+ host?: string;
11
+ }
12
+
13
+ export interface CollectionField {
14
+ name: string;
15
+ type: 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'url' | 'image';
16
+ label: string;
17
+ required?: boolean;
18
+ }
19
+
20
+ export interface CollectionItem {
21
+ _id: string;
22
+ _order: number;
23
+ [field: string]: unknown;
24
+ }
25
+
26
+ export interface FetchCollectionResult {
27
+ collection: { slug: string; label: string; fields: CollectionField[] };
28
+ items: CollectionItem[];
29
+ }
30
+
31
+ export interface ListCollectionsResult {
32
+ collections: Array<{
33
+ slug: string;
34
+ label: string;
35
+ description: string | null;
36
+ fields: CollectionField[];
37
+ }>;
38
+ }
39
+
40
+ /**
41
+ * Fetch a collection's published items at build time.
42
+ * @param slug The collection slug (from dw_define_collection).
43
+ */
44
+ export function fetchCollection(slug: string, opts?: CollectionFetchOptions): Promise<FetchCollectionResult>;
45
+
46
+ /** List every collection (slug + label + field schema) for the workspace. */
47
+ export function listCollections(opts?: CollectionFetchOptions): Promise<ListCollectionsResult>;
package/server.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Server-side helpers for DiscoverWorthy Managed Collections (the headless-CMS
3
+ * content API). Build-time only — these use the workspace token, which is a
4
+ * SECRET. Never import this from client/browser code.
5
+ */
6
+
7
+ const DEFAULT_HOST = 'https://app.discoverworthy.com';
8
+
9
+ function resolveToken(opts) {
10
+ if (opts && opts.token) return opts.token;
11
+ if (typeof process !== 'undefined' && process.env && process.env.DISCOVERWORTHY_DEPLOY_TOKEN) {
12
+ return process.env.DISCOVERWORTHY_DEPLOY_TOKEN;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ function resolveHost(opts) {
18
+ return ((opts && opts.host) || DEFAULT_HOST).replace(/\/+$/, '');
19
+ }
20
+
21
+ /**
22
+ * Fetch a collection's published items at build time.
23
+ * @param {string} slug - The collection slug (from dw_define_collection).
24
+ * @param {{ token?: string, host?: string }} [opts] - token defaults to
25
+ * process.env.DISCOVERWORTHY_DEPLOY_TOKEN.
26
+ * @returns {Promise<{ collection: { slug: string, label: string, fields: any[] }, items: Array<Record<string, any>> }>}
27
+ */
28
+ export async function fetchCollection(slug, opts = {}) {
29
+ if (!slug || typeof slug !== 'string') {
30
+ throw new Error('fetchCollection: slug is required');
31
+ }
32
+ const token = resolveToken(opts);
33
+ if (!token) {
34
+ throw new Error('fetchCollection: no workspace token — pass { token } or set DISCOVERWORTHY_DEPLOY_TOKEN (server-side only)');
35
+ }
36
+ const host = resolveHost(opts);
37
+ const url = `${host}/api/v1/hosting/content/collections/${encodeURIComponent(slug)}`;
38
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
39
+ if (!res.ok) {
40
+ throw new Error(`fetchCollection(${slug}) failed: ${res.status}`);
41
+ }
42
+ return res.json();
43
+ }
44
+
45
+ /**
46
+ * List every collection (slug + label + field schema) for the workspace.
47
+ * @param {{ token?: string, host?: string }} [opts]
48
+ * @returns {Promise<{ collections: Array<{ slug: string, label: string, description: string | null, fields: any[] }> }>}
49
+ */
50
+ export async function listCollections(opts = {}) {
51
+ const token = resolveToken(opts);
52
+ if (!token) {
53
+ throw new Error('listCollections: no workspace token — pass { token } or set DISCOVERWORTHY_DEPLOY_TOKEN (server-side only)');
54
+ }
55
+ const host = resolveHost(opts);
56
+ const res = await fetch(`${host}/api/v1/hosting/content/collections`, {
57
+ headers: { Authorization: `Bearer ${token}` },
58
+ });
59
+ if (!res.ok) {
60
+ throw new Error(`listCollections failed: ${res.status}`);
61
+ }
62
+ return res.json();
63
+ }