@actuate-media/cms-admin 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +13 -9
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/sidebar-posts.test.js +6 -4
  5. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  6. package/dist/__tests__/lib/seo-service-errors.test.d.ts +2 -0
  7. package/dist/__tests__/lib/seo-service-errors.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/seo-service-errors.test.js +46 -0
  9. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -0
  10. package/dist/__tests__/views/use-seo-resource.test.d.ts +2 -0
  11. package/dist/__tests__/views/use-seo-resource.test.d.ts.map +1 -0
  12. package/dist/__tests__/views/use-seo-resource.test.js +56 -0
  13. package/dist/__tests__/views/use-seo-resource.test.js.map +1 -0
  14. package/dist/actuate-admin.css +1 -1
  15. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  16. package/dist/components/Breadcrumbs.js +4 -1
  17. package/dist/components/Breadcrumbs.js.map +1 -1
  18. package/dist/components/seo/Drawer.d.ts +11 -0
  19. package/dist/components/seo/Drawer.d.ts.map +1 -0
  20. package/dist/components/seo/Drawer.js +17 -0
  21. package/dist/components/seo/Drawer.js.map +1 -0
  22. package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
  23. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
  24. package/dist/components/seo/RedirectEditorDrawer.js +94 -0
  25. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
  26. package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
  27. package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
  28. package/dist/components/seo/SeoCopilotDrawer.js +34 -0
  29. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
  30. package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
  31. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
  32. package/dist/components/seo/SeoEditorDrawer.js +120 -0
  33. package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
  34. package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
  35. package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
  36. package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
  37. package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
  38. package/dist/components/seo/primitives.d.ts +53 -0
  39. package/dist/components/seo/primitives.d.ts.map +1 -0
  40. package/dist/components/seo/primitives.js +101 -0
  41. package/dist/components/seo/primitives.js.map +1 -0
  42. package/dist/layout/Sidebar.d.ts.map +1 -1
  43. package/dist/layout/Sidebar.js +8 -5
  44. package/dist/layout/Sidebar.js.map +1 -1
  45. package/dist/lib/seo-service.d.ts +322 -0
  46. package/dist/lib/seo-service.d.ts.map +1 -0
  47. package/dist/lib/seo-service.js +331 -0
  48. package/dist/lib/seo-service.js.map +1 -0
  49. package/dist/router/index.d.ts.map +1 -1
  50. package/dist/router/index.js +4 -1
  51. package/dist/router/index.js.map +1 -1
  52. package/dist/views/ApiKeys.d.ts +4 -1
  53. package/dist/views/ApiKeys.d.ts.map +1 -1
  54. package/dist/views/ApiKeys.js +5 -3
  55. package/dist/views/ApiKeys.js.map +1 -1
  56. package/dist/views/Dashboard.js +2 -2
  57. package/dist/views/Dashboard.js.map +1 -1
  58. package/dist/views/MediaBrowser.d.ts.map +1 -1
  59. package/dist/views/MediaBrowser.js +36 -25
  60. package/dist/views/MediaBrowser.js.map +1 -1
  61. package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
  62. package/dist/views/Posts/post-type-presets.d.ts +5 -0
  63. package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
  64. package/dist/views/Posts/post-type-presets.js +10 -64
  65. package/dist/views/Posts/post-type-presets.js.map +1 -1
  66. package/dist/views/SEO.d.ts.map +1 -1
  67. package/dist/views/SEO.js +89 -132
  68. package/dist/views/SEO.js.map +1 -1
  69. package/dist/views/Settings.d.ts +4 -1
  70. package/dist/views/Settings.d.ts.map +1 -1
  71. package/dist/views/Settings.js +45 -6
  72. package/dist/views/Settings.js.map +1 -1
  73. package/dist/views/Users.d.ts +4 -1
  74. package/dist/views/Users.d.ts.map +1 -1
  75. package/dist/views/Users.js +5 -3
  76. package/dist/views/Users.js.map +1 -1
  77. package/dist/views/seo/AuditTab.d.ts +6 -0
  78. package/dist/views/seo/AuditTab.d.ts.map +1 -0
  79. package/dist/views/seo/AuditTab.js +57 -0
  80. package/dist/views/seo/AuditTab.js.map +1 -0
  81. package/dist/views/seo/ContentTab.d.ts +5 -0
  82. package/dist/views/seo/ContentTab.d.ts.map +1 -0
  83. package/dist/views/seo/ContentTab.js +27 -0
  84. package/dist/views/seo/ContentTab.js.map +1 -0
  85. package/dist/views/seo/OverviewTab.d.ts +8 -0
  86. package/dist/views/seo/OverviewTab.d.ts.map +1 -0
  87. package/dist/views/seo/OverviewTab.js +35 -0
  88. package/dist/views/seo/OverviewTab.js.map +1 -0
  89. package/dist/views/seo/RedirectsTab.d.ts +2 -0
  90. package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
  91. package/dist/views/seo/RedirectsTab.js +130 -0
  92. package/dist/views/seo/RedirectsTab.js.map +1 -0
  93. package/dist/views/seo/TechnicalTab.d.ts +2 -0
  94. package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
  95. package/dist/views/seo/TechnicalTab.js +105 -0
  96. package/dist/views/seo/TechnicalTab.js.map +1 -0
  97. package/dist/views/seo/useSeoResource.d.ts +8 -0
  98. package/dist/views/seo/useSeoResource.d.ts.map +1 -0
  99. package/dist/views/seo/useSeoResource.js +55 -0
  100. package/dist/views/seo/useSeoResource.js.map +1 -0
  101. package/package.json +2 -2
  102. package/src/AdminRoot.tsx +13 -9
  103. package/src/__tests__/layout/sidebar-posts.test.ts +6 -4
  104. package/src/__tests__/lib/seo-service-errors.test.ts +60 -0
  105. package/src/__tests__/views/use-seo-resource.test.tsx +68 -0
  106. package/src/components/Breadcrumbs.tsx +4 -1
  107. package/src/components/seo/Drawer.tsx +65 -0
  108. package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
  109. package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
  110. package/src/components/seo/SeoEditorDrawer.tsx +359 -0
  111. package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
  112. package/src/components/seo/primitives.tsx +240 -0
  113. package/src/layout/Sidebar.tsx +7 -10
  114. package/src/lib/seo-service.ts +660 -0
  115. package/src/router/index.ts +4 -1
  116. package/src/views/ApiKeys.tsx +10 -5
  117. package/src/views/Dashboard.tsx +2 -2
  118. package/src/views/MediaBrowser.tsx +89 -67
  119. package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
  120. package/src/views/Posts/post-type-presets.ts +15 -64
  121. package/src/views/SEO.tsx +186 -863
  122. package/src/views/Settings.tsx +93 -40
  123. package/src/views/Users.tsx +10 -4
  124. package/src/views/seo/AuditTab.tsx +229 -0
  125. package/src/views/seo/ContentTab.tsx +164 -0
  126. package/src/views/seo/OverviewTab.tsx +257 -0
  127. package/src/views/seo/RedirectsTab.tsx +398 -0
  128. package/src/views/seo/TechnicalTab.tsx +315 -0
  129. package/src/views/seo/useSeoResource.ts +66 -0
