@simmalugnt-se/payload-content-health 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - "Fix with the assistant" on a missing meta title or description says how long to write it
6
+ ("Write a meta title for this page, 50–60 characters."), from the same range as the length check.
7
+ Without it the assistant wrote titles well under the range. Findings for a missing meta title or
8
+ description carry that range as `range`.
9
+
10
+ ## 0.2.1
11
+
12
+ - The work list at `/admin/content-health` requires Admin access. Payload leaves custom Admin views
13
+ public, so a visitor who was not signed in saw the list, filled with what anonymous access could
14
+ read. Now they are sent to the login and back, and a signed-in user without Admin access to
15
+ Payload's unauthorized page.
16
+
3
17
  ## 0.2.0
4
18
 
5
19
  - Work list at `/admin/content-health`, linked from the nav: every finding, filterable by check,
package/README.md CHANGED
@@ -115,8 +115,10 @@ or block the unsaved form no longer has opens the nearest enclosing field instea
115
115
 
116
116
  With `@simmalugnt-se/payload-editor-assistant` (0.5.0 or later), rows in the collections the
117
117
  assistant works in also get "Fix with the assistant": the same link with a ready prompt that the
118
- assistant puts in its message box. The editor sends it. The packages do not import each other; they
119
- share these names:
118
+ assistant puts in its message box, with the length to aim for when a meta title or description is
119
+ missing or off. The editor sends it. To write a missing alt text, the assistant needs to see the
120
+ image: allow `media.view` on the media collection (0.6.0 or later). The packages do not import each
121
+ other; they share these names:
120
122
 
121
123
  | Name | Direction | Meaning |
122
124
  | -------------------------------------------- | ----------------------- | ---------------------------------------- |
@@ -136,6 +138,8 @@ import { scanContentHealth } from "@simmalugnt-se/payload-content-health";
136
138
  const { findings, problems } = await scanContentHealth({ payload, user: req.user });
