@bison-lab/payload-core 3.24.0 → 3.25.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
@@ -8,10 +8,10 @@ are extracted.
8
8
  Full guide: <https://payload.bisonlab.ai/site/seo/>
9
9
 
10
10
  ```bash
11
- pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @bison-lab/tokens @bison-lab/fonts
11
+ pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @payloadcms/plugin-redirects @bison-lab/tokens @bison-lab/fonts
12
12
  ```
13
13
 
14
- Peers: `payload` and `@payloadcms/plugin-seo` are required. `@bison-lab/tokens`
14
+ Peers: `payload`, `@payloadcms/plugin-seo`, and `@payloadcms/plugin-redirects` are required. `@bison-lab/tokens`
15
15
  and `@bison-lab/fonts` are needed to adopt the Theme Global; `@payloadcms/ui`
16
16
  and `react` are needed for its admin fields; `@bison-lab/ui` and
17
17
  `@payloadcms/live-preview-react` are needed for the live theme preview.
@@ -22,7 +22,7 @@ lockstep.
22
22
 
23
23
  | Import | Contents | Runs where |
24
24
  | ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
25
- | `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
25
+ | `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createRedirects`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
26
26
  | `@bison-lab/payload-core/metadata` | `pageMetadata`, the title helpers, the same types | Server. What a page route imports; it does not load the plugin. |
27
27
  | `@bison-lab/payload-core/theme` | `getPublishedTheme`, `getPublishedIdentity`, `themeConfigFromDoc`, `themeHead`, `themeHeadFromDoc` | Server. What a root layout imports; it does not load the plugin or React. |
28
28
  | `@bison-lab/payload-core/navigation` | `getNavigation`, the Header and Footer slugs | Server. What a site header imports; it does not load the plugin or React. |
@@ -45,11 +45,12 @@ the site spells its name and its URL scheme once.
45
45
 
46
46
  ```ts
47
47
  // payload.config.ts
48
- import { firstImageIn, seoPlugin } from "@bison-lab/payload-core";
48
+ import { createRedirects, firstImageIn, seoPlugin } from "@bison-lab/payload-core";
49
49
  import type { Page } from "@/payload-types";
50
50
 
