Skip to content

Commit 3161961

Browse files
committed
feat: seo
1 parent 3d63fd6 commit 3161961

File tree

13 files changed

+410
-69
lines changed

13 files changed

+410
-69
lines changed

app/app.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
</template>
1010

1111
<script setup lang="ts">
12-
defineOgImageComponent('NuxtSeo')
13-
1412
const { locale } = useI18n()
1513
const head = useLocaleHead()
1614
@@ -38,4 +36,8 @@ useSeoMeta({
3836
titleTemplate: (title) =>
3937
title ? `${title} | ${$t('nuxtSiteConfig.name')}` : $t('nuxtSiteConfig.name')
4038
})
39+
40+
if (import.meta.server) {
41+
defineOgImageComponent('Page')
42+
}
4143
</script>

app/components/OgImage/Article.vue

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<template>
2+
<div
3+
class="w-full h-full flex justify-between relative p-[60px]"
4+
:class="[colorMode === 'light' ? ['bg-white', 'text-gray-900'] : ['bg-gray-900', 'text-white']]"
5+
>
6+
<div
7+
class="flex absolute top-0 -right-full"
8+
:style="{
9+
width: '200%',
10+
height: '200%',
11+
backgroundImage: `radial-gradient(circle, rgba(${themeRgb}, 0.5) 0%, ${colorMode === 'dark' ? 'rgba(5, 5, 5,0.3)' : 'rgba(255, 255, 255, 0.7)'} 50%, ${props.colorMode === 'dark' ? 'rgba(5, 5, 5,0)' : 'rgba(255, 255, 255, 0)'} 70%)`
12+
}"
13+
/>
14+
<div class="h-full w-full justify-between relative">
15+
<div class="flex flex-row justify-between items-start">
16+
<div class="flex flex-col w-full max-w-[65%]">
17+
<h1
18+
class="m-0 font-bold mb-[30px] text-[75px]"
19+
:style="{ lineClamp: description ? 2 : 3 }"
20+
style="display: block; text-overflow: ellipsis"
21+
>
22+
{{ title }}
23+
</h1>
24+
<p
25+
v-if="description"
26+
class="text-[35px] leading-12"
27+
style="display: block; line-clamp: 3; text-overflow: ellipsis"
28+
:class="[colorMode === 'light' ? ['text-gray-700'] : ['text-gray-300']]"
29+
>
30+
{{ description }}
31+
</p>
32+
</div>
33+
<div v-if="Boolean(icon)" style="width: 30%" class="flex justify-end">
34+
<IconComponent :name="icon" size="250px" style="margin: 0 auto; opacity: 0.7" />
35+
</div>
36+
</div>
37+
<div class="flex flex-row justify-center items-center text-left w-full">
38+
<img v-if="siteLogo" height="30" :src="siteLogo" />
39+
<template v-else>
40+
<svg
41+
width="50"
42+
height="50"
43+
class="mr-3"
44+
viewBox="0 0 200 200"
45+
xmlns="http://www.w3.org/2000/svg"
46+
>
47+
<path
48+
transform="translate(100 100)"
49+
:fill="theme.includes('#') ? theme : `#${theme}`"
50+
d="M62.3,-53.9C74.4,-34.5,73.5,-9,67.1,13.8C60.6,36.5,48.7,56.5,30.7,66.1C12.7,75.7,-11.4,74.8,-31.6,65.2C-51.8,55.7,-67.9,37.4,-73.8,15.7C-79.6,-6,-75.1,-31.2,-61.1,-51C-47.1,-70.9,-23.6,-85.4,0.8,-86C25.1,-86.7,50.2,-73.4,62.3,-53.9Z"
51+
/>
52+
</svg>
53+
<p v-if="siteName" class="font-bold" style="font-size: 25px">
54+
{{ siteName }}
55+
</p>
56+
</template>
57+
</div>
58+
</div>
59+
</div>
60+
</template>
61+
<script setup lang="ts">
62+
/**
63+
* @credits Nuxt SEO <https://nuxtseo.com/>
64+
*/
65+
66+
import { useOgImageRuntimeConfig } from '#og-image/app/utils'
67+
import { useSiteConfig } from '#site-config/app/composables'
68+
import { computed, defineComponent, h, resolveComponent } from 'vue'
69+
70+
// convert to typescript props
71+
const props = withDefaults(
72+
defineProps<{
73+
colorMode?: 'dark' | 'light'
74+
description?: string
75+
icon?: boolean | string
76+
siteLogo?: string
77+
siteName?: string
78+
theme?: string
79+
title?: string
80+
}>(),
81+
{
82+
colorMode: undefined,
83+
description: undefined,
84+
icon: undefined,
85+
siteLogo: undefined,
86+
siteName: undefined,
87+
theme: '#00dc82',
88+
title: 'title'
89+
}
90+
)
91+
92+
const HexRegex = /^#(?:[0-9a-f]{3}){1,2}$/i
93+
94+
const runtimeConfig = useOgImageRuntimeConfig()
95+
96+
const colorMode = computed(() => {
97+
return props.colorMode || runtimeConfig.colorPreference || 'light'
98+
})
99+
100+
const themeHex = computed(() => {
101+
// regex test if valid hex
102+
if (HexRegex.test(props.theme)) return props.theme
103+
104+
// if it's hex without the hash, just add the hash
105+
if (HexRegex.test(`#${props.theme}`)) return `#${props.theme}`
106+
107+
// if it's rgb or rgba, we convert it to hex
108+
if (props.theme.startsWith('rgb')) {
109+
const rgb = props.theme
110+
.replace('rgb(', '')
111+
.replace('rgba(', '')
112+
.replace(')', '')
113+
.split(',')
114+
.map((v) => Number.parseInt(v.trim(), 10))
115+
const hex = rgb
116+
.map((v) => {
117+
const hex = v.toString(16)
118+
return hex.length === 1 ? `0${hex}` : hex
119+
})
120+
.join('')
121+
return `#${hex}`
122+
}
123+
return '#FFFFFF'
124+
})
125+
126+
const themeRgb = computed(() => {
127+
// we want to convert it so it's just `<red>, <green>, <blue>` (255, 255, 255)
128+
return themeHex.value
129+
.replace('#', '')
130+
.match(/.{1,2}/g)
131+
?.map((v) => Number.parseInt(v, 16))
132+
.join(', ')
133+
})
134+
135+
const siteConfig = useSiteConfig()
136+
const siteName = computed(() => {
137+
return props.siteName || siteConfig.name
138+
})
139+
const siteLogo = computed(() => {
140+
return props.siteLogo || siteConfig.logo
141+
})
142+
143+
const IconComponent = runtimeConfig.hasNuxtIcon
144+
? resolveComponent('Icon')
145+
: defineComponent({
146+
render() {
147+
return h('div', 'missing @nuxt/icon')
148+
}
149+
})
150+
if (typeof props.icon === 'string' && !runtimeConfig.hasNuxtIcon && import.meta.dev) {
151+
console.warn('Please install `@nuxt/icon` to use icons with the fallback OG Image component.')
152+
153+
console.log('\nnpx nuxi module add icon\n')
154+
// create simple div renderer component
155+
}
156+
</script>

