@justanarthur/payload-www 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ language switcher share a single source of truth with the rest of your app.
11
11
 
12
12
  - **Composer** — `createWWWConfig({ locales, blocks, defaultPlugins? })` returns `{ withWWWConfig }`. One composer call is enough for the most common cases.
13
13
  - **Collections** — `Pages` (title, blocks tab, slug, drafts, revalidation hook), `Posts` (title, excerpt, richText, drafts, revalidation hook), `StaticPages` (system pages — 404, 500, search-empty, … — addressed by a `kind` discriminator, not a slug)
14
- - **Globals** — `Header` and `Footer` (both `nav` blocks with `navColumn` / `navItem`)
14
+ - **Globals** — `Header` and `Footer` (both `nav` blocks with `navColumn` / `navItem`). Extend the nav link schema (e.g. a `description` or a `navHover` mega-menu group) via `createHeaderGlobal({ navColumnLinkFields, navItemLinkFields })` / `createFooterGlobal({ … })`, or `link({ extraFields })` directly.
15
15
  - **Default render components** — `PagesPage`, `HeaderPage`, `FooterPage` Server Components. Override any of them by setting a different `custom.path` on the collection / global.
16
16
  - **LivePreviewListener** — built in. The lib's `createCollectionPageExports` default page renders it (via `React.lazy` so the server dist stays free of `'use client'` imports) whenever Next.js draft mode is on. Hosts get live preview automatically — no opt-in required. The component itself is also exported from `/render-components` for hosts that want to mount it elsewhere.
17
17
  - **Hooks** — `createRevalidateCollectionHook(opts)` (canonical factory for **all** collections: Pages, Posts, host-defined; per-locale `revalidatePath` fan-out + `revalidateTag('collection_<slug>_<id>', 'max')` + sitemap tag, with a `pathMode: 'tag-only'` mode for collections without a URL like `staticPages`), `createRevalidatePageHooks()` (deprecated alias for Pages preset), `createRevalidateGlobalHook(slug)` (per-locale tag for globals)
@@ -308,6 +308,7 @@ Subpath imports:
308
308
  | `blocks` | `Block[]` | yes | Blocks the Pages collection accepts. |
309
309
  | `linkRelationTo` | `string[]` | no | Collection slugs the Header / Footer nav links can reference. Default: `['pages']`. |
310
310
  | `registerPosts` | `boolean` | no | Register the lib's Posts collection. Default `true`. |
311
+ | `nested` | `boolean` | no | Enable nested Pages slugs (`about_us` → `/about/us`). Relaxes the Pages slug validation to accept the `_` divider. Pass the same `nested: true` to `createCollectionPageExports` on the Pages route (and `nested: { pages: true }` to `createSitemapFile`). Default `false`. |
311
312
  | StaticPages | — | yes | Always registered (every site has 404). Hosts filter it out in `collections:` to opt. |
312
313
  | `defaultPlugins` | `(defaults: Plugin[]) => Plugin[]`| no | Final say on the default `[seoPlugin, imageHashPlugin, translator]` list. |
313
314
 
@@ -76,6 +76,14 @@ type CreatePagesCollectionOptions = {
76
76
  * when omitted.
77
77
  */
78
78
  defaultLocale?: CreateRevalidateCollectionHookOptions["defaultLocale"];
79
+ /**
80
+ * Enable nested (hierarchical) slugs. When `true`, the slug field
81
+ * additionally accepts the `_` nesting divider (so `about_us`
82
+ * renders at `/about/us`) and the admin description documents the
83
+ * convention. Pair with `createCollectionPageExports({ nested: true })`
84
+ * on the matching route. Default: `false` (flat, hyphen-only slugs).
85
+ */
86
+ nested?: boolean;
79
87
  };
80
88
  declare const createPagesCollection: (blocks: Block[], options?: Omit<CreatePagesCollectionOptions, "blocks">) => CollectionConfig;
81
89
  import { CollectionConfig as CollectionConfig2 } from "payload";
@@ -134,7 +142,7 @@ type CreateStaticPagesCollectionOptions = {
134
142
  blocks: Block2[];
135
143
  };
136
144
  declare const createStaticPagesCollection: (blocks: Block2[]) => CollectionConfig3;
