@simmalugnt-se/payload-content-health 0.7.1 → 0.9.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 +12 -0
- package/README.md +12 -3
- package/dist/assistant.d.ts +56 -0
- package/dist/assistant.js +204 -0
- package/dist/categories.js +1 -1
- package/dist/checks.js +5 -0
- package/dist/copy.js +4 -0
- package/dist/group.d.ts +2 -0
- package/dist/group.js +9 -0
- package/dist/plugin.js +10 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.js +1 -0
- package/dist/view/ContentHealthView.js +2 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- New check `media.video-description-missing` (Accessibility), turned on per collection with
|
|
6
|
+
`videoDescription`: a video without a description, its text alternative, in a language. Reads
|
|
7
|
+
`description` by default, as in `@simmalugnt-se/payload-mux`.
|
|
8
|
+
|
|
9
|
+
## 0.8.0
|
|
10
|
+
|
|
11
|
+
- The editor assistant (0.15.0 or later, with `extensions: { "content-health": true }`) reads the
|
|
12
|
+
findings itself: `content-health.findings` and `content-health.open`. Editors can ask what to fix
|
|
13
|
+
first or what is wrong with a page, and be taken to the work list or a batch.
|
|
14
|
+
|
|
3
15
|
## 0.7.1
|
|
4
16
|
|
|
5
17
|
- npm links to simmalugnt.se instead of the private source repository.
|
package/README.md
CHANGED
|
@@ -22,11 +22,12 @@ Requires Payload `>=3.90.2 <4`, Next 16 and React 19. The widget uses Payload's
|
|
|
22
22
|
import { contentHealthPlugin } from "@simmalugnt-se/payload-content-health";
|
|
23
23
|
|
|
24
24
|
plugins: [
|
|
25
|
-
seoPlugin({ collections: ["pages"], uploadsCollection: "
|
|
25
|
+
seoPlugin({ collections: ["pages"], uploadsCollection: "images" }),
|
|
26
26
|
contentHealthPlugin({
|
|
27
27
|
collections: {
|
|
28
28
|
pages: { seo: true },
|
|
29
|
-
|
|
29
|
+
images: { altText: true },
|
|
30
|
+
videos: { videoDescription: true },
|
|
30
31
|
},
|
|
31
32
|
}),
|
|
32
33
|
];
|
|
@@ -41,6 +42,7 @@ Requires Payload `>=3.90.2 <4`, Next 16 and React 19. The widget uses Payload's
|
|
|
41
42
|
| Check | Option | Finds |
|
|
42
43
|
| --------------------------- | ------------------ | ---------------------------------------------- |
|
|
43
44
|
| `media.alt-missing` | `altText` | Images (`image/*`) with an empty alt field |
|
|
45
|
+
| `media.video-description-missing` | `videoDescription` | Videos with an empty description (`description`, as in `@simmalugnt-se/payload-mux`) |
|
|
44
46
|
| `seo.title-missing` | `seo` | Empty meta title |
|
|
45
47
|
| `seo.title-length` | `seo` | Meta title outside 50–60 characters |
|
|
46
48
|
| `seo.title-duplicate` | `seo` | Meta title another document also uses |
|
|
@@ -71,7 +73,7 @@ pages: {
|
|
|
71
73
|
duplicates: false,
|
|
72
74
|
},
|
|
73
75
|
},
|
|
74
|
-
|
|
76
|
+
images: { altText: { field: "altText" } },
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
### Page body
|
|
@@ -167,6 +169,13 @@ other; they share these names:
|
|
|
167
169
|
| `config.admin.custom.editorAssistant` | assistant → this plugin | `{ collections, batchCollections }` |
|
|
168
170
|
| `?batch=…&batchScope=…&ask=…` | link → assistant | Documents to fix at once, and the prompt |
|
|
169
171
|
|
|
172
|
+
With the assistant's `extensions: { "content-health": true }` (editor-assistant 0.15.0 or later),
|
|
173
|
+
the assistant also reads the findings itself, through the extension this plugin registers in
|
|
174
|
+
`config.custom.editorAssistantExtensions`: `content-health.findings` (a summary per category, then
|
|
175
|
+
findings filtered by category, check, collection, document or locale, at most 30) and
|
|
176
|
+
`content-health.open` (the work list, or the batch for one check). Editors can ask "what should I
|
|
177
|
+
fix first?" on the dashboard or "what is wrong with this page?" on a document.
|
|
178
|
+
|
|
170
179
|
Both parameters are removed from the address once read. `editURL(adminRoute, finding, prompt?)` and
|
|
171
180
|
`listURL(adminRoute, filters)` build these links.
|
|
172
181
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PayloadRequest } from "payload";
|
|
2
|
+
import { type ScanResult, scanContentHealth } from "./scan.ts";
|
|
3
|
+
/**
|
|
4
|
+
* How content health gives `@simmalugnt-se/payload-editor-assistant` its findings, without an
|
|
5
|
+
* import: an extension in `config.custom[EXTENSIONS_CUSTOM_KEY]`, which the assistant uses once the
|
|
6
|
+
* host turns it on (`extensions: { "content-health": true }`). The key and types are copies of the
|
|
7
|
+
* assistant's (`extensions/contract.ts`); the assistant's contract tests compare them.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EXTENSIONS_CUSTOM_KEY = "editorAssistantExtensions";
|
|
10
|
+
export type AssistantExtension = {
|
|
11
|
+
id: string;
|
|
12
|
+
instructions?: string;
|
|
13
|
+
views?: Array<{
|
|
14
|
+
path: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}>;
|
|
17
|
+
tools?: AssistantExtensionTool[];
|
|
18
|
+
};
|
|
19
|
+
export type AssistantExtensionTool = {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
input: Record<string, unknown>;
|
|
23
|
+
risk: "auto";
|
|
24
|
+
run: (input: Record<string, unknown>, context: AssistantExtensionContext) => Promise<AssistantExtensionResult>;
|
|
25
|
+
};
|
|
26
|
+
export type AssistantExtensionContext = {
|
|
27
|
+
req: PayloadRequest;
|
|
28
|
+
path?: string;
|
|
29
|
+
open?: {
|
|
30
|
+
collection?: string;
|
|
31
|
+
global?: string;
|
|
32
|
+
id?: string | number;
|
|
33
|
+
locale: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type AssistantExtensionResult = {
|
|
37
|
+
ok: true;
|
|
38
|
+
data: unknown;
|
|
39
|
+
navigate?: {
|
|
40
|
+
href: string;
|
|
41
|
+
label: string;
|
|
42
|
+
};
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
error: string;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
export declare const EXTENSION_ID = "content-health";
|
|
49
|
+
/** Findings one call returns at most; `more` says how many the filters left out. */
|
|
50
|
+
export declare const MAX_FINDINGS = 30;
|
|
51
|
+
type Scan = (args: Parameters<typeof scanContentHealth>[0]) => Promise<ScanResult>;
|
|
52
|
+
/** `scan` is replaceable for tests. */
|
|
53
|
+
export declare function assistantExtension({ scan: run, }?: {
|
|
54
|
+
scan?: Scan;
|
|
55
|
+
}): AssistantExtension;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { CATEGORY_IDS, categoryOf, categoryStats } from "./categories.js";
|
|
2
|
+
import { BATCH_ASK, CATEGORY_LABELS, CHECK_LABELS, findingDetail, uiLanguage } from "./copy.js";
|
|
3
|
+
import { checkStats, countDocuments, sortFindings } from "./group.js";
|
|
4
|
+
import { assistantCollections, BATCH_CHECKS, batchCollections, batchTarget, batchURL, listURL, MAX_BATCH_TARGETS, matchesFilters, VIEW_PATH, } from "./links.js";
|
|
5
|
+
import { scanContentHealth } from "./scan.js";
|
|
6
|
+
import { CHECK_IDS } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* How content health gives `@simmalugnt-se/payload-editor-assistant` its findings, without an
|
|
9
|
+
* import: an extension in `config.custom[EXTENSIONS_CUSTOM_KEY]`, which the assistant uses once the
|
|
10
|
+
* host turns it on (`extensions: { "content-health": true }`). The key and types are copies of the
|
|
11
|
+
* assistant's (`extensions/contract.ts`); the assistant's contract tests compare them.
|
|
12
|
+
*/
|
|
13
|
+
export const EXTENSIONS_CUSTOM_KEY = "editorAssistantExtensions";
|
|
14
|
+
export const EXTENSION_ID = "content-health";
|
|
15
|
+
/** Findings one call returns at most; `more` says how many the filters left out. */
|
|
16
|
+
export const MAX_FINDINGS = 30;
|
|
17
|
+
const DEFAULT_FINDINGS = 20;
|
|
18
|
+
/** One scan per turn: the turn's tool calls share the editor's request. */
|
|
19
|
+
function scanOncePerRequest(run) {
|
|
20
|
+
const scans = new WeakMap();
|
|
21
|
+
return (req) => {
|
|
22
|
+
let pending = scans.get(req);
|
|
23
|
+
if (!pending) {
|
|
24
|
+
pending = run({ payload: req.payload, user: req.user ?? null });
|
|
25
|
+
scans.set(req, pending);
|
|
26
|
+
}
|
|
27
|
+
return pending;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const filterInput = {
|
|
31
|
+
category: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: [...CATEGORY_IDS],
|
|
34
|
+
description: "seo, structure, translations or accessibility.",
|
|
35
|
+
},
|
|
36
|
+
check: { type: "string", enum: [...CHECK_IDS], description: "One check, by id." },
|
|
37
|
+
collection: { type: "string", description: "Collection slug, such as pages or images." },
|
|
38
|
+
locale: { type: "string", description: "Only findings in this content language." },
|
|
39
|
+
};
|
|
40
|
+
/** `scan` is replaceable for tests. */
|
|
41
|
+
export function assistantExtension({ scan: run = scanContentHealth, } = {}) {
|
|
42
|
+
const scan = scanOncePerRequest(run);
|
|
43
|
+
return {
|
|
44
|
+
id: EXTENSION_ID,
|
|
45
|
+
instructions: [
|
|
46
|
+
"Content health checks the site's content: SEO (meta titles, descriptions, share images, duplicates), structure (H1, heading levels, thin content, broken internal links), translations (fields empty in a language) and accessibility (image alt texts).",
|
|
47
|
+
"When the editor asks what needs work, how the site or a page is doing, or what to fix first, call content-health.findings and answer from it, naming pages and problems. Never ask for a screenshot of content health.",
|
|
48
|
+
'Each finding says how it is fixed: "batch" (many documents at once from the work list), "assistant" (on the document, with you) or "by hand". To start fixing, call content-health.open: with batch true for a batch check, else the work list. It takes the editor there; they start the fix and review each change themselves. Say where they are now ("The 9 missing alt texts are open; Fix 9 with the assistant starts them."); never say anything is fixed or started, and do not propose changes yourself for findings outside the open document. Do not mention findings the tool left out unless the editor asks for more.',
|
|
49
|
+
].join(" "),
|
|
50
|
+
views: [
|
|
51
|
+
{
|
|
52
|
+
path: VIEW_PATH,
|
|
53
|
+
description: "The editor is on the content health work list: issues by category and check, each with a link to the document, and buttons to fix one with the assistant or several at once.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: "/",
|
|
57
|
+
description: "The dashboard shows a content health widget: the share of checked fields in order, per category.",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
tools: [
|
|
61
|
+
{
|
|
62
|
+
name: `${EXTENSION_ID}.findings`,
|
|
63
|
+
description: `Content health's findings: first a summary per category (issues, fields checked, documents needing attention), then the findings that match the filters, at most ${MAX_FINDINGS}, each with the document, the problem, the field, the language and how to fix it. Without filters and with a document open: that document's findings; on other views: the whole site's. Read with the editor's access.`,
|
|
64
|
+
input: {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
...filterInput,
|
|
69
|
+
documentId: {
|
|
70
|
+
type: ["string", "number"],
|
|
71
|
+
description: "Only this document (with collection).",
|
|
72
|
+
},
|
|
73
|
+
site: {
|
|
74
|
+
type: "boolean",
|
|
75
|
+
description: "The whole site even when a document is open.",
|
|
76
|
+
},
|
|
77
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_FINDINGS },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
risk: "auto",
|
|
81
|
+
run: async (input, context) => findings(input, context, await scan(context.req)),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: `${EXTENSION_ID}.open`,
|
|
85
|
+
description: `Takes the editor to the content health work list, filtered, or with batch true to the batch that fixes up to ${MAX_BATCH_TARGETS} documents of one check at once (the editor starts it and reviews each). Needs check for a batch.`,
|
|
86
|
+
input: {
|
|
87
|
+
type: "object",
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
properties: { ...filterInput, batch: { type: "boolean" } },
|
|
90
|
+
},
|
|
91
|
+
risk: "auto",
|
|
92
|
+
run: async (input, context) => open(input, context, () => scan(context.req)),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function findings(input, { req, open }, result) {
|
|
98
|
+
const custom = req.payload.config.admin?.custom;
|
|
99
|
+
const withAssistant = new Set(assistantCollections(custom));
|
|
100
|
+
const inBatches = new Set(batchCollections(custom));
|
|
101
|
+
const filters = readInput(input);
|
|
102
|
+
const filtered = Boolean(filters.category || filters.check || filters.collection || input.documentId || input.site);
|
|
103
|
+
// With a document open and nothing asked for, its own findings.
|
|
104
|
+
const document = input.documentId !== undefined
|
|
105
|
+
? { collection: filters.collection, id: String(input.documentId) }
|
|
106
|
+
: !filtered && open?.collection && open.id !== undefined
|
|
107
|
+
? { collection: open.collection, id: String(open.id) }
|
|
108
|
+
: undefined;
|
|
109
|
+
const matching = sortFindings(result.findings).filter((finding) => matchesFilters(finding, filters) &&
|
|
110
|
+
(!document ||
|
|
111
|
+
((!document.collection || finding.collection === document.collection) &&
|
|
112
|
+
String(finding.id) === document.id)));
|
|
113
|
+
const limit = typeof input.limit === "number" ? input.limit : DEFAULT_FINDINGS;
|
|
114
|
+
const stats = checkStats(result.checks, result.checked, result.findings);
|
|
115
|
+
const documents = Object.values(result.documents).reduce((sum, count) => sum + count, 0);
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
data: {
|
|
119
|
+
summary: {
|
|
120
|
+
documentsChecked: documents,
|
|
121
|
+
documentsNeedingAttention: countDocuments(result.findings),
|
|
122
|
+
categories: categoryStats(stats).map((stat) => ({
|
|
123
|
+
category: stat.category,
|
|
124
|
+
label: CATEGORY_LABELS.en[stat.category],
|
|
125
|
+
issues: stat.failing,
|
|
126
|
+
fieldsChecked: stat.checked,
|
|
127
|
+
})),
|
|
128
|
+
},
|
|
129
|
+
scope: document ? "one document" : filtered ? "filtered" : "whole site",
|
|
130
|
+
findings: matching.slice(0, limit).map((finding) => ({
|
|
131
|
+
check: finding.check,
|
|
132
|
+
problem: CHECK_LABELS.en[finding.check],
|
|
133
|
+
detail: findingDetail("en", finding),
|
|
134
|
+
collection: finding.collection,
|
|
135
|
+
id: finding.id,
|
|
136
|
+
title: finding.title,
|
|
137
|
+
field: finding.path,
|
|
138
|
+
...(finding.locale ? { locale: finding.locale } : {}),
|
|
139
|
+
...(finding.status === "draft" ? { draft: true } : {}),
|
|
140
|
+
fix: fixFor(finding, inBatches, withAssistant),
|
|
141
|
+
})),
|
|
142
|
+
more: Math.max(0, matching.length - limit),
|
|
143
|
+
...(result.problems.length ? { notChecked: result.problems } : {}),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function open(input, { req }, scan) {
|
|
148
|
+
const filters = readInput(input);
|
|
149
|
+
const adminRoute = req.payload.config.routes.admin;
|
|
150
|
+
if (input.batch !== true) {
|
|
151
|
+
const category = filters.category ?? (filters.check ? categoryOf(filters.check) : undefined);
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
data: { link: "work list" },
|
|
155
|
+
navigate: {
|
|
156
|
+
href: listURL(adminRoute, { ...filters, category, check: undefined }),
|
|
157
|
+
label: "Content health",
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const check = filters.check;
|
|
162
|
+
if (!check || !BATCH_CHECKS[check]) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error: "not_batchable",
|
|
166
|
+
message: "Only some checks can be fixed in a batch: name one whose findings say batch.",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const inBatches = new Set(batchCollections(req.payload.config.admin?.custom));
|
|
170
|
+
const targets = sortFindings((await scan()).findings).filter((finding) => matchesFilters(finding, filters) && inBatches.has(finding.collection));
|
|
171
|
+
if (!targets.length) {
|
|
172
|
+
return { ok: false, error: "nothing_to_fix", message: "No findings of that check to fix." };
|
|
173
|
+
}
|
|
174
|
+
const language = uiLanguage(req.i18n?.language);
|
|
175
|
+
const ask = BATCH_ASK[language][check]?.(targets[0]) ?? "";
|
|
176
|
+
const count = Math.min(targets.length, MAX_BATCH_TARGETS);
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
data: { documents: count, more: targets.length - count },
|
|
180
|
+
navigate: {
|
|
181
|
+
href: batchURL(adminRoute, { ...filters, check }, targets.map(batchTarget), ask),
|
|
182
|
+
label: language === "sv" ? `Åtgärda ${count} med assistenten` : `Fix ${count} with the assistant`,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** How a finding is fixed: many at once, on its document with the assistant, or by hand. */
|
|
187
|
+
function fixFor(finding, inBatches, withAssistant) {
|
|
188
|
+
if (BATCH_CHECKS[finding.check] && inBatches.has(finding.collection))
|
|
189
|
+
return "batch";
|
|
190
|
+
return withAssistant.has(finding.collection) ? "assistant" : "by hand";
|
|
191
|
+
}
|
|
192
|
+
function readInput(input) {
|
|
193
|
+
const text = (value) => (typeof value === "string" && value ? value : undefined);
|
|
194
|
+
const category = text(input.category);
|
|
195
|
+
const check = text(input.check);
|
|
196
|
+
return {
|
|
197
|
+
...(category && CATEGORY_IDS.includes(category)
|
|
198
|
+
? { category: category }
|
|
199
|
+
: {}),
|
|
200
|
+
...(check && CHECK_IDS.includes(check) ? { check: check } : {}),
|
|
201
|
+
...(text(input.collection) ? { collection: text(input.collection) } : {}),
|
|
202
|
+
...(text(input.locale) ? { locale: text(input.locale) } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
package/dist/categories.js
CHANGED
|
@@ -18,7 +18,7 @@ export const CATEGORY_CHECKS = {
|
|
|
18
18
|
"links.broken",
|
|
19
19
|
],
|
|
20
20
|
translations: ["content.untranslated"],
|
|
21
|
-
accessibility: ["media.alt-missing"],
|
|
21
|
+
accessibility: ["media.alt-missing", "media.video-description-missing"],
|
|
22
22
|
};
|
|
23
23
|
export function categoryOf(check) {
|
|
24
24
|
return CATEGORY_IDS.find((id) => CATEGORY_CHECKS[id].includes(check));
|
package/dist/checks.js
CHANGED
|
@@ -18,6 +18,11 @@ export function rulesFor(options) {
|
|
|
18
18
|
const field = (options.altText === true ? undefined : options.altText.field) ?? "alt";
|
|
19
19
|
rules.push({ check: "media.alt-missing", kind: "required", path: field, imagesOnly: true });
|
|
20
20
|
}
|
|
21
|
+
if (options.videoDescription) {
|
|
22
|
+
const field = (options.videoDescription === true ? undefined : options.videoDescription.field) ??
|
|
23
|
+
"description";
|
|
24
|
+
rules.push({ check: "media.video-description-missing", kind: "required", path: field });
|
|
25
|
+
}
|
|
21
26
|
if (options.seo) {
|
|
22
27
|
const seo = options.seo === true ? {} : options.seo;
|
|
23
28
|
const title = seo.title ?? SEO_DEFAULTS.title;
|
package/dist/copy.js
CHANGED
|
@@ -4,6 +4,7 @@ export function uiLanguage(adminLanguage) {
|
|
|
4
4
|
export const CHECK_LABELS = {
|
|
5
5
|
en: {
|
|
6
6
|
"media.alt-missing": "Missing alt text",
|
|
7
|
+
"media.video-description-missing": "Missing video description",
|
|
7
8
|
"seo.title-missing": "Missing meta title",
|
|
8
9
|
"seo.title-length": "Meta title length",
|
|
9
10
|
"seo.title-duplicate": "Duplicate meta title",
|
|
@@ -20,6 +21,7 @@ export const CHECK_LABELS = {
|
|
|
20
21
|
},
|
|
21
22
|
sv: {
|
|
22
23
|
"media.alt-missing": "Alt-text saknas",
|
|
24
|
+
"media.video-description-missing": "Videobeskrivning saknas",
|
|
23
25
|
"seo.title-missing": "Metatitel saknas",
|
|
24
26
|
"seo.title-length": "Metatitelns längd",
|
|
25
27
|
"seo.title-duplicate": "Dubblerad metatitel",
|
|
@@ -98,6 +100,7 @@ export const COPY = {
|
|
|
98
100
|
export const ASK = {
|
|
99
101
|
en: {
|
|
100
102
|
"media.alt-missing": () => "Write an alt text for this image.",
|
|
103
|
+
"media.video-description-missing": () => "Write a description of this video for readers who cannot see it.",
|
|
101
104
|
"seo.title-missing": ({ range }) => range
|
|
102
105
|
? `Write a meta title for this page, ${range.min}–${range.max} characters.`
|
|
103
106
|
: "Write a meta title for this page.",
|
|
@@ -130,6 +133,7 @@ export const ASK = {
|
|
|
130
133
|
},
|
|
131
134
|
sv: {
|
|
132
135
|
"media.alt-missing": () => "Skriv en alt-text för den här bilden.",
|
|
136
|
+
"media.video-description-missing": () => "Skriv en beskrivning av den här videon för den som inte kan se den.",
|
|
133
137
|
"seo.title-missing": ({ range }) => range
|
|
134
138
|
? `Skriv en metatitel för den här sidan, ${range.min}–${range.max} tecken.`
|
|
135
139
|
: "Skriv en metatitel för den här sidan.",
|
package/dist/group.d.ts
CHANGED
|
@@ -25,3 +25,5 @@ export type CheckStats = {
|
|
|
25
25
|
};
|
|
26
26
|
/** Per check that ran: how many values it looked at and which ones need work. */
|
|
27
27
|
export declare function checkStats(checks: CheckId[], checked: Partial<Record<CheckId, number>>, findings: Finding[]): CheckStats[];
|
|
28
|
+
/** By check, then document, then locale, so a document's locales sit together. */
|
|
29
|
+
export declare function sortFindings(findings: Finding[]): Finding[];
|
package/dist/group.js
CHANGED
|
@@ -36,3 +36,12 @@ export function checkStats(checks, checked, findings) {
|
|
|
36
36
|
return { check, checked: checked[check] ?? 0, failing, documents };
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
/** By check, then document, then locale, so a document's locales sit together. */
|
|
40
|
+
export function sortFindings(findings) {
|
|
41
|
+
return [...findings].sort((a, b) => CHECK_IDS.indexOf(a.check) - CHECK_IDS.indexOf(b.check) ||
|
|
42
|
+
a.collection.localeCompare(b.collection) ||
|
|
43
|
+
a.title.localeCompare(b.title) ||
|
|
44
|
+
String(a.id).localeCompare(String(b.id)) ||
|
|
45
|
+
a.path.localeCompare(b.path) ||
|
|
46
|
+
(a.locale ?? "").localeCompare(b.locale ?? ""));
|
|
47
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assistantExtension, EXTENSIONS_CUSTOM_KEY } from "./assistant.js";
|
|
1
2
|
import { CHECK_LABELS } from "./copy.js";
|
|
2
3
|
import { VIEW_PATH } from "./links.js";
|
|
3
4
|
import { CLIENT_ENTRY, RSC_ENTRY } from "./package-name.js";
|
|
@@ -65,7 +66,15 @@ export function contentHealthPlugin(options) {
|
|
|
65
66
|
...(defaultLayout ? { defaultLayout } : {}),
|
|
66
67
|
},
|
|
67
68
|
},
|
|
68
|
-
custom: {
|
|
69
|
+
custom: {
|
|
70
|
+
...config.custom,
|
|
71
|
+
[CUSTOM_KEY]: options,
|
|
72
|
+
// Used by the editor assistant once the host turns it on; harmless without it.
|
|
73
|
+
[EXTENSIONS_CUSTOM_KEY]: [
|
|
74
|
+
...(config.custom?.[EXTENSIONS_CUSTOM_KEY] ?? []),
|
|
75
|
+
assistantExtension(),
|
|
76
|
+
],
|
|
77
|
+
},
|
|
69
78
|
};
|
|
70
79
|
};
|
|
71
80
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CHECK_IDS: readonly ["media.alt-missing", "seo.title-missing", "seo.title-length", "seo.title-duplicate", "seo.description-missing", "seo.description-length", "seo.description-duplicate", "seo.image-missing", "content.h1-missing", "content.h1-multiple", "content.heading-skip", "content.thin", "links.broken", "content.untranslated"];
|
|
1
|
+
export declare const CHECK_IDS: readonly ["media.alt-missing", "media.video-description-missing", "seo.title-missing", "seo.title-length", "seo.title-duplicate", "seo.description-missing", "seo.description-length", "seo.description-duplicate", "seo.image-missing", "content.h1-missing", "content.h1-multiple", "content.heading-skip", "content.thin", "links.broken", "content.untranslated"];
|
|
2
2
|
export type CheckId = (typeof CHECK_IDS)[number];
|
|
3
3
|
export type LengthRange = {
|
|
4
4
|
min: number;
|
|
@@ -94,11 +94,20 @@ export type AltTextOptions = {
|
|
|
94
94
|
/** Field holding the alt text. Default `alt`. */
|
|
95
95
|
field?: string;
|
|
96
96
|
};
|
|
97
|
+
export type VideoDescriptionOptions = {
|
|
98
|
+
/** Field holding the description. Default `description`, as in `@simmalugnt-se/payload-mux`. */
|
|
99
|
+
field?: string;
|
|
100
|
+
};
|
|
97
101
|
export type CollectionHealthOptions = {
|
|
98
102
|
/** SEO checks on the fields `@payloadcms/plugin-seo` adds, or the paths given here. */
|
|
99
103
|
seo?: boolean | SeoOptions;
|
|
100
104
|
/** Alt text on images in an upload collection. Files that are not images are skipped. */
|
|
101
105
|
altText?: boolean | AltTextOptions;
|
|
106
|
+
/**
|
|
107
|
+
* A description on every video, its text alternative for readers who cannot see it, as an alt
|
|
108
|
+
* text is for an image. For a videos collection such as `@simmalugnt-se/payload-mux`'s.
|
|
109
|
+
*/
|
|
110
|
+
videoDescription?: boolean | VideoDescriptionOptions;
|
|
102
111
|
/** Heading structure and thin content, read from the page body. */
|
|
103
112
|
content?: ContentOptions;
|
|
104
113
|
/**
|
package/dist/types.js
CHANGED
|
@@ -4,10 +4,10 @@ import { Gutter, SetStepNav } from "@payloadcms/ui";
|
|
|
4
4
|
import { redirect } from "next/navigation";
|
|
5
5
|
import { categoryOf, categoryStats, categoryTone, defaultCategory, } from "../categories.js";
|
|
6
6
|
import { ASK, BATCH_ASK, CATEGORY_LABELS, CHECK_LABELS, COPY, findingDetail, LIST_COPY, uiLanguage, } from "../copy.js";
|
|
7
|
-
import { checkStats, countDocuments } from "../group.js";
|
|
7
|
+
import { checkStats, countDocuments, sortFindings } from "../group.js";
|
|
8
8
|
import { accessRedirect, assistantCollections, BATCH_CHECKS, batchCollections, batchTarget, editURL, readFilters, } from "../links.js";
|
|
9
9
|
import { scanContentHealth } from "../scan.js";
|
|
10
|
-
import {
|
|
10
|
+
import {} from "../types.js";
|
|
11
11
|
import { Ring } from "../widget/Ring.js";
|
|
12
12
|
import { WorkList, } from "./WorkList.js";
|
|
13
13
|
import "../styles/tokens.css";
|
|
@@ -95,15 +95,6 @@ export async function ContentHealthView({ initPageResult, params, searchParams,
|
|
|
95
95
|
? copy.summary(result.findings.length, countDocuments(result.findings))
|
|
96
96
|
: copy.none })] })] }), _jsx(WorkList, { adminRoute: adminRoute, batchAsk: batchAsk, categories: categories, collections: collections, initialFilters: initialFilters, language: language, locales: locales, rows: rows }), result.problems.length ? (_jsx("ul", { className: "content-health-list__problems", children: result.problems.map((problem) => (_jsx("li", { children: problem }, problem))) })) : null] }) }));
|
|
97
97
|
}
|
|
98
|
-
/** By check, then document, then locale, so a document's locales sit together. */
|
|
99
|
-
function sortFindings(findings) {
|
|
100
|
-
return [...findings].sort((a, b) => CHECK_IDS.indexOf(a.check) - CHECK_IDS.indexOf(b.check) ||
|
|
101
|
-
a.collection.localeCompare(b.collection) ||
|
|
102
|
-
a.title.localeCompare(b.title) ||
|
|
103
|
-
String(a.id).localeCompare(String(b.id)) ||
|
|
104
|
-
a.path.localeCompare(b.path) ||
|
|
105
|
-
(a.locale ?? "").localeCompare(b.locale ?? ""));
|
|
106
|
-
}
|
|
107
98
|
/** A config label: a string, or one per language. Functions are left to Payload. */
|
|
108
99
|
function label(value, language) {
|
|
109
100
|
if (typeof value === "string") {
|
package/package.json
CHANGED