Skip to content

Commit 2bc9a2d

Browse files
authored
fix(templates): only generate slug from title on demand (#12956)
Currently, the slug field is generated from the title field indefinitely, even after the document has been created and the initial slug has been assigned. This should only occur on create, however, as it currently does, or when the user explicitly requests it. Given that slugs often determine the URL structure of the webpage that the document corresponds to, they should rarely change after being published, and when they do, would require HTTP redirects, etc. to do right in a production environment. But this is also a problem with Live Preview which relies on a constant iframe src. If your Live Preview URL includes the slug as a route param, which is often the case, then changing the slug will result in a broken connection as the queried document can no longer be found. The current workaround is to save the document and refresh the page. Now, the slug is only generated on initial create, or when the user explicitly clicks the new "generate" button above the slug field. In the future we can evaluate supporting dynamic Live Preview URLs. Regenerating this URL on every change would put additional load on the client as it would have to reestablish connection every time it changes, but it should be supported still. See #13055. Discord discussion here: https://discord.com/channels/967097582721572934/1102950643259424828/1387737976892686346 Related: #10536
1 parent 1d81b0c commit 2bc9a2d

File tree

6 files changed

+52
-52
lines changed

6 files changed

+52
-52
lines changed

templates/website/src/fields/slug/SlugComponent.tsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import React, { useCallback, useEffect } from 'react'
2+
import React, { useCallback } from 'react'
33
import { TextFieldClientProps } from 'payload'
44

55
import { useField, Button, TextInput, FieldLabel, useFormFields, useForm } from '@payloadcms/ui'
@@ -27,30 +27,28 @@ export const SlugComponent: React.FC<SlugComponentProps> = ({
2727

2828
const { value, setValue } = useField<string>({ path: path || field.name })
2929

30-
const { dispatchFields } = useForm()
30+
const { dispatchFields, getDataByPath } = useForm()
3131

32-
// The value of the checkbox
33-
// We're using separate useFormFields to minimise re-renders
34-
const checkboxValue = useFormFields(([fields]) => {
32+
const isLocked = useFormFields(([fields]) => {
3533
return fields[checkboxFieldPath]?.value as string
3634
})
3735

38-
// The value of the field we're listening to for the slug
39-
const targetFieldValue = useFormFields(([fields]) => {
40-
return fields[fieldToUse]?.value as string
41-
})
36+
const handleGenerate = useCallback(
37+
(e: React.MouseEvent<Element>) => {
38+
e.preventDefault()
39+
40+
const targetFieldValue = getDataByPath(fieldToUse) as string
4241

43-
useEffect(() => {
44-
if (checkboxValue) {
4542
if (targetFieldValue) {
4643
const formattedSlug = formatSlug(targetFieldValue)
4744

4845
if (value !== formattedSlug) setValue(formattedSlug)
4946
} else {
5047
if (value !== '') setValue('')
5148
}
52-
}
53-
}, [targetFieldValue, checkboxValue, setValue, value])
49+
},
50+
[setValue, value, fieldToUse, getDataByPath],
51+
)
5452

5553
const handleLock = useCallback(
5654
(e: React.MouseEvent<Element>) => {
@@ -59,29 +57,30 @@ export const SlugComponent: React.FC<SlugComponentProps> = ({
5957
dispatchFields({
6058
type: 'UPDATE',
6159
path: checkboxFieldPath,
62-
value: !checkboxValue,
60+
value: !isLocked,
6361
})
6462
},
65-
[checkboxValue, checkboxFieldPath, dispatchFields],
63+
[isLocked, checkboxFieldPath, dispatchFields],
6664
)
6765

68-
const readOnly = readOnlyFromProps || checkboxValue
69-
7066
return (
7167
<div className="field-type slug-field-component">
7268
<div className="label-wrapper">
7369
<FieldLabel htmlFor={`field-${path}`} label={label} />
74-
70+
{!isLocked && (
71+
<Button className="lock-button" buttonStyle="none" onClick={handleGenerate}>
72+
Generate
73+
</Button>
74+
)}
7575
<Button className="lock-button" buttonStyle="none" onClick={handleLock}>
76-
{checkboxValue ? 'Unlock' : 'Lock'}
76+
{isLocked ? 'Unlock' : 'Lock'}
7777
</Button>
7878
</div>
79-
8079
<TextInput
8180
value={value}
8281
onChange={setValue}
8382
path={path || field.name}
84-
readOnly={Boolean(readOnly)}
83+
readOnly={Boolean(readOnlyFromProps || isLocked)}
8584
/>
8685
</div>
8786
)

templates/website/src/fields/slug/formatSlug.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { FieldHook } from 'payload'
22

3-
export const formatSlug = (val: string): string =>
3+
export const formatSlug = (val: string): string | undefined =>
44
val
5-
.replace(/ /g, '-')
5+
?.replace(/ /g, '-')
66
.replace(/[^\w-]+/g, '')
77
.toLowerCase()
88

@@ -13,10 +13,10 @@ export const formatSlugHook =
1313
return formatSlug(value)
1414
}
1515

16-
if (operation === 'create' || !data?.slug) {
17-
const fallbackData = data?.[fallback] || data?.[fallback]
16+
if (operation === 'create' || data?.slug === undefined) {
17+
const fallbackData = data?.[fallback]
1818

19-
if (fallbackData && typeof fallbackData === 'string') {
19+
if (typeof fallbackData === 'string') {
2020
return formatSlug(fallbackData)
2121
}
2222
}

templates/website/src/fields/slug/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
display: flex;
44
justify-content: space-between;
55
align-items: center;
6+
gap: calc(var(--base) / 2);
67
}
78

89
.lock-button {

templates/with-vercel-website/src/fields/slug/SlugComponent.tsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import React, { useCallback, useEffect } from 'react'
2+
import React, { useCallback } from 'react'
33
import { TextFieldClientProps } from 'payload'
44

55
import { useField, Button, TextInput, FieldLabel, useFormFields, useForm } from '@payloadcms/ui'
@@ -27,30 +27,28 @@ export const SlugComponent: React.FC<SlugComponentProps> = ({
2727

2828
const { value, setValue } = useField<string>({ path: path || field.name })
2929

30-
const { dispatchFields } = useForm()
30+
const { dispatchFields, getDataByPath } = useForm()
3131

32-
// The value of the checkbox
33-
// We're using separate useFormFields to minimise re-renders
34-
const checkboxValue = useFormFields(([fields]) => {
32+
const isLocked = useFormFields(([fields]) => {
3533
return fields[checkboxFieldPath]?.value as string
3634
})
3735

38-
// The value of the field we're listening to for the slug
39-
const targetFieldValue = useFormFields(([fields]) => {
40-
return fields[fieldToUse]?.value as string
41-
})
36+
const handleGenerate = useCallback(
37+
(e: React.MouseEvent<Element>) => {
38+
e.preventDefault()
39+
40+
const targetFieldValue = getDataByPath(fieldToUse) as string
4241

43-
useEffect(() => {
44-
if (checkboxValue) {
4542
if (targetFieldValue) {
4643
const formattedSlug = formatSlug(targetFieldValue)
4744

4845
if (value !== formattedSlug) setValue(formattedSlug)
4946
} else {
5047
if (value !== '') setValue('')
5148
}
52-
}
53-
}, [targetFieldValue, checkboxValue, setValue, value])
49+
},
50+
[setValue, value, fieldToUse, getDataByPath],
51+
)
5452

5553
const handleLock = useCallback(
5654
(e: React.MouseEvent<Element>) => {
@@ -59,29 +57,30 @@ export const SlugComponent: React.FC<SlugComponentProps> = ({
5957
dispatchFields({
6058
type: 'UPDATE',
6159
path: checkboxFieldPath,
62-
value: !checkboxValue,
60+
value: !isLocked,
6361
})
6462
},
65-
[checkboxValue, checkboxFieldPath, dispatchFields],
63+
[isLocked, checkboxFieldPath, dispatchFields],
6664
)
6765

68-
const readOnly = readOnlyFromProps || checkboxValue
69-
7066
return (
7167
<div className="field-type slug-field-component">
7268
<div className="label-wrapper">
7369
<FieldLabel htmlFor={`field-${path}`} label={label} />
74-
70+
{!isLocked && (
71+
<Button className="lock-button" buttonStyle="none" onClick={handleGenerate}>
72+
Generate
73+
</Button>
74+
)}
7575
<Button className="lock-button" buttonStyle="none" onClick={handleLock}>
76-
{checkboxValue ? 'Unlock' : 'Lock'}
76+
{isLocked ? 'Unlock' : 'Lock'}
7777
</Button>
7878
</div>
79-
8079
<TextInput
8180
value={value}
8281
onChange={setValue}
8382
path={path || field.name}
84-
readOnly={Boolean(readOnly)}
83+
readOnly={Boolean(readOnlyFromProps || isLocked)}
8584
/>
8685
</div>
8786
)

templates/with-vercel-website/src/fields/slug/formatSlug.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { FieldHook } from 'payload'
22

3-
export const formatSlug = (val: string): string =>
3+
export const formatSlug = (val: string): string | undefined =>
44
val
5-
.replace(/ /g, '-')
5+
?.replace(/ /g, '-')
66
.replace(/[^\w-]+/g, '')
77
.toLowerCase()
88

@@ -13,10 +13,10 @@ export const formatSlugHook =
1313
return formatSlug(value)
1414
}
1515

16-
if (operation === 'create' || !data?.slug) {
17-
const fallbackData = data?.[fallback] || data?.[fallback]
16+
if (operation === 'create' || data?.slug === undefined) {
17+
const fallbackData = data?.[fallback]
1818

19-
if (fallbackData && typeof fallbackData === 'string') {
19+
if (typeof fallbackData === 'string') {
2020
return formatSlug(fallbackData)
2121
}
2222
}

templates/with-vercel-website/src/fields/slug/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
display: flex;
44
justify-content: space-between;
55
align-items: center;
6+
gap: calc(var(--base) / 2);
67
}
78

89
.lock-button {

0 commit comments

Comments
 (0)