@@ -19,6 +19,9 @@ import { cmsApi } from '../lib/api.js'
19
19
 
20
20
  export interface ApiKeysProps {
21
21
  onNavigate?: (path: string) => void
22
+ /** When rendered inside the Settings tab shell, drop the page padding and
23
+ * demote the heading so it reads as a section rather than a second page. */
24
+ embedded?: boolean
22
25
  }
23
26
 
24
27
  interface ApiKeyRecord {
@@ -68,12 +71,14 @@ function formatDate(value: string | null): string {
68
71
  }
69
72
  }
70
73
 
71
- export function ApiKeys(_props: ApiKeysProps) {
74
+ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
72
75
  const { data, loading, error, refetch } = useApiData<ApiKeyRecord[]>('/api-keys')
73
76
  const [showCreate, setShowCreate] = useState(false)
74
77
  const [createdKey, setCreatedKey] = useState<{ key: string; record: ApiKeyRecord } | null>(null)
75
78
 
76
79
  const keys = data ?? []
80
+ const rootPadding = embedded ? '' : 'p-4 sm:p-6'
81
+ const Heading = embedded ? 'h2' : 'h1'
77
82
 
78
83
  const handleRevoke = async (id: string, name: string) => {
79
84
  if (!confirm(`Revoke API key "${name}"? This cannot be undone.`)) return
@@ -93,20 +98,20 @@ export function ApiKeys(_props: ApiKeysProps) {
93
98
 
94
99
  if (loading) {
95
100
  return (
96
- <div className="flex h-64 items-center justify-center p-4 sm:p-6">
101
+ <div className={`flex h-64 items-center justify-center ${embedded ? '' : 'p-4 sm:p-6'}`}>
97
102
  <Loader2 className="h-6 w-6 animate-spin text-blue-600" />
98
103
  </div>
99
104
  )
100
105
  }
101
106
 
102
107
  return (
103
- <div className="p-4 sm:p-6">
108
+ <div className={rootPadding}>
104
109
  <div className="mb-6 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
105
110
  <div>
106
- <h1 className="flex items-center gap-2 text-xl font-semibold text-gray-900 sm:text-2xl">
111
+ <Heading className="flex items-center gap-2 text-xl font-semibold text-gray-900 sm:text-2xl">
107
112
  <KeyRound className="h-5 w-5 text-gray-500" />
108
113
  API Keys
109
- </h1>
114
+ </Heading>
110
115
  <p className="mt-1 text-sm text-gray-500">
111
116
  Long-lived credentials for programmatic access. Use the{' '}
112
117
  <code className="rounded bg-gray-100 px-1 py-0.5 text-xs">Authorization: Bearer</code>{' '}
@@ -425,7 +425,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
425
425
  { label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
426
426
  { label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
427
427
  { label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
428
- { label: 'View API', icon: Globe, onClick: () => nav('/api-keys') },
428
+ { label: 'View API', icon: Globe, onClick: () => nav('/settings?tab=api-keys') },
429
429
  ]
430
430
  return items
431
431
  }, [collections, onNavigate]) // eslint-disable-line react-hooks/exhaustive-deps
@@ -899,7 +899,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
899
899
  </div>
900
900
  <button
901
901
  className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
902
- onClick={() => nav('/api-keys')}
902
+ onClick={() => nav('/settings?tab=api-keys')}
903
903
  >
904
904
  API docs <ExternalLink className="h-3 w-3" />
905
905
  </button>
@@ -30,7 +30,6 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
30
30
  import { useApiData } from '../lib/useApiData.js'
31
31
  import { cmsApi } from '../lib/api.js'
32
32
  import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
33
- import { FocalPointPicker } from '../components/FocalPointPicker.js'
34
33
 
35
34
  interface MediaItem {
36
35
  id: number | string
@@ -120,8 +119,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
120
119
  const [editAlt, setEditAlt] = useState('')
121
120
  const [editTitle, setEditTitle] = useState('')
122
121
  const [editFilename, setEditFilename] = useState('')
123
- const [focalX, setFocalX] = useState(0.5)
124
- const [focalY, setFocalY] = useState(0.5)
125
122
  const [saving, setSaving] = useState(false)
126
123
  const [aiGenerating, setAiGenerating] = useState<string | null>(null)
127
124
  const [uploading, setUploading] = useState(false)
@@ -157,8 +154,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
157
154
  setEditAlt(item.altTag ?? '')
158
155
  setEditTitle(item.title ?? '')
159
156
  setEditFilename(item.name)
160
- setFocalX((item as any).focalPointX ?? 0.5)
161
- setFocalY((item as any).focalPointY ?? 0.5)
162
157
  }
163
158
 
164
159
  const closeDetail = () => {
@@ -187,8 +182,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
187
182
  alt: editAlt,
188
183
  title: editTitle,
189
184
  filename: editFilename,
190
- focalX,
191
- focalY,
192
185
  }),
193
186
  })
194
187
  setSaving(false)
@@ -244,18 +237,38 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
244
237
  return
245
238
  }
