@tetherto/dev-websites-core 0.0.0 → 0.2.1

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.
@@ -0,0 +1,1419 @@
1
+ import { t as createLogger } from "./logger-BPURnavq.mjs";
2
+ import { buildConfig } from "payload";
3
+ import { BlocksFeature, CodeBlock, EXPERIMENTAL_TableFeature, UploadFeature, editorConfigFactory, lexicalEditor } from "@payloadcms/richtext-lexical";
4
+ import { s3Storage } from "@payloadcms/storage-s3";
5
+ //#region src/cms/shared/access.ts
6
+ const isAdminUser = (req) => req.user?.collection === "users";
7
+ const isDisabledApiToken = (req) => req.user?.collection === "api-tokens" && req.user?.enabled === false;
8
+ /**
9
+ * Build access helpers. Pass `{ apiTokens: true }` when the `api-tokens`
10
+ * collection is registered (stock by default via `buildCmsConfig`).
11
+ */
12
+ function createAccessHelpers(options = {}) {
13
+ const apiTokens = options.apiTokens === true;
14
+ const adminOnly = ({ req }) => isAdminUser(req);
15
+ const adminOrPublished = ({ req }) => {
16
+ if (isAdminUser(req)) return true;
17
+ if (apiTokens && isDisabledApiToken(req)) return false;
18
+ return { _status: { equals: "published" } };
19
+ };
20
+ const publicRead = ({ req }) => {
21
+ if (apiTokens && isDisabledApiToken(req)) return false;
22
+ return true;
23
+ };
24
+ const adminOnlyField = ({ req }) => isAdminUser(req);
25
+ return {
26
+ adminOnly,
27
+ adminOrPublished,
28
+ publicRead,
29
+ adminOnlyField
30
+ };
31
+ }
32
+ //#endregion
33
+ //#region src/cms/collections/createApiTokens.ts
34
+ const adminOrSelf = ({ req }) => {
35
+ if (req.user?.collection === "users") return true;
36
+ if (req.user?.collection === "api-tokens") return { id: { equals: req.user.id } };
37
+ return false;
38
+ };
39
+ /**
40
+ * Stock API-key auth collection for server-to-server Payload REST/GraphQL clients.
41
+ * Disable per site with `overrides['api-tokens'] = false`.
42
+ */
43
+ function createApiTokens(access) {
44
+ return {
45
+ slug: "api-tokens",
46
+ labels: {
47
+ singular: "API Token",
48
+ plural: "API Tokens"
49
+ },
50
+ admin: {
51
+ useAsTitle: "name",
52
+ defaultColumns: [
53
+ "name",
54
+ "description",
55
+ "enabled",
56
+ "createdAt"
57
+ ],
58
+ description: "Server-to-server tokens for non-admin clients calling the Payload REST/GraphQL API. Each token authenticates as an api-tokens user and is restricted to published, public-readable content."
59
+ },
60
+ auth: {
61
+ useAPIKey: true,
62
+ disableLocalStrategy: true
63
+ },
64
+ access: {
65
+ read: adminOrSelf,
66
+ create: access.adminOnly,
67
+ update: access.adminOnly,
68
+ delete: access.adminOnly,
69
+ admin: access.adminOnly
70
+ },
71
+ fields: [
72
+ {
73
+ name: "name",
74
+ type: "text",
75
+ required: true,
76
+ admin: { description: "Human-readable label, e.g. \"mobile-app-prod\" or \"partner-foo\"." }
77
+ },
78
+ {
79
+ name: "description",
80
+ type: "textarea",
81
+ admin: { description: "Who is this token for and what is it used for? Free-form notes." }
82
+ },
83
+ {
84
+ name: "enabled",
85
+ type: "checkbox",
86
+ defaultValue: true,
87
+ admin: {
88
+ position: "sidebar",
89
+ description: "Disable to soft-revoke. To hard-revoke, clear the API Key field or delete the document."
90
+ }
91
+ }
92
+ ]
93
+ };
94
+ }
95
+ //#endregion
96
+ //#region src/cms/hooks/slugify.ts
97
+ const slugify = ({ value }) => typeof value === "string" ? value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") : value;
98
+ //#endregion
99
+ //#region src/cms/shared/seo-fields.ts
100
+ const MAX_META_TITLE = 60;
101
+ const MAX_META_DESCRIPTION = 160;
102
+ const validateMetaTitle = (value) => {
103
+ if (!value || value.trim().length === 0) return "Meta title is required.";
104
+ if (value.length > MAX_META_TITLE) return `Meta title must be ${MAX_META_TITLE} characters or fewer (currently ${value.length}).`;
105
+ return true;
106
+ };
107
+ const validateMetaDescription = (value) => {
108
+ if (!value || value.trim().length === 0) return "Meta description is required.";
109
+ if (value.length > MAX_META_DESCRIPTION) return `Meta description must be ${MAX_META_DESCRIPTION} characters or fewer (currently ${value.length}).`;
110
+ return true;
111
+ };
112
+ const validateUrl = (value) => {
113
+ if (!value) return true;
114
+ try {
115
+ new URL(value);
116
+ return true;
117
+ } catch {
118
+ return "Must be a valid URL (e.g. https://example.com/page).";
119
+ }
120
+ };
121
+ const seoFields = {
122
+ name: "seo",
123
+ type: "group",
124
+ label: "SEO",
125
+ fields: [
126
+ {
127
+ name: "metaTitle",
128
+ type: "text",
129
+ required: true,
130
+ localized: true,
131
+ maxLength: MAX_META_TITLE,
132
+ validate: validateMetaTitle,
133
+ admin: { description: `Required. ${MAX_META_TITLE} characters max — shown in browser tabs and search results.` }
134
+ },
135
+ {
136
+ name: "metaDescription",
137
+ type: "textarea",
138
+ required: true,
139
+ localized: true,
140
+ validate: validateMetaDescription,
141
+ admin: { description: `Required. ${MAX_META_DESCRIPTION} characters max — shown in search result snippets.` }
142
+ },
143
+ {
144
+ name: "ogImage",
145
+ type: "upload",
146
+ relationTo: "media",
147
+ admin: { description: "Used when the page is shared on social media (1200×630 recommended). Falls back to cover image if empty." }
148
+ },
149
+ {
150
+ name: "canonicalUrl",
151
+ type: "text",
152
+ validate: validateUrl,
153
+ admin: { description: "Override for syndicated content. Leave empty to auto-derive from slug." }
154
+ },
155
+ {
156
+ name: "noIndex",
157
+ type: "checkbox",
158
+ defaultValue: false,
159
+ admin: { description: "When checked, search engines will not index this page." }
160
+ },
161
+ {
162
+ name: "noFollow",
163
+ type: "checkbox",
164
+ defaultValue: false,
165
+ admin: { description: "When checked, search engines will not follow links on this page (paired with noIndex if needed)." }
166
+ }
167
+ ]
168
+ };
169
+ //#endregion
170
+ //#region src/cms/collections/createAuthors.ts
171
+ function createAuthors(access) {
172
+ return {
173
+ slug: "authors",
174
+ admin: {
175
+ useAsTitle: "name",
176
+ defaultColumns: [
177
+ "name",
178
+ "slug",
179
+ "role"
180
+ ]
181
+ },
182
+ access: {
183
+ read: access.publicRead,
184
+ create: access.adminOnly,
185
+ update: access.adminOnly,
186
+ delete: access.adminOnly
187
+ },
188
+ fields: [
189
+ {
190
+ name: "name",
191
+ type: "text",
192
+ required: true,
193
+ localized: true
194
+ },
195
+ {
196
+ name: "slug",
197
+ type: "text",
198
+ required: true,
199
+ unique: true,
200
+ admin: { position: "sidebar" },
201
+ hooks: { beforeValidate: [slugify] }
202
+ },
203
+ {
204
+ name: "avatar",
205
+ type: "upload",
206
+ relationTo: "media"
207
+ },
208
+ {
209
+ name: "role",
210
+ type: "text",
211
+ localized: true,
212
+ admin: { description: "Job title or role (e.g. \"Lead Engineer\")" }
213
+ },
214
+ {
215
+ name: "bio",
216
+ type: "textarea",
217
+ localized: true
218
+ },
219
+ {
220
+ name: "social",
221
+ type: "group",
222
+ label: "Social Profiles",
223
+ admin: { position: "sidebar" },
224
+ fields: [
225
+ {
226
+ name: "website",
227
+ type: "text"
228
+ },
229
+ {
230
+ name: "twitter",
231
+ type: "text",
232
+ admin: { description: "@handle or full URL" }
233
+ },
234
+ {
235
+ name: "linkedin",
236
+ type: "text"
237
+ },
238
+ {
239
+ name: "github",
240
+ type: "text"
241
+ }
242
+ ]
243
+ },
244
+ {
245
+ ...seoFields,
246
+ fields: [...seoFields.fields, {
247
+ name: "jsonLd",
248
+ type: "group",
249
+ label: "Structured Data Overrides",
250
+ admin: { description: "Used in JSON-LD Person schema on author pages and article bylines." },
251
+ fields: [
252
+ {
253
+ name: "jobTitle",
254
+ type: "text",
255
+ admin: { description: "schema.org jobTitle (defaults to role if empty)" }
256
+ },
257
+ {
258
+ name: "worksFor",
259
+ type: "text",
260
+ admin: { description: "Organization name for schema.org worksFor" }
261
+ },
262
+ {
263
+ name: "sameAs",
264
+ type: "array",
265
+ admin: { description: "Extra canonical URLs for this person (Wikipedia, Crunchbase, etc.)" },
266
+ fields: [{
267
+ name: "url",
268
+ type: "text",
269
+ required: true
270
+ }]
271
+ }
272
+ ]
273
+ }]
274
+ }
275
+ ]
276
+ };
277
+ }
278
+ //#endregion
279
+ //#region src/cms/blocks/language-options.ts
280
+ /** Shared language options for CMS code blocks and Lexical CodeBlock feature. */
281
+ const languageOptions = [
282
+ {
283
+ label: "TypeScript",
284
+ value: "typescript"
285
+ },
286
+ {
287
+ label: "JavaScript",
288
+ value: "javascript"
289
+ },
290
+ {
291
+ label: "Python",
292
+ value: "python"
293
+ },
294
+ {
295
+ label: "Bash / Shell",
296
+ value: "bash"
297
+ },
298
+ {
299
+ label: "Shell",
300
+ value: "shell"
301
+ },
302
+ {
303
+ label: "JSON",
304
+ value: "json"
305
+ },
306
+ {
307
+ label: "CSS",
308
+ value: "css"
309
+ },
310
+ {
311
+ label: "HTML",
312
+ value: "html"
313
+ },
314
+ {
315
+ label: "Rust",
316
+ value: "rust"
317
+ },
318
+ {
319
+ label: "Go",
320
+ value: "go"
321
+ },
322
+ {
323
+ label: "YAML",
324
+ value: "yaml"
325
+ },
326
+ {
327
+ label: "Markdown",
328
+ value: "markdown"
329
+ },
330
+ {
331
+ label: "Plain Text",
332
+ value: "plaintext"
333
+ }
334
+ ];
335
+ const codeBlockLanguages = Object.fromEntries(languageOptions.map((opt) => [opt.value, opt.label]));
336
+ const defaultCodeLanguage = "plaintext";
337
+ //#endregion
338
+ //#region src/cms/blocks/code.ts
339
+ const codeBlock = {
340
+ slug: "code",
341
+ labels: {
342
+ singular: "Code Block",
343
+ plural: "Code Blocks"
344
+ },
345
+ fields: [
346
+ {
347
+ name: "language",
348
+ type: "select",
349
+ defaultValue: "typescript",
350
+ options: languageOptions
351
+ },
352
+ {
353
+ name: "filename",
354
+ type: "text",
355
+ admin: { description: "Optional filename label shown above the block" }
356
+ },
357
+ {
358
+ name: "code",
359
+ type: "code",
360
+ required: true,
361
+ admin: { language: "typescript" }
362
+ }
363
+ ]
364
+ };
365
+ //#endregion
366
+ //#region src/cms/blocks/code-tabs.ts
367
+ const codeTabsBlock = {
368
+ slug: "code-tabs",
369
+ labels: {
370
+ singular: "Code Tabs",
371
+ plural: "Code Tabs"
372
+ },
373
+ fields: [{
374
+ name: "tabs",
375
+ type: "array",
376
+ required: true,
377
+ minRows: 1,
378
+ fields: [
379
+ {
380
+ name: "label",
381
+ type: "text",
382
+ required: true,
383
+ admin: { description: "Tab label (e.g. Windows, Linux, Android)" }
384
+ },
385
+ {
386
+ name: "language",
387
+ type: "select",
388
+ defaultValue: "bash",
389
+ options: languageOptions
390
+ },
391
+ {
392
+ name: "code",
393
+ type: "code",
394
+ required: true,
395
+ admin: { language: "bash" }
396
+ }
397
+ ]
398
+ }]
399
+ };
400
+ //#endregion
401
+ //#region src/cms/blocks/download-cta.ts
402
+ const downloadCtaBlock = {
403
+ slug: "download-cta",
404
+ labels: {
405
+ singular: "Download CTA",
406
+ plural: "Download CTAs"
407
+ },
408
+ fields: [
409
+ {
410
+ name: "eyebrow",
411
+ type: "text",
412
+ localized: true
413
+ },
414
+ {
415
+ name: "heading",
416
+ type: "text",
417
+ required: true,
418
+ localized: true
419
+ },
420
+ {
421
+ name: "headingAccent",
422
+ type: "text",
423
+ localized: true
424
+ },
425
+ {
426
+ name: "description",
427
+ type: "textarea",
428
+ localized: true
429
+ },
430
+ {
431
+ name: "submitLabel",
432
+ type: "text",
433
+ required: true,
434
+ localized: true
435
+ },
436
+ {
437
+ name: "downloadUrl",
438
+ type: "text",
439
+ required: true
440
+ },
441
+ {
442
+ name: "downloadLabel",
443
+ type: "text",
444
+ required: true,
445
+ localized: true
446
+ },
447
+ {
448
+ name: "successMessage",
449
+ type: "textarea",
450
+ localized: true
451
+ },
452
+ {
453
+ name: "consent",
454
+ type: "textarea",
455
+ localized: true
456
+ },
457
+ {
458
+ name: "footnote",
459
+ type: "textarea",
460
+ localized: true
461
+ }
462
+ ]
463
+ };
464
+ //#endregion
465
+ //#region src/cms/blocks/image.ts
466
+ const imageBlock = {
467
+ slug: "image",
468
+ labels: {
469
+ singular: "Image",
470
+ plural: "Images"
471
+ },
472
+ fields: [
473
+ {
474
+ name: "image",
475
+ type: "upload",
476
+ relationTo: "media",
477
+ required: true
478
+ },
479
+ {
480
+ name: "caption",
481
+ type: "text",
482
+ localized: true
483
+ },
484
+ {
485
+ name: "size",
486
+ type: "select",
487
+ defaultValue: "full",
488
+ options: [
489
+ {
490
+ label: "Full Width",
491
+ value: "full"
492
+ },
493
+ {
494
+ label: "Wide",
495
+ value: "wide"
496
+ },
497
+ {
498
+ label: "Medium",
499
+ value: "medium"
500
+ }
501
+ ]
502
+ }
503
+ ]
504
+ };
505
+ //#endregion
506
+ //#region src/cms/blocks/video.ts
507
+ const videoBlock = {
508
+ slug: "video",
509
+ labels: {
510
+ singular: "Video",
511
+ plural: "Videos"
512
+ },
513
+ fields: [
514
+ {
515
+ name: "video",
516
+ type: "upload",
517
+ relationTo: "media",
518
+ required: true,
519
+ filterOptions: { mimeType: { contains: "video" } }
520
+ },
521
+ {
522
+ name: "caption",
523
+ type: "text",
524
+ localized: true
525
+ },
526
+ {
527
+ name: "size",
528
+ type: "select",
529
+ defaultValue: "full",
530
+ options: [
531
+ {
532
+ label: "Full Width",
533
+ value: "full"
534
+ },
535
+ {
536
+ label: "Wide",
537
+ value: "wide"
538
+ },
539
+ {
540
+ label: "Medium",
541
+ value: "medium"
542
+ }
543
+ ]
544
+ }
545
+ ]
546
+ };
547
+ //#endregion
548
+ //#region src/cms/lexical/editor.ts
549
+ const richTextFeatures = ({ defaultFeatures }) => [
550
+ ...defaultFeatures.filter((feature) => feature.key !== "upload"),
551
+ UploadFeature({ collections: { media: { fields: [{
552
+ name: "caption",
553
+ type: "text"
554
+ }] } } }),
555
+ BlocksFeature({ blocks: [CodeBlock({
556
+ languages: codeBlockLanguages,
557
+ defaultLanguage: defaultCodeLanguage
558
+ }), videoBlock] }),
559
+ EXPERIMENTAL_TableFeature()
560
+ ];
561
+ /**
562
+ * Shared Lexical editor for every `richText` field.
563
+ *
564
+ * Adds `CodeBlock`, inline `video` block, media upload captions, and
565
+ * `EXPERIMENTAL_TableFeature` on top of Payload defaults.
566
+ */
567
+ const richTextEditor = () => lexicalEditor({ features: richTextFeatures });
568
+ /** Resolve Lexical editor config for seed scripts / markdown conversion. */
569
+ async function getRichTextEditorConfig(config) {
570
+ return editorConfigFactory.fromFeatures({
571
+ config,
572
+ features: richTextFeatures
573
+ });
574
+ }
575
+ //#endregion
576
+ //#region src/cms/blocks/rich-text.ts
577
+ const richTextBlock = {
578
+ slug: "rich-text",
579
+ labels: {
580
+ singular: "Rich Text",
581
+ plural: "Rich Text Blocks"
582
+ },
583
+ fields: [{
584
+ name: "content",
585
+ type: "richText",
586
+ localized: true,
587
+ editor: richTextEditor()
588
+ }]
589
+ };
590
+ //#endregion
591
+ //#region src/cms/blocks/steps.ts
592
+ const stepsBlock = {
593
+ slug: "steps",
594
+ labels: {
595
+ singular: "Steps List",
596
+ plural: "Steps Lists"
597
+ },
598
+ fields: [{
599
+ name: "items",
600
+ type: "array",
601
+ required: true,
602
+ minRows: 1,
603
+ fields: [
604
+ {
605
+ name: "title",
606
+ type: "text",
607
+ required: true,
608
+ localized: true
609
+ },
610
+ {
611
+ name: "description",
612
+ type: "textarea",
613
+ localized: true
614
+ },
615
+ {
616
+ name: "avatar",
617
+ type: "upload",
618
+ relationTo: "media",
619
+ admin: { description: "Optional avatar or icon shown next to the step" }
620
+ }
621
+ ]
622
+ }]
623
+ };
624
+ //#endregion
625
+ //#region src/cms/blocks/leaf-blocks.ts
626
+ const leafBlocks = [
627
+ richTextBlock,
628
+ codeBlock,
629
+ codeTabsBlock,
630
+ imageBlock,
631
+ videoBlock,
632
+ downloadCtaBlock,
633
+ stepsBlock
634
+ ];
635
+ //#endregion
636
+ //#region src/cms/blocks/accordion.ts
637
+ const accordionBlock = {
638
+ slug: "accordion",
639
+ labels: {
640
+ singular: "Accordion",
641
+ plural: "Accordions"
642
+ },
643
+ fields: [{
644
+ name: "items",
645
+ type: "array",
646
+ required: true,
647
+ minRows: 1,
648
+ fields: [
649
+ {
650
+ name: "title",
651
+ type: "text",
652
+ required: true,
653
+ localized: true
654
+ },
655
+ {
656
+ name: "defaultOpen",
657
+ type: "checkbox",
658
+ defaultValue: false
659
+ },
660
+ {
661
+ name: "content",
662
+ type: "blocks",
663
+ blocks: leafBlocks,
664
+ localized: true
665
+ }
666
+ ]
667
+ }]
668
+ };
669
+ //#endregion
670
+ //#region src/cms/blocks/section-heading.ts
671
+ const sectionHeadingBlock = {
672
+ slug: "section-heading",
673
+ labels: {
674
+ singular: "Section Heading",
675
+ plural: "Section Headings"
676
+ },
677
+ fields: [{
678
+ name: "text",
679
+ type: "text",
680
+ required: true,
681
+ localized: true
682
+ }]
683
+ };
684
+ //#endregion
685
+ //#region src/cms/blocks/index.ts
686
+ const blogContentBlocks = [
687
+ richTextBlock,
688
+ codeBlock,
689
+ codeTabsBlock,
690
+ imageBlock,
691
+ videoBlock,
692
+ downloadCtaBlock,
693
+ stepsBlock,
694
+ accordionBlock
695
+ ];
696
+ const pageContentBlocks = [
697
+ richTextBlock,
698
+ codeBlock,
699
+ codeTabsBlock,
700
+ imageBlock,
701
+ stepsBlock,
702
+ accordionBlock
703
+ ];
704
+ const showcaseContentBlocks = [
705
+ sectionHeadingBlock,
706
+ richTextBlock,
707
+ codeBlock,
708
+ imageBlock
709
+ ];
710
+ //#endregion
711
+ //#region src/cms/hooks/set-published-at.ts
712
+ const setPublishedAt = ({ data }) => {
713
+ if (data && !data.publishedAt && data._status === "published") data.publishedAt = (/* @__PURE__ */ new Date()).toISOString();
714
+ return data;
715
+ };
716
+ //#endregion
717
+ //#region src/cms/shared/drafts-with-autosave.ts
718
+ const draftsWithAutosave = {
719
+ maxPerDoc: 25,
720
+ drafts: {
721
+ autosave: { interval: 800 },
722
+ schedulePublish: true
723
+ }
724
+ };
725
+ //#endregion
726
+ //#region src/cms/collections/createBlogPosts.ts
727
+ /** Stock blog-posts collection (no site revalidation — add via overrides.hooks). */
728
+ function createBlogPosts(access) {
729
+ return {
730
+ slug: "blog-posts",
731
+ defaultSort: "-publishedAt",
732
+ admin: {
733
+ useAsTitle: "title",
734
+ defaultColumns: [
735
+ "title",
736
+ "slug",
737
+ "author",
738
+ "publishedAt",
739
+ "_status"
740
+ ]
741
+ },
742
+ access: {
743
+ read: access.adminOrPublished,
744
+ create: access.adminOnly,
745
+ update: access.adminOnly,
746
+ delete: access.adminOnly
747
+ },
748
+ versions: draftsWithAutosave,
749
+ hooks: { beforeChange: [setPublishedAt] },
750
+ fields: [
751
+ {
752
+ name: "title",
753
+ type: "text",
754
+ required: true,
755
+ localized: true
756
+ },
757
+ {
758
+ name: "slug",
759
+ type: "text",
760
+ required: true,
761
+ unique: true,
762
+ admin: {
763
+ position: "sidebar",
764
+ description: "URL-friendly identifier. Not localized — one slug per post."
765
+ },
766
+ hooks: { beforeValidate: [slugify] }
767
+ },
768
+ {
769
+ name: "coverImage",
770
+ type: "upload",
771
+ relationTo: "media"
772
+ },
773
+ {
774
+ name: "imageAlt",
775
+ type: "text",
776
+ localized: true,
777
+ admin: { description: "Alt text used for the auto-generated social/OG image (`og:image:alt`). Should describe the post for screen readers." }
778
+ },
779
+ {
780
+ name: "excerpt",
781
+ type: "textarea",
782
+ localized: true,
783
+ admin: { description: "Short summary shown in listing cards and meta description fallback." }
784
+ },
785
+ {
786
+ name: "content",
787
+ type: "blocks",
788
+ localized: true,
789
+ blocks: blogContentBlocks
790
+ },
791
+ {
792
+ name: "author",
793
+ type: "relationship",
794
+ relationTo: "authors",
795
+ admin: { position: "sidebar" }
796
+ },
797
+ {
798
+ name: "category",
799
+ type: "relationship",
800
+ relationTo: "categories",
801
+ admin: { position: "sidebar" }
802
+ },
803
+ {
804
+ name: "readingTime",
805
+ type: "number",
806
+ min: 1,
807
+ admin: {
808
+ position: "sidebar",
809
+ description: "Estimated reading time in minutes."
810
+ }
811
+ },
812
+ {
813
+ name: "publishedAt",
814
+ type: "date",
815
+ admin: {
816
+ position: "sidebar",
817
+ date: { pickerAppearance: "dayAndTime" },
818
+ description: "Auto-filled on first publish if left empty."
819
+ }
820
+ },
821
+ {
822
+ name: "showLatestNews",
823
+ type: "checkbox",
824
+ defaultValue: true,
825
+ admin: {
826
+ position: "sidebar",
827
+ description: "Show a \"Latest News\" section at the bottom of this post."
828
+ }
829
+ },
830
+ seoFields
831
+ ]
832
+ };
833
+ }
834
+ //#endregion
835
+ //#region src/cms/collections/createCategories.ts
836
+ function createCategories(access) {
837
+ return {
838
+ slug: "categories",
839
+ admin: {
840
+ useAsTitle: "name",
841
+ defaultColumns: ["name", "slug"]
842
+ },
843
+ access: {
844
+ read: access.publicRead,
845
+ create: access.adminOnly,
846
+ update: access.adminOnly,
847
+ delete: access.adminOnly
848
+ },
849
+ fields: [
850
+ {
851
+ name: "name",
852
+ type: "text",
853
+ required: true,
854
+ localized: true
855
+ },
856
+ {
857
+ name: "slug",
858
+ type: "text",
859
+ required: true,
860
+ unique: true,
861
+ admin: { position: "sidebar" },
862
+ hooks: { beforeValidate: [slugify] }
863
+ },
864
+ {
865
+ name: "description",
866
+ type: "textarea",
867
+ localized: true
868
+ }
869
+ ]
870
+ };
871
+ }
872
+ //#endregion
873
+ //#region src/cms/collections/createChangelog.ts
874
+ /**
875
+ * Stock changelog collection (shared fields only).
876
+ * Site-specific body / GitHub / related-block fields — add via `overrides.fields`.
877
+ * Revalidation — add via `overrides.hooks`.
878
+ */
879
+ function createChangelog(access) {
880
+ return {
881
+ slug: "changelog",
882
+ labels: {
883
+ singular: "Changelog",
884
+ plural: "Changelog"
885
+ },
886
+ defaultSort: "-releaseDate",
887
+ admin: {
888
+ useAsTitle: "title",
889
+ defaultColumns: [
890
+ "title",
891
+ "version",
892
+ "versionType",
893
+ "tags",
894
+ "releaseDate",
895
+ "_status"
896
+ ]
897
+ },
898
+ access: {
899
+ read: access.adminOrPublished,
900
+ create: access.adminOnly,
901
+ update: access.adminOnly,
902
+ delete: access.adminOnly
903
+ },
904
+ versions: draftsWithAutosave,
905
+ fields: [
906
+ {
907
+ name: "title",
908
+ type: "text",
909
+ required: true,
910
+ localized: true,
911
+ admin: { description: "Release headline shown on the changelog detail page (e.g. \"Added Solana Support\")." }
912
+ },
913
+ {
914
+ name: "slug",
915
+ type: "text",
916
+ required: true,
917
+ unique: true,
918
+ admin: {
919
+ position: "sidebar",
920
+ description: "URL-friendly identifier. Not localized — one slug per entry."
921
+ },
922
+ hooks: { beforeValidate: [slugify] }
923
+ },
924
+ {
925
+ name: "version",
926
+ type: "text",
927
+ admin: {
928
+ position: "sidebar",
929
+ description: "Optional semantic version label shown in the badge, e.g. v1.1.0 or 2.3.0. Omit for non-versioned announcements."
930
+ }
931
+ },
932
+ {
933
+ name: "versionType",
934
+ type: "select",
935
+ options: [
936
+ {
937
+ label: "Major",
938
+ value: "major"
939
+ },
940
+ {
941
+ label: "Minor",
942
+ value: "minor"
943
+ },
944
+ {
945
+ label: "Patch",
946
+ value: "patch"
947
+ }
948
+ ],
949
+ admin: {
950
+ position: "sidebar",
951
+ description: "Release line for badge styling (major / minor / patch). Editors set this explicitly."
952
+ }
953
+ },
954
+ {
955
+ name: "releaseDate",
956
+ type: "date",
957
+ required: true,
958
+ admin: {
959
+ position: "sidebar",
960
+ date: {
961
+ pickerAppearance: "dayOnly",
962
+ displayFormat: "MMM d, yyyy"
963
+ }
964
+ }
965
+ },
966
+ {
967
+ name: "tags",
968
+ type: "relationship",
969
+ relationTo: "changelog-tags",
970
+ hasMany: true,
971
+ label: "Tags",
972
+ admin: {
973
+ position: "sidebar",
974
+ description: "Editor-managed tags from the Changelog Tags collection."
975
+ }
976
+ },
977
+ {
978
+ name: "heroImage",
979
+ type: "upload",
980
+ relationTo: "media",
981
+ admin: { description: "Large visual above the article body on the changelog detail page." }
982
+ },
983
+ {
984
+ name: "summary",
985
+ type: "textarea",
986
+ localized: true,
987
+ admin: { description: "Short excerpt for listing cards and previews." }
988
+ }
989
+ ]
990
+ };
991
+ }
992
+ //#endregion
993
+ //#region src/cms/collections/createChangelogTags.ts
994
+ /** Stock changelog-tags (no site revalidation — add via overrides.hooks). */
995
+ function createChangelogTags(access) {
996
+ return {
997
+ slug: "changelog-tags",
998
+ defaultSort: "order",
999
+ admin: {
1000
+ useAsTitle: "name",
1001
+ defaultColumns: [
1002
+ "name",
1003
+ "slug",
1004
+ "order"
1005
+ ],
1006
+ description: "Tags shown on changelog entries."
1007
+ },
1008
+ access: {
1009
+ read: access.publicRead,
1010
+ create: access.adminOnly,
1011
+ update: access.adminOnly,
1012
+ delete: access.adminOnly
1013
+ },
1014
+ fields: [
1015
+ {
1016
+ name: "name",
1017
+ type: "text",
1018
+ required: true,
1019
+ localized: true
1020
+ },
1021
+ {
1022
+ name: "slug",
1023
+ type: "text",
1024
+ required: true,
1025
+ unique: true,
1026
+ admin: {
1027
+ position: "sidebar",
1028
+ description: "URL-friendly identifier used by listing filters."
1029
+ },
1030
+ hooks: { beforeValidate: [slugify] }
1031
+ },
1032
+ {
1033
+ name: "order",
1034
+ type: "number",
1035
+ defaultValue: 0,
1036
+ admin: {
1037
+ position: "sidebar",
1038
+ description: "Display order in admin lists (ascending). Ties broken by name."
1039
+ }
1040
+ },
1041
+ {
1042
+ name: "description",
1043
+ type: "textarea",
1044
+ localized: true,
1045
+ admin: { description: "Optional context shown to editors. Not rendered publicly yet." }
1046
+ }
1047
+ ]
1048
+ };
1049
+ }
1050
+ //#endregion
1051
+ //#region src/cms/collections/createMedia.ts
1052
+ /** Stock media upload collection (always publicly readable once uploaded). */
1053
+ function createMedia(access) {
1054
+ return {
1055
+ slug: "media",
1056
+ admin: {
1057
+ useAsTitle: "filename",
1058
+ defaultColumns: [
1059
+ "filename",
1060
+ "alt",
1061
+ "updatedAt"
1062
+ ]
1063
+ },
1064
+ access: {
1065
+ read: access.publicRead,
1066
+ create: access.adminOnly,
1067
+ update: access.adminOnly,
1068
+ delete: access.adminOnly
1069
+ },
1070
+ upload: {
1071
+ imageSizes: [
1072
+ {
1073
+ name: "thumbnail",
1074
+ width: 400,
1075
+ height: 300,
1076
+ position: "centre"
1077
+ },
1078
+ {
1079
+ name: "card",
1080
+ width: 768,
1081
+ height: 512,
1082
+ position: "centre"
1083
+ },
1084
+ {
1085
+ name: "tablet",
1086
+ width: 1024,
1087
+ height: void 0,
1088
+ position: "centre"
1089
+ }
1090
+ ],
1091
+ adminThumbnail: "thumbnail",
1092
+ mimeTypes: [
1093
+ "image/*",
1094
+ "video/*",
1095
+ "application/pdf"
1096
+ ]
1097
+ },
1098
+ fields: [{
1099
+ name: "alt",
1100
+ type: "text",
1101
+ required: true,
1102
+ localized: true
1103
+ }]
1104
+ };
1105
+ }
1106
+ //#endregion
1107
+ //#region src/cms/collections/createPages.ts
1108
+ function createContentPageCollection(access, slug) {
1109
+ return {
1110
+ slug,
1111
+ admin: {
1112
+ useAsTitle: "title",
1113
+ defaultColumns: [
1114
+ "title",
1115
+ "slug",
1116
+ "publishedAt",
1117
+ "_status"
1118
+ ]
1119
+ },
1120
+ access: {
1121
+ read: access.adminOrPublished,
1122
+ create: access.adminOnly,
1123
+ update: access.adminOnly,
1124
+ delete: access.adminOnly
1125
+ },
1126
+ versions: draftsWithAutosave,
1127
+ hooks: { beforeChange: [setPublishedAt] },
1128
+ fields: [
1129
+ {
1130
+ name: "title",
1131
+ type: "text",
1132
+ required: true,
1133
+ localized: true
1134
+ },
1135
+ {
1136
+ name: "slug",
1137
+ type: "text",
1138
+ required: true,
1139
+ unique: true,
1140
+ admin: {
1141
+ position: "sidebar",
1142
+ description: "URL-friendly identifier. Not localized — one slug per page."
1143
+ },
1144
+ hooks: { beforeValidate: [slugify] }
1145
+ },
1146
+ {
1147
+ name: "content",
1148
+ type: "blocks",
1149
+ localized: true,
1150
+ blocks: pageContentBlocks
1151
+ },
1152
+ {
1153
+ name: "publishedAt",
1154
+ type: "date",
1155
+ admin: {
1156
+ position: "sidebar",
1157
+ date: { pickerAppearance: "dayAndTime" },
1158
+ description: "Auto-filled on first publish if left empty."
1159
+ }
1160
+ },
1161
+ seoFields
1162
+ ]
1163
+ };
1164
+ }
1165
+ function createPages(access) {
1166
+ return createContentPageCollection(access, "pages");
1167
+ }
1168
+ function createLegalPages(access) {
1169
+ return createContentPageCollection(access, "legal-pages");
1170
+ }
1171
+ //#endregion
1172
+ //#region src/cms/collections/createUsers.ts
1173
+ function createUsers(access) {
1174
+ return {
1175
+ slug: "users",
1176
+ admin: { useAsTitle: "email" },
1177
+ auth: true,
1178
+ access: {
1179
+ read: access.adminOnly,
1180
+ create: access.adminOnly,
1181
+ update: access.adminOnly,
1182
+ delete: access.adminOnly,
1183
+ admin: access.adminOnly
1184
+ },
1185
+ fields: []
1186
+ };
1187
+ }
1188
+ //#endregion
1189
+ //#region src/cms/globals/createSiteSettings.ts
1190
+ function createSiteSettings(access) {
1191
+ return {
1192
+ slug: "site-settings",
1193
+ label: "Site Settings",
1194
+ access: {
1195
+ read: access.publicRead,
1196
+ update: access.adminOnly
1197
+ },
1198
+ fields: [
1199
+ {
1200
+ name: "name",
1201
+ type: "text",
1202
+ required: true,
1203
+ localized: true,
1204
+ admin: { description: "Public site name used in titles and structured data." }
1205
+ },
1206
+ {
1207
+ name: "defaultDescription",
1208
+ type: "textarea",
1209
+ required: true,
1210
+ localized: true,
1211
+ admin: { description: "Fallback meta description when a page does not define its own." }
1212
+ },
1213
+ {
1214
+ name: "defaultOgImage",
1215
+ type: "upload",
1216
+ relationTo: "media",
1217
+ admin: { description: "Default Open Graph image (1200×630 recommended)." }
1218
+ },
1219
+ {
1220
+ name: "organization",
1221
+ type: "group",
1222
+ label: "Organization",
1223
+ fields: [
1224
+ {
1225
+ name: "name",
1226
+ type: "text",
1227
+ required: true
1228
+ },
1229
+ {
1230
+ name: "legalName",
1231
+ type: "text"
1232
+ },
1233
+ {
1234
+ name: "description",
1235
+ type: "textarea",
1236
+ localized: true
1237
+ },
1238
+ {
1239
+ name: "logo",
1240
+ type: "upload",
1241
+ relationTo: "media"
1242
+ },
1243
+ {
1244
+ name: "foundingDate",
1245
+ type: "text",
1246
+ admin: { description: "Year or ISO date for schema.org structured data." }
1247
+ }
1248
+ ]
1249
+ },
1250
+ {
1251
+ name: "social",
1252
+ type: "group",
1253
+ label: "Social Profiles",
1254
+ fields: [
1255
+ {
1256
+ name: "twitter",
1257
+ type: "text"
1258
+ },
1259
+ {
1260
+ name: "github",
1261
+ type: "text"
1262
+ },
1263
+ {
1264
+ name: "linkedin",
1265
+ type: "text"
1266
+ }
1267
+ ]
1268
+ }
1269
+ ]
1270
+ };
1271
+ }
1272
+ //#endregion
1273
+ //#region src/cms/applyOverride.ts
1274
+ function mergeHooks(base, extra) {
1275
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base ?? {}), ...Object.keys(extra ?? {})]);
1276
+ const merged = {};
1277
+ keys.forEach((key) => {
1278
+ const combined = [...base?.[key] ?? [], ...extra?.[key] ?? []];
1279
+ if (combined.length) merged[key] = combined;
1280
+ });
1281
+ return merged;
1282
+ }
1283
+ function applyOverride(base, override) {
1284
+ if (override === false) return null;
1285
+ if (!override) return base;
1286
+ const next = {
1287
+ ...base,
1288
+ ...override.admin && { admin: {
1289
+ ...base.admin,
1290
+ ...override.admin
1291
+ } },
1292
+ ...override.access && { access: {
1293
+ ...base.access,
1294
+ ...override.access
1295
+ } },
1296
+ ...override.fields && { fields: override.fields(base.fields ?? []) },
1297
+ ...override.hooks && { hooks: mergeHooks(base.hooks, override.hooks) }
1298
+ };
1299
+ return override.config ? override.config(next) : next;
1300
+ }
1301
+ const applyCollectionOverride = (base, override) => applyOverride(base, override);
1302
+ const applyGlobalOverride = (base, override) => applyOverride(base, override);
1303
+ //#endregion
1304
+ //#region src/cms/overrides.ts
1305
+ /** Slugs of the stock collections shipped by `buildCmsConfig`, in registration order. */
1306
+ const STOCK_COLLECTION_SLUGS = [
1307
+ "users",
1308
+ "api-tokens",
1309
+ "media",
1310
+ "authors",
1311
+ "categories",
1312
+ "blog-posts",
1313
+ "pages",
1314
+ "legal-pages",
1315
+ "changelog-tags",
1316
+ "changelog"
1317
+ ];
1318
+ //#endregion
1319
+ //#region src/cms/buildCmsConfig.ts
1320
+ /** Drop keys whose value is `undefined` so they don't clobber Payload defaults. */
1321
+ function defined(obj) {
1322
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
1323
+ }
1324
+ /**
1325
+ * Build a Payload config from shared stock collections/globals plus site
1326
+ * overrides and extras. Types are still generated in the consuming app.
1327
+ */
1328
+ function buildCmsConfig(options) {
1329
+ const overrides = options.overrides ?? {};
1330
+ const access = createAccessHelpers({
1331
+ ...options.access,
1332
+ apiTokens: options.access?.apiTokens ?? overrides["api-tokens"] !== false
1333
+ });
1334
+ const stock = {
1335
+ "users": createUsers(access),
1336
+ "api-tokens": createApiTokens(access),
1337
+ "media": createMedia(access),
1338
+ "authors": createAuthors(access),
1339
+ "categories": createCategories(access),
1340
+ "blog-posts": createBlogPosts(access),
1341
+ "pages": createPages(access),
1342
+ "legal-pages": createLegalPages(access),
1343
+ "changelog-tags": createChangelogTags(access),
1344
+ "changelog": createChangelog(access)
1345
+ };
1346
+ const collections = [...STOCK_COLLECTION_SLUGS.map((slug) => applyCollectionOverride(stock[slug], overrides[slug])).filter((c) => c !== null), ...options.extraCollections ?? []];
1347
+ const siteSettings = applyGlobalOverride(createSiteSettings(access), overrides["site-settings"]);
1348
+ const globals = [...siteSettings ? [siteSettings] : [], ...options.extraGlobals ?? []];
1349
+ return buildConfig({
1350
+ ...options.payload,
1351
+ ...defined({
1352
+ sharp: options.sharp,
1353
+ cors: options.cors,
1354
+ csrf: options.csrf,
1355
+ plugins: options.plugins
1356
+ }),
1357
+ secret: options.secret,
1358
+ db: options.db,
1359
+ admin: {
1360
+ user: "users",
1361
+ ...options.admin
1362
+ },
1363
+ editor: options.editor ?? richTextEditor(),
1364
+ collections,
1365
+ ...globals.length ? { globals } : {},
1366
+ localization: {
1367
+ locales: options.locales.map((code) => ({
1368
+ code,
1369
+ label: code.toUpperCase()
1370
+ })),
1371
+ defaultLocale: options.defaultLocale,
1372
+ fallback: true
1373
+ },
1374
+ typescript: options.typescript ?? { outputFile: "src/payload-types.ts" }
1375
+ });
1376
+ }
1377
+ //#endregion
1378
+ //#region src/cms/requireEnv.ts
1379
+ const log = createLogger("env");
1380
+ /**
1381
+ * Read a required env var. During Next.js `phase-production-build` and
1382
+ * `generate:importmap` (when NEXT_PHASE is unset) returns "" instead of throwing,
1383
+ * because those phases do not connect to the DB.
1384
+ */
1385
+ function requireEnvAtRuntime(name) {
1386
+ const value = process.env[name];
1387
+ const isBuildTime = !process.env.NEXT_PHASE || process.env.NEXT_PHASE === "phase-production-build";
1388
+ if (!value && !isBuildTime) {
1389
+ log.error(`Missing required environment variable: ${name}`);
1390
+ throw new Error(`Missing required environment variable: ${name}`);
1391
+ }
1392
+ return value ?? "";
1393
+ }
1394
+ //#endregion
1395
+ //#region src/cms/createS3MediaPlugin.ts
1396
+ /** Payload S3 storage plugin wired for the stock `media` collection. */
1397
+ function createS3MediaPlugin(options = {}) {
1398
+ const bucket = options.bucket ?? process.env.AWS_S3_BUCKET ?? "";
1399
+ const endpoint = options.endpoint ?? process.env.AWS_S3_ENDPOINT ?? "";
1400
+ const forcePathStyle = options.forcePathStyle ?? process.env.AWS_S3_FORCE_PATH_STYLE === "true";
1401
+ return s3Storage({
1402
+ enabled: Boolean(bucket),
1403
+ collections: { [options.collection ?? "media"]: true },
1404
+ bucket,
1405
+ config: {
1406
+ credentials: {
1407
+ accessKeyId: options.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID ?? "",
1408
+ secretAccessKey: options.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY ?? ""
1409
+ },
1410
+ region: options.region ?? process.env.AWS_S3_REGION ?? "us-east-1",
1411
+ ...endpoint ? { endpoint } : {},
1412
+ ...forcePathStyle ? { forcePathStyle: true } : {}
1413
+ }
1414
+ });
1415
+ }
1416
+ //#endregion
1417
+ export { downloadCtaBlock as A, createAccessHelpers as B, leafBlocks as C, richTextEditor as D, getRichTextEditorConfig as E, languageOptions as F, createAuthors as I, seoFields as L, codeBlock as M, codeBlockLanguages as N, videoBlock as O, defaultCodeLanguage as P, slugify as R, accordionBlock as S, richTextBlock as T, setPublishedAt as _, applyCollectionOverride as a, showcaseContentBlocks as b, createUsers as c, createMedia as d, createChangelogTags as f, draftsWithAutosave as g, createBlogPosts as h, STOCK_COLLECTION_SLUGS as i, codeTabsBlock as j, imageBlock as k, createLegalPages as l, createCategories as m, requireEnvAtRuntime as n, applyGlobalOverride as o, createChangelog as p, buildCmsConfig as r, createSiteSettings as s, createS3MediaPlugin as t, createPages as u, blogContentBlocks as v, stepsBlock as w, sectionHeadingBlock as x, pageContentBlocks as y, createApiTokens as z };
1418
+
1419
+ //# sourceMappingURL=cms-5bS91IRZ.mjs.map