137
139
  ```
138
140
 
139
- Each finding is `{ check, collection, id, title, path, locale?, length?, heading?, words?, status? }`,
140
- where `path` is the form path with row indexes (`layout.1.items.0.title`). `problems`
141
- lists configured checks that could not run, such as a field path that does not exist.
141
+ Each finding is
142
+ `{ check, collection, id, title, path, locale?, length?, range?, heading?, words?, status? }`, where
143
+ `path` is the form path with row indexes (`layout.1.items.0.title`) and `range` is the length a
144
+ missing meta title or description should have. `problems` lists configured checks that could not
145
+ run, such as a field path that does not exist.
package/dist/checks.d.ts CHANGED
@@ -19,6 +19,7 @@ export type FieldRule = {
19
19
  kind: "required";
20
20
  path: string;
21
21
  imagesOnly?: boolean;
22
+ range?: LengthRange;
22
23
  } | {
23
24
  check: CheckId;
24
25
  kind: "length";
package/dist/checks.js CHANGED
@@ -25,13 +25,23 @@ export function rulesFor(options) {
25
25
  const titleLength = seo.titleLength ?? SEO_DEFAULTS.titleLength;
26
26
  const descriptionLength = seo.descriptionLength ?? SEO_DEFAULTS.descriptionLength;
27
27
  if (title) {
28
- rules.push({ check: "seo.title-missing", kind: "required", path: title });
28
+ rules.push({
29
+ check: "seo.title-missing",
30
+ kind: "required",
31
+ path: title,
32
+ ...(titleLength ? { range: titleLength } : {}),
33
+ });
29
34
  if (titleLength) {
30
35
  rules.push({ check: "seo.title-length", kind: "length", path: title, range: titleLength });
31
36
  }
32
37
  }
33
38
  if (description) {
34
- rules.push({ check: "seo.description-missing", kind: "required", path: description });
39
+ rules.push({
40
+ check: "seo.description-missing",
41
+ kind: "required",
42
+ path: description,
43
+ ...(descriptionLength ? { range: descriptionLength } : {}),
44
+ });
35
45
  if (descriptionLength) {
36
46
  rules.push({
37
47
  check: "seo.description-length",
@@ -105,7 +115,8 @@ export function checkDocument(input) {
105
115
  }
106
116
  checked.push(rule.check);
107
117
  if (isEmpty(value)) {
108
- findings.push(base);
118
+ // The assistant writes to the length the length check will expect.
119
+ findings.push(rule.range ? { ...base, range: rule.range } : base);
109
120
  }
110
121
  continue;
111
122
  }
package/dist/copy.js CHANGED
@@ -122,11 +122,15 @@ export const COPY = {
122
122
  export const ASK = {
123
123
  en: {
124
124
  "media.alt-missing": () => "Write an alt text for this image.",
125
- "seo.title-missing": () => "Write a meta title for this page.",
125
+ "seo.title-missing": ({ range }) => range
126
+ ? `Write a meta title for this page, ${range.min}–${range.max} characters.`
127
+ : "Write a meta title for this page.",
126
128
  "seo.title-length": ({ length }) => length
127
129
  ? `The meta title is ${length.actual} characters. Rewrite it to ${length.min}–${length.max} characters.`
128
130
  : "Rewrite the meta title to a better length.",
129
- "seo.description-missing": () => "Write a meta description for this page.",
131
+ "seo.description-missing": ({ range }) => range
132
+ ? `Write a meta description for this page, ${range.min}–${range.max} characters.`
133
+ : "Write a meta description for this page.",
130
134
  "seo.description-length": ({ length }) => length
131
135
  ? `The meta description is ${length.actual} characters. Rewrite it to ${length.min}–${length.max} characters.`
132
136
  : "Rewrite the meta description to a better length.",
@@ -142,11 +146,15 @@ export const ASK = {
142
146
  },
143
147
  sv: {
144
148
  "media.alt-missing": () => "Skriv en alt-text för den här bilden.",
145
- "seo.title-missing": () => "Skriv en metatitel för den här sidan.",
149
+ "seo.title-missing": ({ range }) => range
150
+ ? `Skriv en metatitel för den här sidan, ${range.min}–${range.max} tecken.`
151
+ : "Skriv en metatitel för den här sidan.",
146
152
  "seo.title-length": ({ length }) => length
147
153
  ? `Metatiteln är ${length.actual} tecken. Skriv om den till ${length.min}–${length.max} tecken.`
148
154
  : "Skriv om metatiteln till en bättre längd.",
149
- "seo.description-missing": () => "Skriv en metabeskrivning för den här sidan.",
155
+ "seo.description-missing": ({ range }) => range
156
+ ? `Skriv en metabeskrivning för den här sidan, ${range.min}–${range.max} tecken.`
157
+ : "Skriv en metabeskrivning för den här sidan.",
150
158
  "seo.description-length": ({ length }) => length
151
159
  ? `Metabeskrivningen är ${length.actual} tecken. Skriv om den till ${length.min}–${length.max} tecken.`
152
160
  : "Skriv om metabeskrivningen till en bättre längd.",
package/dist/links.d.ts CHANGED
@@ -21,6 +21,21 @@ export declare const SELECT_EVENT = "content-health:select";
21
21
  * `config.admin.custom.editorAssistant.collections`. The work list offers the assistant only there.
22
22
  */
23
23
  export declare const ASSISTANT_CUSTOM_KEY = "editorAssistant";
24
+ /**
25
+ * Where to send a visitor who may not use Admin, or null to show the work list. Payload leaves
26
+ * custom views public, so the view checks this itself, as Payload does for its own: to the login
27
+ * with a way back when signed out, to Payload's unauthorized page when signed in without access.
28
+ */
29
+ export declare function accessRedirect(args: {
30
+ routes: {
31
+ admin: string;
32
+ login: `/${string}`;
33
+ unauthorized: `/${string}`;
34
+ };
35
+ canAccessAdmin: boolean;
36
+ signedIn: boolean;
37
+ searchParams?: Record<string, string | string[] | undefined>;
38
+ }): string | null;
24
39
  export declare function assistantCollections(adminCustom: unknown): string[];
25
40
  /** The document in the finding's locale with its field brought into view, and a prompt when given. */
26
41
  export declare function editURL(adminRoute: string, finding: Finding, ask?: string): string;
package/dist/links.js CHANGED
@@ -27,6 +27,31 @@ export const SELECT_EVENT = "content-health:select";
27
27
  * `config.admin.custom.editorAssistant.collections`. The work list offers the assistant only there.
28
28
  */
29
29
  export const ASSISTANT_CUSTOM_KEY = "editorAssistant";
30
+ /**
31
+ * Where to send a visitor who may not use Admin, or null to show the work list. Payload leaves
32
+ * custom views public, so the view checks this itself, as Payload does for its own: to the login
33
+ * with a way back when signed out, to Payload's unauthorized page when signed in without access.
34
+ */
35
+ export function accessRedirect(args) {
36
+ const { routes } = args;
37
+ if (args.canAccessAdmin) {
38
+ return null;
39
+ }
40
+ if (args.signedIn) {
41
+ return formatAdminURL({ adminRoute: routes.admin, path: routes.unauthorized });
42
+ }
43
+ const query = new URLSearchParams();
44
+ for (const [key, value] of Object.entries(args.searchParams ?? {})) {
45
+ if (key === "redirect")
46
+ continue;
47
+ for (const item of Array.isArray(value) ? value : value === undefined ? [] : [value]) {
48
+ query.append(key, item);
49
+ }
50
+ }
51
+ const back = `${VIEW_PATH}${query.size ? `?${query}` : ""}`;
52
+ const login = formatAdminURL({ adminRoute: routes.admin, path: routes.login });
53
+ return `${login}?${new URLSearchParams({ redirect: back })}`;
54
+ }
30
55
  export function assistantCollections(adminCustom) {
31
56
  const entry = adminCustom?.[ASSISTANT_CUSTOM_KEY];
32
57
  const collections = entry?.collections;
package/dist/types.d.ts CHANGED
@@ -19,6 +19,8 @@ export type Finding = {
19
19
  length?: LengthRange & {
20
20
  actual: number;
21
21
  };
22
+ /** For a missing meta title or description: the length its length check expects. */
23
+ range?: LengthRange;
22
24
  /** For heading checks: the heading's level and, for a skip, the level before it. */
23
25
  heading?: {
24
26
  level: number;
@@ -4,5 +4,6 @@ import "../styles/view.css";
4
4
  * The work list: every finding with filters on check, collection and locale. Each row opens the
5
5
  * document in the finding's locale with the field selected; where the editor assistant works, a
6
6
  * second link opens it with a ready prompt too. Scans once per visit; the filters work in the browser.
7
+ * Only for visitors who may use Admin: Payload leaves custom views public.
7
8
  */
8
9
  export declare function ContentHealthView({ initPageResult, params, searchParams, }: AdminViewServerProps): Promise<import("react").JSX.Element>;
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DefaultTemplate } from "@payloadcms/next/templates";
3
3
  import { Gutter, SetStepNav } from "@payloadcms/ui";
4
+ import { redirect } from "next/navigation";
4
5
  import { ASK, CHECK_LABELS, findingDetail, LIST_COPY, uiLanguage } from "../copy.js";
5
- import { assistantCollections, editURL, readFilters } from "../links.js";
6
+ import { accessRedirect, assistantCollections, editURL, readFilters } from "../links.js";
6
7
  import { scanContentHealth } from "../scan.js";
7
8
  import { CHECK_IDS } from "../types.js";
8
9
  import { WorkList } from "./WorkList.js";
@@ -11,9 +12,19 @@ import "../styles/view.css";
11
12
  * The work list: every finding with filters on check, collection and locale. Each row opens the
12
13
  * document in the finding's locale with the field selected; where the editor assistant works, a
13
14
  * second link opens it with a ready prompt too. Scans once per visit; the filters work in the browser.
15
+ * Only for visitors who may use Admin: Payload leaves custom views public.
14
16
  */
15
17
  export async function ContentHealthView({ initPageResult, params, searchParams, }) {
16
18
  const { req, locale, permissions, visibleEntities } = initPageResult;
19
+ const denied = accessRedirect({
20
+ routes: { admin: req.payload.config.routes.admin, ...req.payload.config.admin.routes },
21
+ canAccessAdmin: Boolean(permissions?.canAccessAdmin),
22
+ signedIn: Boolean(req.user),
23
+ searchParams,
24
+ });
25
+ if (denied) {
26
+ redirect(denied);
27
+ }
17
28
  const language = uiLanguage(req.i18n?.language);
18
29
  const copy = LIST_COPY[language];
19
30
  const { config } = req.payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-content-health",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Content health checks and dashboard widgets for Payload Admin: alt texts, SEO fields and more",
5
5
  "keywords": [
6
6
  "payload",