nuxt-og-image 6.5.2 → 6.6.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.
Files changed (88) hide show
  1. package/dist/chunks/tw4.cjs +1 -1
  2. package/dist/chunks/tw4.mjs +1 -1
  3. package/dist/chunks/uno.cjs +1 -1
  4. package/dist/chunks/uno.mjs +1 -1
  5. package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
  6. package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
  7. package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
  8. package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
  9. package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
  10. package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
  11. package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
  12. package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
  13. package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
  14. package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
  15. package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
  16. package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
  17. package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
  18. package/dist/devtools/lib/og-image/keys.ts +8 -0
  19. package/dist/devtools/lib/og-image/og-image.ts +536 -0
  20. package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
  21. package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
  22. package/dist/devtools/lib/og-image/rpc.ts +73 -0
  23. package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
  24. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
  25. package/dist/devtools/lib/og-image/shared.ts +30 -0
  26. package/dist/devtools/lib/og-image/templates.ts +9 -0
  27. package/dist/devtools/lib/og-image/types.ts +38 -0
  28. package/dist/devtools/lib/og-image/util/logic.ts +21 -0
  29. package/dist/devtools/nuxt.config.ts +6 -0
  30. package/dist/devtools/pages/og-image/debug.vue +32 -0
  31. package/dist/devtools/pages/og-image/docs.vue +3 -0
  32. package/dist/devtools/pages/og-image/index.vue +1682 -0
  33. package/dist/devtools/pages/og-image/templates.vue +150 -0
  34. package/dist/devtools/pages/og-image.vue +184 -0
  35. package/dist/module.cjs +1 -1
  36. package/dist/module.json +1 -1
  37. package/dist/module.mjs +1 -1
  38. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
  39. package/dist/runtime/server/og-image/core/style-attr.d.ts +8 -0
  40. package/dist/runtime/server/og-image/core/style-attr.js +34 -0
  41. package/dist/runtime/server/og-image/core/vnodes.d.ts +2 -1
  42. package/dist/runtime/server/og-image/core/vnodes.js +3 -27
  43. package/dist/shared/{nuxt-og-image.CgPzmzQY.cjs → nuxt-og-image.CfTPCtaS.cjs} +38 -24
  44. package/dist/shared/{nuxt-og-image.DGAMxBol.mjs → nuxt-og-image.DdbTs-xp.mjs} +37 -23
  45. package/package.json +17 -18
  46. package/dist/devtools/200.html +0 -1
  47. package/dist/devtools/404.html +0 -1
  48. package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
  49. package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
  50. package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
  51. package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
  52. package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
  53. package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
  54. package/dist/devtools/_nuxt/B9jrmesR.js +0 -1
  55. package/dist/devtools/_nuxt/BA-4cUNc.js +0 -1
  56. package/dist/devtools/_nuxt/BOEXnX7x.js +0 -3
  57. package/dist/devtools/_nuxt/BWKJ0Uxb.js +0 -30
  58. package/dist/devtools/_nuxt/Bdtz4ZK9.js +0 -4
  59. package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
  60. package/dist/devtools/_nuxt/C5jzcy9i.js +0 -1
  61. package/dist/devtools/_nuxt/CCTv7mmB.js +0 -1
  62. package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
  63. package/dist/devtools/_nuxt/C_JnDlx-.js +0 -1
  64. package/dist/devtools/_nuxt/CdmciVPJ.js +0 -1
  65. package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
  66. package/dist/devtools/_nuxt/CuJezOxb.js +0 -1
  67. package/dist/devtools/_nuxt/D4EkL0XJ.js +0 -6
  68. package/dist/devtools/_nuxt/DKaW7clv.js +0 -1
  69. package/dist/devtools/_nuxt/DSl3mlLY.js +0 -2
  70. package/dist/devtools/_nuxt/DYta7Mdi.js +0 -3
  71. package/dist/devtools/_nuxt/DevtoolsSection.CcOMr_vO.css +0 -1
  72. package/dist/devtools/_nuxt/DevtoolsSnippet.BhrTdbXn.css +0 -1
  73. package/dist/devtools/_nuxt/E8AZ6HoH.js +0 -1
  74. package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
  75. package/dist/devtools/_nuxt/O5eLyffU.js +0 -1
  76. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  77. package/dist/devtools/_nuxt/builds/meta/a36bc212-7179-4aa2-a534-6366229bd7b1.json +0 -1
  78. package/dist/devtools/_nuxt/entry.CcrXpo2y.css +0 -2
  79. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  80. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  81. package/dist/devtools/_nuxt/pages.Ci_xvfJ8.css +0 -1
  82. package/dist/devtools/_nuxt/renderer-select.COGJ4ZQe.css +0 -1
  83. package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
  84. package/dist/devtools/_nuxt/wMBdlVF-.js +0 -152
  85. package/dist/devtools/debug/index.html +0 -1
  86. package/dist/devtools/docs/index.html +0 -1
  87. package/dist/devtools/index.html +0 -1
  88. package/dist/devtools/templates/index.html +0 -1
