Skip to content

Commit

Permalink
feat: add open graph tab (#209)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
arashsheyda and antfu committed May 8, 2023
1 parent ea66558 commit b94de30
Show file tree
Hide file tree
Showing 32 changed files with 1,034 additions and 147 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -31,7 +31,7 @@
"@unocss/eslint-config": "^0.51.12",
"bumpp": "^9.1.0",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^8.40.0",
"eslint": "8.39.0",
"esno": "^0.16.3",
"execa": "^7.1.1",
"lint-staged": "^13.2.2",
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools-ui-kit/src/assets/styles.css
Expand Up @@ -11,3 +11,7 @@ html.dark {
background-color: #151515;
color: white;
}

::selection {
background: #8884;
}
3 changes: 2 additions & 1 deletion packages/devtools-ui-kit/src/components/NSectionBlock.vue
Expand Up @@ -7,6 +7,7 @@ const props = withDefaults(
text: string
description?: string
containerClass?: string
headerClass?: string
collapse?: boolean
open?: boolean
padding?: boolean | string
Expand All @@ -27,7 +28,7 @@ function onToggle(e: any) {
<template>
<details :open="open" @toggle="onToggle">
<summary class="cursor-pointer select-none hover:bg-active p4" :class="collapse ? '' : 'pointer-events-none'">
<NIconTitle :icon="icon" :text="text" text-xl transition :class="open ? 'op100' : 'op60'">
<NIconTitle :icon="icon" :text="text" text-xl transition :class="[open ? 'op100' : 'op60', headerClass]">
<div>
<div text-base>
<slot name="text">
Expand Down
24 changes: 24 additions & 0 deletions packages/devtools-ui-kit/src/components/NTextExternalLink.vue
@@ -0,0 +1,24 @@
<script setup lang="ts">
import NLink from './NLink.vue'
defineProps<{
link?: string
}>()
</script>

<template>
<component
:is="link ? NLink : 'div'"
v-bind="link ? {
href: link,
target: '_blank',
rel: 'noopener noreferrer',
} : {}"
>
<slot />
<div
v-if="link"
i-carbon:arrow-up-right translate-y--1 text-xs op50
/>
</component>
</template>
2 changes: 1 addition & 1 deletion packages/devtools-ui-kit/src/unocss.ts
Expand Up @@ -98,7 +98,7 @@ export function unocssPreset(): Preset {

// link
'n-link-base': 'underline underline-offset-2 underline-black/20 dark:underline-white/40',
'n-link-hover': 'decoration-dotted text-context underline-context',
'n-link-hover': 'decoration-dotted text-context underline-context! op100!',

// card
'n-card-base': 'border n-border-base rounded n-bg-base shadow-sm',
Expand Down
132 changes: 132 additions & 0 deletions packages/devtools/client/components/OpenGraphMissingTabs.vue
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { defu } from 'defu'
import type { ReactiveHead } from '@unhead/vue'
import type { NormalizedHeadTag } from '../../src/types'
import { ogTags } from '../data/open-graph'
const props = defineProps<{
tags: NormalizedHeadTag[]
matchedRouteFilepath?: string
}>()
const missingTags = computed(() => {
return ogTags.filter(define => !props.tags?.some(tag => tag.name === define.name))
})
const missingRequiredTags = computed(() => {
return missingTags.value.filter(i => i.suggestion === 'required')
})
const missingRecommendedTags = computed(() => {
return missingTags.value.filter(i => i.suggestion === 'recommended')
})
const mergedMissingTags = computed(() => {
let data: Partial<ReactiveHead> = {}
missingTags.value
.forEach((tag) => {
data = defu(data, tag.default)
})
return data
})
const codeSnippet = computed(() => {
const body = JSON.stringify(mergedMissingTags.value, null, 2)
.replace(/"([^"]+)":/g, '$1:')
.replace(/"/g, '\'')
return `useHead(${body})`
})
const copy = useCopy()
const openInEditor = useOpenInEditor()
const tabs = [
'Missing Tags',
'Code Snippet',
]
const selectedTab = ref(tabs[0])
</script>

<template>
<template v-if="missingTags.length">
<NSectionBlock
text="Missing Tags"
:description="`${missingTags.length} missing tags (${missingRequiredTags.length} required, ${missingRecommendedTags.length} recommended)`"
icon="carbon:warning-other"
header-class="text-orange op100! [[open]_&]:text-inherit"
:padding="false"
>
<div flex="~ wrap" mt--2 w-full flex-none>
<template v-for="name, idx of tabs" :key="idx">
<button
px4 py2 border="r t base"
hover="bg-active"
:class="name === selectedTab ? '' : 'border-b'"
@click="selectedTab = name"
>
<div :class="name === selectedTab ? '' : 'op30' " capitalize>
{{ name }}
</div>
</button>
</template>
<div border="b base" flex-auto />
</div>

<NCard v-if="selectedTab === tabs[0]" grid="~ cols-[max-content_1fr]" m4 items-center justify-between of-hidden>
<template v-for="item, index of missingTags" :key="index">
<div v-if="index" x-divider />
<div v-if="index" x-divider />
<div flex="~ gap-1 items-center" px4 py2>
<div i-carbon-warning text-orange />
<NTextExternalLink
op50
:link="item.docs"
n="orange"
>
{{ item.name }}
</NTextExternalLink>
</div>
<!-- TODO: use icons instead, show link to documentation -->
<div w-full p2 op75>
{{ item.description }}
</div>
</template>
</NCard>
<div v-else m4 flex="~ col gap-2">
<p flex="~ gap-1 wrap items-center">
<NButton
icon="carbon-copy" n="xs" px-2
@click="copy(codeSnippet)"
>
Copy
</NButton>
the following code snippet and paste it into your
<NButton
v-if="matchedRouteFilepath"
icon="carbon-launch" n="xs" px-2
@click="openInEditor(matchedRouteFilepath)"
>
page component
</NButton>
<span v-else>page component</span>
to full fill the missing tags.
</p>
<NCard relative n-code-block>
<NCodeBlock
:code="codeSnippet"
lang="ts"
:lines="false"
w-full of-auto p3
/>
<div flex="~ gap-2" n="sm primary" absolute right-2 top-2>
<NButton
icon="carbon-copy"
@click="copy(codeSnippet)"
>
Copy
</NButton>
</div>
</NCard>
</div>
</NSectionBlock>
</template>
</template>
14 changes: 14 additions & 0 deletions packages/devtools/client/components/docs/open-graph.md
@@ -0,0 +1,14 @@
# Open Graph

Nuxt provides several different ways to manage your meta tags using [`unhead`](https://unhead.harlanzw.com/). Improve your Nuxt app's SEO with powerful head config, composables and components.

[Learn more on the documentation](https://nuxt.com/docs/getting-started/seo-meta)

---

You can also find how open graph specs are defined in:

- [The Open Graph protocol](https://ogp.me/)
- [Twitter Cards](https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started)

<!-- and maybe also add reference to SEO modules(https://nuxt.com/modules?category=SEO) ? -->
30 changes: 30 additions & 0 deletions packages/devtools/client/components/social/SocialFacebook.vue
@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { SocialPreviewResolved } from '~/../src/types'
defineProps<{
card: SocialPreviewResolved
}>()
</script>

<template>
<div class="max-w-[524px] min-w-[524px] cursor-pointer">
<div
class="h-[274px] border border-b-0 border-base bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${JSON.stringify(card.image)})` }"
/>
<div class="break-words border border-base px-[12px] py-[10px] antialiased">
<div class="overflow-hidden truncate whitespace-nowrap text-[12px] leading-[11px] uppercase op50">
{{ card.url }}
</div><div class="block h-[46px] max-h-[46px] border-separate select-none overflow-hidden break-words text-left" style="border-spacing: 0px;">
<div class="mt-[3px] truncate pt-[2px] text-[16px] font-semibold leading-[20px]">
{{ card.title }}
</div><div
class="mt-[3px] block h-[18px] max-h-[80px] border-separate select-none overflow-hidden truncate whitespace-nowrap break-words text-left text-[14px] leading-[20px] op50"
style="-webkit-line-clamp: 1; border-spacing: 0px; -webkit-box-orient: vertical;"
>
{{ card.description }}
</div>
</div>
</div>
</div>
</template>
24 changes: 24 additions & 0 deletions packages/devtools/client/components/social/SocialLinkedin.vue
@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { SocialPreviewResolved } from '~/../src/types'
defineProps<{
card: SocialPreviewResolved
}>()
</script>

<template>
<div class="max-w-[520px] min-w-[520px] cursor-pointer overflow-hidden border border-base rounded-[2px] shadow-md">
<div
class="h-[270px] border-b border-base bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${JSON.stringify(card.image)})` }"
/><div class="break-words p-[10px] antialiased">
<div class="block h-auto max-h-[50px] border-separate select-none break-words text-left" style="border-spacing: 0px;">
<div class="pb-[2px] text-[16px] font-semibold leading-[24px]">
{{ card.title }}
</div><div class="overflow-hidden truncate whitespace-nowrap text-xs font-normal uppercase op85">
{{ card.url }}
</div>
</div>
</div>
</div>
</template>
57 changes: 57 additions & 0 deletions packages/devtools/client/components/social/SocialPreviewGroup.vue
@@ -0,0 +1,57 @@
<script setup lang="ts">
import type { NormalizedHeadTag, SocialPreviewResolved } from '../../../src/types'
const props = defineProps<{
tags: NormalizedHeadTag[]
}>()
const types = [
'twitter',
'facebook',
'linkedin',
]
const selected = ref(types[0])
const card = computed((): SocialPreviewResolved => {
return {
url: window.location.host,
title: props.tags.find(tag => tag.tag === 'title')?.value,
image: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'og:image')?.value,
imageAlt: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'og:image:alt')?.value,
description: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'og:description')?.value,
favicon: props.tags.find(tag => tag.tag === 'link' && tag.name === 'icon')?.value,
}
})
</script>

<template>
<div h-full w-max flex="~ col">
<div flex="~ wrap" w-full flex-none>
<template v-for="name, idx of types" :key="idx">
<button
px4 py2 border="r base"
hover="bg-active"
:class="name === selected ? '' : 'border-b'"
@click="selected = name"
>
<div :class="name === selected ? '' : 'op30' " capitalize>
{{ name }}
</div>
</button>
</template>
<div border="b base" flex-auto />
</div>
<div flex="~ items-center justify-center" flex-auto p4>
<div v-if="selected === 'facebook'">
<SocialFacebook :card="card" />
</div>
<div v-else-if="selected === 'twitter'">
<SocialTwitter :tags="tags" />
</div>
<div v-else-if="selected === 'linkedin'">
<SocialLinkedin :card="card" />
</div>
</div>
</div>
</template>
54 changes: 54 additions & 0 deletions packages/devtools/client/components/social/SocialTwitter.vue
@@ -0,0 +1,54 @@
<script setup lang="ts">
import type { NormalizedHeadTag, SocialPreviewResolved } from '~/../src/types'
const props = defineProps<{
tags: NormalizedHeadTag[]
}>()
const card = computed((): SocialPreviewResolved => {
return {
url: window.location.host,
title: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'twitter:title')?.value || props.tags.find(tag => tag.tag === 'title')?.value,
image: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'twitter:image')?.value,
imageAlt: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'twitter:image:alt')?.value,
description: props.tags.find(tag => tag.tag === 'meta' && tag.name === 'twitter:description')?.value || props.tags.find(tag => tag.tag === 'meta' && tag.name === 'description')?.value,
favicon: props.tags.find(tag => tag.tag === 'link' && tag.name === 'icon')?.value,
}
})
const type = computed(() => {
if (!card.value.image)
return 'summary'
return props.tags.find(tag => tag.tag === 'meta' && tag.name === 'twitter:card')?.value || 'summary_large_image'
})
</script>

<template>
<div
class="max-w-[438px] min-w-[438px] cursor-pointer overflow-hidden border border-base rounded-[0.85714em] leading-[1.3em] -outline-offset-1"
:class="type === 'summary_large_image' ? '' : 'flex'"
hover="bg-[#88888805]"
>
<div
v-if="type === 'summary_large_image'"
class="h-[220px] border-b border-base bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${JSON.stringify(card.image)})` }"
/>
<div
v-else
class="h-[122px] w-[122px] flex-none border-r border-base bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url(${JSON.stringify(card.image)})` }"
/>
<div class="break-words border-base p-[0.75em] antialiased" flex="~ col justify-center gap-1">
<div class="mt-[0.32333em] overflow-hidden truncate whitespace-nowrap text-[14px] leading-[18px] lowercase op50">
{{ card.url }}
</div>
<div class="m-0 truncate text-[14px] font-semibold leading-[19px]">
{{ card.title }}
</div>
<div class="line-clamp-2 block select-none overflow-hidden break-words text-left text-[14px] leading-[18px] op50">
{{ card.description }}
</div>
</div>
</div>
</template>

0 comments on commit b94de30

Please sign in to comment.