137
- import { GlobalConfig } from "payload";
145
+ import { Field, GlobalConfig } from "payload";
138
146
  type CreateHeaderGlobalOptions = {
139
147
  /**
140
148
  * Optional override for the `custom.path` Payload uses to resolve
@@ -148,6 +156,17 @@ type CreateHeaderGlobalOptions = {
148
156
  * different list when your host has additional linkable collections.
149
157
  */
150
158
  linkRelationTo?: string[];
159
+ /**
160
+ * Extra fields appended to each `navColumn` link — e.g. a
161
+ * `description` text field or a `navHover` group for a mega-menu.
162
+ * Lets hosts enrich the column-link schema without redefining the
163
+ * whole nav.
164
+ */
165
+ navColumnLinkFields?: Field[];
166
+ /**
167
+ * Extra fields appended to each top-level `navItem` link.
168
+ */
169
+ navItemLinkFields?: Field[];
151
170
  };
152
171
  /**
153
172
  * Build the Header global. The lib's default shape: a `nav` blocks
@@ -155,7 +174,7 @@ type CreateHeaderGlobalOptions = {
155
174
  * and `navItem` (a single link).
156
175
  */
157
176
  declare const createHeaderGlobal: (options?: CreateHeaderGlobalOptions) => GlobalConfig;
158
- import { GlobalConfig as GlobalConfig2 } from "payload";
177
+ import { Field as Field2, GlobalConfig as GlobalConfig2 } from "payload";
159
178
  type CreateFooterGlobalOptions = {
160
179
  /**
161
180
  * Optional override for the `custom.path` Payload uses to resolve
@@ -169,6 +188,16 @@ type CreateFooterGlobalOptions = {
169
188
  * different list when your host has additional linkable collections.
170
189
  */
171
190
  linkRelationTo?: string[];
191
+ /**
192
+ * Extra fields appended to each `navColumn` link — e.g. a
193
+ * `description` text field. Lets hosts enrich the column-link
194
+ * schema without redefining the whole nav.
195
+ */
196
+ navColumnLinkFields?: Field2[];
197
+ /**
198
+ * Extra fields appended to each top-level `navItem` link.
199
+ */
200
+ navItemLinkFields?: Field2[];
172
201
  };
173
202
  /**
174
203
  * Build the Footer global. The lib's default shape mirrors the
@@ -195,20 +195,25 @@ var createPagesCollection = (blocks, options = {}) => {
195
195
  renderPath = PAGES_RENDER_PATH,
196
196
  slug: collectionSlug = PAGES_SLUG2,
197
197
  localePrefix,
198
- defaultLocale
198
+ defaultLocale,
199
+ nested = false
199
200
  } = options;
201
+ const slugPattern = nested ? /^[a-z0-9_-]+$/ : /^[a-z0-9-]+$/;
200
202
  const slugField = {
201
203
  name: "slug",
202
204
  type: "text",
203
205
  required: true,
204
206
  unique: true,
205
207
  index: true,
206
- admin: { position: "sidebar" },
208
+ admin: {
209
+ position: "sidebar",
210
+ ...nested ? { description: "Lowercase, hyphens for words. Use the `_` divider for nesting, e.g. `about_us` → `/about/us`." } : {}
211
+ },
207
212
  validate: (value) => {
208
213
  if (typeof value !== "string")
209
214
  return "Slug must be a string";
210
- if (value !== "" && !/^[a-z0-9-]+$/.test(value)) {
211
- return "Slug must be lowercase, with hyphens (no spaces or special characters)";
215
+ if (value !== "" && !slugPattern.test(value)) {
216
+ return nested ? "Slug must be lowercase, with hyphens or the `_` nesting divider (no spaces or other special characters)" : "Slug must be lowercase, with hyphens (no spaces or special characters)";
212
217
  }
213
218
  return true;
214
219
  }
@@ -420,7 +425,8 @@ var link = (options = {}) => {
420
425
  disableLabel = false,
421
426
  relationTo = [PAGES_SLUG],
422
427
  localized = false,
423
- overrides = {}
428
+ overrides = {},
429
+ extraFields = []
424
430
  } = options;
425
431
  const result = {
426
432
  name: "link",
@@ -497,6 +503,9 @@ var link = (options = {}) => {
497
503
  options: opts
498
504
  });
499
505
  }
506
+ if (extraFields.length) {
507
+ result.fields.push(...extraFields);
508
+ }
500
509
  return { ...result, ...overrides };
501
510
  };
502
511
 
@@ -516,17 +525,22 @@ function createRevalidateGlobalHook(slug) {
516
525
 
517
526
  // src/data/collections/globals/Header/config.ts
518
527
  var createHeaderGlobal = (options = {}) => {
519
- const { renderPath = HEADER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
528
+ const {
529
+ renderPath = HEADER_RENDER_PATH,
530
+ linkRelationTo = [PAGES_SLUG],
531
+ navColumnLinkFields = [],
532
+ navItemLinkFields = []
533
+ } = options;
520
534
  const navColumnBlock = {
521
535
  slug: "navColumn",
522
536
  fields: [
523
537
  { name: "title", type: "text", required: true, localized: true },
524
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
538
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
525
539
  ]
526
540
  };
527
541
  const navItemBlock = {
528
542
  slug: "navItem",
529
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
543
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
530
544
  };
531
545
  return {
532
546
  slug: "header",
@@ -548,17 +562,22 @@ var createHeaderGlobal = (options = {}) => {
548
562
 
549
563
  // src/data/collections/globals/Footer/config.ts
550
564
  var createFooterGlobal = (options = {}) => {
551
- const { renderPath = FOOTER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
565
+ const {
566
+ renderPath = FOOTER_RENDER_PATH,
567
+ linkRelationTo = [PAGES_SLUG],
568
+ navColumnLinkFields = [],
569
+ navItemLinkFields = []
570
+ } = options;
552
571
  const navColumnBlock = {
553
572
  slug: "navColumn",
554
573
  fields: [
555
574
  { name: "title", type: "text", required: true, localized: true },
556
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
575
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
557
576
  ]
558
577
  };
559
578
  const navItemBlock = {
560
579
  slug: "navItem",
561
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
580
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
562
581
  };
563
582
  return {
564
583
  slug: "footer",
package/dist/config.d.ts CHANGED
@@ -58,6 +58,14 @@ type WWWConfigOptions = {
58
58
  */
