feat(plugin-seo): working plugin-seo

This commit is contained in:
Alessio Gravili
2024-03-06 09:50:37 -05:00
parent 8be0296fc1
commit 78bf9e5993
8 changed files with 176 additions and 102 deletions

View File

@@ -1,7 +1,6 @@
'use client'
import type { FieldType, Options } from '@payloadcms/ui'
import type { TextareaField } from 'payload/types'
import type { FieldType, FormFieldBase, Options } from '@payloadcms/ui'
import { useFieldPath } from '@payloadcms/ui'
import { useTranslation } from '@payloadcms/ui'
@@ -9,7 +8,7 @@ import { TextareaInput } from '@payloadcms/ui'
import { useAllFormFields, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'
import React, { useCallback } from 'react'
import type { PluginConfig } from '../types'
import type { GenerateDescription } from '../types'
import { defaults } from '../defaults'
import { LengthIndicator } from '../ui/LengthIndicator'
@@ -17,13 +16,13 @@ import { LengthIndicator } from '../ui/LengthIndicator'
const { maxLength, minLength } = defaults.description
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type MetaDescriptionProps = TextareaField & {
type MetaDescriptionProps = FormFieldBase & {
hasGenerateDescriptionFn: boolean
path: string
pluginConfig: PluginConfig
}
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const { name, label, path, pluginConfig, required } = props
const { Label, hasGenerateDescriptionFn, path, required } = props
const { path: pathFromContext, schemaPath } = useFieldPath()
const { t } = useTranslation()
@@ -33,27 +32,31 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const docInfo = useDocumentInfo()
const field: FieldType<string> = useField({
name,
label,
path,
} as Options)
const { errorMessage, setValue, showError, value } = field
const regenerateDescription = useCallback(async () => {
/*const { generateDescription } = pluginConfig
let generatedDescription
if (!hasGenerateDescriptionFn) return
if (typeof generateDescription === 'function') {
generatedDescription = await generateDescription({
const genDescriptionResponse = await fetch('/api/plugin-seo/generate-description', {
body: JSON.stringify({
...docInfo,
doc: { ...fields },
locale: typeof locale === 'object' ? locale?.code : locale,
} satisfies Parameters<GenerateDescription>[0]),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
}
setValue(generatedDescription)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
const { result: generatedDescription } = await genDescriptionResponse.json()
setValue(generatedDescription || '')
}, [fields, setValue, hasGenerateDescriptionFn, locale, docInfo])
return (
<div
@@ -67,8 +70,8 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
position: 'relative',
}}
>
<div>
{label && typeof label === 'string' && label}
<div className="plugin-seo__field">
{Label}
{required && (
<span
@@ -81,7 +84,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
</span>
)}
{typeof pluginConfig?.generateDescription === 'function' && (
{hasGenerateDescriptionFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -126,7 +129,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
<TextareaInput
Error={errorMessage} // TODO: Fix
onChange={setValue}
path={name || pathFromContext}
path={pathFromContext}
required={required}
showError={showError}
style={{
@@ -147,7 +150,3 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
</div>
)
}
export const getMetaDescriptionField = (props: MetaDescriptionProps) => (
<MetaDescription {...props} />
)

View File

@@ -13,18 +13,17 @@ import {
} from '@payloadcms/ui'
import React, { useCallback } from 'react'
import type { PluginConfig } from '../types'
import type { GenerateImage } from '../types'
import { Pill } from '../ui/Pill'
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type MetaImageProps = UploadInputProps & {
path: string
pluginConfig: PluginConfig
hasGenerateImageFn: boolean
}
export const MetaImage: React.FC<MetaImageProps> = (props) => {
const { label, pluginConfig, relationTo, required } = props || {}
const { Label, hasGenerateImageFn, path, relationTo, required } = props || {}
const field: FieldType<string> = useField(props as Options)
@@ -37,19 +36,25 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
const { errorMessage, setValue, showError, value } = field
const regenerateImage = useCallback(async () => {
/*const { generateImage } = pluginConfig
let generatedImage
if (!hasGenerateImageFn) return
if (typeof generateImage === 'function') {
generatedImage = await generateImage({
const genImageResponse = await fetch('/api/plugin-seo/generate-image', {
body: JSON.stringify({
...docInfo,
doc: { ...fields },
locale: typeof locale === 'object' ? locale?.code : locale,
} satisfies Parameters<GenerateImage>[0]),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
}
setValue(generatedImage)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
const { result: generatedImage } = await genImageResponse.json()
setValue(generatedImage || '')
}, [fields, setValue, hasGenerateImageFn, locale, docInfo])
const hasImage = Boolean(value)
@@ -71,8 +76,8 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
position: 'relative',
}}
>
<div>
{label && typeof label === 'string' && label}
<div className="plugin-seo__field">
{Label}
{required && (
<span
@@ -85,7 +90,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
</span>
)}
{typeof pluginConfig?.generateImage === 'function' && (
{hasGenerateImageFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -106,7 +111,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
</React.Fragment>
)}
</div>
{typeof pluginConfig?.generateImage === 'function' && (
{hasGenerateImageFn && (
<div
style={{
color: '#9A9A9A',
@@ -162,5 +167,3 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
</div>
)
}
export const getMetaImageField = (props: MetaImageProps) => <MetaImage {...props} />

View File

@@ -1,7 +1,6 @@
'use client'
import type { FieldType, Options } from '@payloadcms/ui'
import type { TextField as TextFieldType } from 'payload/types'
import type { FieldType, FormFieldBase, Options } from '@payloadcms/ui'
import { useFieldPath } from '@payloadcms/ui'
import {
@@ -14,29 +13,26 @@ import {
} from '@payloadcms/ui'
import React, { useCallback } from 'react'
import type { PluginConfig } from '../types'
import type { GenerateTitle } from '../types'
import { defaults } from '../defaults'
import { LengthIndicator } from '../ui/LengthIndicator'
import './index.scss'
const { maxLength, minLength } = defaults.title
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type MetaTitleProps = TextFieldType & {
path: string
pluginConfig: PluginConfig
type MetaTitleProps = FormFieldBase & {
hasGenerateTitleFn: boolean
}
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { name, label, path, pluginConfig, required } = props || {}
console.log('props tit', props)
const { Label, hasGenerateTitleFn, path, required } = props || {}
const { path: pathFromContext, schemaPath } = useFieldPath()
const { t } = useTranslation()
const field: FieldType<string> = useField({
name,
label,
path,
} as Options)
@@ -47,19 +43,25 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { errorMessage, setValue, showError, value } = field
const regenerateTitle = useCallback(async () => {
/* const { generateTitle } = pluginConfig
let generatedTitle
if (!hasGenerateTitleFn) return
if (typeof generateTitle === 'function') {
generatedTitle = await generateTitle({
const genTitleResponse = await fetch('/api/plugin-seo/generate-title', {
body: JSON.stringify({
...docInfo,
doc: { ...fields },
locale: typeof locale === 'object' ? locale?.code : locale,
} satisfies Parameters<GenerateTitle>[0]),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
}
setValue(generatedTitle)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
const { result: generatedTitle } = await genTitleResponse.json()
setValue(generatedTitle || '')
}, [fields, setValue, hasGenerateTitleFn, locale, docInfo])
return (
<div
@@ -73,8 +75,8 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
position: 'relative',
}}
>
<div>
{label && typeof label === 'string' && label}
<div className="plugin-seo__field">
{Label}
{required && (
<span
@@ -87,7 +89,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
</span>
)}
{typeof pluginConfig?.generateTitle === 'function' && (
{hasGenerateTitleFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -133,7 +135,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
<TextInput
Error={errorMessage} // TODO: fix errormessage
onChange={setValue}
path={name || pathFromContext}
path={pathFromContext}
required={required}
showError={showError}
style={{
@@ -154,5 +156,3 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
</div>
)
}
export const getMetaTitleField = (props: MetaTitleProps) => <MetaTitle {...props} />

View File

@@ -0,0 +1,5 @@
.plugin-seo__field {
.field-label {
display: inline;
}
}

View File

@@ -4,14 +4,20 @@ import type { Field, GroupField, TabsField, TextField } from 'payload/types'
import { deepMerge } from 'payload/utilities'
import React from 'react'
import type { PluginConfig } from './types'
import type {
GenerateDescription,
GenerateImage,
GenerateTitle,
GenerateURL,
PluginConfig,
} from './types'
import { MetaDescription, getMetaDescriptionField } from './fields/MetaDescription'
import { MetaImage, getMetaImageField } from './fields/MetaImage'
import { MetaTitle, getMetaTitleField } from './fields/MetaTitle'
import { MetaDescription } from './fields/MetaDescription'
import { MetaImage } from './fields/MetaImage'
import { MetaTitle } from './fields/MetaTitle'
import translations from './translations'
import { Overview } from './ui/Overview'
import { Preview, getPreviewField } from './ui/Preview'
import { Preview } from './ui/Preview'
const seo =
(pluginConfig: PluginConfig) =>
@@ -36,22 +42,30 @@ const seo =
type: 'text',
admin: {
components: {
Field: (props) => {
return <MetaTitle {...props} />
},
Field: (props) => (
<MetaTitle
{...props}
hasGenerateTitleFn={typeof pluginConfig.generateTitle === 'function'}
/>
),
},
},
localized: true,
...((pluginConfig?.fieldOverrides?.title as TextField) ?? {}),
...((pluginConfig?.fieldOverrides?.title as unknown as TextField) ?? {}),
},
{
name: 'description',
type: 'textarea',
admin: {
components: {
Field: (props) => {
return <MetaDescription {...props} />
},
Field: (props) => (
<MetaDescription
{...props}
hasGenerateDescriptionFn={
typeof pluginConfig.generateDescription === 'function'
}
/>
),
},
},
localized: true,
@@ -65,9 +79,12 @@ const seo =
type: 'upload',
admin: {
components: {
Field: (props) => {
return <MetaImage {...props} />
},
Field: (props) => (
<MetaImage
{...props}
hasGenerateImageFn={typeof pluginConfig.generateImage === 'function'}
/>
),
},
description:
'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
@@ -85,9 +102,12 @@ const seo =
type: 'ui',
admin: {
components: {
Field: (props) => {
return <Preview {...props} />
},
Field: (props) => (
<Preview
{...props}
hasGenerateURLFn={typeof pluginConfig.generateURL === 'function'}
/>
),
},
},
label: 'Preview',
@@ -159,6 +179,48 @@ const seo =
return collection
}) || [],
endpoints: [
{
handler: async (req) => {
const args: Parameters<GenerateTitle>[0] =
req.data as unknown as Parameters<GenerateTitle>[0]
const result = await pluginConfig.generateTitle(args)
return new Response(JSON.stringify({ result }), { status: 200 })
},
method: 'post',
path: '/plugin-seo/generate-title',
},
{
handler: async (req) => {
const args: Parameters<GenerateDescription>[0] =
req.data as unknown as Parameters<GenerateDescription>[0]
const result = await pluginConfig.generateDescription(args)
return new Response(JSON.stringify({ result }), { status: 200 })
},
method: 'post',
path: '/plugin-seo/generate-description',
},
{
handler: async (req) => {
const args: Parameters<GenerateURL>[0] =
req.data as unknown as Parameters<GenerateURL>[0]
const result = await pluginConfig.generateURL(args)
return new Response(JSON.stringify({ result }), { status: 200 })
},
method: 'post',
path: '/plugin-seo/generate-url',
},
{
handler: async (req) => {
const args: Parameters<GenerateImage>[0] =
req.data as unknown as Parameters<GenerateImage>[0]
const result = await pluginConfig.generateImage(args)
return new Response(result, { status: 200 })
},
method: 'post',
path: '/plugin-seo/generate-image',
},
],
globals:
config.globals?.map((global) => {
const { slug } = global

View File

@@ -5,16 +5,14 @@ import type { FormField, UIField } from 'payload/types'
import { useAllFormFields, useDocumentInfo, useLocale, useTranslation } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react'
import type { PluginConfig } from '../types'
import type { GenerateURL } from '../types'
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type PreviewProps = UIField & {
pluginConfig: PluginConfig
hasGenerateURLFn: boolean
}
export const Preview: React.FC<PreviewProps> = (props) => {
const { pluginConfig: { generateURL } = {} } = props || {}
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
const { t } = useTranslation()
const locale = useLocale()
@@ -29,20 +27,29 @@ export const Preview: React.FC<PreviewProps> = (props) => {
const [href, setHref] = useState<string>()
useEffect(() => {
/* const getHref = async () => {
if (typeof generateURL === 'function' && !href) {
const newHref = await generateURL({
const getHref = async () => {
const genURLResponse = await fetch('/api/plugin-seo/generate-url', {
body: JSON.stringify({
...docInfo,
doc: { ...fields },
locale: typeof locale === 'object' ? locale?.code : locale,
} satisfies Parameters<GenerateURL>[0]),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const { result: newHref } = await genURLResponse.json()
setHref(newHref)
}
}
getHref() // eslint-disable-line @typescript-eslint/no-floating-promises*/
}, [generateURL, fields, href, locale, docInfo])
if (hasGenerateURLFn && !href) {
void getHref()
}
}, [fields, href, locale, docInfo, hasGenerateURLFn])
return (
<div>
@@ -101,5 +108,3 @@ export const Preview: React.FC<PreviewProps> = (props) => {
</div>
)
}
export const getPreviewField = (props: PreviewProps) => <Preview {...props} />

View File

@@ -14,7 +14,7 @@ export default buildConfigWithDefaults({
locales: ['en', 'es', 'de'],
},
i18n: {
resources: {
translations: {
es: {
'plugin-seo': {
autoGenerate: 'Auto-génerar',
@@ -46,7 +46,7 @@ export default buildConfigWithDefaults({
},
},
generateTitle: (data: any) => `Website.com — ${data?.doc?.title?.value}`,
generateDescription: ({ doc }: any) => doc?.excerpt?.value,
generateDescription: ({ doc }: any) => doc?.excerpt?.value || 'generated description',
generateURL: ({ doc, locale }: any) =>
`https://yoursite.com/${locale ? locale + '/' : ''}${doc?.slug?.value || ''}`,
}),

View File

@@ -13,7 +13,7 @@ export const seed = async (payload: Payload): Promise<boolean> => {
try {
// Create image
const filePath = path.resolve(__dirname, '../image-1.jpg')
const filePath = path.resolve(process.cwd(), './test/plugin-seo/image-1.jpg')
const file = await getFileByPath(filePath)
const mediaDoc = await payload.create({