app/components/OgImage/Page.vue

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<template>
2+
<div
3+
class="w-full h-full flex justify-between relative p-[60px]"
4+
:class="[colorMode === 'light' ? ['bg-white', 'text-gray-900'] : ['bg-gray-900', 'text-white']]"
5+
>
6+
<div
7+
class="flex absolute top-0 -right-full"
8+
:style="{
9+
width: '200%',
10+
height: '200%',
11+
backgroundImage: `radial-gradient(circle, rgba(${themeRgb}, 0.5) 0%, ${colorMode === 'dark' ? 'rgba(5, 5, 5,0.3)' : 'rgba(255, 255, 255, 0.7)'} 50%, ${props.colorMode === 'dark' ? 'rgba(5, 5, 5,0)' : 'rgba(255, 255, 255, 0)'} 70%)`
12+
}"
13+
/>
14+
<div class="h-full w-full justify-between relative">
15+
<div class="flex flex-row justify-between items-start">
16+
<div class="flex flex-col w-full max-w-[65%]">
17+
<h1
18+
class="m-0 font-bold mb-[30px] text-[75px]"
19+
:style="{ lineClamp: description ? 2 : 3 }"
20+
style="display: block; text-overflow: ellipsis"
21+
>
22+
{{ title }}
23+
</h1>
24+
<p
25+
v-if="description"
26+
class="text-[35px] leading-12"
27+
style="display: block; line-clamp: 3; text-overflow: ellipsis"
28+
:class="[colorMode === 'light' ? ['text-gray-700'] : ['text-gray-300']]"
29+
>
30+
{{ description }}
31+
</p>
32+
</div>
33+
<div v-if="Boolean(icon)" style="width: 30%" class="flex justify-end">
34+
<IconComponent :name="icon" size="250px" style="margin: 0 auto; opacity: 0.7" />
35+
</div>
36+
</div>
37+
<div class="flex flex-row justify-center items-center text-left w-full">
38+
<img v-if="siteLogo" height="30" :src="siteLogo" />
39+
<template v-else>
40+
<svg
41+
width="50"
42+
height="50"
43+
class="mr-3"
44+
viewBox="0 0 200 200"
45+
xmlns="http://www.w3.org/2000/svg"
46+
>
47+
<path
48+
transform="translate(100 100)"
49+
:fill="theme.includes('#') ? theme : `#${theme}`"
50+
d="M62.3,-53.9C74.4,-34.5,73.5,-9,67.1,13.8C60.6,36.5,48.7,56.5,30.7,66.1C12.7,75.7,-11.4,74.8,-31.6,65.2C-51.8,55.7,-67.9,37.4,-73.8,15.7C-79.6,-6,-75.1,-31.2,-61.1,-51C-47.1,-70.9,-23.6,-85.4,0.8,-86C25.1,-86.7,50.2,-73.4,62.3,-53.9Z"
51+
/>
52+
</svg>
53+
<p v-if="siteName" class="font-bold" style="font-size: 25px">
54+
{{ siteName }}
55+
</p>
56+
</template>
57+
</div>
58+
</div>
59+
</div>
60+
</template>
61+
<script setup lang="ts">
62+
/**
63+
* @credits Nuxt SEO <https://nuxtseo.com/>
64+
*/
65+
66+
import { useOgImageRuntimeConfig } from '#og-image/app/utils'
67+
import { useSiteConfig } from '#site-config/app/composables'
68+
import { computed, defineComponent, h, resolveComponent } from 'vue'
69+
70+
// convert to typescript props
71+
const props = withDefaults(
72+
defineProps<{
73+
colorMode?: 'dark' | 'light'
74+
description?: string
75+
icon?: boolean | string
76+
siteLogo?: string
77+
siteName?: string
78+
theme?: string
79+
title?: string
80+
}>(),
81+
{
82+
colorMode: undefined,
83+
description: undefined,
84+
icon: undefined,
85+
siteLogo: undefined,
86+
siteName: undefined,
87+
theme: '#00dc82',
88+
title: 'title'
89+
}
90+
)
91+
92+
const HexRegex = /^#(?:[0-9a-f]{3}){1,2}$/i
93+
94+
const runtimeConfig = useOgImageRuntimeConfig()
95+
96+
const colorMode = computed(() => {
97+
return props.colorMode || runtimeConfig.colorPreference || 'light'
98+
})
99+
100+
const themeHex = computed(() => {
101+
// regex test if valid hex
102+
if (HexRegex.test(props.theme)) return props.theme
103+
104+
// if it's hex without the hash, just add the hash
105+
if (HexRegex.test(`#${props.theme}`)) return `#${props.theme}`
106+
107+
// if it's rgb or rgba, we convert it to hex
108+
if (props.theme.startsWith('rgb')) {
109+
const rgb = props.theme
110+
.replace('rgb(', '')
111+
.replace('rgba(', '')
112+
.replace(')', '')
113+
.split(',')
114+
.map((v) => Number.parseInt(v.trim(), 10))
115+
const hex = rgb
116+
.map((v) => {
117+
const hex = v.toString(16)
118+
return hex.length === 1 ? `0${hex}` : hex
119+
})
120+
.join('')
121+
return `#${hex}`
122+
}
123+
return '#FFFFFF'
124+
})
125+
126+
const themeRgb = computed(() => {
127+
// we want to convert it so it's just `<red>, <green>, <blue>` (255, 255, 255)
128+
return themeHex.value
129+
.replace('#', '')
130+
.match(/.{1,2}/g)
131+
?.map((v) => Number.parseInt(v, 16))
132+
.join(', ')
133+
})
134+
135+
const siteConfig = useSiteConfig()
136+
const siteName = computed(() => {
137+
return props.siteName || siteConfig.name
138+
})
139+
const siteLogo = computed(() => {
140+
return props.siteLogo || siteConfig.logo
141+
})
142+
143+
const IconComponent = runtimeConfig.hasNuxtIcon
144+
? resolveComponent('Icon')
145+
: defineComponent({
146+
render() {
147+
return h('div', 'missing @nuxt/icon')
148+
}
149+
})
150+
if (typeof props.icon === 'string' && !runtimeConfig.hasNuxtIcon && import.meta.dev) {
151+
console.warn('Please install `@nuxt/icon` to use icons with the fallback OG Image component.')
152+
153+
console.log('\nnpx nuxi module add icon\n')
154+
// create simple div renderer component
155+
}
156+
</script>