59
59
  registerPosts?: boolean;
60
60
  /**
61
+ * Enable nested (hierarchical) Pages slugs. When `true`, the lib's
62
+ * Pages collection accepts the `_` nesting divider in its slug
63
+ * (`about_us` → `/about/us`). Pass the same `nested: true` to
64
+ * `createCollectionPageExports` on the Pages route so URL building,
65
+ * hreflang, and static params expand the divider. Default: `false`.
66
+ */
67
+ nested?: boolean;
68
+ /**
61
69
  * Final say on the lib's default plugin list. Receives the
62
70
  * `[seoPlugin, imageHashPlugin, translator]` array; return the
63
71
  * list to apply. Common uses: drop the translator (no AI
package/dist/config.js CHANGED
@@ -195,20 +195,25 @@ var createPagesCollection = (blocks, options = {}) => {
195
195
  renderPath = PAGES_RENDER_PATH,
196
196
  slug: collectionSlug = PAGES_SLUG2,
197
197
  localePrefix,
198
- defaultLocale
198
+ defaultLocale,
199
+ nested = false
199
200
  } = options;
201
+ const slugPattern = nested ? /^[a-z0-9_-]+$/ : /^[a-z0-9-]+$/;
200
202
  const slugField = {
201
203
  name: "slug",
202
204
  type: "text",
203
205
  required: true,
204
206
  unique: true,
205
207
  index: true,
206
- admin: { position: "sidebar" },
208
+ admin: {
209
+ position: "sidebar",
210
+ ...nested ? { description: "Lowercase, hyphens for words. Use the `_` divider for nesting, e.g. `about_us` → `/about/us`." } : {}
211
+ },
207
212
  validate: (value) => {
208
213
  if (typeof value !== "string")
209
214
  return "Slug must be a string";
210
- if (value !== "" && !/^[a-z0-9-]+$/.test(value)) {
211
- return "Slug must be lowercase, with hyphens (no spaces or special characters)";
215
+ if (value !== "" && !slugPattern.test(value)) {
216
+ return nested ? "Slug must be lowercase, with hyphens or the `_` nesting divider (no spaces or other special characters)" : "Slug must be lowercase, with hyphens (no spaces or special characters)";
212
217
  }
213
218
  return true;
214
219
  }
@@ -420,7 +425,8 @@ var link = (options = {}) => {
420
425
  disableLabel = false,
421
426
  relationTo = [PAGES_SLUG],
422
427
  localized = false,
423
- overrides = {}
428
+ overrides = {},
429
+ extraFields = []
424
430
  } = options;
425
431
  const result = {
426
432
  name: "link",
@@ -497,6 +503,9 @@ var link = (options = {}) => {
497
503
  options: opts
498
504
  });
499
505
  }
506
+ if (extraFields.length) {
507
+ result.fields.push(...extraFields);
508
+ }
500
509
  return { ...result, ...overrides };
501
510
  };
502
511
 
@@ -516,17 +525,22 @@ function createRevalidateGlobalHook(slug) {
516
525
 
517
526
  // src/data/collections/globals/Header/config.ts
518
527
  var createHeaderGlobal = (options = {}) => {
519
- const { renderPath = HEADER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
528
+ const {
529
+ renderPath = HEADER_RENDER_PATH,
530
+ linkRelationTo = [PAGES_SLUG],
531
+ navColumnLinkFields = [],
532
+ navItemLinkFields = []
533
+ } = options;
520
534
  const navColumnBlock = {
521
535
  slug: "navColumn",
522
536
  fields: [
523
537
  { name: "title", type: "text", required: true, localized: true },
524
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
538
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
525
539
  ]
526
540
  };
527
541
  const navItemBlock = {
528
542
  slug: "navItem",
529
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
543
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
530
544
  };
531
545
  return {
532
546
  slug: "header",
@@ -548,17 +562,22 @@ var createHeaderGlobal = (options = {}) => {
548
562
 
549
563
  // src/data/collections/globals/Footer/config.ts
550
564
  var createFooterGlobal = (options = {}) => {
551
- const { renderPath = FOOTER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
565
+ const {
566
+ renderPath = FOOTER_RENDER_PATH,
567
+ linkRelationTo = [PAGES_SLUG],
568
+ navColumnLinkFields = [],
569
+ navItemLinkFields = []
570
+ } = options;
552
571
  const navColumnBlock = {
553
572
  slug: "navColumn",
554
573
  fields: [
555
574
  { name: "title", type: "text", required: true, localized: true },
556
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
575
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
557
576
  ]
558
577
  };
559
578
  const navItemBlock = {
560
579
  slug: "navItem",
561
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
580
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
562
581
  };
563
582
  return {
564
583
  slug: "footer",
@@ -756,6 +775,7 @@ function createSitemapFile(options) {
756
775
  const collectionDefaults = options.perCollection?.[collectionSlug] ?? {};
757
776
  const urlPrefix = (options.urlPrefixes?.[collectionSlug] ?? "").replace(/\/$/, "");
758
777
  const siteUrl = options.getServerSideURL().replace(/\/$/, "");
778
+ const isNested = options.nested?.[collectionSlug] ?? false;
759
779
  for (const locale of activeLocales) {
760
780
  const docs = await queryAllDocs({
761
781
  collectionSlug,
@@ -771,13 +791,14 @@ function createSitemapFile(options) {
771
791
  if (seen.has(dedupeKey))
772
792
  continue;
773
793
  seen.add(dedupeKey);
774
- const url = `${siteUrl}${prefixFor(locale, defaultLocale, options.localePrefix ?? "always")}${urlPrefix}/${storedSlug}`;
794
+ const slugPath = isNested ? storedSlug.replaceAll("_", "/") : storedSlug;
795
+ const url = `${siteUrl}${prefixFor(locale, defaultLocale, options.localePrefix ?? "always")}${urlPrefix}/${slugPath}`;
775
796
  const languages = await buildHreflangAlternates({
776
797
  siteUrl,
777
798
  locale,
778
799
  urlPrefix,
779
800
  storedSlug,
780
- nested: false,
801
+ nested: isNested,
781
802
  homeSlug: "",
782
803
  defaultLocale,
783
804
  locales: activeLocales,
@@ -808,7 +829,7 @@ function createSitemapFile(options) {
808
829
  }
809
830
  // src/config/createWWWConfig.ts
810
831
  function createWWWConfig(options) {
811
- const { locales, routing, blocks, linkRelationTo, registerPosts = true, defaultPlugins } = options;
832
+ const { locales, routing, blocks, linkRelationTo, registerPosts = true, nested = false, defaultPlugins } = options;
812
833
  const defaultLocale = routing?.defaultLocale ?? locales[0] ?? "";
813
834
  const localePrefixMode = (() => {
814
835
  const raw = routing?.localePrefix;
@@ -821,7 +842,8 @@ function createWWWConfig(options) {
821
842
  }
822
843
  const buildPagesCollection = () => createPagesCollection(blocks, {
823
844
  localePrefix: localePrefixMode,
824
- defaultLocale
845
+ defaultLocale,
846
+ nested
825
847
  });
826
848
  const buildPostsCollection = () => createPostsCollection({
827
849
  localePrefix: localePrefixMode,
@@ -20,6 +20,14 @@ type LinkOptions = {
20
20
  overrides?: Partial<GroupField>;
21
21
  /** Legacy alias for the host's custom field overrides. */
