Skip to content

Commit

Permalink
feat: export useContentHead for seo header
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz committed Nov 4, 2024
1 parent 99f25df commit 0e02c09
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default defineNuxtModule<ModuleOptions>({
{ 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') },
Expand Down Expand Up @@ -145,7 +146,6 @@ export default defineNuxtModule<ModuleOptions>({
config.handlers.push({
route: '/api/content/:collection/query',
handler: resolver.resolve('./runtime/api/query.post'),
method: 'POST',
})
})

Expand Down
113 changes: 111 additions & 2 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends keyof Collections>(collection: T): CollectionQueryBuilder<Collections[T]> => {
return collectionQureyBuilder<T>(collection, (collection, sql) => executeContentQuery(collection, sql))
Expand Down Expand Up @@ -45,3 +46,111 @@ async function queryContentSqlClientWasm<T extends keyof Collections, Result = C

return rows as Result[]
}

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<PageCollectionItemBase['seo'], undefined>, 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)
}
8 changes: 5 additions & 3 deletions src/types/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Partial<Record<'id' | 'name' | 'property' | 'content', string>>>
link?: Array<Partial<Record<'color' | 'rel' | 'href' | 'hreflang' | 'imagesizes' | 'imagesrcset' | 'integrity' | 'media' | 'sizes' | 'id', string>>>

[key: string]: unknown
}
Expand Down
4 changes: 3 additions & 1 deletion src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 6 additions & 0 deletions src/utils/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 0e02c09

Please sign in to comment.