@softwareinfocus/next-og-image 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Software InFocus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @softwareinfocus/next-og-image
2
+
3
+ Reusable Open Graph image helpers for Next.js App Router metadata image routes, with an optional Software InFocus preset template.
4
+
5
+ ## Features
6
+
7
+ - `next/og` image response wrapper with standard `1200x630` PNG defaults
8
+ - Helpers for static and dynamic `opengraph-image.tsx` routes
9
+ - Typed OG payload contract (`OgImageData`)
10
+ - SIF preset renderer + fallback data builders
11
+ - Works with Next.js 15+ App Router route metadata image files
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @softwareinfocus/next-og-image
17
+ ```
18
+
19
+ Peer dependencies:
20
+
21
+ - `next@15`
22
+ - `react@19`
23
+
24
+ ## Basic usage (static route)
25
+
26
+ ```tsx
27
+ import {
28
+ OG_IMAGE_CONTENT_TYPE_PNG,
29
+ OG_IMAGE_SIZE_1200x630,
30
+ createStaticOgImageHandler,
31
+ } from '@softwareinfocus/next-og-image'
32
+ import { createSifFallbacks, createSifOgRenderer } from '@softwareinfocus/next-og-image/presets/sif'
33
+
34
+ export const alt = 'Site social preview image'
35
+ export const size = OG_IMAGE_SIZE_1200x630
36
+ export const contentType = OG_IMAGE_CONTENT_TYPE_PNG
37
+ export const runtime = 'edge'
38
+ export const revalidate = 3600
39
+
40
+ const render = createSifOgRenderer({
41
+ brandName: 'Software InFocus',
42
+ domainLabel: 'softwareinfocus.com',
43
+ })
44
+
45
+ const fallbacks = createSifFallbacks({ brandName: 'Software InFocus' })
46
+
47
+ export default createStaticOgImageHandler({
48
+ fallbackData: fallbacks.global,
49
+ render,
50
+ })
51
+ ```
52
+
53
+ ## Basic usage (dynamic route)
54
+
55
+ ```tsx
56
+ import { createDynamicOgImageHandler } from '@softwareinfocus/next-og-image'
57
+ import { createSifFallbacks, createSifOgRenderer } from '@softwareinfocus/next-og-image/presets/sif'
58
+
59
+ const render = createSifOgRenderer()
60
+ const fallbacks = createSifFallbacks()
61
+
62
+ export default createDynamicOgImageHandler<{ slug: string }, 'blog'>({
63
+ resolveData: async ({ slug }) => {
64
+ const post = await getPostBySlug(slug)
65
+ if (!post) return null
66
+
67
+ return {
68
+ type: 'blog',
69
+ title: post.title,
70
+ subtitle: post.description,
71
+ chips: post.tags,
72
+ }
73
+ },
74
+ fallbackData: fallbacks.section.blog,
75
+ render,
76
+ })
77
+ ```
78
+
79
+ ## Notes
80
+
81
+ - Keep `export const runtime = 'edge'` in your route file for predictable `next/og` behavior.
82
+ - Keep `export const revalidate = 3600` (or your preferred cache interval) in each route.
83
+ - `twitter-image.tsx` can re-export from `./opengraph-image` in most projects.
84
+ - Fonts are optional. Provide `loadFonts` in `createSifOgRenderer()` if you want custom typography.
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pnpm install
90
+ pnpm build
91
+ pnpm test
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,12 @@
1
+ // src/core/constants.ts
2
+ var OG_IMAGE_SIZE_1200x630 = {
3
+ width: 1200,
4
+ height: 630
5
+ };
6
+ var OG_IMAGE_CONTENT_TYPE_PNG = "image/png";
7
+
8
+ export {
9
+ OG_IMAGE_SIZE_1200x630,
10
+ OG_IMAGE_CONTENT_TYPE_PNG
11
+ };
12
+ //# sourceMappingURL=chunk-ZZJXXDAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/constants.ts"],"sourcesContent":["export const OG_IMAGE_SIZE_1200x630 = {\n width: 1200,\n height: 630,\n} as const\n\nexport const OG_IMAGE_CONTENT_TYPE_PNG = 'image/png' as const\n"],"mappings":";AAAO,IAAM,yBAAyB;AAAA,EACpC,OAAO;AAAA,EACP,QAAQ;AACV;AAEO,IAAM,4BAA4B;","names":[]}
@@ -0,0 +1,22 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import { O as OgImageData, M as MetadataImageRouteProps } from './types-CaggCk6z.js';
3
+ export { a as OgFontDefinition } from './types-CaggCk6z.js';
4
+
5
+ declare const OG_IMAGE_SIZE_1200x630: {
6
+ readonly width: 1200;
7
+ readonly height: 630;
8
+ };
9
+ declare const OG_IMAGE_CONTENT_TYPE_PNG: "image/png";
10
+
11
+ declare function createStaticOgImageHandler<TType extends string>(options: {
12
+ resolveData?: () => Promise<OgImageData<TType> | null> | OgImageData<TType> | null;
13
+ fallbackData: OgImageData<TType> | (() => OgImageData<TType>);
14
+ render: (data: OgImageData<TType>) => Promise<ImageResponse> | ImageResponse;
15
+ }): () => Promise<ImageResponse>;
16
+ declare function createDynamicOgImageHandler<TParams extends Record<string, string>, TType extends string>(options: {
17
+ resolveData: (params: TParams) => Promise<OgImageData<TType> | null> | OgImageData<TType> | null;
18
+ fallbackData: OgImageData<TType> | ((params: TParams) => OgImageData<TType>);
19
+ render: (data: OgImageData<TType>) => Promise<ImageResponse> | ImageResponse;
20
+ }): (props: MetadataImageRouteProps<TParams>) => Promise<ImageResponse>;
21
+
22
+ export { MetadataImageRouteProps, OG_IMAGE_CONTENT_TYPE_PNG, OG_IMAGE_SIZE_1200x630, OgImageData, createDynamicOgImageHandler, createStaticOgImageHandler };
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ import {
2
+ OG_IMAGE_CONTENT_TYPE_PNG,
3
+ OG_IMAGE_SIZE_1200x630
4
+ } from "./chunk-ZZJXXDAB.js";
5
+
6
+ // src/core/route-handlers.ts
7
+ function resolveStaticFallback(fallbackData) {
8
+ return typeof fallbackData === "function" ? fallbackData() : fallbackData;
9
+ }
10
+ function resolveDynamicFallback(fallbackData, params) {
11
+ return typeof fallbackData === "function" ? fallbackData(params) : fallbackData;
12
+ }
13
+ function createStaticOgImageHandler(options) {
14
+ return async function StaticOgImageHandler() {
15
+ const resolved = options.resolveData ? await options.resolveData() : null;
16
+ return options.render(resolved ?? resolveStaticFallback(options.fallbackData));
17
+ };
18
+ }
19
+ function createDynamicOgImageHandler(options) {
20
+ return async function DynamicOgImageHandler(props) {
21
+ const params = await props.params;
22
+ const resolved = await options.resolveData(params);
23
+ return options.render(resolved ?? resolveDynamicFallback(options.fallbackData, params));
24
+ };
25
+ }
26
+ export {
27
+ OG_IMAGE_CONTENT_TYPE_PNG,
28
+ OG_IMAGE_SIZE_1200x630,
29
+ createDynamicOgImageHandler,
30
+ createStaticOgImageHandler
31
+ };
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/route-handlers.ts"],"sourcesContent":["import type { ImageResponse } from 'next/og'\nimport type { MetadataImageRouteProps, OgImageData } from './types'\n\nfunction resolveStaticFallback<TType extends string>(\n fallbackData: OgImageData<TType> | (() => OgImageData<TType>),\n): OgImageData<TType> {\n return typeof fallbackData === 'function'\n ? (fallbackData as () => OgImageData<TType>)()\n : fallbackData\n}\n\nfunction resolveDynamicFallback<TParams extends Record<string, string>, TType extends string>(\n fallbackData: OgImageData<TType> | ((params: TParams) => OgImageData<TType>),\n params: TParams,\n): OgImageData<TType> {\n return typeof fallbackData === 'function'\n ? (fallbackData as (params: TParams) => OgImageData<TType>)(params)\n : fallbackData\n}\n\nexport function createStaticOgImageHandler<TType extends string>(options: {\n resolveData?: () => Promise<OgImageData<TType> | null> | OgImageData<TType> | null\n fallbackData: OgImageData<TType> | (() => OgImageData<TType>)\n render: (data: OgImageData<TType>) => Promise<ImageResponse> | ImageResponse\n}) {\n return async function StaticOgImageHandler() {\n const resolved = options.resolveData ? await options.resolveData() : null\n return options.render(resolved ?? resolveStaticFallback(options.fallbackData))\n }\n}\n\nexport function createDynamicOgImageHandler<\n TParams extends Record<string, string>,\n TType extends string,\n>(options: {\n resolveData: (params: TParams) => Promise<OgImageData<TType> | null> | OgImageData<TType> | null\n fallbackData: OgImageData<TType> | ((params: TParams) => OgImageData<TType>)\n render: (data: OgImageData<TType>) => Promise<ImageResponse> | ImageResponse\n}) {\n return async function DynamicOgImageHandler(props: MetadataImageRouteProps<TParams>) {\n const params = await props.params\n const resolved = await options.resolveData(params)\n return options.render(resolved ?? resolveDynamicFallback(options.fallbackData, params))\n }\n}\n"],"mappings":";;;;;;AAGA,SAAS,sBACP,cACoB;AACpB,SAAO,OAAO,iBAAiB,aAC1B,aAA0C,IAC3C;AACN;AAEA,SAAS,uBACP,cACA,QACoB;AACpB,SAAO,OAAO,iBAAiB,aAC1B,aAAyD,MAAM,IAChE;AACN;AAEO,SAAS,2BAAiD,SAI9D;AACD,SAAO,eAAe,uBAAuB;AAC3C,UAAM,WAAW,QAAQ,cAAc,MAAM,QAAQ,YAAY,IAAI;AACrE,WAAO,QAAQ,OAAO,YAAY,sBAAsB,QAAQ,YAAY,CAAC;AAAA,EAC/E;AACF;AAEO,SAAS,4BAGd,SAIC;AACD,SAAO,eAAe,sBAAsB,OAAyC;AACnF,UAAM,SAAS,MAAM,MAAM;AAC3B,UAAM,WAAW,MAAM,QAAQ,YAAY,MAAM;AACjD,WAAO,QAAQ,OAAO,YAAY,uBAAuB,QAAQ,cAAc,MAAM,CAAC;AAAA,EACxF;AACF;","names":[]}
@@ -0,0 +1,31 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import { O as OgImageData, a as OgFontDefinition } from '../../types-CaggCk6z.js';
3
+
4
+ type SifOgType = 'brand' | 'blog' | 'service' | 'portfolio';
5
+ type SifOgData = OgImageData<SifOgType>;
6
+ type SifOgTheme = {
7
+ label: string;
8
+ accent: string;
9
+ accentSoft: string;
10
+ glow: string;
11
+ chipBorder: string;
12
+ chipBg: string;
13
+ };
14
+ type SifPresetConfig = {
15
+ brandName?: string;
16
+ domainLabel?: string;
17
+ themes?: Partial<Record<SifOgType, SifOgTheme>>;
18
+ loadFonts?: () => Promise<OgFontDefinition[]>;
19
+ };
20
+ type SifOgRenderer = (data: SifOgData) => Promise<ImageResponse>;
21
+
22
+ declare function createSifFallbacks(config?: {
23
+ brandName?: string;
24
+ }): {
25
+ global: SifOgData;
26
+ section: Record<SifOgType, SifOgData>;
27
+ };
28
+
29
+ declare function createSifOgRenderer(config?: SifPresetConfig): SifOgRenderer;
30
+
31
+ export { type SifOgData, type SifOgRenderer, type SifOgTheme, type SifOgType, type SifPresetConfig, createSifFallbacks, createSifOgRenderer };
@@ -0,0 +1,488 @@
1
+ import {
2
+ OG_IMAGE_SIZE_1200x630
3
+ } from "../../chunk-ZZJXXDAB.js";
4
+
5
+ // src/core/image-response.tsx
6
+ import { ImageResponse } from "next/og";
7
+ function createPngImageResponse(element, options = {}) {
8
+ const size = options.size ?? OG_IMAGE_SIZE_1200x630;
9
+ const fonts = options.fonts?.length ? options.fonts : void 0;
10
+ return new ImageResponse(element, {
11
+ width: size.width,
12
+ height: size.height,
13
+ ...fonts ? { fonts } : {}
14
+ });
15
+ }
16
+
17
+ // src/core/normalize.ts
18
+ function clampText(text, maxLength) {
19
+ const normalized = text.replace(/\s+/g, " ").trim();
20
+ if (normalized.length <= maxLength) return normalized;
21
+ const clipped = normalized.slice(0, Math.max(1, maxLength - 1));
22
+ const lastSpace = clipped.lastIndexOf(" ");
23
+ const safe = lastSpace > Math.floor(maxLength * 0.6) ? clipped.slice(0, lastSpace) : clipped;
24
+ return `${safe.trimEnd()}\u2026`;
25
+ }
26
+ function normalizeChips(chips, max = 3) {
27
+ if (!chips?.length) return [];
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const result = [];
30
+ for (const chip of chips) {
31
+ const normalized = chip.replace(/\s+/g, " ").trim();
32
+ if (!normalized) continue;
33
+ const key = normalized.toLowerCase();
34
+ if (seen.has(key)) continue;
35
+ seen.add(key);
36
+ result.push(clampText(normalized, 24));
37
+ if (result.length >= max) break;
38
+ }
39
+ return result;
40
+ }
41
+ function normalizeOgImageData(data, options = {}) {
42
+ const title = clampText(data.title, options.maxTitleLength ?? 140);
43
+ const brand = (data.brand ?? options.defaultBrand ?? "").trim();
44
+ const subtitleSeed = data.subtitle ?? options.defaultSubtitle ?? brand;
45
+ const subtitle = clampText(subtitleSeed || title, options.maxSubtitleLength ?? 160);
46
+ const chips = normalizeChips(data.chips, options.maxChips ?? 3);
47
+ return {
48
+ type: data.type,
49
+ title,
50
+ subtitle,
51
+ chips: chips.length > 0 ? chips : brand ? [brand] : [],
52
+ brand
53
+ };
54
+ }
55
+
56
+ // src/presets/sif/theme.ts
57
+ var SIF_BRAND_COLORS = {
58
+ primary: "#EE6C4D",
59
+ primaryBright: "#FF8A67",
60
+ ink: "#292A38",
61
+ slate: "#C7D3DD",
62
+ paper: "#E8EEF2",
63
+ night: "#121726"
64
+ };
65
+ var SIF_DEFAULT_THEMES = {
66
+ blog: {
67
+ label: "Blog",
68
+ accent: SIF_BRAND_COLORS.primary,
69
+ accentSoft: "rgba(238, 108, 77, 0.16)",
70
+ glow: "rgba(238, 108, 77, 0.28)",
71
+ chipBorder: "rgba(238, 108, 77, 0.28)",
72
+ chipBg: "rgba(238, 108, 77, 0.1)"
73
+ },
74
+ service: {
75
+ label: "Service",
76
+ accent: "#F27C62",
77
+ accentSoft: "rgba(242, 124, 98, 0.16)",
78
+ glow: "rgba(242, 124, 98, 0.27)",
79
+ chipBorder: "rgba(242, 124, 98, 0.26)",
80
+ chipBg: "rgba(242, 124, 98, 0.09)"
81
+ },
82
+ portfolio: {
83
+ label: "Portfolio",
84
+ accent: SIF_BRAND_COLORS.primaryBright,
85
+ accentSoft: "rgba(255, 138, 103, 0.16)",
86
+ glow: "rgba(255, 138, 103, 0.25)",
87
+ chipBorder: "rgba(255, 138, 103, 0.24)",
88
+ chipBg: "rgba(255, 138, 103, 0.08)"
89
+ },
90
+ brand: {
91
+ label: "Software InFocus",
92
+ accent: SIF_BRAND_COLORS.primary,
93
+ accentSoft: "rgba(238, 108, 77, 0.18)",
94
+ glow: "rgba(238, 108, 77, 0.28)",
95
+ chipBorder: "rgba(238, 108, 77, 0.28)",
96
+ chipBg: "rgba(238, 108, 77, 0.1)"
97
+ }
98
+ };
99
+ var SIF_LOGO_MARK_VIEWBOX = "0 0 256 128";
100
+ var SIF_LOGO_MARK_PATHS = [
101
+ "M64 24 L24 64 L64 104",
102
+ "M144 16 L112 112",
103
+ "M192 24 L232 64 L192 104"
104
+ ];
105
+
106
+ // src/presets/sif/template.tsx
107
+ import { jsx, jsxs } from "react/jsx-runtime";
108
+ function SifLogoMark({
109
+ width = 132,
110
+ height = 66,
111
+ color = "#EE6C4D",
112
+ opacity = 1
113
+ }) {
114
+ return /* @__PURE__ */ jsx(
115
+ "svg",
116
+ {
117
+ width,
118
+ height,
119
+ viewBox: SIF_LOGO_MARK_VIEWBOX,
120
+ fill: "none",
121
+ xmlns: "http://www.w3.org/2000/svg",
122
+ style: { opacity },
123
+ children: SIF_LOGO_MARK_PATHS.map((path) => /* @__PURE__ */ jsx(
124
+ "path",
125
+ {
126
+ d: path,
127
+ stroke: color,
128
+ strokeWidth: "16",
129
+ strokeLinecap: "square",
130
+ strokeLinejoin: "miter"
131
+ },
132
+ path
133
+ ))
134
+ }
135
+ );
136
+ }
137
+ function SifOgTemplate({ data, theme, domainLabel }) {
138
+ const chipsToRender = data.chips.slice(0, 3);
139
+ const titleFontSize = data.title.length < 36 ? 58 : data.title.length < 60 ? 50 : 42;
140
+ return /* @__PURE__ */ jsxs(
141
+ "div",
142
+ {
143
+ style: {
144
+ width: OG_IMAGE_SIZE_1200x630.width,
145
+ height: OG_IMAGE_SIZE_1200x630.height,
146
+ display: "flex",
147
+ position: "relative",
148
+ overflow: "hidden",
149
+ background: `linear-gradient(145deg, ${SIF_BRAND_COLORS.night} 0%, #0d1424 38%, #171c2b 68%, ${SIF_BRAND_COLORS.ink} 100%)`,
150
+ color: SIF_BRAND_COLORS.paper
151
+ },
152
+ children: [
153
+ /* @__PURE__ */ jsx(
154
+ "div",
155
+ {
156
+ style: {
157
+ position: "absolute",
158
+ inset: 0,
159
+ backgroundImage: "linear-gradient(rgba(199,211,221,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(199,211,221,0.025) 1px, transparent 1px)",
160
+ backgroundSize: "36px 36px",
161
+ opacity: 0.35
162
+ }
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsx(
166
+ "div",
167
+ {
168
+ style: {
169
+ position: "absolute",
170
+ top: -140,
171
+ right: -80,
172
+ width: 480,
173
+ height: 480,
174
+ borderRadius: 480,
175
+ background: `radial-gradient(circle, ${theme.glow} 0%, rgba(0,0,0,0) 70%)`
176
+ }
177
+ }
178
+ ),
179
+ /* @__PURE__ */ jsx(
180
+ "div",
181
+ {
182
+ style: {
183
+ position: "absolute",
184
+ bottom: -120,
185
+ left: -80,
186
+ width: 320,
187
+ height: 320,
188
+ borderRadius: 320,
189
+ background: "radial-gradient(circle, rgba(238,108,77,0.14) 0%, rgba(0,0,0,0) 72%)"
190
+ }
191
+ }
192
+ ),
193
+ /* @__PURE__ */ jsx(
194
+ "div",
195
+ {
196
+ style: {
197
+ position: "absolute",
198
+ right: -30,
199
+ top: 183,
200
+ display: "flex"
201
+ },
202
+ children: /* @__PURE__ */ jsx(SifLogoMark, { width: 528, height: 264, color: theme.accent, opacity: 0.055 })
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsxs(
206
+ "div",
207
+ {
208
+ style: {
209
+ position: "relative",
210
+ zIndex: 2,
211
+ display: "flex",
212
+ flexDirection: "column",
213
+ width: "100%",
214
+ height: "100%",
215
+ padding: "52px 60px"
216
+ },
217
+ children: [
218
+ /* @__PURE__ */ jsxs(
219
+ "div",
220
+ {
221
+ style: {
222
+ display: "flex",
223
+ alignItems: "center",
224
+ justifyContent: "space-between"
225
+ },
226
+ children: [
227
+ /* @__PURE__ */ jsxs(
228
+ "div",
229
+ {
230
+ style: {
231
+ display: "flex",
232
+ alignItems: "center",
233
+ gap: 12
234
+ },
235
+ children: [
236
+ /* @__PURE__ */ jsx(SifLogoMark, { width: 52, height: 26, color: theme.accent, opacity: 0.95 }),
237
+ /* @__PURE__ */ jsx(
238
+ "span",
239
+ {
240
+ style: {
241
+ display: "flex",
242
+ fontSize: 20,
243
+ fontWeight: 600,
244
+ color: SIF_BRAND_COLORS.paper,
245
+ letterSpacing: 0.2
246
+ },
247
+ children: data.brand
248
+ }
249
+ )
250
+ ]
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsxs(
254
+ "div",
255
+ {
256
+ style: {
257
+ display: "flex",
258
+ alignItems: "center",
259
+ gap: 10,
260
+ borderRadius: 999,
261
+ border: `1px solid ${theme.chipBorder}`,
262
+ background: "rgba(18,23,38,0.55)",
263
+ height: 44,
264
+ padding: "0 20px"
265
+ },
266
+ children: [
267
+ /* @__PURE__ */ jsx(
268
+ "div",
269
+ {
270
+ style: {
271
+ width: 8,
272
+ height: 8,
273
+ borderRadius: 8,
274
+ background: theme.accent,
275
+ boxShadow: `0 0 14px ${theme.glow}`
276
+ }
277
+ }
278
+ ),
279
+ /* @__PURE__ */ jsx(
280
+ "span",
281
+ {
282
+ style: {
283
+ fontSize: 17,
284
+ fontWeight: 500,
285
+ color: SIF_BRAND_COLORS.paper,
286
+ letterSpacing: 0.2,
287
+ display: "flex"
288
+ },
289
+ children: theme.label
290
+ }
291
+ )
292
+ ]
293
+ }
294
+ )
295
+ ]
296
+ }
297
+ ),
298
+ /* @__PURE__ */ jsxs(
299
+ "div",
300
+ {
301
+ style: {
302
+ display: "flex",
303
+ flexDirection: "column",
304
+ flex: 1,
305
+ justifyContent: "center",
306
+ paddingBottom: 8
307
+ },
308
+ children: [
309
+ /* @__PURE__ */ jsx(
310
+ "div",
311
+ {
312
+ style: {
313
+ width: 52,
314
+ height: 3,
315
+ borderRadius: 999,
316
+ background: theme.accent,
317
+ boxShadow: `0 0 20px ${theme.glow}`,
318
+ marginBottom: 24
319
+ }
320
+ }
321
+ ),
322
+ /* @__PURE__ */ jsx(
323
+ "div",
324
+ {
325
+ style: {
326
+ fontSize: titleFontSize,
327
+ fontWeight: 700,
328
+ lineHeight: 1.18,
329
+ color: SIF_BRAND_COLORS.paper,
330
+ maxWidth: 760,
331
+ letterSpacing: -0.5,
332
+ display: "flex",
333
+ flexWrap: "wrap"
334
+ },
335
+ children: data.title
336
+ }
337
+ ),
338
+ /* @__PURE__ */ jsx(
339
+ "div",
340
+ {
341
+ style: {
342
+ fontSize: 21,
343
+ lineHeight: 1.55,
344
+ color: "rgba(232,238,242,0.62)",
345
+ maxWidth: 680,
346
+ marginTop: 18,
347
+ display: "flex",
348
+ flexWrap: "wrap"
349
+ },
350
+ children: data.subtitle
351
+ }
352
+ )
353
+ ]
354
+ }
355
+ ),
356
+ /* @__PURE__ */ jsxs(
357
+ "div",
358
+ {
359
+ style: {
360
+ display: "flex",
361
+ alignItems: "center",
362
+ justifyContent: "space-between"
363
+ },
364
+ children: [
365
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 10 }, children: chipsToRender.map((chip) => /* @__PURE__ */ jsx(
366
+ "div",
367
+ {
368
+ style: {
369
+ display: "flex",
370
+ alignItems: "center",
371
+ height: 38,
372
+ borderRadius: 999,
373
+ padding: "0 16px",
374
+ fontSize: 15,
375
+ border: `1px solid ${theme.chipBorder}`,
376
+ background: theme.chipBg,
377
+ color: SIF_BRAND_COLORS.paper
378
+ },
379
+ children: chip
380
+ },
381
+ chip
382
+ )) }),
383
+ /* @__PURE__ */ jsx(
384
+ "span",
385
+ {
386
+ style: {
387
+ fontSize: 15,
388
+ color: "rgba(232,238,242,0.38)",
389
+ letterSpacing: 0.4,
390
+ display: "flex"
391
+ },
392
+ children: domainLabel
393
+ }
394
+ )
395
+ ]
396
+ }
397
+ )
398
+ ]
399
+ }
400
+ )
401
+ ]
402
+ }
403
+ );
404
+ }
405
+
406
+ // src/presets/sif/fallbacks.ts
407
+ function createSifFallbacks(config = {}) {
408
+ const brandName = config.brandName?.trim() || "Software InFocus";
409
+ const global = {
410
+ type: "brand",
411
+ title: brandName,
412
+ subtitle: "Custom websites, software, and growth systems for Delaware businesses",
413
+ chips: ["Web Development", "Design", "SEO"],
414
+ brand: brandName
415
+ };
416
+ const section = {
417
+ brand: global,
418
+ blog: {
419
+ type: "blog",
420
+ title: `${brandName} Blog`,
421
+ subtitle: "Practical insights on websites, software, SEO, and growth",
422
+ chips: ["Articles", "Guides", "Case Notes"],
423
+ brand: brandName
424
+ },
425
+ portfolio: {
426
+ type: "portfolio",
427
+ title: `${brandName} Portfolio`,
428
+ subtitle: "Case studies and project outcomes across web, SEO, and software delivery",
429
+ chips: ["Case Studies", "Web Builds", "Results"],
430
+ brand: brandName
431
+ },
432
+ service: {
433
+ type: "service",
434
+ title: `${brandName} Services`,
435
+ subtitle: "Design, development, SEO, hosting, and long-term support",
436
+ chips: ["Delaware", "Custom", "Lead-Focused"],
437
+ brand: brandName
438
+ }
439
+ };
440
+ return { global, section };
441
+ }
442
+
443
+ // src/presets/sif/index.tsx
444
+ import { jsx as jsx2 } from "react/jsx-runtime";
445
+ function mergeSifThemes(overrides) {
446
+ return {
447
+ brand: overrides?.brand ?? SIF_DEFAULT_THEMES.brand,
448
+ blog: overrides?.blog ?? SIF_DEFAULT_THEMES.blog,
449
+ service: overrides?.service ?? SIF_DEFAULT_THEMES.service,
450
+ portfolio: overrides?.portfolio ?? SIF_DEFAULT_THEMES.portfolio
451
+ };
452
+ }
453
+ function normalizeSifOgData(input, brandName) {
454
+ const normalized = normalizeOgImageData(input, {
455
+ defaultBrand: brandName,
456
+ defaultSubtitle: brandName,
457
+ maxTitleLength: 140,
458
+ maxSubtitleLength: 160,
459
+ maxChips: 3
460
+ });
461
+ return {
462
+ type: normalized.type,
463
+ title: normalized.title,
464
+ subtitle: normalized.subtitle,
465
+ chips: normalized.chips,
466
+ brand: normalized.brand || brandName
467
+ };
468
+ }
469
+ function createSifOgRenderer(config = {}) {
470
+ const brandName = config.brandName?.trim() || "Software InFocus";
471
+ const domainLabel = config.domainLabel?.trim() || "softwareinfocus.com";
472
+ const themes = mergeSifThemes(config.themes);
473
+ const loadFonts = config.loadFonts;
474
+ return async (input) => {
475
+ const data = normalizeSifOgData(input, brandName);
476
+ const theme = themes[data.type];
477
+ const fonts = loadFonts ? await loadFonts() : [];
478
+ return createPngImageResponse(
479
+ /* @__PURE__ */ jsx2(SifOgTemplate, { data, theme, domainLabel }),
480
+ { fonts }
481
+ );
482
+ };
483
+ }
484
+ export {
485
+ createSifFallbacks,
486
+ createSifOgRenderer
487
+ };
488
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/image-response.tsx","../../../src/core/normalize.ts","../../../src/presets/sif/theme.ts","../../../src/presets/sif/template.tsx","../../../src/presets/sif/fallbacks.ts","../../../src/presets/sif/index.tsx"],"sourcesContent":["import { ImageResponse } from 'next/og'\nimport type { ReactElement } from 'react'\nimport { OG_IMAGE_SIZE_1200x630 } from './constants'\nimport type { OgFontDefinition, OgImageSize } from './types'\n\nexport function createPngImageResponse(\n element: ReactElement,\n options: {\n fonts?: OgFontDefinition[]\n size?: OgImageSize\n } = {},\n) {\n const size = options.size ?? OG_IMAGE_SIZE_1200x630\n const fonts = options.fonts?.length ? options.fonts : undefined\n\n return new ImageResponse(element, {\n width: size.width,\n height: size.height,\n ...(fonts ? { fonts } : {}),\n })\n}\n","import type { OgImageData } from './types'\n\nexport function clampText(text: string, maxLength: number): string {\n const normalized = text.replace(/\\s+/g, ' ').trim()\n if (normalized.length <= maxLength) return normalized\n\n const clipped = normalized.slice(0, Math.max(1, maxLength - 1))\n const lastSpace = clipped.lastIndexOf(' ')\n const safe = lastSpace > Math.floor(maxLength * 0.6) ? clipped.slice(0, lastSpace) : clipped\n\n return `${safe.trimEnd()}…`\n}\n\nexport function normalizeChips(chips: string[] | undefined, max = 3): string[] {\n if (!chips?.length) return []\n\n const seen = new Set<string>()\n const result: string[] = []\n\n for (const chip of chips) {\n const normalized = chip.replace(/\\s+/g, ' ').trim()\n if (!normalized) continue\n\n const key = normalized.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n\n result.push(clampText(normalized, 24))\n if (result.length >= max) break\n }\n\n return result\n}\n\nexport function normalizeOgImageData<TType extends string>(\n data: OgImageData<TType>,\n options: {\n defaultBrand?: string\n defaultSubtitle?: string\n maxTitleLength?: number\n maxSubtitleLength?: number\n maxChips?: number\n } = {},\n): Required<Pick<OgImageData<TType>, 'type' | 'title' | 'subtitle' | 'chips' | 'brand'>> {\n const title = clampText(data.title, options.maxTitleLength ?? 140)\n const brand = (data.brand ?? options.defaultBrand ?? '').trim()\n const subtitleSeed = data.subtitle ?? options.defaultSubtitle ?? brand\n const subtitle = clampText(subtitleSeed || title, options.maxSubtitleLength ?? 160)\n const chips = normalizeChips(data.chips, options.maxChips ?? 3)\n\n return {\n type: data.type,\n title,\n subtitle,\n chips: chips.length > 0 ? chips : (brand ? [brand] : []),\n brand,\n }\n}\n","import type { SifOgTheme, SifOgType } from './types'\n\nexport const SIF_BRAND_COLORS = {\n primary: '#EE6C4D',\n primaryBright: '#FF8A67',\n ink: '#292A38',\n slate: '#C7D3DD',\n paper: '#E8EEF2',\n night: '#121726',\n} as const\n\nexport const SIF_DEFAULT_THEMES: Record<SifOgType, SifOgTheme> = {\n blog: {\n label: 'Blog',\n accent: SIF_BRAND_COLORS.primary,\n accentSoft: 'rgba(238, 108, 77, 0.16)',\n glow: 'rgba(238, 108, 77, 0.28)',\n chipBorder: 'rgba(238, 108, 77, 0.28)',\n chipBg: 'rgba(238, 108, 77, 0.1)',\n },\n service: {\n label: 'Service',\n accent: '#F27C62',\n accentSoft: 'rgba(242, 124, 98, 0.16)',\n glow: 'rgba(242, 124, 98, 0.27)',\n chipBorder: 'rgba(242, 124, 98, 0.26)',\n chipBg: 'rgba(242, 124, 98, 0.09)',\n },\n portfolio: {\n label: 'Portfolio',\n accent: SIF_BRAND_COLORS.primaryBright,\n accentSoft: 'rgba(255, 138, 103, 0.16)',\n glow: 'rgba(255, 138, 103, 0.25)',\n chipBorder: 'rgba(255, 138, 103, 0.24)',\n chipBg: 'rgba(255, 138, 103, 0.08)',\n },\n brand: {\n label: 'Software InFocus',\n accent: SIF_BRAND_COLORS.primary,\n accentSoft: 'rgba(238, 108, 77, 0.18)',\n glow: 'rgba(238, 108, 77, 0.28)',\n chipBorder: 'rgba(238, 108, 77, 0.28)',\n chipBg: 'rgba(238, 108, 77, 0.1)',\n },\n}\n\nexport const SIF_LOGO_MARK_VIEWBOX = '0 0 256 128'\n\nexport const SIF_LOGO_MARK_PATHS = [\n 'M64 24 L24 64 L64 104',\n 'M144 16 L112 112',\n 'M192 24 L232 64 L192 104',\n] as const\n","import { OG_IMAGE_SIZE_1200x630 } from '../../core/constants'\nimport type { SifNormalizedOgData, SifOgTheme } from './types'\nimport { SIF_BRAND_COLORS, SIF_LOGO_MARK_PATHS, SIF_LOGO_MARK_VIEWBOX } from './theme'\n\ntype SifLogoMarkProps = {\n width?: number\n height?: number\n color?: string\n opacity?: number\n}\n\nfunction SifLogoMark({\n width = 132,\n height = 66,\n color = '#EE6C4D',\n opacity = 1,\n}: SifLogoMarkProps) {\n return (\n <svg\n width={width}\n height={height}\n viewBox={SIF_LOGO_MARK_VIEWBOX}\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{ opacity }}\n >\n {SIF_LOGO_MARK_PATHS.map((path) => (\n <path\n key={path}\n d={path}\n stroke={color}\n strokeWidth=\"16\"\n strokeLinecap=\"square\"\n strokeLinejoin=\"miter\"\n />\n ))}\n </svg>\n )\n}\n\ntype SifOgTemplateProps = {\n data: SifNormalizedOgData\n theme: SifOgTheme\n domainLabel: string\n}\n\nexport function SifOgTemplate({ data, theme, domainLabel }: SifOgTemplateProps) {\n const chipsToRender = data.chips.slice(0, 3)\n const titleFontSize = data.title.length < 36 ? 58 : data.title.length < 60 ? 50 : 42\n\n return (\n <div\n style={{\n width: OG_IMAGE_SIZE_1200x630.width,\n height: OG_IMAGE_SIZE_1200x630.height,\n display: 'flex',\n position: 'relative',\n overflow: 'hidden',\n background: `linear-gradient(145deg, ${SIF_BRAND_COLORS.night} 0%, #0d1424 38%, #171c2b 68%, ${SIF_BRAND_COLORS.ink} 100%)`,\n color: SIF_BRAND_COLORS.paper,\n }}\n >\n <div\n style={{\n position: 'absolute',\n inset: 0,\n backgroundImage:\n 'linear-gradient(rgba(199,211,221,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(199,211,221,0.025) 1px, transparent 1px)',\n backgroundSize: '36px 36px',\n opacity: 0.35,\n }}\n />\n\n <div\n style={{\n position: 'absolute',\n top: -140,\n right: -80,\n width: 480,\n height: 480,\n borderRadius: 480,\n background: `radial-gradient(circle, ${theme.glow} 0%, rgba(0,0,0,0) 70%)`,\n }}\n />\n\n <div\n style={{\n position: 'absolute',\n bottom: -120,\n left: -80,\n width: 320,\n height: 320,\n borderRadius: 320,\n background: 'radial-gradient(circle, rgba(238,108,77,0.14) 0%, rgba(0,0,0,0) 72%)',\n }}\n />\n\n <div\n style={{\n position: 'absolute',\n right: -30,\n top: 183,\n display: 'flex',\n }}\n >\n <SifLogoMark width={528} height={264} color={theme.accent} opacity={0.055} />\n </div>\n\n <div\n style={{\n position: 'relative',\n zIndex: 2,\n display: 'flex',\n flexDirection: 'column',\n width: '100%',\n height: '100%',\n padding: '52px 60px',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 12,\n }}\n >\n <SifLogoMark width={52} height={26} color={theme.accent} opacity={0.95} />\n <span\n style={{\n display: 'flex',\n fontSize: 20,\n fontWeight: 600,\n color: SIF_BRAND_COLORS.paper,\n letterSpacing: 0.2,\n }}\n >\n {data.brand}\n </span>\n </div>\n\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 10,\n borderRadius: 999,\n border: `1px solid ${theme.chipBorder}`,\n background: 'rgba(18,23,38,0.55)',\n height: 44,\n padding: '0 20px',\n }}\n >\n <div\n style={{\n width: 8,\n height: 8,\n borderRadius: 8,\n background: theme.accent,\n boxShadow: `0 0 14px ${theme.glow}`,\n }}\n />\n <span\n style={{\n fontSize: 17,\n fontWeight: 500,\n color: SIF_BRAND_COLORS.paper,\n letterSpacing: 0.2,\n display: 'flex',\n }}\n >\n {theme.label}\n </span>\n </div>\n </div>\n\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n flex: 1,\n justifyContent: 'center',\n paddingBottom: 8,\n }}\n >\n <div\n style={{\n width: 52,\n height: 3,\n borderRadius: 999,\n background: theme.accent,\n boxShadow: `0 0 20px ${theme.glow}`,\n marginBottom: 24,\n }}\n />\n\n <div\n style={{\n fontSize: titleFontSize,\n fontWeight: 700,\n lineHeight: 1.18,\n color: SIF_BRAND_COLORS.paper,\n maxWidth: 760,\n letterSpacing: -0.5,\n display: 'flex',\n flexWrap: 'wrap',\n }}\n >\n {data.title}\n </div>\n\n <div\n style={{\n fontSize: 21,\n lineHeight: 1.55,\n color: 'rgba(232,238,242,0.62)',\n maxWidth: 680,\n marginTop: 18,\n display: 'flex',\n flexWrap: 'wrap',\n }}\n >\n {data.subtitle}\n </div>\n </div>\n\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <div style={{ display: 'flex', gap: 10 }}>\n {chipsToRender.map((chip) => (\n <div\n key={chip}\n style={{\n display: 'flex',\n alignItems: 'center',\n height: 38,\n borderRadius: 999,\n padding: '0 16px',\n fontSize: 15,\n border: `1px solid ${theme.chipBorder}`,\n background: theme.chipBg,\n color: SIF_BRAND_COLORS.paper,\n }}\n >\n {chip}\n </div>\n ))}\n </div>\n\n <span\n style={{\n fontSize: 15,\n color: 'rgba(232,238,242,0.38)',\n letterSpacing: 0.4,\n display: 'flex',\n }}\n >\n {domainLabel}\n </span>\n </div>\n </div>\n </div>\n )\n}\n","import type { SifOgData } from './types'\n\nexport function createSifFallbacks(config: { brandName?: string } = {}) {\n const brandName = config.brandName?.trim() || 'Software InFocus'\n\n const global: SifOgData = {\n type: 'brand',\n title: brandName,\n subtitle: 'Custom websites, software, and growth systems for Delaware businesses',\n chips: ['Web Development', 'Design', 'SEO'],\n brand: brandName,\n }\n\n const section: Record<SifOgData['type'], SifOgData> = {\n brand: global,\n blog: {\n type: 'blog',\n title: `${brandName} Blog`,\n subtitle: 'Practical insights on websites, software, SEO, and growth',\n chips: ['Articles', 'Guides', 'Case Notes'],\n brand: brandName,\n },\n portfolio: {\n type: 'portfolio',\n title: `${brandName} Portfolio`,\n subtitle: 'Case studies and project outcomes across web, SEO, and software delivery',\n chips: ['Case Studies', 'Web Builds', 'Results'],\n brand: brandName,\n },\n service: {\n type: 'service',\n title: `${brandName} Services`,\n subtitle: 'Design, development, SEO, hosting, and long-term support',\n chips: ['Delaware', 'Custom', 'Lead-Focused'],\n brand: brandName,\n },\n }\n\n return { global, section }\n}\n","import { createPngImageResponse } from '../../core/image-response'\nimport { normalizeOgImageData } from '../../core/normalize'\nimport { SifOgTemplate } from './template'\nimport { createSifFallbacks } from './fallbacks'\nimport { SIF_DEFAULT_THEMES } from './theme'\nimport type {\n SifNormalizedOgData,\n SifOgData,\n SifOgRenderer,\n SifOgTheme,\n SifPresetConfig,\n SifOgType,\n} from './types'\n\nfunction mergeSifThemes(\n overrides: Partial<Record<SifOgType, SifOgTheme>> | undefined,\n): Record<SifOgType, SifOgTheme> {\n return {\n brand: overrides?.brand ?? SIF_DEFAULT_THEMES.brand,\n blog: overrides?.blog ?? SIF_DEFAULT_THEMES.blog,\n service: overrides?.service ?? SIF_DEFAULT_THEMES.service,\n portfolio: overrides?.portfolio ?? SIF_DEFAULT_THEMES.portfolio,\n }\n}\n\nfunction normalizeSifOgData(input: SifOgData, brandName: string): SifNormalizedOgData {\n const normalized = normalizeOgImageData(input, {\n defaultBrand: brandName,\n defaultSubtitle: brandName,\n maxTitleLength: 140,\n maxSubtitleLength: 160,\n maxChips: 3,\n })\n\n return {\n type: normalized.type,\n title: normalized.title,\n subtitle: normalized.subtitle,\n chips: normalized.chips,\n brand: normalized.brand || brandName,\n }\n}\n\nexport function createSifOgRenderer(config: SifPresetConfig = {}): SifOgRenderer {\n const brandName = config.brandName?.trim() || 'Software InFocus'\n const domainLabel = config.domainLabel?.trim() || 'softwareinfocus.com'\n const themes = mergeSifThemes(config.themes)\n const loadFonts = config.loadFonts\n\n return async (input: SifOgData) => {\n const data = normalizeSifOgData(input, brandName)\n const theme = themes[data.type]\n const fonts = loadFonts ? await loadFonts() : []\n\n return createPngImageResponse(\n <SifOgTemplate data={data} theme={theme} domainLabel={domainLabel} />,\n { fonts },\n )\n }\n}\n\nexport { createSifFallbacks }\nexport type {\n SifOgData,\n SifOgRenderer,\n SifOgTheme,\n SifOgType,\n SifPresetConfig,\n} from './types'\n"],"mappings":"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","names":["jsx"]}
@@ -0,0 +1,18 @@
1
+ type OgImageData<TType extends string = string> = {
2
+ type: TType;
3
+ title: string;
4
+ subtitle?: string;
5
+ chips?: string[];
6
+ brand?: string;
7
+ };
8
+ type OgFontDefinition = {
9
+ name: string;
10
+ data: ArrayBuffer;
11
+ style?: 'normal' | 'italic';
12
+ weight: 400 | 500 | 600 | 700;
13
+ };
14
+ type MetadataImageRouteProps<TParams extends Record<string, string>> = {
15
+ params: Promise<TParams> | TParams;
16
+ };
17
+
18
+ export type { MetadataImageRouteProps as M, OgImageData as O, OgFontDefinition as a };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@softwareinfocus/next-og-image",
3
+ "version": "0.1.0",
4
+ "description": "Reusable Open Graph image toolkit for Next.js App Router with an optional Software InFocus preset",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./presets/sif": {
19
+ "types": "./dist/presets/sif/index.d.ts",
20
+ "import": "./dist/presets/sif/index.js"
21
+ }
22
+ },
23
+ "types": "./dist/index.d.ts",
24
+ "peerDependencies": {
25
+ "next": ">=15 <16",
26
+ "react": ">=19 <20"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "react": {
30
+ "optional": false
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "@changesets/cli": "^2.29.7",
35
+ "@eslint/js": "^9.20.0",
36
+ "@types/node": "^22.13.5",
37
+ "@types/react": "^19.0.10",
38
+ "eslint": "^9.20.1",
39
+ "globals": "^15.15.0",
40
+ "next": "^15.3.3",
41
+ "react": "^19.0.0",
42
+ "react-dom": "^19.0.0",
43
+ "tsup": "^8.3.5",
44
+ "typescript": "^5.7.3",
45
+ "typescript-eslint": "^8.24.1",
46
+ "vitest": "^3.0.7"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "clean": "rm -rf dist",
51
+ "lint": "eslint .",
52
+ "test": "vitest run",
53
+ "typecheck": "tsc --noEmit",
54
+ "release:check": "changeset status --verbose"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "provenance": true
59
+ }
60
+ }