22
22
  linkOverrides?: Partial<GroupField>;
23
+ /**
24
+ * Extra fields appended to the link group — e.g. a `description`
25
+ * text field or a `navHover` group for a mega-menu. Lets hosts
26
+ * extend the nav link schema without re-implementing the whole
27
+ * `link` field. Appended after the standard type/label/appearance
28
+ * fields.
29
+ */
30
+ extraFields?: Field[];
23
31
  };
24
32
  declare const link: (options?: LinkOptions) => Field;
25
33
  import { Field as Field2 } from "payload";
@@ -36,7 +36,8 @@ var link = (options = {}) => {
36
36
  disableLabel = false,
37
37
  relationTo = [PAGES_SLUG],
38
38
  localized = false,
39
- overrides = {}
39
+ overrides = {},
40
+ extraFields = []
40
41
  } = options;
41
42
  const result = {
42
43
  name: "link",
@@ -113,6 +114,9 @@ var link = (options = {}) => {
113
114
  options: opts
114
115
  });
115
116
  }
117
+ if (extraFields.length) {
118
+ result.fields.push(...extraFields);
119
+ }
116
120
  return { ...result, ...overrides };
117
121
  };
118
122
 
@@ -76,6 +76,14 @@ type CreatePagesCollectionOptions = {
76
76
  * when omitted.
77
77
  */
78
78
  defaultLocale?: CreateRevalidateCollectionHookOptions["defaultLocale"];
79
+ /**
80
+ * Enable nested (hierarchical) slugs. When `true`, the slug field
81
+ * additionally accepts the `_` nesting divider (so `about_us`
82
+ * renders at `/about/us`) and the admin description documents the
83
+ * convention. Pair with `createCollectionPageExports({ nested: true })`
84
+ * on the matching route. Default: `false` (flat, hyphen-only slugs).
85
+ */
86
+ nested?: boolean;
79
87
  };
