From 13363cc788288636de53ccb6cd1717e5c74e7b20 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 13:15:52 +0000 Subject: [PATCH 1/9] fix: add `og:title` meta tags --- src/runtime/composables/head.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 1a7996c28..cc3fc3a21 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -17,12 +17,19 @@ export const useContentHead = ( // Default head to `data?.head` const head: HeadObjectPlain = Object.assign({}, data?.head || {}) + head.meta = [...(head.meta || [])] + // Great basic informations from the data const title = head.title || data?.title if (title) { head.title = title + if (!head.meta.some(m => m.property === 'og:title')) { + head.meta.push({ + name: 'og:title', + content: title + }) + } } - head.meta = [...(head.meta || [])] // Grab description from `head.description` or fallback to `data.description` // @ts-ignore - We expect `head.description` from Nuxt configurations... @@ -42,7 +49,7 @@ export const useContentHead = ( // 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'` + // Handles `image: '/image/src.jpg'` if (typeof image === 'string') { head.meta.push({ property: 'og:image', @@ -53,7 +60,7 @@ export const useContentHead = ( // 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', @@ -65,7 +72,7 @@ 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) { head.meta.push({ property: 'og:image', From 12db71c9b48f2173688b5bf357467d9ffbbf8a74 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 14:38:30 +0000 Subject: [PATCH 2/9] fix: add `og:url` and prefix `og:image` with full url --- src/module.ts | 2 ++ src/runtime/composables/head.ts | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/module.ts b/src/module.ts index d4356baeb..eff0dcb4b 100644 --- a/src/module.ts +++ b/src/module.ts @@ -204,6 +204,7 @@ export interface ModuleOptions { * @default false */ documentDriven: boolean | { + host?: string page?: boolean navigation?: boolean surround?: boolean @@ -572,6 +573,7 @@ export default defineNuxtModule({ wsUrl: '', // Document-driven configuration documentDriven: options.documentDriven as any, + host: typeof options.documentDriven !== 'boolean' ? options.documentDriven?.host ?? '' : '', // Anchor link generation config anchorLinks: options.markdown.anchorLinks }) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index cc3fc3a21..a2507148a 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -1,6 +1,7 @@ import { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router' import type { HeadObjectPlain } from '@vueuse/head' import type { Ref } from 'vue' +import { joinURL } from 'ufo' import { ParsedContent } from '../types' import { useRoute, nextTick, useHead, unref, watch } from '#imports' @@ -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 @@ -18,6 +20,7 @@ export const useContentHead = ( 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 @@ -31,6 +34,22 @@ export const useContentHead = ( } } + const host = config.public.content.host + if (host && !head.link.some(m => m.rel === 'canonical')) { + head.link.push({ + rel: 'canonical', + href: joinURL(host, config.app.baseURL, to.fullPath) + }) + } + const url = joinURL(host, config.app.baseURL, to.fullPath) + if (host && !head.meta.some(m => m.property === 'og:url')) { + head.meta.push({ + name: 'og:url', + // TODO: support configuration for trailing slash + content: url + }) + } + // Grab description from `head.description` or fallback to `data.description` // @ts-ignore - We expect `head.description` from Nuxt configurations... const description = head?.description || data?.description @@ -54,7 +73,7 @@ export const useContentHead = ( head.meta.push({ property: 'og:image', // @ts-ignore - We expect `head.image` from Nuxt configurations... - content: image + content: new URL(joinURL(config.app.baseURL, image), url).href }) } @@ -76,12 +95,12 @@ export const useContentHead = ( if (key === 'src' && image.src) { head.meta.push({ property: 'og:image', - content: image[key] + content: new URL(joinURL(config.app.baseURL, image[key]), url).href }) } else if (image[key]) { head.meta.push({ property: `og:image:${key}`, - content: image[key] + content: new URL(joinURL(config.app.baseURL, image[key]), url).href }) } } From aa9119e0161fc48cb8e57a68d08013bb1f165ea5 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 14:40:00 +0000 Subject: [PATCH 3/9] fix: disable canonical urls until trailing slash config is set --- src/runtime/composables/head.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index a2507148a..0436e9e1e 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -35,12 +35,6 @@ export const useContentHead = ( } const host = config.public.content.host - if (host && !head.link.some(m => m.rel === 'canonical')) { - head.link.push({ - rel: 'canonical', - href: joinURL(host, config.app.baseURL, to.fullPath) - }) - } const url = joinURL(host, config.app.baseURL, to.fullPath) if (host && !head.meta.some(m => m.property === 'og:url')) { head.meta.push({ @@ -49,6 +43,13 @@ export const useContentHead = ( content: url }) } + // TODO: add support for rel=canonical once trailing slash config + // if (host && !head.link.some(m => m.rel === 'canonical')) { + // head.link.push({ + // rel: 'canonical', + // href: joinURL(host, config.app.baseURL, to.fullPath) + // }) + // } // Grab description from `head.description` or fallback to `data.description` // @ts-ignore - We expect `head.description` from Nuxt configurations... From 8768669ac0ce47acc2dd9536baf7134bac4911ce Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 15:09:04 +0000 Subject: [PATCH 4/9] fix: only prefix image urls if host is available --- src/runtime/composables/head.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 0436e9e1e..c9bea5a37 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -35,7 +35,7 @@ export const useContentHead = ( } const host = config.public.content.host - const url = joinURL(host, config.app.baseURL, to.fullPath) + const url = joinURL(host ?? '/', config.app.baseURL, to.fullPath) if (host && !head.meta.some(m => m.property === 'og:url')) { head.meta.push({ name: 'og:url', @@ -74,7 +74,7 @@ export const useContentHead = ( head.meta.push({ property: 'og:image', // @ts-ignore - We expect `head.image` from Nuxt configurations... - content: new URL(joinURL(config.app.baseURL, image), url).href + content: host ? new URL(joinURL(config.app.baseURL, image), url).href : image }) } @@ -94,14 +94,15 @@ export const useContentHead = ( for (const key of imageKeys) { // `src` is a shorthand for the URL. if (key === 'src' && image.src) { + const imageURL = joinURL(config.app.baseURL, image.src ?? '/') head.meta.push({ property: 'og:image', - content: new URL(joinURL(config.app.baseURL, image[key]), url).href + content: host ? new URL(imageURL, url).href : imageURL }) } else if (image[key]) { head.meta.push({ property: `og:image:${key}`, - content: new URL(joinURL(config.app.baseURL, image[key]), url).href + content: image[key] }) } } From 5e2ce94318ffafe0a3c12dd1f6f7f33aa511ae03 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 15:13:02 +0000 Subject: [PATCH 5/9] fix: support absolute image urls --- src/runtime/composables/head.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index c9bea5a37..7031c7491 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -1,7 +1,7 @@ import { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router' import type { HeadObjectPlain } from '@vueuse/head' import type { Ref } from 'vue' -import { joinURL } from 'ufo' +import { hasProtocol, joinURL } from 'ufo' import { ParsedContent } from '../types' import { useRoute, nextTick, useHead, unref, watch } from '#imports' @@ -94,10 +94,11 @@ export const useContentHead = ( for (const key of imageKeys) { // `src` is a shorthand for the URL. if (key === 'src' && image.src) { - const imageURL = joinURL(config.app.baseURL, 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: host ? new URL(imageURL, url).href : imageURL + content: host && !isAbsoluteURL ? new URL(imageURL, url).href : imageURL }) } else if (image[key]) { head.meta.push({ From fa11a81e65cbee89aa246322e5c20ce9db8c445d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 15:17:52 +0000 Subject: [PATCH 6/9] fix: test for protocol with simple string too --- src/runtime/composables/head.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 7031c7491..46d688c85 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -74,7 +74,7 @@ export const useContentHead = ( head.meta.push({ property: 'og:image', // @ts-ignore - We expect `head.image` from Nuxt configurations... - content: host ? new URL(joinURL(config.app.baseURL, image), url).href : image + content: host && !hasProtocol(image) ? new URL(joinURL(config.app.baseURL, image), url).href : image }) } From f75e0b49a4e700009c8fa8b03b3846ee0c24943e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Dec 2022 16:43:56 +0000 Subject: [PATCH 7/9] feat: add `og:description` --- src/runtime/composables/head.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 46d688c85..944970d39 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -62,6 +62,12 @@ export const useContentHead = ( content: description }) } + if (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... From a6dfa9d4b6785b83ef60e8c91a367e7da55f002f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 16 Jan 2023 16:47:36 +0000 Subject: [PATCH 8/9] perf: only inject opengraph data on server side --- src/runtime/composables/head.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 944970d39..0fc14e31d 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -26,7 +26,7 @@ export const useContentHead = ( const title = head.title || data?.title if (title) { head.title = title - if (!head.meta.some(m => m.property === 'og:title')) { + if (process.server && !head.meta.some(m => m.property === 'og:title')) { head.meta.push({ name: 'og:title', content: title @@ -62,7 +62,7 @@ export const useContentHead = ( content: description }) } - if (description && !head.meta.some(m => m.property === 'og:description')) { + if (process.server && description && !head.meta.some(m => m.property === 'og:description')) { head.meta.push({ name: 'og:description', content: description @@ -74,7 +74,7 @@ export const useContentHead = ( 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) { + 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({ From 550c6e31b7963e91269cf03d7cf4f5449d343b69 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 16 Jan 2023 17:01:35 +0000 Subject: [PATCH 9/9] feat: add trailingSlash option and guessed canonical urls --- src/module.ts | 2 ++ src/runtime/composables/head.ts | 40 ++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/module.ts b/src/module.ts index eff0dcb4b..4e14e54fa 100644 --- a/src/module.ts +++ b/src/module.ts @@ -213,6 +213,7 @@ export interface ModuleOptions { } layoutFallbacks?: string[] injectPage?: boolean + trailingSlash?: boolean }, experimental: { clientDB: boolean @@ -574,6 +575,7 @@ export default defineNuxtModule({ // 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 }) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 0fc14e31d..75de28527 100644 --- a/src/runtime/composables/head.ts +++ b/src/runtime/composables/head.ts @@ -1,7 +1,7 @@ import { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router' import type { HeadObjectPlain } from '@vueuse/head' import type { Ref } from 'vue' -import { hasProtocol, joinURL } from 'ufo' +import { hasProtocol, joinURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { ParsedContent } from '../types' import { useRoute, nextTick, useHead, unref, watch } from '#imports' @@ -34,22 +34,30 @@ export const useContentHead = ( } } - const host = config.public.content.host - const url = joinURL(host ?? '/', config.app.baseURL, to.fullPath) - if (host && !head.meta.some(m => m.property === 'og:url')) { - head.meta.push({ - name: 'og:url', - // TODO: support configuration for trailing slash - content: url - }) + 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 + }) + } } - // TODO: add support for rel=canonical once trailing slash config - // if (host && !head.link.some(m => m.rel === 'canonical')) { - // head.link.push({ - // rel: 'canonical', - // href: joinURL(host, config.app.baseURL, to.fullPath) - // }) - // } // Grab description from `head.description` or fallback to `data.description` // @ts-ignore - We expect `head.description` from Nuxt configurations...