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.
- package/dist/chunks/tw4.cjs +1 -1
- package/dist/chunks/tw4.mjs +1 -1
- package/dist/chunks/uno.cjs +1 -1
- package/dist/chunks/uno.mjs +1 -1
- package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
- package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
- package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
- package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
- package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
- package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
- package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
- package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
- package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
- package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
- package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
- package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
- package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
- package/dist/devtools/lib/og-image/keys.ts +8 -0
- package/dist/devtools/lib/og-image/og-image.ts +536 -0
- package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
- package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
- package/dist/devtools/lib/og-image/rpc.ts +73 -0
- package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
- package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
- package/dist/devtools/lib/og-image/shared.ts +30 -0
- package/dist/devtools/lib/og-image/templates.ts +9 -0
- package/dist/devtools/lib/og-image/types.ts +38 -0
- package/dist/devtools/lib/og-image/util/logic.ts +21 -0
- package/dist/devtools/nuxt.config.ts +6 -0
- package/dist/devtools/pages/og-image/debug.vue +32 -0
- package/dist/devtools/pages/og-image/docs.vue +3 -0
- package/dist/devtools/pages/og-image/index.vue +1682 -0
- package/dist/devtools/pages/og-image/templates.vue +150 -0
- package/dist/devtools/pages/og-image.vue +184 -0
- package/dist/module.cjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
- package/dist/runtime/server/og-image/core/style-attr.d.ts +8 -0
- package/dist/runtime/server/og-image/core/style-attr.js +34 -0
- package/dist/runtime/server/og-image/core/vnodes.d.ts +2 -1
- package/dist/runtime/server/og-image/core/vnodes.js +3 -27
- package/dist/shared/{nuxt-og-image.CgPzmzQY.cjs → nuxt-og-image.CfTPCtaS.cjs} +38 -24
- package/dist/shared/{nuxt-og-image.DGAMxBol.mjs → nuxt-og-image.DdbTs-xp.mjs} +37 -23
- package/package.json +17 -18
- package/dist/devtools/200.html +0 -1
- package/dist/devtools/404.html +0 -1
- package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
- package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
- package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
- package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
- package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
- package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
- package/dist/devtools/_nuxt/B9jrmesR.js +0 -1
- package/dist/devtools/_nuxt/BA-4cUNc.js +0 -1
- package/dist/devtools/_nuxt/BOEXnX7x.js +0 -3
- package/dist/devtools/_nuxt/BWKJ0Uxb.js +0 -30
- package/dist/devtools/_nuxt/Bdtz4ZK9.js +0 -4
- package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
- package/dist/devtools/_nuxt/C5jzcy9i.js +0 -1
- package/dist/devtools/_nuxt/CCTv7mmB.js +0 -1
- package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
- package/dist/devtools/_nuxt/C_JnDlx-.js +0 -1
- package/dist/devtools/_nuxt/CdmciVPJ.js +0 -1
- package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
- package/dist/devtools/_nuxt/CuJezOxb.js +0 -1
- package/dist/devtools/_nuxt/D4EkL0XJ.js +0 -6
- package/dist/devtools/_nuxt/DKaW7clv.js +0 -1
- package/dist/devtools/_nuxt/DSl3mlLY.js +0 -2
- package/dist/devtools/_nuxt/DYta7Mdi.js +0 -3
- package/dist/devtools/_nuxt/DevtoolsSection.CcOMr_vO.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.BhrTdbXn.css +0 -1
- package/dist/devtools/_nuxt/E8AZ6HoH.js +0 -1
- package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
- package/dist/devtools/_nuxt/O5eLyffU.js +0 -1
- package/dist/devtools/_nuxt/builds/latest.json +0 -1
- package/dist/devtools/_nuxt/builds/meta/a36bc212-7179-4aa2-a534-6366229bd7b1.json +0 -1
- package/dist/devtools/_nuxt/entry.CcrXpo2y.css +0 -2
- package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
- package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
- package/dist/devtools/_nuxt/pages.Ci_xvfJ8.css +0 -1
- package/dist/devtools/_nuxt/renderer-select.COGJ4ZQe.css +0 -1
- package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
- package/dist/devtools/_nuxt/wMBdlVF-.js +0 -152
- package/dist/devtools/debug/index.html +0 -1
- package/dist/devtools/docs/index.html +0 -1
- package/dist/devtools/index.html +0 -1
- 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,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
|