80
88
  declare const createPagesCollection: (blocks: Block[], options?: Omit<CreatePagesCollectionOptions, "blocks">) => CollectionConfig;
81
89
  import { CollectionConfig as CollectionConfig2 } from "payload";
@@ -134,7 +142,7 @@ type CreateStaticPagesCollectionOptions = {
134
142
  blocks: Block2[];
135
143
  };
136
144
  declare const createStaticPagesCollection: (blocks: Block2[]) => CollectionConfig3;
137
- import { GlobalConfig } from "payload";
145
+ import { Field, GlobalConfig } from "payload";
138
146
  type CreateHeaderGlobalOptions = {
139
147
  /**
140
148
  * Optional override for the `custom.path` Payload uses to resolve
@@ -148,6 +156,17 @@ type CreateHeaderGlobalOptions = {
148
156
  * different list when your host has additional linkable collections.
149
157
  */
150
158
  linkRelationTo?: string[];
159
+ /**
160
+ * Extra fields appended to each `navColumn` link — e.g. a
161
+ * `description` text field or a `navHover` group for a mega-menu.
162
+ * Lets hosts enrich the column-link schema without redefining the
163
+ * whole nav.
164
+ */
165
+ navColumnLinkFields?: Field[];
166
+ /**
167
+ * Extra fields appended to each top-level `navItem` link.
168
+ */
169
+ navItemLinkFields?: Field[];
151
170
  };
152
171
  /**
153
172
  * Build the Header global. The lib's default shape: a `nav` blocks
@@ -155,7 +174,7 @@ type CreateHeaderGlobalOptions = {
155
174
  * and `navItem` (a single link).
156
175
  */
157
176
  declare const createHeaderGlobal: (options?: CreateHeaderGlobalOptions) => GlobalConfig;
158
- import { GlobalConfig as GlobalConfig2 } from "payload";
177
+ import { Field as Field2, GlobalConfig as GlobalConfig2 } from "payload";
159
178
  type CreateFooterGlobalOptions = {
160
179
  /**
161
180
  * Optional override for the `custom.path` Payload uses to resolve
@@ -169,6 +188,16 @@ type CreateFooterGlobalOptions = {
169
188
  * different list when your host has additional linkable collections.
170
189
  */
171
190
  linkRelationTo?: string[];
191
+ /**
192
+ * Extra fields appended to each `navColumn` link — e.g. a
193
+ * `description` text field. Lets hosts enrich the column-link
194
+ * schema without redefining the whole nav.
195
+ */
196
+ navColumnLinkFields?: Field2[];
197
+ /**
198
+ * Extra fields appended to each top-level `navItem` link.
199
+ */
200
+ navItemLinkFields?: Field2[];
172
201
  };
