From 0e02c09155033baa8fc642af9637f444b4ead57f Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 4 Nov 2024 18:21:45 +0100 Subject: [PATCH] feat: export `useContentHead` for seo header --- src/module.ts | 2 +- src/runtime/app.ts | 113 +++++++++++++++++++++++++++++++++++++++- src/types/collection.ts | 8 +-- src/utils/schema.ts | 4 +- src/utils/templates.ts | 6 +++ 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/src/module.ts b/src/module.ts index d47a4fa1c..1230f2513 100644 --- a/src/module.ts +++ b/src/module.ts @@ -115,6 +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') }, ]) addServerImports([ { name: 'queryCollectionWithEvent', as: 'queryCollection', from: resolver.resolve('./runtime/nitro') }, @@ -145,7 +146,6 @@ export default defineNuxtModule({ config.handlers.push({ route: '/api/content/:collection/query', handler: resolver.resolve('./runtime/api/query.post'), - method: 'POST', }) }) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index d6af97e4c..b4bb25319 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -1,11 +1,12 @@ -import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions } from '@nuxt/content' +import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions, PageCollectionItemBase } from '@nuxt/content' +import { hasProtocol, withTrailingSlash, withoutTrailingSlash, joinURL } from 'ufo' 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 } from '#imports' +import { tryUseNuxtApp, useRuntimeConfig, useRoute, useHead } from '#imports' export const queryCollection = (collection: T): CollectionQueryBuilder => { return collectionQureyBuilder(collection, (collection, sql) => executeContentQuery(collection, sql)) @@ -45,3 +46,111 @@ 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 + const title = head.title || content?.title + if (title) { + head.title = title + if (import.meta.server && !head.meta?.some(m => m.property === 'og:title')) { + head.meta.push({ + property: 'og:title', + content: 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` + 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/types/collection.ts b/src/types/collection.ts index ce73e71f8..dfaafe9f6 100644 --- a/src/types/collection.ts +++ b/src/types/collection.ts @@ -80,9 +80,11 @@ export interface PageCollectionItemBase extends CollectionItemBase { path: string title: string description: string - seo?: { - title: string - description: string + seo: { + title?: string + description?: string + meta?: Array>> + link?: Array>> [key: string]: unknown } diff --git a/src/utils/schema.ts b/src/utils/schema.ts index 7abcdea1c..a772317a6 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -17,9 +17,11 @@ export const pageSchema = z.object({ z.object({ title: z.string().optional(), description: z.string().optional(), + meta: z.array(z.record(z.string(), z.any())).optional(), + link: z.array(z.record(z.string(), z.any())).optional(), }), z.record(z.string(), z.any()), - ).optional(), + ).optional().default({}), body: z.object({ type: z.string(), children: z.any(), diff --git a/src/utils/templates.ts b/src/utils/templates.ts index 0ce250cff..ece12995f 100644 --- a/src/utils/templates.ts +++ b/src/utils/templates.ts @@ -84,6 +84,12 @@ export const fullDatabaseCompressedDumpTemplate = (manifest: Manifest) => ({ filename: moduleTemplates.fullCompressedDump, getContents: ({ options }: { options: { manifest: Manifest } }) => { return Object.entries(options.manifest.dump).map(([key, value]) => { + const collection = options.manifest.collections.find(c => c.name === key) + // Ignore provate collections + if (collection?.private) { + return '' + } + const str = Buffer.from(deflate(value.join('\n')).buffer).toString('base64') return `export const ${key} = "${str}"` }).join('\n')