diff --git a/src/module.ts b/src/module.ts index d4356baeb..4e14e54fa 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 @@ -212,6 +213,7 @@ export interface ModuleOptions { } layoutFallbacks?: string[] injectPage?: boolean + trailingSlash?: boolean }, experimental: { clientDB: boolean @@ -572,6 +574,8 @@ export default defineNuxtModule({ 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 }) diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts index 1a7996c28..75de28527 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 { hasProtocol, joinURL, withTrailingSlash, withoutTrailingSlash } 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 @@ -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... @@ -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', @@ -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({