@@ -29,47 +29,124 @@ import { WideHeroBlock } from "@/blocks/WideHeroBlock"
2929import { StatsBlock } from "@/blocks/StatsBlock"
3030import { FlowExampleBlock } from "@/blocks/FlowExampleBlock"
3131
32+ const RESERVED_PAGE_SLUGS = [
33+ "main" ,
34+ "jobs" ,
35+ "blog" ,
36+ "features" ,
37+ "about-us" ,
38+ "legal-notice" ,
39+ "privacy" ,
40+ "terms" ,
41+ "contact" ,
42+ "actions" ,
43+ "action-details" ,
44+ "community-edition" ,
45+ "enterprise-edition" ,
46+ "subscription" ,
47+ ] as const
48+
49+ function formatSlug ( value : string ) {
50+ return value
51+ . normalize ( "NFKD" )
52+ . replace ( / [ \u0300 - \u036f ] / g, "" )
53+ . toLowerCase ( )
54+ . trim ( )
55+ . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" )
56+ . replace ( / ^ - + | - + $ / g, "" )
57+ }
58+
59+ function isCustomPage ( siblingData : unknown ) {
60+ return Boolean ( siblingData && typeof siblingData === "object" && "customPage" in siblingData && siblingData . customPage )
61+ }
62+
3263export const Pages : CollectionConfig = {
3364 slug : "pages" ,
3465 admin : {
3566 useAsTitle : "title" ,
36- defaultColumns : [ "title" , "slug" , "updatedAt" ] ,
67+ defaultColumns : [ "title" , "slug" , "customSlug" , " updatedAt"] ,
3768 } ,
3869 access : {
3970 read : ( ) => true ,
4071 create : ( { req } ) => Boolean ( req . user ) ,
4172 update : ( { req } ) => Boolean ( req . user ) ,
4273 delete : ( { req } ) => Boolean ( req . user ) ,
4374 } ,
75+ hooks : {
76+ beforeValidate : [
77+ ( { data, originalDoc } ) => {
78+ if ( ! data ) return data
79+
80+ const customPage = Boolean ( data . customPage ?? originalDoc ?. customPage )
81+ if ( ! customPage ) {
82+ data . customSlug = null
83+ return data
84+ }
85+
86+ const submittedSlug = typeof data . customSlug === "string" ? data . customSlug : typeof originalDoc ?. customSlug === "string" ? originalDoc . customSlug : ""
87+ const title = typeof data . title === "string" ? data . title : typeof originalDoc ?. title === "string" ? originalDoc . title : ""
88+
89+ data . slug = null
90+ data . customSlug = formatSlug ( submittedSlug . trim ( ) || title )
91+
92+ return data
93+ } ,
94+ ] ,
95+ } ,
4496 fields : [
4597 {
4698 name : "title" ,
4799 type : "text" ,
48100 required : true ,
49101 localized : true ,
50102 } ,
103+ {
104+ name : "customPage" ,
105+ label : "Custom Page" ,
106+ type : "checkbox" ,
107+ defaultValue : false ,
108+ admin : {
109+ description : "Enable this to use a custom URL slug instead of one of the predefined pages." ,
110+ } ,
111+ } ,
51112 {
52113 name : "slug" ,
114+ label : "Slug" ,
53115 type : "select" ,
54- required : true ,
116+ required : false ,
55117 unique : true ,
56118 index : true ,
57- options : [
58- { label : "main" , value : "main" } ,
59- { label : "jobs" , value : "jobs" } ,
60- { label : "blog" , value : "blog" } ,
61- { label : "features" , value : "features" } ,
62- { label : "about-us" , value : "about-us" } ,
63- { label : "legal-notice" , value : "legal-notice" } ,
64- { label : "privacy" , value : "privacy" } ,
65- { label : "terms" , value : "terms" } ,
66- { label : "contact" , value : "contact" } ,
67- { label : "actions" , value : "actions" } ,
68- { label : "action-details" , value : "action-details" } ,
69- { label : "community-edition" , value : "community-edition" } ,
70- { label : "enterprise-edition" , value : "enterprise-edition" } ,
71- { label : "subscription" , value : "subscription" } ,
72- ] ,
119+ options : RESERVED_PAGE_SLUGS . map ( ( slug ) => ( { label : slug , value : slug } ) ) ,
120+ admin : {
121+ condition : ( _ , siblingData ) => ! isCustomPage ( siblingData ) ,
122+ } ,
123+ validate : ( value : string | null | undefined , { siblingData } : { siblingData : unknown } ) => {
124+ if ( isCustomPage ( siblingData ) ) return true
125+ return value ? true : "Select a predefined slug or enable Custom Page."
126+ } ,
127+ } ,
128+ {
129+ name : "customSlug" ,
130+ label : "Slug" ,
131+ type : "text" ,
132+ required : false ,
133+ unique : true ,
134+ index : true ,
135+ admin : {
136+ condition : ( _ , siblingData ) => isCustomPage ( siblingData ) ,
137+ description : "Generated from the title when left empty. Predefined page slugs cannot be used." ,
138+ } ,
139+ validate : ( value : string | null | undefined , { siblingData } : { siblingData : unknown } ) => {
140+ if ( ! isCustomPage ( siblingData ) ) return true
141+
142+ const slug = formatSlug ( typeof value === "string" ? value : "" )
143+ if ( ! slug ) return true
144+ if ( ( RESERVED_PAGE_SLUGS as readonly string [ ] ) . includes ( slug ) ) {
145+ return `"${ slug } " is reserved for a predefined page.`
146+ }
147+
148+ return true
149+ } ,
73150 } ,
74151 {
75152 name : "layout" ,
0 commit comments