Skip to content

Commit fad35f7

Browse files
committedNov 23, 2024
feat: support previewing existing og images
Closes #129
1 parent 284890e commit fad35f7

File tree

3 files changed

+101
-18
lines changed

3 files changed

+101
-18
lines changed
 

‎client/app.vue

+70-14
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useLocalStorage, useWindowSize } from '@vueuse/core'
1212
import defu from 'defu'
1313
import JsonEditorVue from 'json-editor-vue'
1414
import { Pane, Splitpanes } from 'splitpanes'
15-
import { joinURL, parseURL, withHttps, withQuery } from 'ufo'
15+
import { hasProtocol, joinURL, parseURL, withHttps, withQuery } from 'ufo'
1616
import { ref } from 'vue'
1717
import { fetchGlobalDebug } from '~/composables/fetch'
1818
import { devtoolsClient } from '~/composables/rpc'
@@ -42,20 +42,13 @@ await loadShiki()
4242
4343
const { data: globalDebug } = fetchGlobalDebug()
4444
45-
// const clientPath = computed(() => devtoolsClient.value?.host.nuxt.vueApp.config?.globalProperties?.$route?.path || undefined)
46-
// path.value = clientPath.value || useRoute().query.path as string || '/'
47-
// base.value = useRoute().query.base as string || '/'
48-
// watch(() => clientPath, (v) => {
49-
// optionsOverrides.value = {}
50-
// propsEdited.value = false
51-
// path.value = v
52-
// })
53-
5445
const emojis = ref('noto')
5546
5647
const debugAsyncData = fetchPathDebug()
5748
const { data: debug, pending, error } = debugAsyncData
58-
49+
const isCustomOgImage = computed(() => {
50+
return debug.value?.custom
51+
})
5952
watch(debug, (val) => {
6053
if (!val)
6154
return
@@ -127,6 +120,12 @@ const src = computed(() => {
127120
// wait until we know what we're rendering
128121
if (!debug.value)
129122
return ''
123+
if (isCustomOgImage.value) {
124+
if (hasProtocol(debug.value.options.url, { acceptRelative: true })) {
125+
return debug.value.options.url
126+
}
127+
return joinURL(host.value, debug.value.options.url)
128+
}
130129
return withQuery(joinURL(host.value, '/__og-image__/image', path.value, `/og.${imageFormat.value}`), {
131130
timestamp: refreshTime.value,
132131
...optionsOverrides.value,
@@ -148,7 +147,11 @@ const socialPreviewDescription = computed(() => {
148147
149148
const socialSiteUrl = computed(() => {
150149
// need to turn this URL into just an origin
151-
return parseURL(debug.value?.siteConfig?.url || '/').host || debug.value?.siteConfig?.url || '/'
150+
const url = parseURL(debug.value?.siteConfig?.url || '/').host || debug.value?.siteConfig?.url || '/'
151+
if (url === '/') {
152+
return globalDebug.value?.siteConfigUrl
153+
}
154+
return url
152155
})
153156
const slackSocialPreviewSiteName = computed(() => {
154157
return options.value?.socialPreview?.og.site_name || socialSiteUrl.value
@@ -375,7 +378,60 @@ const currentPageFile = computed(() => {
375378
<div class="flex-row flex p4 h-full" style="min-height: calc(100vh - 64px);">
376379
<main class="mx-auto flex flex-col w-full">
377380
<div v-if="tab === 'design'" class="h-full relative max-h-full">
378-
<div v-if="error">
381+
<div v-if="isCustomOgImage" class="w-full flex h-full justify-center items-center relative pr-4" style="padding-top: 30px;">
382+
<div class="flex justify-between items-center text-sm w-full absolute pr-[30px] top-0 left-0">
383+
<div class="text-xs">
384+
Your prebuilt OG Image: {{ debug?.options.url }}
385+
</div>
386+
<div class="flex items-center w-[100px]">
387+
<NButton icon="carbon:drag-horizontal" :border="!socialPreview" @click="toggleSocialPreview()" />
388+
<NButton icon="logos:twitter" :border="socialPreview === 'twitter'" @click="toggleSocialPreview('twitter')" />
389+
<NButton icon="logos:slack-icon" :border="socialPreview === 'slack'" @click="toggleSocialPreview('slack')" />
390+
</div>
391+
</div>
392+
<TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="socialPreviewTitle">
393+
<template #domain>
394+
<a target="_blank" :href="withHttps(socialSiteUrl)">From {{ socialSiteUrl }}</a>
395+
</template>
396+
<ImageLoader
397+
:src="src"
398+
:aspect-ratio="aspectRatio"
399+
@load="generateLoadTime"
400+
@click="openImage"
401+
@refresh="refreshSources"
402+
/>
403+
</TwitterCardRenderer>
404+
<SlackCardRenderer v-else-if="socialPreview === 'slack'">
405+
<template #favIcon>
406+
<img :src="`https://www.google.com/s2/favicons?domain=${encodeURIComponent(socialSiteUrl)}&sz=30`">
407+
</template>
408+
<template #siteName>
409+
{{ slackSocialPreviewSiteName }}
410+
</template>
411+
<template #title>
412+
{{ socialPreviewTitle }}
413+
</template>
414+
<template #description>
415+
{{ socialPreviewDescription }}
416+
</template>
417+
<ImageLoader
418+
:src="src"
419+
class="!h-[300px]"
420+
:aspect-ratio="aspectRatio"
421+
@load="generateLoadTime"
422+
@refresh="refreshSources"
423+
/>
424+
</SlackCardRenderer>
425+
<div v-else class="w-full h-full">
426+
<ImageLoader
427+
:src="src"
428+
:aspect-ratio="aspectRatio"
429+
@load="generateLoadTime"
430+
@refresh="refreshSources"
431+
/>
432+
</div>
433+
</div>
434+
<div v-else-if="error">
379435
<div v-if="error.message.includes('missing the Nuxt OG Image payload') || error.message.includes('Got invalid response')">
380436
<!-- nicely tell the user they should use defineOgImage to get started -->
381437
<div class="flex flex-col items-center justify-center mx-auto max-w-135 h-85vh">
@@ -460,7 +516,7 @@ const currentPageFile = computed(() => {
460516
</TwitterCardRenderer>
461517
<SlackCardRenderer v-else-if="socialPreview === 'slack'">
462518
<template #favIcon>
463-
<img :src="`${socialSiteUrl?.includes('localhost') ? 'http' : 'https'}://${socialSiteUrl}/favicon.ico`">
519+
<img :src="`https://www.google.com/s2/favicons?domain=${encodeURIComponent(socialSiteUrl)}&sz=30`">
464520
</template>
465521
<template #siteName>
466522
{{ slackSocialPreviewSiteName }}

‎client/composables/fetch.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1+
import type { V as VueHeadClient } from '@unhead/vue/dist/shared/vue.71760da0'
12
import type { OgImageComponent, OgImageOptions, OgImageRuntimeConfig } from '../../src/runtime/types'
2-
import { appFetch, useAsyncData } from '#imports'
3+
import { appFetch, devtoolsClient, useAsyncData } from '#imports'
34
import { joinURL } from 'ufo'
45
import { globalRefreshTime, optionsOverrides, path, refreshTime } from '~/util/logic'
56

67
export function fetchPathDebug() {
7-
return useAsyncData<{ siteConfig: { url?: string }, options: OgImageOptions, vnodes: Record<string, any> }>(() => {
8+
return useAsyncData<{ siteConfig: { url?: string }, options: OgImageOptions, vnodes: Record<string, any> }>(async () => {
89
if (!appFetch.value)
910
return { siteCofig: {}, options: {}, vnodes: {} }
11+
12+
const clientHead = devtoolsClient.value?.host.nuxt.vueApp.config?.globalProperties?.$head as VueHeadClient
13+
const tags = await clientHead?.resolveTags() || []
14+
const ogImageSrc = tags.find(d => d._d === 'meta:property:og:image')?.props.content
15+
if (ogImageSrc && !ogImageSrc.startsWith('/__og-image__/image')) {
16+
// generate the social
17+
return {
18+
siteConfig: {},
19+
options: {
20+
url: ogImageSrc,
21+
socialPreview: {
22+
og: {
23+
title: tags.find(d => d._d === 'meta:property:og:title')?.props.content,
24+
description: tags.find(d => d._d === 'meta:property:og:description')?.props.content,
25+
},
26+
twitter: {
27+
title: tags.find(d => d._d === 'meta:name:twitter:title')?.props.content,
28+
description: tags.find(d => d._d === 'meta:name:twitter:description')?.props.content,
29+
},
30+
},
31+
},
32+
vnodes: {},
33+
custom: true,
34+
}
35+
}
1036
return appFetch.value(joinURL('/__og-image__/image', path.value, 'og.json'), {
1137
query: optionsOverrides.value,
1238
})

‎src/runtime/server/routes/debug.json.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import { useSiteConfig } from '#imports'
12
// @ts-expect-error virtual module
23
import compatibility from '#og-image/compatibility'
4+
35
// @ts-expect-error untyped
46
import { componentNames } from '#og-image-virtual/component-names.mjs'
57

68
import { defineEventHandler, setHeader } from 'h3'
7-
89
import { useOgImageRuntimeConfig } from '../../shared'
910

1011
export default defineEventHandler(async (e) => {
1112
// set json header
1213
setHeader(e, 'Content-Type', 'application/json')
1314
const runtimeConfig = useOgImageRuntimeConfig()
14-
1515
return {
16+
siteConfigUrl: useSiteConfig(e).url,
1617
componentNames,
1718
runtimeConfig,
1819
compatibility,

0 commit comments

Comments
 (0)
Please sign in to comment.