diff --git a/src/module.ts b/src/module.ts index 2f8646bc8..59aae4a52 100644 --- a/src/module.ts +++ b/src/module.ts @@ -115,7 +115,7 @@ export default defineNuxtModule({ { name: 'queryCollectionSearchSections', from: resolver.resolve('./runtime/app') }, { name: 'queryCollectionNavigation', from: resolver.resolve('./runtime/app') }, { name: 'queryCollectionItemSurroundings', from: resolver.resolve('./runtime/app') }, - { name: 'useContentHead', from: resolver.resolve('./runtime/app') }, + { name: 'useContentHead', from: resolver.resolve('./runtime/composables/useContentHead') }, ]) addServerImports([ { name: 'queryCollectionWithEvent', as: 'queryCollection', from: resolver.resolve('./runtime/nitro') }, diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 9bf3b6438..d6af97e4c 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -1,12 +1,11 @@ -import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions, PageCollectionItemBase } from '@nuxt/content' -import { hasProtocol, withTrailingSlash, withoutTrailingSlash, joinURL } from 'ufo' +import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions } from '@nuxt/content' import { collectionQureyBuilder } from './internal/query' import { measurePerformance } from './internal/performance' import { generateNavigationTree } from './internal/navigation' import { generateItemSurround } from './internal/surround' import { generateSearchSections } from './internal/search' import { fetchQuery } from './internal/api' -import { tryUseNuxtApp, useRuntimeConfig, useRoute, useHead } from '#imports' +import { tryUseNuxtApp } from '#imports' export const queryCollection = (collection: T): CollectionQueryBuilder => { return collectionQureyBuilder(collection, (collection, sql) => executeContentQuery(collection, sql)) @@ -46,111 +45,3 @@ async function queryContentSqlClientWasm { - if (!content) { - return - } - - const config = useRuntimeConfig() - const route = useRoute() - // Default head to `data?.seo` - const head = Object.assign({} as unknown as Exclude, content?.seo || {}) - - head.meta = [...(head.meta || [])] - head.link = [...(head.link || [])] - - // Great basic informations from the content - head.title = head.title || content?.title - if (head.title) { - if (import.meta.server && !head.meta?.some(m => m.property === 'og:title')) { - head.meta.push({ - property: 'og:title', - content: head.title as string, - }) - } - } - - if (import.meta.server && host) { - const _url = joinURL(host ?? '/', config.app.baseURL, route.fullPath) - const url = trailingSlash ? withTrailingSlash(_url) : withoutTrailingSlash(_url) - if (!head.meta.some(m => m.property === 'og:url')) { - head.meta.push({ - property: 'og:url', - content: url, - }) - } - if (!head.link.some(m => m.rel === 'canonical')) { - head.link.push({ - rel: 'canonical', - href: url, - }) - } - } - - // Grab description from `head.description` or fallback to `data.description` - const description = head?.description || content?.description - - // Shortcut for head.description - if (description && head.meta.filter(m => m.name === 'description').length === 0) { - head.meta.push({ - name: 'description', - content: description, - }) - } - if (import.meta.server && description && !head.meta.some(m => m.property === 'og:description')) { - head.meta.push({ - property: 'og:description', - content: description, - }) - } - - // Grab description from `head` or fallback to `data.description` - // @ts-expect-error - image does not exists in content - const image = head?.image || content?.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 (typeof image === 'string') { - head.meta.push({ - property: 'og:image', - content: host && !hasProtocol(image) ? new URL(joinURL(config.app.baseURL, image), host).href : image, - }) - } - - // Handles: `image.src: '/image/src.jpg'` & `image.alt: 200`... - if (typeof image === 'object') { - // https://ogp.me/#structured - const imageKeys = [ - 'src', - 'secure_url', - 'type', - 'width', - 'height', - 'alt', - ] - - // Look on available keys - for (const key of imageKeys) { - // `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: host && !isAbsoluteURL ? new URL(imageURL, host).href : imageURL, - }) - } - else if (image[key]) { - head.meta.push({ - property: `og:image:${key}`, - content: image[key], - }) - } - } - } - } - - useHead(head) -} diff --git a/src/runtime/composables/useContentHead.ts b/src/runtime/composables/useContentHead.ts new file mode 100644 index 000000000..6f137ec03 --- /dev/null +++ b/src/runtime/composables/useContentHead.ts @@ -0,0 +1,111 @@ +import type { PageCollectionItemBase } from '@nuxt/content' +import { hasProtocol, withTrailingSlash, withoutTrailingSlash, joinURL } from 'ufo' +import { useRuntimeConfig, useRoute, useHead } from '#imports' + +export const useContentHead = (content: PageCollectionItemBase | null, { host, trailingSlash }: { host?: string, trailingSlash?: boolean } = {}) => { + if (!content) { + return + } + + const config = useRuntimeConfig() + const route = useRoute() + // Default head to `data?.seo` + const head = Object.assign({} as unknown as Exclude, content?.seo || {}) + + head.meta = [...(head.meta || [])] + head.link = [...(head.link || [])] + + // Great basic informations from the content + head.title = head.title || content?.title + if (head.title) { + if (import.meta.server && !head.meta?.some(m => m.property === 'og:title')) { + head.meta.push({ + property: 'og:title', + content: head.title as string, + }) + } + } + + if (import.meta.server && host) { + const _url = joinURL(host ?? '/', config.app.baseURL, route.fullPath) + const url = trailingSlash ? withTrailingSlash(_url) : withoutTrailingSlash(_url) + if (!head.meta.some(m => m.property === 'og:url')) { + head.meta.push({ + property: 'og:url', + content: url, + }) + } + if (!head.link.some(m => m.rel === 'canonical')) { + head.link.push({ + rel: 'canonical', + href: url, + }) + } + } + + // Grab description from `head.description` or fallback to `data.description` + const description = head?.description || content?.description + + // Shortcut for head.description + if (description && head.meta.filter(m => m.name === 'description').length === 0) { + head.meta.push({ + name: 'description', + content: description, + }) + } + if (import.meta.server && description && !head.meta.some(m => m.property === 'og:description')) { + head.meta.push({ + property: 'og:description', + content: description, + }) + } + + // Grab description from `head` or fallback to `data.description` + // @ts-expect-error - image does not exists in content + const image = head?.image || content?.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 (typeof image === 'string') { + head.meta.push({ + property: 'og:image', + content: host && !hasProtocol(image) ? new URL(joinURL(config.app.baseURL, image), host).href : image, + }) + } + + // Handles: `image.src: '/image/src.jpg'` & `image.alt: 200`... + if (typeof image === 'object') { + // https://ogp.me/#structured + const imageKeys = [ + 'src', + 'secure_url', + 'type', + 'width', + 'height', + 'alt', + ] + + // Look on available keys + for (const key of imageKeys) { + // `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: host && !isAbsoluteURL ? new URL(imageURL, host).href : imageURL, + }) + } + else if (image[key]) { + head.meta.push({ + property: `og:image:${key}`, + content: image[key], + }) + } + } + } + } + + useHead(head) +}