Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add og:title,url,description meta tags and prefix og:image with host #1769

Merged
merged 10 commits into from
Jan 20, 2023
4 changes: 4 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export interface ModuleOptions {
* @default false
*/
documentDriven: boolean | {
host?: string
page?: boolean
navigation?: boolean
surround?: boolean
Expand All @@ -212,6 +213,7 @@ export interface ModuleOptions {
}
layoutFallbacks?: string[]
injectPage?: boolean
trailingSlash?: boolean
},
experimental: {
clientDB: boolean
Expand Down Expand Up @@ -572,6 +574,8 @@ export default defineNuxtModule<ModuleOptions>({
wsUrl: '',
// Document-driven configuration
documentDriven: options.documentDriven as any,
host: typeof options.documentDriven !== 'boolean' ? options.documentDriven?.host ?? '' : '',
trailingSlash: typeof options.documentDriven !== 'boolean' ? options.documentDriven?.trailingSlash ?? false : false,
// Anchor link generation config
anchorLinks: options.markdown.anchorLinks
})
Expand Down
57 changes: 50 additions & 7 deletions src/runtime/composables/head.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import type { HeadObjectPlain } from '@vueuse/head'
import type { Ref } from 'vue'
import { hasProtocol, joinURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { ParsedContent } from '../types'
import { useRoute, nextTick, useHead, unref, watch } from '#imports'

Expand All @@ -9,6 +10,7 @@ export const useContentHead = (
to: RouteLocationNormalized | RouteLocationNormalizedLoaded = useRoute()
) => {
const content = unref(_content)
const config = useRuntimeConfig()

const refreshHead = (data: ParsedContent = content) => {
// Don't call this function if no route is yet available
Expand All @@ -17,12 +19,45 @@ export const useContentHead = (
// Default head to `data?.head`
const head: HeadObjectPlain = Object.assign({}, data?.head || {})

head.meta = [...(head.meta || [])]
head.link = [...(head.link || [])]

// Great basic informations from the data
const title = head.title || data?.title
if (title) {
head.title = title
if (process.server && !head.meta.some(m => m.property === 'og:title')) {
head.meta.push({
name: 'og:title',
content: title
})
}
}

let host = config.public.content.host
if (process.server && !host) {
const req = useRequestEvent().node?.req
if (req) {
const protocol = req.headers['x-forwarded-proto'] || req.connection.encrypted ? 'https' : 'http'
host = `${protocol}://${req.headers.host}`
}
}
if (process.server && host) {
const _url = joinURL(host ?? '/', config.app.baseURL, to.fullPath)
const url = config.public.content.trailingSlash ? withTrailingSlash(_url) : withoutTrailingSlash(_url)
if (!head.meta.some(m => m.property === 'og:url')) {
head.meta.push({
name: 'og:url',
content: url
})
}
if (!head.link.some(m => m.rel === 'canonical')) {
head.link.push({
rel: 'canonical',
href: url
})
}
}
head.meta = [...(head.meta || [])]

// Grab description from `head.description` or fallback to `data.description`
// @ts-ignore - We expect `head.description` from Nuxt configurations...
Expand All @@ -35,25 +70,31 @@ export const useContentHead = (
content: description
})
}
if (process.server && description && !head.meta.some(m => m.property === 'og:description')) {
head.meta.push({
name: 'og:description',
content: description
})
}

// Grab description from `head` or fallback to `data.description`
// @ts-ignore - We expect `head.image` from Nuxt configurations...
const image = head?.image || data?.image

// Shortcut for head.image to og:image in meta
if (image && head.meta.filter(m => m.property === 'og:image').length === 0) {
// Handles `image: '/image/src.jpg'`
if (process.server && image && head.meta.filter(m => m.property === 'og:image').length === 0) {
// Handles `image: '/image/src.jpg'`
if (typeof image === 'string') {
head.meta.push({
property: 'og:image',
// @ts-ignore - We expect `head.image` from Nuxt configurations...
content: image
content: host && !hasProtocol(image) ? new URL(joinURL(config.app.baseURL, image), url).href : image
})
}

// Handles: `image.src: '/image/src.jpg'` & `image.alt: 200`...
if (typeof image === 'object') {
// https://ogp.me/#structured
// https://ogp.me/#structured
const imageKeys = [
'src',
'secure_url',
Expand All @@ -65,11 +106,13 @@ export const useContentHead = (

// Look on available keys
for (const key of imageKeys) {
// `src` is a shorthand for the URL.
// `src` is a shorthand for the URL.
if (key === 'src' && image.src) {
const isAbsoluteURL = hasProtocol(image.src)
const imageURL = isAbsoluteURL ? image.src : joinURL(config.app.baseURL, image.src ?? '/')
head.meta.push({
property: 'og:image',
content: image[key]
content: host && !isAbsoluteURL ? new URL(imageURL, url).href : imageURL
})
} else if (image[key]) {
head.meta.push({
Expand Down