@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 +14 -0
- package/README.md +9 -5
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +14 -3
- package/dist/copy.js +12 -4
- package/dist/links.d.ts +15 -0
- package/dist/links.js +25 -0
- package/dist/types.d.ts +2 -0
- package/dist/view/ContentHealthView.d.ts +1 -0
- package/dist/view/ContentHealthView.js +12 -1
- package/package.json +1 -1
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
|
|
119
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
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({
|
|
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({
|
|
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
|
-
|
|
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": () =>
|
|
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": () =>
|
|
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": () =>
|
|
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": () =>
|
|
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