@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 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: "media" }),
25
+ seoPlugin({ collections: ["pages"], uploadsCollection: "images" }),
26
26
  contentHealthPlugin({
27
27
  collections: {
28
28
  pages: { seo: true },
29
- media: { altText: true },
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
- media: { altText: { field: "altText" } },
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
+ }
@@ -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: { ...config.custom, [CUSTOM_KEY]: options },
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
@@ -1,5 +1,6 @@
1
1
  export const CHECK_IDS = [
2
2
  "media.alt-missing",
3
+ "media.video-description-missing",
3
4
  "seo.title-missing",
4
5
  "seo.title-length",
5
6
  "seo.title-duplicate",
@@ -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 { CHECK_IDS } from "../types.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-content-health",
3
- "version": "0.7.1",
3
+ "version": "0.9.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",