Skip to content

Commit 3d3e993

Browse files
authoredMar 26, 2025··
feat(devtools): whatsapp and updated X (twitter) (#347)
* chore: safer class check * chore: broken build * chore: broken build
1 parent ad31b65 commit 3d3e993

9 files changed

+335
-1226
lines changed
 

‎client/app.vue

+52-23
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const slackSocialPreviewSiteName = computed(() => {
161161
})
162162
163163
function toggleSocialPreview(preview?: string) {
164-
if (!preview || preview === socialPreview.value)
164+
if (!preview)
165165
socialPreview.value = ''
166166
else
167167
socialPreview.value = preview!
@@ -380,16 +380,9 @@ async function ejectComponent(component: string) {
380380
</VTooltip>
381381
</div>
382382
<div class="items-center space-x-3 hidden lg:flex">
383-
<div class="opacity-80 text-sm">
384-
<NLink href="https://github.com/sponsors/harlan-zw" target="_blank">
385-
<NIcon icon="carbon:favorite" class="mr-[2px]" />
386-
Sponsor
387-
</NLink>
388-
</div>
389383
<div class="opacity-80 text-sm">
390384
<NLink href="https://github.com/nuxt-modules/og-image" target="_blank">
391-
<NIcon icon="logos:github-icon" class="mr-[2px]" />
392-
Submit an issue
385+
GitHub
393386
</NLink>
394387
</div>
395388
<a href="https://nuxtseo.com" target="_blank" class="flex items-end gap-1.5 font-semibold text-xl dark:text-white font-title">
@@ -408,13 +401,13 @@ async function ejectComponent(component: string) {
408401
</div>
409402
<div class="flex items-center w-[100px]">
410403
<NButton icon="carbon:drag-horizontal" :border="!socialPreview" @click="toggleSocialPreview()" />
411-
<NButton icon="logos:twitter" :border="socialPreview === 'twitter'" @click="toggleSocialPreview('twitter')" />
404+
<NButton icon="fa6-brands:x-twitter" :border="socialPreview === 'twitter'" @click="toggleSocialPreview('twitter')" />
412405
<NButton icon="logos:slack-icon" :border="socialPreview === 'slack'" @click="toggleSocialPreview('slack')" />
413406
</div>
414407
</div>
415408
<TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="socialPreviewTitle">
416409
<template #domain>
417-
<a target="_blank" :href="withHttps(socialSiteUrl)">From {{ socialSiteUrl }}</a>
410+
<a target="_blank" :href="withHttps(socialSiteUrl)">{{ socialSiteUrl }}</a>
418411
</template>
419412
<ImageLoader
420413
:src="src"
@@ -485,10 +478,10 @@ async function ejectComponent(component: string) {
485478
<Pane size="60" class="flex h-full justify-center items-center relative n-panel-grids-center pr-4" style="padding-top: 30px;">
486479
<div class="flex justify-between items-center text-sm w-full absolute pr-[30px] top-0 left-0">
487480
<div class="flex items-center text-lg space-x-1 w-[100px]">
488-
<NButton v-if="!!globalDebug?.compatibility?.sharp || renderer === 'chromium'" icon="carbon:jpg" :border="imageFormat === 'jpeg' || imageFormat === 'jpg'" @click="patchOptions({ extension: 'jpg' })" />
489-
<NButton icon="carbon:png" :border="imageFormat === 'png'" @click="patchOptions({ extension: 'png' })" />
490-
<NButton v-if="renderer !== 'chromium'" icon="carbon:svg" :border="imageFormat === 'svg'" @click="patchOptions({ extension: 'svg' })" />
491-
<NButton v-if="!isPageScreenshot" icon="carbon:html" :border="imageFormat === 'html'" @click="patchOptions({ extension: 'html' })" />
481+
<NButton v-if="!!globalDebug?.compatibility?.sharp || renderer === 'chromium'" icon="carbon:jpg" :class="imageFormat === 'jpeg' || imageFormat === 'jpg' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'jpg' })" />
482+
<NButton icon="carbon:png" :class="imageFormat === 'png' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'png' })" />
483+
<NButton v-if="renderer !== 'chromium'" icon="carbon:svg" :class="imageFormat === 'svg' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'svg' })" />
484+
<NButton v-if="!isPageScreenshot" icon="carbon:html" :class="imageFormat === 'html' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'html' })" />
492485
</div>
493486
<div class="text-xs">
494487
<div v-if="!isPageScreenshot" class="opacity-70 space-x-1 hover:opacity-90 transition cursor-pointer">
@@ -500,13 +493,18 @@ async function ejectComponent(component: string) {
500493
Screenshot of the current page.
501494
</div>
502495
</div>
503-
<div class="flex items-center w-[100px]">
504-
<NButton icon="carbon:drag-horizontal" :border="!socialPreview" @click="toggleSocialPreview()" />
505-
<NButton icon="logos:twitter" :border="socialPreview === 'twitter'" @click="toggleSocialPreview('twitter')" />
496+
<div class="flex items-center space-x-1">
497+
<VTooltip v-if="!isCustomOgImage">
498+
<NButton class="p-4" icon="carbon:drag-horizontal" :class="!socialPreview ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="toggleSocialPreview()" />
499+
<template #popper>
500+
Preview full width
501+
</template>
502+
</VTooltip>
503+
<NButton class="p-4" :class="socialPreview === 'twitter' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" icon="simple-icons:x" @click="toggleSocialPreview('twitter')" />
506504
<!-- <NButton icon="logos:facebook" :border="socialPreview === 'facebook'" @click="socialPreview = 'facebook'" /> -->
507-
<NButton icon="logos:slack-icon" :border="socialPreview === 'slack'" @click="toggleSocialPreview('slack')" />
508-
<!-- <NButton icon="logos:whatsapp-icon" :border="socialPreview === 'discord'" @click="socialPreview = 'discord'" /> -->
509-
<VTooltip>
505+
<NButton class="p-4" :class="socialPreview === 'slack' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" icon="simple-icons:slack" @click="toggleSocialPreview('slack')" />
506+
<NButton class="p-4" :class="socialPreview === 'whatsapp' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" icon="simple-icons:whatsapp" @click="toggleSocialPreview('whatsapp')" />
507+
<VTooltip v-if="!isCustomOgImage">
510508
<button text-lg="" type="button" class=" n-icon-button n-button n-transition n-disabled:n-disabled" @click="sidePanelOpen = !sidePanelOpen">
511509
<div v-if="sidePanelOpen" class="n-icon carbon:side-panel-open" />
512510
<div v-else class="n-icon carbon:open-panel-right" />
@@ -517,7 +515,7 @@ async function ejectComponent(component: string) {
517515
</VTooltip>
518516
</div>
519517
</div>
520-
<TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="socialPreviewTitle">
518+
<TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="socialPreviewTitle" :aspect-ratio="aspectRatio">
521519
<template #domain>
522520
<a target="_blank" :href="withHttps(socialSiteUrl)">From {{ socialSiteUrl }}</a>
523521
</template>
@@ -567,6 +565,37 @@ async function ejectComponent(component: string) {
567565
@refresh="refreshSources"
568566
/>
569567
</SlackCardRenderer>
568+
<WhatsAppRenderer v-else-if="socialPreview === 'whatsapp'">
569+
<template #siteName>
570+
{{ slackSocialPreviewSiteName }}
571+
</template>
572+
<template #title>
573+
{{ socialPreviewTitle }}
574+
</template>
575+
<template #description>
576+
{{ socialPreviewDescription }}
577+
</template>
578+
<template #url>
579+
{{ socialSiteUrl }}
580+
</template>
581+
<ImageLoader
582+
v-if="imageFormat !== 'html'"
583+
:src="src"
584+
class="!h-[90px]"
585+
min-height="90"
586+
:aspect-ratio="1"
587+
style="background-size: cover; background-position: center center;"
588+
@load="generateLoadTime"
589+
@refresh="refreshSources"
590+
/>
591+
<IFrameLoader
592+
v-else
593+
:src="src"
594+
:aspect-ratio="1 / 1"
595+
@load="generateLoadTime"
596+
@refresh="refreshSources"
597+
/>
598+
</WhatsAppRenderer>
570599
<div v-else class="w-full h-full">
571600
<ImageLoader
572601
v-if="imageFormat !== 'html'"
@@ -583,7 +612,7 @@ async function ejectComponent(component: string) {
583612
@refresh="refreshSources"
584613
/>
585614
</div>
586-
<div v-if="description" class="mt-3 text-sm opacity-50 absolute bottom-3">
615+
<div v-if="description" class="mt-5 text-sm opacity-50">
587616
{{ description }}
588617
</div>
589618
</Pane>

‎client/components/ImageLoader.vue

+8-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const props = defineProps<{
66
aspectRatio: number
77
maxHeight?: number
88
maxWidth?: number
9+
minHeight?: number
910
}>()
1011
// emits a load event
1112
const emit = defineEmits(['load'])
@@ -20,6 +21,10 @@ function setSource(src: string) {
2021
lastSrc.value = src
2122
loading.value = true
2223
img.style.backgroundImage = ''
24+
img.style.backgroundRepeat = 'no-repeat'
25+
img.style.backgroundSize = 'contain'
26+
img.style.backgroundPosition = 'center'
27+
img.style.maxWidth = '1200px'
2328
const now = Date.now()
2429
// we want to do a fetch of the image so we can get the size of it in kb
2530
$fetch.raw(src, {
@@ -52,22 +57,19 @@ onMounted(() => {
5257
</script>
5358

5459
<template>
55-
<div ref="image" :style="{ aspectRatio }">
60+
<div ref="image" :style="{ aspectRatio, minHeight }">
5661
<NLoading v-if="loading" />
5762
</div>
5863
</template>
5964

6065
<style scoped>
6166
div {
6267
cursor: pointer;
63-
max-height: 600px;
64-
height: auto;
65-
width: auto;
68+
height: 100%;
6669
margin: 0 auto;
67-
max-width: 1200px;
70+
width: 100%;
6871
transition: 0.4s ease-in-out;
6972
background-color: white;
7073
background-size: contain;
71-
aspect-ratio: 2 / 1
7274
}
7375
</style>

‎client/components/SlackCardRenderer.vue

+4-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
word-wrap: break-word;
4343
-webkit-font-smoothing: antialiased;
4444
text-rendering: optimizeLegibility;
45-
height: 300px;
45+
min-height: 300px;
4646
}
4747
.siteName {
4848
font-weight: bold;
@@ -51,7 +51,9 @@
5151
word-wrap: break-word;
5252
}
5353
.description {
54-
color: #2c2d30;
54+
color: white;
55+
opacity: 0.7;
56+
margin-bottom: 8px;
5557
}
5658
.title {
5759
color: #0576b9;

‎client/components/TemplateComponentPreview.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const props = defineProps<{
1010
component: OgImageComponent
1111
active: boolean
1212
imageFormat: string
13+
width: string
14+
height: string
1315
}>()
1416
1517
function openComponent() {
@@ -39,7 +41,7 @@ const loadStats = ref<{ timeTaken: string, sizeKb: string }>()
3941
</div>
4042
<div class="border-2 group-hover:shadow-sm rounded-[0.35rem] border-transparent hover:border-yellow-500 transition-all">
4143
<VTooltip>
42-
<div class="w-[300px] h-[150px] relative">
44+
<div class="h-[150px] relative" :style="{ aspectRatio }">
4345
<NIcon v-if="active" icon="carbon:checkmark-filled" class="absolute top-2 right-2 text-green-500" />
4446
<ImageLoader
4547
v-if="!isHtml"
+49-63
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,60 @@
11
<script setup lang="ts">
22
defineProps<{
3+
aspectRatio: number
34
title?: string
45
}>()
6+
7+
const currTime = computed(() => {
8+
// need to return in format 2:17 AM · Mar 25, 2025
9+
const date = new Date()
10+
const options: Intl.DateTimeFormatOptions = {
11+
hour: 'numeric',
12+
minute: 'numeric',
13+
hour12: true,
14+
month: 'short',
15+
day: '2-digit',
16+
year: 'numeric',
17+
}
18+
const formatter = new Intl.DateTimeFormat('en-US', options)
19+
const parts = formatter.formatToParts(date)
20+
const time = `${parts.find(part => part.type === 'hour')?.value}:${
21+
parts.find(part => part.type === 'minute')?.value} ${
22+
parts.find(part => part.type === 'dayPeriod')?.value} · ${
23+
parts.find(part => part.type === 'month')?.value} ${
24+
parts.find(part => part.type === 'day')?.value}, ${
25+
parts.find(part => part.type === 'year')?.value}`
26+
return time
27+
})
528
</script>
629

730
<template>
8-
<div class="root max-h-full relative flex">
9-
<div class="max-h-full border-1 border-solid border-[#cfd9de] rounded-[16px] overflow-hidden">
10-
<div class="image-wrap">
11-
<slot />
31+
<div class="root max-h-full relative flex flex-col">
32+
<div class="w-[600px] mx-auto">
33+
<div class="w-full flex items-start flex-col space-x-3">
34+
<div class="w-full">
35+
<div class="w-full">
36+
<div class="border border-gray-300 dark:border-gray-700 rounded-xl overflow-hidden">
37+
<div class="-mx-px" :style="{ aspectRatio }">
38+
<slot />
39+
</div>
40+
<div class="px-2 py-1">
41+
<p class="opacity-50 text-sm">
42+
<slot name="domain" />
43+
</p>
44+
<p class="">
45+
<slot name="title">
46+
{{ title }}
47+
</slot>
48+
</p>
49+
</div>
50+
</div>
51+
52+
<p class="text-gray-500 mt-3 text-sm">
53+
{{ currTime }}
54+
</p>
55+
</div>
56+
</div>
1257
</div>
1358
</div>
14-
<div v-if="title" class="title absolute bottom-3 z-2 left-3 text-white">
15-
<slot name="title">
16-
{{ title }}
17-
</slot>
18-
</div>
19-
<div class="domain absolute -bottom-5 z-2 left-0">
20-
<slot name="domain" />
21-
</div>
2259
</div>
2360
</template>
24-
25-
<style scoped>
26-
.image-wrap {
27-
aspect-ratio: 2 / 1;
28-
background-color: rgba(0, 0, 0, 0.1);
29-
opacity: 1;
30-
height: 300px;
31-
}
32-
.domain:hover {
33-
text-decoration: underline;
34-
}
35-
.domain {
36-
line-height: 16px;
37-
cursor: pointer;
38-
min-width: 0px;
39-
word-wrap: break-word;
40-
text-overflow: unset;
41-
color: rgb(83, 100, 113);
42-
font-weight: 400;
43-
background-color: rgba(0,0,0,0.00);
44-
border: 0 solid black;
45-
box-sizing: border-box;
46-
display: inline;
47-
font: 13px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
48-
list-style: none;
49-
margin: 0px;
50-
padding: 0px;
51-
text-align: inherit;
52-
text-decoration: none;
53-
white-space: pre-wrap;
54-
}
55-
.title {
56-
word-wrap: break-word;
57-
text-overflow: unset;
58-
min-width: 0px;
59-
line-height: 16px;
60-
font-weight: 400;
61-
user-select: none;
62-
font-size: 13px;
63-
text-align: center;
64-
height: 20px;
65-
padding: 0 4px;
66-
border-radius: 4px;
67-
background-color: rgba(0, 0, 0, 0.77);
68-
}
69-
.root {
70-
outline-style: none;
71-
cursor: pointer;
72-
transition-duration: 0.2s;
73-
}
74-
</style>
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
</script>
3+
4+
<template>
5+
<div style="max-width: 600px; background-color: #00574B; font-size: 14.2px; line-height: 19px; color: white; padding: 6px; border-radius: 7.5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
6+
<div class="flex items-center" style="background-color: #025144; border-radius: 8px; overflow: hidden;">
7+
<div style="aspect-ratio: 1/1;">
8+
<slot />
9+
</div>
10+
<div style="padding: 6px 10px;">
11+
<div class="font-bold" style="-webkit-line-clamp: 2; color: rgba(233, 237, 239, 0.88)">
12+
<slot name="title" />
13+
</div>
14+
<div style="font-size: .75rem; -webkit-line-clamp: 2; color: rgba(233, 237, 239, 0.88)">
15+
<slot name="description" />
16+
</div>
17+
<div style="font-size: .75rem; color: rgba(233, 237, 239, 0.3)">
18+
<slot name="url" />
19+
</div>
20+
</div>
21+
</div>
22+
<div style="padding: 6px 10px;">
23+
When someone sends quite a long message with a link that has an og:image, the og:image is made into a square. For example <span style="color: #53bdeb"><slot name="url" /></span>
24+
</div>
25+
</div>
26+
</template>

‎client/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
"private": true,
44
"devDependencies": {
55
"@iconify-json/carbon": "^1.2.8",
6-
"@nuxt/devtools-kit": "2.1.0",
7-
"@nuxt/devtools-ui-kit": "2.1.0",
6+
"@iconify-json/simple-icons": "^1.2.29",
7+
"@nuxt/devtools-kit": "^2.3.2",
8+
"@nuxt/devtools-ui-kit": "^2.3.2",
89
"@nuxt/kit": "^3.16.1",
910
"@vueuse/core": "^13.0.0",
1011
"floating-vue": "^5.2.2",
1112
"json-editor-vue": "^0.18.1",
12-
"nuxt": "3.16.0",
13+
"nuxt": "^3.16.1",
1314
"shiki": "^3.2.1",
1415
"splitpanes": "^4.0.3",
1516
"vue": "^3.5.13",

‎pnpm-lock.yaml

+187-1,128
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/runtime/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface OgImageRuntimeConfig {
4444
strictNuxtContentPaths: boolean
4545
zeroRuntime: boolean
4646

47+
componentDirs?: string[]
48+
4749
app: {
4850
baseURL: string
4951
}

0 commit comments

Comments
 (0)
Please sign in to comment.