app/error.vue

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<UApp>
2+
<UApp :locale="uiLocales[locale]">
33
<UError :error="error" />
44
</UApp>
55
</template>
@@ -11,19 +11,24 @@ const props = defineProps<{
1111
error: NuxtError
1212
}>()
1313
14-
useSeoMeta({
15-
description: 'We are sorry but this page could not be found.',
16-
title: 'Page not found'
17-
})
14+
const { locale } = useI18n()
15+
const head = useLocaleHead()
16+
17+
const colorMode = useColorMode()
18+
const color = computed(() => (colorMode.value === 'dark' ? '#1b1718' : 'white'))
1819
1920
useHead({
20-
htmlAttrs: {
21-
lang: 'en'
22-
}
21+
htmlAttrs: head.value.htmlAttrs,
22+
link: [{ href: '/favicon.ico', rel: 'icon' }],
23+
meta: [
24+
{ charset: 'utf-8' },
25+
{ content: 'width=device-width, initial-scale=1', name: 'viewport' },
26+
{ content: color, key: 'theme-color', name: 'theme-color' }
27+
]
2328
})
2429
25-
defineOgImageComponent('NuxtSeo', {
30+
useSeoMeta({
2631
description: props.error.statusMessage,
27-
title: props.error.statusCode.toString()
32+
title: props.error.statusCode
2833
})
2934
</script>

0 commit comments

Comments
 (0)