feat(plugin-seo): working plugin-seo
This commit is contained in:
@@ -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>
|
||||
—
|
||||
<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} />
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
<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} />
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
<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} />
|
||||
|
||||
5
packages/plugin-seo/src/fields/index.scss
Normal file
5
packages/plugin-seo/src/fields/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.plugin-seo__field {
|
||||
.field-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 || ''}`,
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user