173
202
  /**
174
203
  * Build the Footer global. The lib's default shape mirrors the
@@ -195,20 +195,25 @@ var createPagesCollection = (blocks, options = {}) => {
195
195
  renderPath = PAGES_RENDER_PATH,
196
196
  slug: collectionSlug = PAGES_SLUG2,
197
197
  localePrefix,
198
- defaultLocale
198
+ defaultLocale,
199
+ nested = false
199
200
  } = options;
201
+ const slugPattern = nested ? /^[a-z0-9_-]+$/ : /^[a-z0-9-]+$/;
200
202
  const slugField = {
201
203
  name: "slug",
202
204
  type: "text",
203
205
  required: true,
204
206
  unique: true,
205
207
  index: true,
206
- admin: { position: "sidebar" },
208
+ admin: {
209
+ position: "sidebar",
210
+ ...nested ? { description: "Lowercase, hyphens for words. Use the `_` divider for nesting, e.g. `about_us` → `/about/us`." } : {}
211
+ },
207
212
  validate: (value) => {
208
213
  if (typeof value !== "string")
209
214
  return "Slug must be a string";
210
- if (value !== "" && !/^[a-z0-9-]+$/.test(value)) {
211
- return "Slug must be lowercase, with hyphens (no spaces or special characters)";
215
+ if (value !== "" && !slugPattern.test(value)) {
216
+ return nested ? "Slug must be lowercase, with hyphens or the `_` nesting divider (no spaces or other special characters)" : "Slug must be lowercase, with hyphens (no spaces or special characters)";
212
217
  }
213
218
  return true;
214
219
  }
@@ -420,7 +425,8 @@ var link = (options = {}) => {
420
425
  disableLabel = false,
421
426
  relationTo = [PAGES_SLUG],
422
427
  localized = false,
423
- overrides = {}
428
+ overrides = {},
429
+ extraFields = []
424
430
  } = options;
425
431
  const result = {
426
432
  name: "link",
@@ -497,6 +503,9 @@ var link = (options = {}) => {
497
503
  options: opts
498
504
  });
499
505
  }
506
+ if (extraFields.length) {
507
+ result.fields.push(...extraFields);
508
+ }
500
509
  return { ...result, ...overrides };
501
510
  };
502
511
 
@@ -516,17 +525,22 @@ function createRevalidateGlobalHook(slug) {
516
525
 
517
526
  // src/data/collections/globals/Header/config.ts
518
527
  var createHeaderGlobal = (options = {}) => {
519
- const { renderPath = HEADER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
528
+ const {
529
+ renderPath = HEADER_RENDER_PATH,
530
+ linkRelationTo = [PAGES_SLUG],
531
+ navColumnLinkFields = [],
532
+ navItemLinkFields = []
533
+ } = options;
520
534
  const navColumnBlock = {
521
535
  slug: "navColumn",
522
536
  fields: [
523
537
  { name: "title", type: "text", required: true, localized: true },
524
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
538
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
525
539
  ]
526
540
  };
527
541
  const navItemBlock = {
528
542
  slug: "navItem",
529
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
543
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
530
544
  };
531
545
  return {
532
546
  slug: "header",
@@ -548,17 +562,22 @@ var createHeaderGlobal = (options = {}) => {
548
562
 
549
563
  // src/data/collections/globals/Footer/config.ts
550
564
  var createFooterGlobal = (options = {}) => {
551
- const { renderPath = FOOTER_RENDER_PATH, linkRelationTo = [PAGES_SLUG] } = options;
565
+ const {
566
+ renderPath = FOOTER_RENDER_PATH,
567
+ linkRelationTo = [PAGES_SLUG],
568
+ navColumnLinkFields = [],
569
+ navItemLinkFields = []
570
+ } = options;
552
571
  const navColumnBlock = {
553
572
  slug: "navColumn",
554
573
  fields: [
555
574
  { name: "title", type: "text", required: true, localized: true },
556
- { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })] }
575
+ { name: "links", type: "array", fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navColumnLinkFields })] }
557
576
  ]
558
577
  };
559
578
  const navItemBlock = {
560
579
  slug: "navItem",
561
- fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo })]
580
+ fields: [link({ appearances: false, localized: true, relationTo: linkRelationTo, extraFields: navItemLinkFields })]
562
581
  };
563
582
  return {
564
583
  slug: "footer",