246
239
 
247
- await new Promise((r) => setTimeout(r, 1500))
240
+ if (!activeItem) {
241
+ setAiGenerating(null)
242
+ return
243
+ }
244
+
245
+ const res = await cmsApi<{ alt?: string; title?: string; alternatives?: string[] }>(
246
+ '/ai/media-metadata',
247
+ { method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) },
248
+ )
249
+
250
+ if (res.error) {
251
+ // AI plugin/provider not configured (e.g. local dev): fall back to a
252
+ // basic filename-derived suggestion so the action still helps.
253
+ const base = (activeItem.name ?? '')
254
+ .replace(/\.\w+$/, '')
255
+ .replace(/[-_]/g, ' ')
256
+ .trim()
257
+ if (field === 'alt') {
258
+ setEditAlt(base ? `Image of ${base}` : '')
259
+ } else {
260
+ setEditTitle(base.replace(/\b\w/g, (c) => c.toUpperCase()))
261
+ }
262
+ toast.info('AI unavailable — generated a basic suggestion from the filename')
263
+ setAiGenerating(null)
264
+ return
265
+ }
266
+
248
267
  if (field === 'alt') {
249
- const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content`
250
- setEditAlt(generated)
268
+ setEditAlt(res.data?.alt ?? '')
251
269
  toast.success('Alt tag generated by AI')
252
- } else if (field === 'title') {
253
- const generated =
254
- activeItem?.name
255
- .replace(/[-_]/g, ' ')
256
- .replace(/\.\w+$/, '')
257
- .replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
258
- setEditTitle(generated)
270
+ } else {
271
+ setEditTitle(res.data?.title ?? '')
259
272
  toast.success('Title generated by AI')
260
273
  }
261
274
  setAiGenerating(null)
@@ -533,7 +546,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
533
546
  >
534
547
  {viewMode === 'grid' ? (
535
548
  <div
536
- className={`grid h-full gap-2 overflow-y-auto p-2 sm:gap-3 sm:p-3 ${
549
+ className={`grid h-full auto-rows-min content-start gap-3 overflow-y-auto p-2 sm:gap-4 sm:p-3 ${
537
550
  panelOpen
538
551
  ? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
539
552
  : 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
@@ -542,10 +555,17 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
542
555
  {filteredAndSorted.map((item) => {
543
556
  const isActive = activeItem?.id === item.id
544
557
  const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
558
+ const typeLabel = (
559
+ item.name.includes('.')
560
+ ? item.name.split('.').pop()!
561
+ : item.format || item.type || ''
562
+ )
563
+ .replace(/^.*\//, '')
564
+ .toUpperCase()
545
565
  return (
546
566
  <div
547
567
  key={item.id}
548
- className={`group relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 transition-all ${
568
+ className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border-2 transition-all ${
549
569
  isActive
550
570
  ? 'border-blue-500 ring-2 ring-blue-200'
551
571
  : selectedMedia.includes(item.id)
@@ -556,45 +576,61 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
556
576
  draggable
557
577
  onDragStart={(e) => handleDragStart(e, item.id)}
558
578
  >
559
- <div className="flex h-full w-full items-center justify-center bg-gray-100">
560
- <MediaPreview item={item} />
561
- </div>
562
- <div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
563
- <div className="absolute right-0 bottom-0 left-0 p-2">
564
- <p className="truncate text-xs font-medium text-white">{item.name}</p>
565
- <p className="text-xs text-white/80">{item.size}</p>
579
+ <div className="relative aspect-square w-full overflow-hidden">
580
+ <div className="flex h-full w-full items-center justify-center bg-gray-100">
581
+ <MediaPreview item={item} />
566
582
  </div>
567
- </div>
568
- {hasIssues && (
583
+ {hasIssues && (
584
+ <div
585
+ className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
586
+ title="Needs attention"
587
+ >
588
+ <AlertTriangle className="h-3 w-3 text-white" />
589
+ </div>
590
+ )}
569
591
  <div
570
- className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
571
- title="Needs attention"
592
+ className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
593
+ onClick={(e) => handleCheckbox(e, item.id)}
572
594
  >
573
- <AlertTriangle className="h-3 w-3 text-white" />
595
+ <div
596
+ className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
597
+ selectedMedia.includes(item.id)
598
+ ? 'border-blue-600 bg-blue-600'
599
+ : 'border-gray-400 bg-white/80'
600
+ }`}
601
+ >
602
+ {selectedMedia.includes(item.id) && (
603
+ <svg
604
+ className="h-3 w-3 text-white"
605
+ fill="none"
606
+ viewBox="0 0 12 12"
607
+ >
608
+ <path
609
+ d="M10 3L4.5 8.5L2 6"
610
+ stroke="currentColor"
611
+ strokeWidth="2"
612
+ strokeLinecap="round"
613
+ strokeLinejoin="round"
614
+ />
615
+ </svg>
616
+ )}
617
+ </div>
574
618
  </div>
575
- )}
576
- <div
577
- className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
578
- onClick={(e) => handleCheckbox(e, item.id)}
579
- >
580
- <div
581
- className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
582
- selectedMedia.includes(item.id)
583
- ? 'border-blue-600 bg-blue-600'
584
- : 'border-gray-400 bg-white/80'
619
+ </div>
620
+ <div className="flex items-center gap-2 border-t border-gray-200 bg-white px-2 py-1.5">
621
+ <span
622
+ className={`mt-0.5 h-2 w-2 shrink-0 self-start rounded-full ${
623
+ hasIssues ? 'bg-yellow-500' : 'bg-green-500'
585
624
  }`}
586
- >
587
- {selectedMedia.includes(item.id) && (
588
- <svg className="h-3 w-3 text-white" fill="none" viewBox="0 0 12 12">
589
- <path
590
- d="M10 3L4.5 8.5L2 6"
591
- stroke="currentColor"
592
- strokeWidth="2"
593
- strokeLinecap="round"
594
- strokeLinejoin="round"
595
- />
596
- </svg>
597
- )}
625
+ title={hasIssues ? 'Needs attention' : 'SEO complete'}
626
+ />
627
+ <div className="min-w-0 flex-1">
628
+ <p className="truncate text-xs font-medium text-gray-900">
629
+ {item.name}
630
+ </p>
631
+ <p className="truncate text-xs text-gray-500">
632
+ {[typeLabel, item.size].filter(Boolean).join(' · ')}
633
+ </p>
598
634
  </div>
599
635
  </div>
600
636
  </div>
@@ -872,20 +908,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
872
908
  </div>
873
909
  </div>
874
910
 
875
- {isImageMedia(activeItem) && activeItem.url && (
876
- <div className="border-b border-gray-200 p-4">
877
- <FocalPointPicker
878
- imageUrl={activeItem.url}
879
- focalX={focalX}
880
- focalY={focalY}
881
- onChange={(x, y) => {
882
- setFocalX(x)
883
- setFocalY(y)
884
- }}
885
- />
886
- </div>
887
- )}
888
-
889
911
  <div className="border-b border-gray-200 p-4">
890
912
  <h4 className="mb-3 text-xs font-semibold tracking-wide text-gray-500 uppercase">
891
913
  Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
@@ -11,8 +11,8 @@ import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-ty
11
11
  * /posts/types/new — Quick Post Type gallery + snippet generator.
12
12
  *
13
13
  * Two ways to start:
14
- * 1. Pick a Quick Post Type preset (Case Studies, News, Portfolio,
15
- * Events, Team) — pre-fills the form and tailored extra fields.
14
+ * 1. Pick a Quick Post Type preset (Blog) pre-fills the form and
15
+ * tailored extra fields.
16
16
  * 2. Fill the form from scratch.
17
17
  *
18
18
  * Either way the page emits a paste-ready `actuate.config.ts` snippet.
@@ -10,6 +10,11 @@ import type { PostTypeIconName } from '../../components/posts/icons.js'
10
10
  * Presets are NOT a runtime data model — they only seed the snippet.
11
11
  * Collections stay code-defined; this just saves the developer from
12
12
  * hand-authoring common field sets.
13
+ *
14
+ * Only the canonical **Blog** post type ships as a built-in preset. Other
15
+ * content types (case studies, portfolio, team, …) are available as opt-in
16
+ * collection factories in `@actuate-media/cms-core` (`collections/presets.ts`)
17
+ * rather than shipped here as defaults.
13
18
  */
14
19
 
15
20
  export type PresetColor = 'purple' | 'green' | 'orange' | 'blue' | 'red' | 'pink'
@@ -34,72 +39,18 @@ export interface QuickPreset {
34
39
 
35
40
  export const POST_TYPE_PRESETS: QuickPreset[] = [
36
41
  {
37
- key: 'case-studies',
38
- label: 'Case Studies',
39
- singular: 'Case Study',
40
- plural: 'Case Studies',
41
- slug: 'case-studies',
42
- description: 'Client success stories and detailed project case studies.',
43
- color: 'green',
44
- icon: 'briefcase',
45
- extraFields: [
46
- "client: { type: 'text', label: 'Client Name' },",
47
- "industry: { type: 'text', label: 'Industry' },",
48
- "results: { type: 'richText', label: 'Results & Outcomes' },",
49
- ],
50
- },
51
- {
52
- key: 'news',
53
- label: 'News',
54
- singular: 'News Item',
55
- plural: 'News',
56
- slug: 'news',
57
- description: 'Company announcements, press releases, and industry news.',
58
- color: 'orange',
59
- icon: 'newspaper',
60
- extraFields: ["source: { type: 'text', label: 'Source' },"],
61
- },
62
- {
63
- key: 'portfolio',
64
- label: 'Portfolio',
65
- singular: 'Portfolio Item',
66
- plural: 'Portfolio',
67
- slug: 'portfolio',
68
- description: 'Showcase projects, work samples, and creative pieces.',
69
- color: 'blue',
70
- icon: 'image',
71
- extraFields: [
72
- "projectUrl: { type: 'url', label: 'Project URL' },",
73
- "gallery: { type: 'media', label: 'Gallery' },",
74
- ],
75
- },
76
- {
77
- key: 'events',
78
- label: 'Events',
79
- singular: 'Event',
80
- plural: 'Events',
81
- slug: 'events',
82
- description: 'Webinars, conferences, and scheduled happenings.',
83
- color: 'pink',
84
- icon: 'calendar',
85
- extraFields: [
86
- "startDate: { type: 'date', label: 'Start Date', includeTime: true },",
87
- "endDate: { type: 'date', label: 'End Date', includeTime: true },",
88
- "location: { type: 'text', label: 'Location' },",
89
- ],
90
- },
91
- {
92
- key: 'team',
93
- label: 'Team Members',
94
- singular: 'Team Member',
95
- plural: 'Team',
96
- slug: 'team',
97
- description: 'Staff bios, headshots, and role information.',
42
+ key: 'blog',
43
+ label: 'Blog',
44
+ singular: 'Post',
45
+ plural: 'Posts',
46
+ slug: 'blog',
47
+ description: 'Long-form articles and blog posts with author, category, and tags.',
98
48
  color: 'purple',
99
- icon: 'users',
49
+ icon: 'newspaper',
100
50
  extraFields: [
101
- "role: { type: 'text', label: 'Role' },",
102
- "headshot: { type: 'media', label: 'Headshot' },",
51
+ "author: { type: 'text', label: 'Author' },",
52
+ "category: { type: 'text', label: 'Category' },",
53
+ "tags: { type: 'array', label: 'Tags', fields: { value: { type: 'text', label: 'Tag' } } },",
103
54
  ],
104
55
  },
105
56
  ]