51
51
  export default buildConfig({
52
52
  plugins: [
53
+ createRedirects(),
53
54
  seoPlugin<Page>({
54
55
  siteName: "Acme Clinics",
55
56
  urlFor: (doc) => (doc.slug ? absoluteUrl(pagePath(doc.slug)) : undefined),
@@ -318,10 +319,16 @@ read from the live Features and Roles Globals) so Payload's
318
319
  ## Pages, users, and media
319
320
 
320
321
  `createPages`, `createUsers`, `createMedia`, `slugField`,
321
- `adminOnlyApiTab`, and `documentTitleActions` ship from the same
322
+ `createRedirects`, `adminOnlyApiTab`, and `documentTitleActions` ship from the same
322
323
  React-free entry. Pages take the
323
324
  site's hero and layout blocks, reserved-slug predicate, and preview
324
- path. Create follows the Content tick. Update without Publish is
325
+ path. The slug follows the title until first publish, then locks. Unlock
326
+ warns in plain words and the same save writes a 308. `createRedirects()`
327
+ wraps `@payloadcms/plugin-redirects` for `pages` only (permanent 308) and
328
+ collapses chains when a published slug changes. The plugin stores the
329
+ rows; the site still has to serve them. Pin the plugin to the same
330
+ version as `payload`, then `payload generate:importmap`,
331
+ `payload generate:types`, and `payload migrate:create`. Create follows the Content tick. Update without Publish is
325
332
  constrained to `_status: draft` — an Author can save a draft and cannot
326
333
  publish or edit a live page. Users sit under Settings next to Roles,
327
334
  take `secureCookies`, and offer whatever rows the Global (or the seed
@@ -418,6 +425,7 @@ to a type that has one.
418
425
 
419
426
  `noIndexField`, the plugin's own fields, the Theme Global fields, the
420
427
  Roles Global fields, the Admin nav fields, the Header and Footer fields,
421
- and the brand-assets collection fields are columns
428
+ the Pages slug lock fields, the Redirects collection, and the
429
+ brand-assets collection fields are columns
422
430
  in every consuming site. A change to them is a schema change: say "run
423
431
  `payload migrate:create`" in the changeset.
package/dist/admin.d.mts CHANGED
@@ -126,6 +126,13 @@ declare function RolesRowLabel(): react_jsx_runtime0.JSX.Element;
126
126
  */
127
127
  declare const RoleSlugField: TextFieldClientComponent;
128
128
  //#endregion
129
+ //#region src/admin/slug-field.d.ts
130
+ /**
131
+ * Slug follows the title until first publish. After that it is read-only
132
+ * until Unlock, which warns that the old address will redirect.
133
+ */
134
+ declare const SlugField: TextFieldClientComponent;
135
+ //#endregion
129
136
  //#region src/admin/role-name-field.d.ts
130
137
  /**
131
138
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -214,5 +221,5 @@ declare function AdminNavItemRowLabel(): react_jsx_runtime0.JSX.Element;
214
221
  */
215
222
  declare const AdminNavEntityField: TextFieldClientComponent;
216
223
  //#endregion
217
- export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
224
+ export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
218
225
  //# sourceMappingURL=admin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;cCCf,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;AnBZ/B;;coB0Ca,mBAAA,EAAqB,yBAAA;;;iBCflB,oBAAA,CAAA;EAAuB;AAAA;EAAc,QAAA,GAAW,SAAA;AAAA,IAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;;;iBCxB3D,iBAAA,CAAA;EAAoB;AAAA;EAAU,IAAA;AAAA,IAAc,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCmB3C,uBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;UC5Be,aAAA;EACf,eAAA,GAAkB,uBAAA;AAAA;;;AxBGpB;;;iBwB6CgB,QAAA,CAAA;EAAW;AAAA,GAAmB,aAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBCnD3C,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;iBCGhB,oBAAA,CAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;cCEvB,mBAAA,EAAqB,wBAAA"}
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/slug-field.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;cCGf,SAAA,EAAW,wBAAA;;;;;;;cCFX,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;ApBZ/B;;cqB0Ca,mBAAA,EAAqB,yBAAA;;;iBCflB,oBAAA,CAAA;EAAuB;AAAA;EAAc,QAAA,GAAW,SAAA;AAAA,IAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;;;iBCxB3D,iBAAA,CAAA;EAAoB;AAAA;EAAU,IAAA;AAAA,IAAc,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCmB3C,uBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;UC5Be,aAAA;EACf,eAAA,GAAkB,uBAAA;AAAA;;;AzBGpB;;;iByB6CgB,QAAA,CAAA;EAAW;AAAA,GAAmB,aAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBCnD3C,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;iBCGhB,oBAAA,CAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;cCEvB,mBAAA,EAAqB,wBAAA"}
package/dist/admin.mjs CHANGED
@@ -4,10 +4,11 @@ import { ArrayField, Button, CheckboxInput, FieldDescription, FieldError, FieldL
4
4
  import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, deriveDarkPalette, deriveLightPalette, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, setColorScaleInclude } from "@bison-lab/tokens";
5
5
  import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import { Badge, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
7
+ import { Badge, Button as Button$1, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
8
8
  import { HexColorPicker } from "react-colorful";
9
9
  import { createPortal } from "react-dom";
10
10
  import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
11
+ import "payload";
11
12
  //#region src/admin/color-field.tsx
12
13
  const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
13
14
  /**
@@ -3004,6 +3005,90 @@ const RoleSlugField = ({ field, path, readOnly }) => {
3004
3005
  });
3005
3006
  };
3006
3007
  //#endregion
3008
+ //#region src/fields/slug.ts
3009
+ const SLUG_UNLOCK_WARNING = "This page is live. Changing the address will send visitors from the old URL to the new one.";
3010
+ function slugifySegment(segment) {
3011
+ return segment.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3012
+ }
3013
+ /**
3014
+ * A stored slug from whatever was typed: NFKD, hyphenated, slash segments
3015
+ * kept, so `Café / About Us` becomes `cafe/about-us`.
3016
+ */
3017
+ function normalizeSlug(value) {
3018
+ return value.split("/").map(slugifySegment).filter(Boolean).join("/");
3019
+ }
3020
+ //#endregion
3021
+ //#region src/admin/slug-field.tsx
3022
+ /**
3023
+ * Slug follows the title until first publish. After that it is read-only
3024
+ * until Unlock, which warns that the old address will redirect.
3025
+ */
3026
+ const SlugField = ({ field, path, readOnly }) => {
3027
+ const { value, setValue, showError, errorMessage } = useField({ path });
3028
+ const { value: unlock, setValue: setUnlock } = useField({ path: "slugUnlock" });
3029
+ const title = useFormFields(([fields]) => fields.title?.value);
3030
+ const slugLocked = Boolean(useFormFields(([fields]) => fields.slugLocked?.value));
3031
+ const locked = Boolean(readOnly) || slugLocked && !unlock;
3032
+ const required = Boolean(field.required);
3033
+ const id = `field-${path.replace(/\./g, "__")}`;
3034
+ useEffect(() => {
3035
+ if (slugLocked || typeof title !== "string") return;
3036
+ const next = normalizeSlug(title);
3037
+ if (next !== (value ?? "")) setValue(next);
3038
+ }, [
3039
+ setValue,
3040
+ slugLocked,
3041
+ title,
3042
+ value
3043
+ ]);
3044
+ return /* @__PURE__ */ jsxs("div", {
3045
+ className: [
3046
+ fieldBaseClass,
3047
+ "text",
3048
+ locked && "read-only"
3049
+ ].filter(Boolean).join(" "),
3050
+ children: [
3051
+ /* @__PURE__ */ jsx(FieldLabel, {
3052
+ htmlFor: id,
3053
+ label: field.label,
3054
+ required
3055
+ }),
3056
+ /* @__PURE__ */ jsxs("div", {
3057
+ className: `${fieldBaseClass}__wrap`,
3058
+ children: [
3059
+ /* @__PURE__ */ jsx(FieldError, {
3060
+ message: errorMessage,
3061
+ path,
3062
+ showError
3063
+ }),
3064
+ /* @__PURE__ */ jsx("input", {
3065
+ id,
3066
+ type: "text",
3067
+ autoComplete: "off",
3068
+ spellCheck: false,
3069
+ readOnly: locked,
3070
+ disabled: locked,
3071
+ value: value ?? "",
3072
+ onChange: (event) => {
3073
+ if (!locked) setValue(event.target.value);
3074
+ }
3075
+ }),
3076
+ slugLocked && !unlock ? /* @__PURE__ */ jsx(Button$1, {
3077
+ variant: "outline",
3078
+ onClick: () => setUnlock(true),
3079
+ children: "Unlock"
3080
+ }) : null
3081
+ ]
3082
+ }),
3083
+ unlock ? /* @__PURE__ */ jsx("p", { children: SLUG_UNLOCK_WARNING }) : null,
3084
+ /* @__PURE__ */ jsx(FieldDescription, {
3085
+ description: field.admin?.description,
3086
+ path
3087
+ })
3088
+ ]
3089
+ });
3090
+ };
3091
+ //#endregion
3007
3092
  //#region src/admin/role-name-field.tsx
3008
3093
  /**
3009
3094
  * Role name. Letters and spaces only, so the hidden slug stays
@@ -3743,6 +3828,6 @@ const AdminNavEntityField = ({ field, path, permissions, readOnly, schemaPath })
3743
3828
  });
3744
3829
  };
3745
3830
  //#endregion
3746
- export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
3831
+ export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
3747
3832
 
3748
3833
  //# sourceMappingURL=admin.mjs.map