@@ -0,0 +1,536 @@
1
+ import type { Ref } from 'vue'
2
+ import type { DevToolsMetaDataExtraction, OgImageComponent, OgImageOptions, OgImageOptionsInternal, RendererType } from './runtime-types'
3
+ import { useLocalStorage, useWindowSize } from '@vueuse/core'
4
+ import defu from 'defu'
5
+ import { colorMode } from 'nuxtseo-layer-devtools/composables/rpc'
6
+ import { path, query, refreshSources, refreshTime, slowRefreshSources } from 'nuxtseo-layer-devtools/composables/state'
7
+ import { relative } from 'pathe'
8
+ import { hasProtocol, joinURL, parseURL, withQuery } from 'ufo'
9
+ import { ref } from 'vue'
10
+ import { computed, inject, toValue, watch } from '#imports'
11
+ import { GlobalDebugKey, PathDebugKey, PathDebugStatusKey, RefetchPathDebugKey } from './keys'
12
+ import { host, ogImageRpc } from './rpc'
13
+ import { encodeOgImageParams, separateProps } from './shared'
14
+ import { AddComponentDialogPromise, CreateOgImageDialogPromise } from './templates'
15
+ import { description, hasMadeChanges, ogImageKey, options, optionsOverrides, previewHost, propEditor } from './util/logic'
16
+
17
+ const RE_SUFFIX_SATORI = /Satori$/
18
+ const RE_SUFFIX_BROWSER = /Browser$/
19
+ const RE_SUFFIX_TAKUMI = /Takumi$/
20
+
21
+ export function useOgImage() {
22
+ const globalDebug = inject(GlobalDebugKey, ref(null) as Ref<any>)
23
+
24
+ const emojis = ref<OgImageOptions['emojis']>('noto')
25
+
26
+ const debug = inject(PathDebugKey, ref(null) as Ref<any>)
27
+ const debugStatus = inject(PathDebugStatusKey, ref('idle') as Ref<'idle' | 'pending' | 'success' | 'error'>)
28
+ const refreshPathDebug = inject(RefetchPathDebugKey, async () => {})
29
+
30
+ const isDebugLoading = computed(() => debugStatus.value === 'pending' && !debug.value?.extract?.options?.length)
31
+ const error = ref(null)
32
+
33
+ // Multi-image support
34
+ const selectedOgImage = computed(() => {
35
+ const images = debug.value?.extract?.socialPreview?.images || []
36
+ return images.find((e: DevToolsMetaDataExtraction) => e.key === (ogImageKey.value || 'og')) || images[0]
37
+ })
38
+
39
+ const currentOptions = computed(() => {
40
+ const opts = debug.value?.extract?.options || []
41
+ return opts.find((o: OgImageOptions) => o.key === (ogImageKey.value || 'og')) || opts[0]
42
+ })
43
+
44
+ const isCustomOgImage = computed(() => {
45
+ const url = toValue(currentOptions.value?.url)
46
+ return url && !url.includes('/_og/')
47
+ })
48
+
49
+ const isValidDebugError = computed(() => {
50
+ if (error.value) {
51
+ // @ts-expect-error untyped
52
+ const message = error.value.message
53
+ if (message) {
54
+ return message.includes('missing the #nuxt-og-') || message.includes('missing the Nuxt OG Image payload') || message.includes('Got invalid response')
55
+ }
56
+ }
57
+ return false
58
+ })
59
+
60
+ const hasDefinedOgImage = computed(() => {
61
+ const opts = debug.value?.extract?.options || []
62
+ return opts.length > 0
63
+ })
64
+
65
+ const fetchError = computed(() => debug.value?.fetchError || null)
66
+
67
+ watch(debug, (val) => {
68
+ if (!val)
69
+ return
70
+ options.value = separateProps(toValue(currentOptions.value) || {}, ['socialPreview', 'options'])
71
+ emojis.value = options.value.emojis
72
+ if (!hasMadeChanges.value)
73
+ propEditor.value = options.value.props || {}
74
+ }, {
75
+ immediate: true,
76
+ })
77
+
78
+ const defaults = computed(() => {
79
+ return defu(globalDebug.value?.runtimeConfig.defaults, {
80
+ height: 600,
81
+ width: 1200,
82
+ })
83
+ })
84
+
85
+ const height = computed((): number => {
86
+ const h = toValue(optionsOverrides.value?.height)
87
+ if (typeof h === 'number')
88
+ return h
89
+ const ogHeight = Number(selectedOgImage.value?.og?.['image:height'])
90
+ if (ogHeight)
91
+ return ogHeight
92
+ return toValue(defaults.value.height) || 600
93
+ })
94
+
95
+ const width = computed((): number => {
96
+ const w = toValue(optionsOverrides.value?.width)
97
+ if (typeof w === 'number')
98
+ return w
99
+ const ogWidth = Number(selectedOgImage.value?.og?.['image:width'])
100
+ if (ogWidth)
101
+ return ogWidth
102
+ return toValue(defaults.value.width) || 1200
103
+ })
104
+
105
+ const aspectRatio = computed(() => {
106
+ return width.value / height.value
107
+ })
108
+
109
+ const imageFormat = computed(() => {
110
+ return optionsOverrides.value?.extension || options.value?.extension || 'png'
111
+ })
112
+
113
+ const socialPreview = useLocalStorage('nuxt-og-image:social-preview', 'twitter')
114
+ const imageColorMode = ref<'dark' | 'light'>(colorMode.value)
115
+
116
+ function buildSrcForKey(key: string) {
117
+ const opts = debug.value?.extract?.options || []
118
+ const keyOpts = opts.find((o: OgImageOptions) => o.key === key)
119
+ const params = defu(
120
+ { key, _path: path.value, _query: query.value },
121
+ keyOpts || {},
122
+ )
123
+ const encoded = encodeOgImageParams(params)
124
+ return withQuery(joinURL(previewHost.value, `/_og/d/${encoded || 'default'}.${imageFormat.value}`), {
125
+ timestamp: refreshTime.value,
126
+ colorMode: imageColorMode.value,
127
+ })
128
+ }
129
+
130
+ const src = computed(() => {
131
+ if (isCustomOgImage.value) {
132
+ const url = toValue(currentOptions.value?.url) || ''
133
+ if (hasProtocol(url, { acceptRelative: true })) {
134
+ return url
135
+ }
136
+ return joinURL(previewHost.value, url)
137
+ }
138
+ // Build encoded URL with options (Cloudinary-style)
139
+ // Use defu to deep-merge props (shallow spread would drop original props like title)
140
+ const params = defu(
141
+ { key: ogImageKey.value || 'og', _path: path.value, _query: query.value },
142
+ optionsOverrides.value,
143
+ options.value,
144
+ )
145
+ const encoded = encodeOgImageParams(params)
146
+ return withQuery(joinURL(previewHost.value, `/_og/d/${encoded || 'default'}.${imageFormat.value}`), {
147
+ timestamp: refreshTime.value, // Cache bust for devtools
148
+ colorMode: imageColorMode.value, // Pass color mode to renderer
149
+ })
150
+ })
151
+
152
+ // Multi-image keys
153
+ const allImageKeys = computed(() => {
154
+ return debug.value?.extract?.socialPreview?.images?.map((i: DevToolsMetaDataExtraction) => i.key) || []
155
+ })
156
+
157
+ // Auto-resolve a square/smaller image for WhatsApp inline preview
158
+ const whatsappInlineSrc = computed(() => {
159
+ const keys = allImageKeys.value
160
+ if (keys.length <= 1)
161
+ return src.value
162
+ const squareKey = keys.find((k: string) => k === 'whatsapp' || k === 'square')
163
+ if (squareKey)
164
+ return buildSrcForKey(squareKey)
165
+ return src.value
166
+ })
167
+
168
+ const socialPreviewTitle = computed(() => {
169
+ const root = debug.value?.extract?.socialPreview?.root || {}
170
+ if (socialPreview.value === 'twitter' && root['twitter:title'])
171
+ return root['twitter:title']
172
+ return root['og:title']
173
+ })
174
+
175
+ const socialPreviewDescription = computed(() => {
176
+ const root = debug.value?.extract?.socialPreview?.root || {}
177
+ if (socialPreview.value === 'twitter' && root['twitter:description'])
178
+ return root['twitter:description']
179
+ return root['og:description']
180
+ })
181
+
182
+ const socialSiteUrl = computed(() => {
183
+ return parseURL(globalDebug.value?.siteConfigUrl || '/').host || globalDebug.value?.siteConfigUrl || '/'
184
+ })
185
+
186
+ const slackSocialPreviewSiteName = computed(() => {
187
+ return selectedOgImage.value?.og?.site_name || socialSiteUrl.value
188
+ })
189
+
190
+ function toggleSocialPreview(preview?: string) {
191
+ if (!preview)
192
+ socialPreview.value = ''
193
+ else
194
+ socialPreview.value = preview!
195
+ }
196
+
197
+ const activeComponentName = computed(() => {
198
+ let componentName = String(optionsOverrides.value?.component || options.value?.component || 'NuxtSeo')
199
+ for (const componentDirName of (globalDebug?.value?.runtimeConfig.componentDirs || [])) {
200
+ componentName = componentName.replace(componentDirName, '')
201
+ }
202
+ return componentName
203
+ })
204
+
205
+ // Strip component directory prefixes from a pascalName (same logic as activeComponentName)
206
+ function stripComponentDirPrefix(pascalName: string): string {
207
+ let result = pascalName
208
+ for (const dir of (globalDebug?.value?.runtimeConfig.componentDirs || [])) {
209
+ result = result.replace(dir, '')
210
+ }
211
+ return result
212
+ }
213
+
214
+ const activeComponent = computed(() => {
215
+ const components = globalDebug.value?.componentNames || []
216
+ const name = activeComponentName.value
217
+ // Normalize dot-notation to PascalCase (NuxtSeo.takumi → NuxtSeoTakumi)
218
+ const normalizedName = name.split('.').map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join('')
219
+
220
+ // First try exact match (also comparing dir-stripped names since activeComponentName strips prefixes)
221
+ let matching = components.filter((c: OgImageComponent) => {
222
+ const stripped = stripComponentDirPrefix(c.pascalName)
223
+ return c.pascalName === normalizedName || c.pascalName === name
224
+ || stripped === normalizedName || stripped === name
225
+ })
226
+
227
+ // If no exact match, try matching by base name (without renderer suffix)
228
+ // This handles cases like component: 'BlogPost' matching 'BlogPostSatori'
229
+ if (matching.length === 0) {
230
+ const rendererSuffixes = ['Satori', 'Browser', 'Takumi']
231
+ matching = components.filter((c: OgImageComponent) => {
232
+ for (const suffix of rendererSuffixes) {
233
+ if (c.pascalName.endsWith(suffix)) {
234
+ const baseName = c.pascalName.slice(0, -suffix.length)
235
+ const strippedBase = stripComponentDirPrefix(baseName)
236
+ if (baseName === normalizedName || baseName === name
237
+ || strippedBase === normalizedName || strippedBase === name) { return true }
238
+ }
239
+ }
240
+ return false
241
+ })
242
+ }
243
+
244
+ // Prefer app components over community/pro templates
245
+ return matching.find((c: OgImageComponent) => c.category === 'app') || matching[0]
246
+ })
247
+
248
+ const activeComponentRelativePath = computed(() => {
249
+ const component = activeComponent.value
250
+ if (!component?.path)
251
+ return null
252
+
253
+ const rootDir = globalDebug.value?.runtimeConfig?.rootDir
254
+ if (rootDir) {
255
+ return relative(rootDir, component.path)
256
+ }
257
+
258
+ return component.path
259
+ })
260
+
261
+ const isOgImageTemplate = computed(() => {
262
+ const component = activeComponent.value
263
+ return component?.path?.includes('node_modules') || component?.path?.includes('og-image/src/runtime/app/components/Templates/Community/')
264
+ })
265
+
266
+ // Derive renderer from: explicit override > options > active component > default
267
+ const renderer = computed<RendererType>(() => {
268
+ if (optionsOverrides.value?.renderer)
269
+ return optionsOverrides.value.renderer
270
+ if (options.value?.renderer)
271
+ return options.value.renderer
272
+ // Infer from active component's renderer
273
+ return activeComponent.value?.renderer || 'satori'
274
+ })
275
+
276
+ const allComponents = computed<OgImageComponent[]>(() => {
277
+ return globalDebug.value?.componentNames || []
278
+ })
279
+
280
+ // Components filtered by current renderer
281
+ const componentNames = computed<OgImageComponent[]>(() => {
282
+ const components = allComponents.value.filter(c => c.renderer === renderer.value)
283
+ return [
284
+ components.find((name: OgImageComponent) => name.pascalName === activeComponentName.value),
285
+ ...components.filter((name: OgImageComponent) => name.pascalName !== activeComponentName.value),
286
+ ].filter((c): c is OgImageComponent => Boolean(c))
287
+ })
288
+
289
+ const communityComponents = computed(() => {
290
+ return componentNames.value.filter(c => c.category === 'community')
291
+ })
292
+
293
+ const appComponents = computed(() => {
294
+ return componentNames.value.filter(c => c.category === 'app')
295
+ })
296
+
297
+ // Check if active component has a variant for a given renderer
298
+ function getComponentVariantForRenderer(targetRenderer: RendererType): OgImageComponent | undefined {
299
+ // Normalize dot-notation to PascalCase first (NuxtSeo.takumi → NuxtSeoTakumi)
300
+ const normalized = activeComponentName.value.split('.').map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join('')
301
+ const currentBase = normalized
302
+ .replace(RE_SUFFIX_SATORI, '')
303
+ .replace(RE_SUFFIX_BROWSER, '')
304
+ .replace(RE_SUFFIX_TAKUMI, '')
305
+ return allComponents.value.find(c =>
306
+ c.renderer === targetRenderer
307
+ && (c.pascalName.replace(RE_SUFFIX_SATORI, '').replace(RE_SUFFIX_BROWSER, '').replace(RE_SUFFIX_TAKUMI, '') === currentBase),
308
+ )
309
+ }
310
+
311
+ // Check if current component is compatible with selected renderer
312
+ const isComponentCompatibleWithRenderer = computed(() => {
313
+ const component = activeComponent.value
314
+ if (!component)
315
+ return true // No component selected
316
+ return component.renderer === renderer.value
317
+ })
318
+
319
+ // Check which renderers have available components
320
+ const availableRenderers = computed(() => {
321
+ const renderers = new Set<RendererType>()
322
+ for (const c of allComponents.value) {
323
+ renderers.add(c.renderer)
324
+ }
325
+ return renderers
326
+ })
327
+
328
+ const windowSize = useWindowSize()
329
+ const sidePanelOpen = useLocalStorage('nuxt-og-image:side-panel-open', windowSize.width.value >= 1024)
330
+
331
+ watch(windowSize.width, (v) => {
332
+ if (v < 1024 && sidePanelOpen.value)
333
+ sidePanelOpen.value = false
334
+ }, {
335
+ immediate: true,
336
+ })
337
+
338
+ const isLoading = ref(false)
339
+
340
+ function generateLoadTime(payload: { timeTaken: string, sizeKb: string }) {
341
+ const extension = (imageFormat.value || '').toUpperCase()
342
+ let rendererLabel = ''
343
+ const r = renderer.value
344
+ switch (imageFormat.value) {
345
+ case 'png':
346
+ rendererLabel = r === 'satori' ? 'Satori and ReSVG' : r === 'takumi' ? 'Takumi' : 'Browser'
347
+ break
348
+ case 'jpeg':
349
+ case 'jpg':
350
+ rendererLabel = r === 'satori' ? 'Satori, ReSVG and Sharp' : r === 'takumi' ? 'Takumi' : 'Browser'
351
+ break
352
+ case 'svg':
353
+ rendererLabel = r === 'takumi' ? 'Takumi' : 'Satori'
354
+ break
355
+ }
356
+ isLoading.value = false
357
+ if (extension !== 'HTML') {
358
+ if (isCustomOgImage.value) {
359
+ description.value = `Loaded ${width.value}x${height.value} ${payload.sizeKb ? `${payload.sizeKb}kB` : ''} ${extension} in ${payload.timeTaken}ms.`
360
+ }
361
+ else {
362
+ description.value = `Generated ${width.value}x${height.value} ${payload.sizeKb ? `${payload.sizeKb}kB` : ''} ${extension} ${rendererLabel ? `with ${rendererLabel}` : ''} in ${payload.timeTaken}ms.`
363
+ }
364
+ }
365
+ else {
366
+ description.value = ''
367
+ }
368
+ }
369
+
370
+ watch([renderer, optionsOverrides], () => {
371
+ description.value = 'Loading...'
372
+ isLoading.value = true
373
+ })
374
+
375
+ function openImage() {
376
+ window.open(src.value, '_blank')
377
+ }
378
+
379
+ const pageFile = computed(() => {
380
+ const component = host.value?.route?.value?.matched?.[0]?.components?.default as { __file?: string } | undefined
381
+ return component?.__file
382
+ })
383
+
384
+ function openCurrentPageFile() {
385
+ if (pageFile.value)
386
+ host.value?.openInEditor(pageFile.value)
387
+ }
388
+
389
+ function openCurrentComponent() {
390
+ const component = componentNames.value.find(c => c.pascalName === activeComponentName.value)
391
+ if (component?.path)
392
+ host.value?.openInEditor(component.path)
393
+ }
394
+
395
+ const isPageScreenshot = computed(() => {
396
+ return activeComponentName.value === 'PageScreenshot'
397
+ })
398
+
399
+ function patchOptions(opts: OgImageOptionsInternal & { options?: unknown }) {
400
+ delete opts.options
401
+ optionsOverrides.value = defu(opts, optionsOverrides.value) as OgImageOptionsInternal
402
+ hasMadeChanges.value = true
403
+ refreshSources()
404
+ }
405
+
406
+ watch(emojis, (v) => {
407
+ if (v !== options.value?.emojis) {
408
+ patchOptions({
409
+ emojis: v,
410
+ })
411
+ }
412
+ })
413
+
414
+ const currentPageFile = computed(() => {
415
+ const component = host.value?.route?.value?.matched?.[0]?.components?.default as { __file?: string } | undefined
416
+ const path = component?.__file
417
+ return `pages/${path?.split('pages/')[1]}`
418
+ })
419
+
420
+ async function ejectComponent(component: string) {
421
+ const dir = await CreateOgImageDialogPromise.start(component)
422
+ if (!dir)
423
+ return
424
+ const v = await ogImageRpc.value!.ejectCommunityTemplate(`${dir}/${component}.vue`)
425
+ refreshSources()
426
+ if (v)
427
+ await host.value?.openInEditor(v)
428
+ }
429
+
430
+ async function createComponent() {
431
+ const result = await AddComponentDialogPromise.start()
432
+ if (!result)
433
+ return
434
+ const v = await ogImageRpc.value!.createComponent({
435
+ name: result.name,
436
+ renderer: renderer.value,
437
+ pageFile: pageFile.value || '',
438
+ })
439
+ refreshSources()
440
+ if (v)
441
+ await host.value?.openInEditor(v)
442
+ }
443
+
444
+ async function addExistingComponent(componentName: string) {
445
+ await ogImageRpc.value!.addOgImageToPage(componentName, pageFile.value || '')
446
+ refreshSources()
447
+ if (pageFile.value)
448
+ await host.value?.openInEditor(pageFile.value)
449
+ }
450
+
451
+ async function resetProps(fetch = true) {
452
+ if (fetch)
453
+ await refreshPathDebug()
454
+ optionsOverrides.value = {}
455
+ hasMadeChanges.value = false
456
+ if (fetch)
457
+ refreshSources()
458
+ }
459
+
460
+ function updateProps(props: Record<string, any>) {
461
+ optionsOverrides.value = defu({ props }, optionsOverrides.value)
462
+ hasMadeChanges.value = true
463
+ refreshSources()
464
+ }
465
+
466
+ return {
467
+ // Data
468
+ globalDebug,
469
+ debug,
470
+ isDebugLoading,
471
+ error,
472
+ emojis,
473
+
474
+ // Computed
475
+ selectedOgImage,
476
+ currentOptions,
477
+ isCustomOgImage,
478
+ isValidDebugError,
479
+ hasDefinedOgImage,
480
+ fetchError,
481
+ defaults,
482
+ height,
483
+ width,
484
+ aspectRatio,
485
+ imageFormat,
486
+ socialPreview,
487
+ imageColorMode,
488
+ src,
489
+ whatsappInlineSrc,
490
+ socialPreviewTitle,
491
+ socialPreviewDescription,
492
+ socialSiteUrl,
493
+ slackSocialPreviewSiteName,
494
+ activeComponentName,
495
+ activeComponent,
496
+ activeComponentRelativePath,
497
+ isOgImageTemplate,
498
+ renderer,
499
+ allComponents,
500
+ componentNames,
501
+ communityComponents,
502
+ appComponents,
503
+ isComponentCompatibleWithRenderer,
504
+ getComponentVariantForRenderer,
505
+ availableRenderers,
506
+ sidePanelOpen,
507
+ isLoading,
508
+ pageFile,
509
+ isPageScreenshot,
510
+ currentPageFile,
511
+ allImageKeys,
512
+
513
+ // Methods
514
+ toggleSocialPreview,
515
+ generateLoadTime,
516
+ openImage,
517
+ openCurrentPageFile,
518
+ openCurrentComponent,
519
+ patchOptions,
520
+ ejectComponent,
521
+ createComponent,
522
+ addExistingComponent,
523
+ resetProps,
524
+ updateProps,
525
+
526
+ // Re-export from util/logic
527
+ description,
528
+ hasMadeChanges,
529
+ options,
530
+ optionsOverrides,
531
+ propEditor,
532
+ ogImageKey,
533
+ refreshSources,
534
+ slowRefreshSources,
535
+ }
536
+ }
@@ -0,0 +1,3 @@
1
+ import { createTemplatePromise } from '@vueuse/core'
2
+
3
+ export const RendererSelectDialogPromise = createTemplatePromise<void>()
@@ -0,0 +1,2 @@
1
+ export type ClientFunctions = any
2
+ export type ServerFunctions = any
@@ -0,0 +1,73 @@
1
+ import type { BirpcReturn } from 'birpc'
2
+ import type { DevtoolsHost } from 'nuxtseo-layer-devtools/composables/host'
3
+ import type { ClientFunctions, ServerFunctions } from './rpc-types'
4
+ import { appFetch, useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'
5
+ import { base, path, refreshSources } from 'nuxtseo-layer-devtools/composables/state'
6
+ import { computed, ref } from 'vue'
7
+
8
+ export const host = ref<DevtoolsHost>()
9
+
10
+ export const ogImageRpc = ref<BirpcReturn<ServerFunctions>>()
11
+
12
+ // Connection state tracking
13
+ const connectionState = ref<'connecting' | 'connected' | 'fallback' | 'failed'>('connecting')
14
+ export const isConnectionFailed = computed(() => connectionState.value === 'failed')
15
+ export const isFallbackMode = computed(() => connectionState.value === 'fallback')
16
+
17
+ // Fallback fetch for localhost:3000
18
+ async function tryFallbackConnection() {
19
+ const fallbackUrl = 'http://localhost:3000'
20
+ const res = await fetch(`${fallbackUrl}/_og/debug.json`).catch(() => null)
21
+ if (res?.ok) {
22
+ appFetch.value = ((url: string, opts?: any) => fetch(`${fallbackUrl}${url}`, opts).then(r => r.json())) as any
23
+ base.value = '/'
24
+ path.value = '/'
25
+ connectionState.value = 'fallback'
26
+ return true
27
+ }
28
+ return false
29
+ }
30
+
31
+ let timer: null | NodeJS.Timeout = null
32
+
33
+ // Set timeout for connection - if not connected within 2s, try fallback
34
+ onMounted(() => {
35
+ timer = setTimeout(async () => {
36
+ if (connectionState.value === 'connecting') {
37
+ const fallbackWorked = await tryFallbackConnection()
38
+ if (!fallbackWorked) {
39
+ connectionState.value = 'failed'
40
+ }
41
+ }
42
+ timer = null
43
+ }, 2000)
44
+
45
+ onUnmounted(() => {
46
+ if (timer) {
47
+ clearTimeout(timer)
48
+ }
49
+ })
50
+ })
51
+
52
+ useDevtoolsConnection({
53
+ onConnected(connectedHost) {
54
+ if (timer) {
55
+ clearTimeout(timer)
56
+ }
57
+ connectionState.value = 'connected'
58
+ host.value = connectedHost
59
+ ogImageRpc.value = connectedHost.rpc<ServerFunctions, ClientFunctions>('nuxt-og-image', {
60
+ refreshRouteData(path: string) {
61
+ const file = connectedHost.route?.value?.matched?.[0]?.components?.default?.__file as string | undefined
62
+ if (file?.includes(path) || path.endsWith('.md'))
63
+ refreshSources()
64
+ },
65
+ refresh() {
66
+ refreshSources()
67
+ },
68
+ refreshGlobalData() {
69
+ refreshSources()
70
+ },
71
+ })
72
+ },
73
+ })
@@ -0,0 +1,10 @@
1
+ // Devtools-only contract types, kept local so the shipped layer has no dependency on
2
+ // the module's src (published packages ship dist, not src).
3
+ export type DevToolsMetaDataExtraction = any
4
+ export type FontConfig = any
5
+ export type OgImageComponent = any
6
+ export type OgImageOptions = any
7
+ export type OgImageOptionsInternal = any
8
+ export type OgImageRuntimeConfig = any
9
+ export type RendererType = any
10
+ export type RuntimeCompatibilitySchema = any