@consilioweb/payload-seo-analyzer 1.11.0 → 1.12.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 +1 -0
- package/dist/client.cjs +180 -43
- package/dist/client.js +180 -43
- package/dist/index.cjs +290 -82
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +290 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -643,6 +643,7 @@ All endpoints are prefixed with the configured `endpointBasePath` (default: `/se
|
|
|
643
643
|
| `POST` | `/ai-generate` | Heuristic meta title/description generation (no API key needed) |
|
|
644
644
|
| `POST` | `/ai-rewrite` | Rewrite a single meta field (Claude if `ANTHROPIC_API_KEY` set, else heuristic) |
|
|
645
645
|
| `POST` | `/ai-optimize` | **AI SEO Optimize** — scan a page, propose optimized meta (Claude Opus 4.8 by default), server-validated; applied in one click from the sidebar |
|
|
646
|
+
| `POST` | `/ai-optimize-bulk` | **Bulk meta correction** — propose (dry-run) or apply optimized meta across many pages at once; powers the dashboard preview/export/apply |
|
|
646
647
|
| `GET` `POST` | `/alt-text-audit` `/ai-alt-text` | **AI image alt-text** (Claude vision, `features.aiFeatures`) — list images missing alt, generate + apply |
|
|
647
648
|
| `POST` | `/ai-content-brief` | **AI content brief** (`features.aiFeatures`) — outline, entities, questions, word count for a keyword |
|
|
648
649
|
| `GET` | `/cannibalization` | Detect keyword cannibalization |
|
package/dist/client.cjs
CHANGED
|
@@ -1066,8 +1066,15 @@ var fr = {
|
|
|
1066
1066
|
markCornerstone: "Marquer pilier",
|
|
1067
1067
|
unmarkCornerstone: "D\xE9marquer pilier",
|
|
1068
1068
|
bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
|
|
1069
|
-
bulkOptimizing: "
|
|
1069
|
+
bulkOptimizing: "Analyse\u2026",
|
|
1070
1070
|
bulkConfirm: "Confirmer ?",
|
|
1071
|
+
bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
|
|
1072
|
+
bulkApply: "Appliquer",
|
|
1073
|
+
bulkApplying: "Application\u2026",
|
|
1074
|
+
bulkCancel: "Annuler",
|
|
1075
|
+
bulkExport: "Exporter CSV",
|
|
1076
|
+
bulkNoChanges: "Aucune correction n\xE9cessaire sur la s\xE9lection.",
|
|
1077
|
+
bulkCappedNote: "limite atteinte (affine la s\xE9lection)",
|
|
1071
1078
|
searchPlaceholder: "Rechercher (titre, slug, keyword)...",
|
|
1072
1079
|
allCollections: "Toutes les collections",
|
|
1073
1080
|
allScores: "Tous les scores",
|
|
@@ -1661,8 +1668,15 @@ var en = {
|
|
|
1661
1668
|
markCornerstone: "Mark as cornerstone",
|
|
1662
1669
|
unmarkCornerstone: "Unmark cornerstone",
|
|
1663
1670
|
bulkOptimizeMeta: "Optimize meta (AI)",
|
|
1664
|
-
bulkOptimizing: "
|
|
1671
|
+
bulkOptimizing: "Analyzing\u2026",
|
|
1665
1672
|
bulkConfirm: "Confirm?",
|
|
1673
|
+
bulkPreviewTitle: "Proposed meta corrections",
|
|
1674
|
+
bulkApply: "Apply",
|
|
1675
|
+
bulkApplying: "Applying\u2026",
|
|
1676
|
+
bulkCancel: "Cancel",
|
|
1677
|
+
bulkExport: "Export CSV",
|
|
1678
|
+
bulkNoChanges: "No corrections needed on the selection.",
|
|
1679
|
+
bulkCappedNote: "limit reached (narrow the selection)",
|
|
1666
1680
|
searchPlaceholder: "Search (title, slug, keyword)...",
|
|
1667
1681
|
allCollections: "All collections",
|
|
1668
1682
|
allScores: "All scores",
|
|
@@ -9211,7 +9225,6 @@ function BulkActionBar({
|
|
|
9211
9225
|
optimizing,
|
|
9212
9226
|
t
|
|
9213
9227
|
}) {
|
|
9214
|
-
const [confirmOptimize, setConfirmOptimize] = React4.useState(false);
|
|
9215
9228
|
if (count === 0) return null;
|
|
9216
9229
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9217
9230
|
"div",
|
|
@@ -9261,18 +9274,11 @@ function BulkActionBar({
|
|
|
9261
9274
|
"button",
|
|
9262
9275
|
{
|
|
9263
9276
|
onClick: () => {
|
|
9264
|
-
if (optimizing)
|
|
9265
|
-
if (confirmOptimize) {
|
|
9266
|
-
setConfirmOptimize(false);
|
|
9267
|
-
onOptimizeMeta();
|
|
9268
|
-
} else {
|
|
9269
|
-
setConfirmOptimize(true);
|
|
9270
|
-
setTimeout(() => setConfirmOptimize(false), 4e3);
|
|
9271
|
-
}
|
|
9277
|
+
if (!optimizing) onOptimizeMeta();
|
|
9272
9278
|
},
|
|
9273
9279
|
disabled: optimizing,
|
|
9274
9280
|
style: { ...btnBase, backgroundColor: "#7c3aed", color: "#fff", opacity: optimizing ? 0.6 : 1 },
|
|
9275
|
-
children: optimizing ? t.seoView.bulkOptimizing :
|
|
9281
|
+
children: optimizing ? t.seoView.bulkOptimizing : `\u2728 ${t.seoView.bulkOptimizeMeta}`
|
|
9276
9282
|
}
|
|
9277
9283
|
),
|
|
9278
9284
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -9316,6 +9322,8 @@ function SeoView() {
|
|
|
9316
9322
|
const [saving, setSaving] = React4.useState(false);
|
|
9317
9323
|
const [saveError, setSaveError] = React4.useState(null);
|
|
9318
9324
|
const [bulkOptimizing, setBulkOptimizing] = React4.useState(false);
|
|
9325
|
+
const [bulkApplying, setBulkApplying] = React4.useState(false);
|
|
9326
|
+
const [bulkPreview, setBulkPreview] = React4.useState(null);
|
|
9319
9327
|
const PAGE_SIZE = 50;
|
|
9320
9328
|
const fetchAudit = React4.useCallback(async (forceRefresh = false) => {
|
|
9321
9329
|
setLoading(true);
|
|
@@ -9537,44 +9545,83 @@ function SeoView() {
|
|
|
9537
9545
|
[selectedIds, fetchAudit]
|
|
9538
9546
|
);
|
|
9539
9547
|
const handleBulkOptimizeMeta = React4.useCallback(async () => {
|
|
9540
|
-
const
|
|
9541
|
-
if (
|
|
9548
|
+
const ids = Array.from(selectedIds).filter((k) => !k.startsWith("global:"));
|
|
9549
|
+
if (ids.length === 0) return;
|
|
9542
9550
|
setBulkOptimizing(true);
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
});
|
|
9553
|
-
if (!res.ok) continue;
|
|
9551
|
+
setBulkPreview(null);
|
|
9552
|
+
try {
|
|
9553
|
+
const res = await fetch("/api/seo-plugin/ai-optimize-bulk", {
|
|
9554
|
+
method: "POST",
|
|
9555
|
+
headers: { "Content-Type": "application/json" },
|
|
9556
|
+
credentials: "include",
|
|
9557
|
+
body: JSON.stringify({ ids, apply: false, limit: 100 })
|
|
9558
|
+
});
|
|
9559
|
+
if (res.ok) {
|
|
9554
9560
|
const data = await res.json();
|
|
9555
|
-
const
|
|
9556
|
-
|
|
9557
|
-
if (sug.metaTitle || sug.metaDescription) {
|
|
9558
|
-
patch.meta = { title: sug.metaTitle, description: sug.metaDescription };
|
|
9559
|
-
}
|
|
9560
|
-
if (sug.focusKeyword && sug.focusKeyword !== data.current?.focusKeyword) {
|
|
9561
|
-
patch.focusKeyword = sug.focusKeyword;
|
|
9562
|
-
}
|
|
9563
|
-
if (Object.keys(patch).length > 0) {
|
|
9564
|
-
await fetch(`/api/${collection}/${id}`, {
|
|
9565
|
-
method: "PATCH",
|
|
9566
|
-
headers: { "Content-Type": "application/json" },
|
|
9567
|
-
credentials: "include",
|
|
9568
|
-
body: JSON.stringify(patch)
|
|
9569
|
-
});
|
|
9570
|
-
}
|
|
9571
|
-
} catch {
|
|
9561
|
+
const changed = (data.results || []).filter((r) => r.changed && !r.error);
|
|
9562
|
+
setBulkPreview({ results: changed, capped: !!data.capped, total: data.processed || 0 });
|
|
9572
9563
|
}
|
|
9564
|
+
} catch {
|
|
9573
9565
|
}
|
|
9574
9566
|
setBulkOptimizing(false);
|
|
9567
|
+
}, [selectedIds]);
|
|
9568
|
+
const handleBulkApplyPreview = React4.useCallback(async () => {
|
|
9569
|
+
if (!bulkPreview || bulkPreview.results.length === 0) return;
|
|
9570
|
+
setBulkApplying(true);
|
|
9571
|
+
const corrections = bulkPreview.results.map((r) => ({
|
|
9572
|
+
collection: r.collection,
|
|
9573
|
+
id: r.id,
|
|
9574
|
+
metaTitle: r.after.metaTitle,
|
|
9575
|
+
metaDescription: r.after.metaDescription,
|
|
9576
|
+
focusKeyword: r.after.focusKeyword
|
|
9577
|
+
}));
|
|
9578
|
+
try {
|
|
9579
|
+
await fetch("/api/seo-plugin/ai-optimize-bulk", {
|
|
9580
|
+
method: "POST",
|
|
9581
|
+
headers: { "Content-Type": "application/json" },
|
|
9582
|
+
credentials: "include",
|
|
9583
|
+
body: JSON.stringify({ corrections, apply: true, limit: 100 })
|
|
9584
|
+
});
|
|
9585
|
+
} catch {
|
|
9586
|
+
}
|
|
9587
|
+
setBulkApplying(false);
|
|
9588
|
+
setBulkPreview(null);
|
|
9575
9589
|
setSelectedIds(/* @__PURE__ */ new Set());
|
|
9576
9590
|
fetchAudit();
|
|
9577
|
-
}, [
|
|
9591
|
+
}, [bulkPreview, fetchAudit]);
|
|
9592
|
+
const handleExportBulkPreviewCsv = React4.useCallback(() => {
|
|
9593
|
+
if (!bulkPreview) return;
|
|
9594
|
+
const header = [
|
|
9595
|
+
"collection",
|
|
9596
|
+
"id",
|
|
9597
|
+
"title",
|
|
9598
|
+
"before_title",
|
|
9599
|
+
"after_title",
|
|
9600
|
+
"before_description",
|
|
9601
|
+
"after_description",
|
|
9602
|
+
"before_keyword",
|
|
9603
|
+
"after_keyword"
|
|
9604
|
+
];
|
|
9605
|
+
const rows = bulkPreview.results.map((r) => [
|
|
9606
|
+
r.collection,
|
|
9607
|
+
r.id,
|
|
9608
|
+
r.title,
|
|
9609
|
+
r.before.metaTitle,
|
|
9610
|
+
r.after.metaTitle,
|
|
9611
|
+
r.before.metaDescription,
|
|
9612
|
+
r.after.metaDescription,
|
|
9613
|
+
r.before.focusKeyword,
|
|
9614
|
+
r.after.focusKeyword
|
|
9615
|
+
]);
|
|
9616
|
+
const csv = [header, ...rows].map((row) => row.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
|
|
9617
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
9618
|
+
const url = URL.createObjectURL(blob);
|
|
9619
|
+
const a = document.createElement("a");
|
|
9620
|
+
a.href = url;
|
|
9621
|
+
a.download = `seo-bulk-optimize-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
9622
|
+
a.click();
|
|
9623
|
+
URL.revokeObjectURL(url);
|
|
9624
|
+
}, [bulkPreview]);
|
|
9578
9625
|
const handleInlineSave = React4.useCallback(
|
|
9579
9626
|
async (item, metaTitle, metaDescription) => {
|
|
9580
9627
|
setSaving(true);
|
|
@@ -10327,6 +10374,96 @@ function SeoView() {
|
|
|
10327
10374
|
optimizing: bulkOptimizing,
|
|
10328
10375
|
t
|
|
10329
10376
|
}
|
|
10377
|
+
),
|
|
10378
|
+
bulkPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10379
|
+
"div",
|
|
10380
|
+
{
|
|
10381
|
+
style: {
|
|
10382
|
+
position: "fixed",
|
|
10383
|
+
inset: 0,
|
|
10384
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
10385
|
+
zIndex: 100,
|
|
10386
|
+
display: "flex",
|
|
10387
|
+
alignItems: "center",
|
|
10388
|
+
justifyContent: "center",
|
|
10389
|
+
padding: 24,
|
|
10390
|
+
fontFamily: "var(--font-body, system-ui)"
|
|
10391
|
+
},
|
|
10392
|
+
onClick: () => {
|
|
10393
|
+
if (!bulkApplying) setBulkPreview(null);
|
|
10394
|
+
},
|
|
10395
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10396
|
+
"div",
|
|
10397
|
+
{
|
|
10398
|
+
onClick: (e) => e.stopPropagation(),
|
|
10399
|
+
style: {
|
|
10400
|
+
width: "min(900px, 100%)",
|
|
10401
|
+
maxHeight: "85vh",
|
|
10402
|
+
display: "flex",
|
|
10403
|
+
flexDirection: "column",
|
|
10404
|
+
backgroundColor: V2.bgCard,
|
|
10405
|
+
border: `1px solid ${V2.border}`,
|
|
10406
|
+
borderRadius: 12,
|
|
10407
|
+
overflow: "hidden"
|
|
10408
|
+
},
|
|
10409
|
+
children: [
|
|
10410
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "16px 20px", borderBottom: `1px solid ${V2.border}` }, children: [
|
|
10411
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 16, fontWeight: 800, color: V2.text }, children: t.seoView.bulkPreviewTitle }),
|
|
10412
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, color: V2.textSecondary, marginTop: 4 }, children: [
|
|
10413
|
+
bulkPreview.results.length,
|
|
10414
|
+
" / ",
|
|
10415
|
+
bulkPreview.total,
|
|
10416
|
+
bulkPreview.capped ? ` \xB7 ${t.seoView.bulkCappedNote}` : ""
|
|
10417
|
+
] })
|
|
10418
|
+
] }),
|
|
10419
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { overflowY: "auto", padding: "8px 20px", flex: 1 }, children: [
|
|
10420
|
+
bulkPreview.results.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 24, textAlign: "center", color: V2.textSecondary, fontSize: 14 }, children: t.seoView.bulkNoChanges }),
|
|
10421
|
+
bulkPreview.results.map((r) => {
|
|
10422
|
+
const fields = [];
|
|
10423
|
+
if (r.before.metaTitle !== r.after.metaTitle) fields.push({ label: "Title", before: r.before.metaTitle, after: r.after.metaTitle });
|
|
10424
|
+
if (r.before.metaDescription !== r.after.metaDescription) fields.push({ label: "Description", before: r.before.metaDescription, after: r.after.metaDescription });
|
|
10425
|
+
if (r.before.focusKeyword !== r.after.focusKeyword) fields.push({ label: "Keyword", before: r.before.focusKeyword, after: r.after.focusKeyword });
|
|
10426
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "10px 0", borderBottom: `1px solid ${V2.border}` }, children: [
|
|
10427
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 13, fontWeight: 700, color: V2.text, marginBottom: 6 }, children: [
|
|
10428
|
+
r.title || `${r.collection}/${r.id}`,
|
|
10429
|
+
" ",
|
|
10430
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 10, color: V2.textSecondary, fontWeight: 400 }, children: r.collection })
|
|
10431
|
+
] }),
|
|
10432
|
+
fields.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: 4, fontSize: 12 }, children: [
|
|
10433
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 10, fontWeight: 700, color: V2.textSecondary, textTransform: "uppercase" }, children: f.label }),
|
|
10434
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: V2.red, textDecoration: f.before ? "line-through" : "none", opacity: 0.75 }, children: f.before || t.seoAnalyzer.emptyValue }),
|
|
10435
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: V2.green, fontWeight: 600 }, children: f.after || t.seoAnalyzer.emptyValue })
|
|
10436
|
+
] }, i))
|
|
10437
|
+
] }, `${r.collection}::${r.id}`);
|
|
10438
|
+
})
|
|
10439
|
+
] }),
|
|
10440
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "14px 20px", borderTop: `1px solid ${V2.border}`, display: "flex", justifyContent: "space-between", gap: 8 }, children: [
|
|
10441
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10442
|
+
"button",
|
|
10443
|
+
{
|
|
10444
|
+
onClick: handleExportBulkPreviewCsv,
|
|
10445
|
+
disabled: bulkPreview.results.length === 0,
|
|
10446
|
+
style: { ...btnBase, backgroundColor: V2.cyan, color: "#000", opacity: bulkPreview.results.length === 0 ? 0.5 : 1 },
|
|
10447
|
+
children: t.seoView.bulkExport
|
|
10448
|
+
}
|
|
10449
|
+
),
|
|
10450
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8 }, children: [
|
|
10451
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setBulkPreview(null), disabled: bulkApplying, style: { ...btnBase, backgroundColor: V2.bg, color: V2.text }, children: t.seoView.bulkCancel }),
|
|
10452
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10453
|
+
"button",
|
|
10454
|
+
{
|
|
10455
|
+
onClick: handleBulkApplyPreview,
|
|
10456
|
+
disabled: bulkApplying || bulkPreview.results.length === 0,
|
|
10457
|
+
style: { ...btnBase, backgroundColor: "#7c3aed", color: "#fff", opacity: bulkApplying || bulkPreview.results.length === 0 ? 0.6 : 1 },
|
|
10458
|
+
children: bulkApplying ? t.seoView.bulkApplying : `${t.seoView.bulkApply} (${bulkPreview.results.length})`
|
|
10459
|
+
}
|
|
10460
|
+
)
|
|
10461
|
+
] })
|
|
10462
|
+
] })
|
|
10463
|
+
]
|
|
10464
|
+
}
|
|
10465
|
+
)
|
|
10466
|
+
}
|
|
10330
10467
|
)
|
|
10331
10468
|
]
|
|
10332
10469
|
}
|
package/dist/client.js
CHANGED
|
@@ -1060,8 +1060,15 @@ var fr = {
|
|
|
1060
1060
|
markCornerstone: "Marquer pilier",
|
|
1061
1061
|
unmarkCornerstone: "D\xE9marquer pilier",
|
|
1062
1062
|
bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
|
|
1063
|
-
bulkOptimizing: "
|
|
1063
|
+
bulkOptimizing: "Analyse\u2026",
|
|
1064
1064
|
bulkConfirm: "Confirmer ?",
|
|
1065
|
+
bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
|
|
1066
|
+
bulkApply: "Appliquer",
|
|
1067
|
+
bulkApplying: "Application\u2026",
|
|
1068
|
+
bulkCancel: "Annuler",
|
|
1069
|
+
bulkExport: "Exporter CSV",
|
|
1070
|
+
bulkNoChanges: "Aucune correction n\xE9cessaire sur la s\xE9lection.",
|
|
1071
|
+
bulkCappedNote: "limite atteinte (affine la s\xE9lection)",
|
|
1065
1072
|
searchPlaceholder: "Rechercher (titre, slug, keyword)...",
|
|
1066
1073
|
allCollections: "Toutes les collections",
|
|
1067
1074
|
allScores: "Tous les scores",
|
|
@@ -1655,8 +1662,15 @@ var en = {
|
|
|
1655
1662
|
markCornerstone: "Mark as cornerstone",
|
|
1656
1663
|
unmarkCornerstone: "Unmark cornerstone",
|
|
1657
1664
|
bulkOptimizeMeta: "Optimize meta (AI)",
|
|
1658
|
-
bulkOptimizing: "
|
|
1665
|
+
bulkOptimizing: "Analyzing\u2026",
|
|
1659
1666
|
bulkConfirm: "Confirm?",
|
|
1667
|
+
bulkPreviewTitle: "Proposed meta corrections",
|
|
1668
|
+
bulkApply: "Apply",
|
|
1669
|
+
bulkApplying: "Applying\u2026",
|
|
1670
|
+
bulkCancel: "Cancel",
|
|
1671
|
+
bulkExport: "Export CSV",
|
|
1672
|
+
bulkNoChanges: "No corrections needed on the selection.",
|
|
1673
|
+
bulkCappedNote: "limit reached (narrow the selection)",
|
|
1660
1674
|
searchPlaceholder: "Search (title, slug, keyword)...",
|
|
1661
1675
|
allCollections: "All collections",
|
|
1662
1676
|
allScores: "All scores",
|
|
@@ -9205,7 +9219,6 @@ function BulkActionBar({
|
|
|
9205
9219
|
optimizing,
|
|
9206
9220
|
t
|
|
9207
9221
|
}) {
|
|
9208
|
-
const [confirmOptimize, setConfirmOptimize] = useState(false);
|
|
9209
9222
|
if (count === 0) return null;
|
|
9210
9223
|
return /* @__PURE__ */ jsxs(
|
|
9211
9224
|
"div",
|
|
@@ -9255,18 +9268,11 @@ function BulkActionBar({
|
|
|
9255
9268
|
"button",
|
|
9256
9269
|
{
|
|
9257
9270
|
onClick: () => {
|
|
9258
|
-
if (optimizing)
|
|
9259
|
-
if (confirmOptimize) {
|
|
9260
|
-
setConfirmOptimize(false);
|
|
9261
|
-
onOptimizeMeta();
|
|
9262
|
-
} else {
|
|
9263
|
-
setConfirmOptimize(true);
|
|
9264
|
-
setTimeout(() => setConfirmOptimize(false), 4e3);
|
|
9265
|
-
}
|
|
9271
|
+
if (!optimizing) onOptimizeMeta();
|
|
9266
9272
|
},
|
|
9267
9273
|
disabled: optimizing,
|
|
9268
9274
|
style: { ...btnBase, backgroundColor: "#7c3aed", color: "#fff", opacity: optimizing ? 0.6 : 1 },
|
|
9269
|
-
children: optimizing ? t.seoView.bulkOptimizing :
|
|
9275
|
+
children: optimizing ? t.seoView.bulkOptimizing : `\u2728 ${t.seoView.bulkOptimizeMeta}`
|
|
9270
9276
|
}
|
|
9271
9277
|
),
|
|
9272
9278
|
/* @__PURE__ */ jsx(
|
|
@@ -9310,6 +9316,8 @@ function SeoView() {
|
|
|
9310
9316
|
const [saving, setSaving] = useState(false);
|
|
9311
9317
|
const [saveError, setSaveError] = useState(null);
|
|
9312
9318
|
const [bulkOptimizing, setBulkOptimizing] = useState(false);
|
|
9319
|
+
const [bulkApplying, setBulkApplying] = useState(false);
|
|
9320
|
+
const [bulkPreview, setBulkPreview] = useState(null);
|
|
9313
9321
|
const PAGE_SIZE = 50;
|
|
9314
9322
|
const fetchAudit = useCallback(async (forceRefresh = false) => {
|
|
9315
9323
|
setLoading(true);
|
|
@@ -9531,44 +9539,83 @@ function SeoView() {
|
|
|
9531
9539
|
[selectedIds, fetchAudit]
|
|
9532
9540
|
);
|
|
9533
9541
|
const handleBulkOptimizeMeta = useCallback(async () => {
|
|
9534
|
-
const
|
|
9535
|
-
if (
|
|
9542
|
+
const ids = Array.from(selectedIds).filter((k) => !k.startsWith("global:"));
|
|
9543
|
+
if (ids.length === 0) return;
|
|
9536
9544
|
setBulkOptimizing(true);
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
});
|
|
9547
|
-
if (!res.ok) continue;
|
|
9545
|
+
setBulkPreview(null);
|
|
9546
|
+
try {
|
|
9547
|
+
const res = await fetch("/api/seo-plugin/ai-optimize-bulk", {
|
|
9548
|
+
method: "POST",
|
|
9549
|
+
headers: { "Content-Type": "application/json" },
|
|
9550
|
+
credentials: "include",
|
|
9551
|
+
body: JSON.stringify({ ids, apply: false, limit: 100 })
|
|
9552
|
+
});
|
|
9553
|
+
if (res.ok) {
|
|
9548
9554
|
const data = await res.json();
|
|
9549
|
-
const
|
|
9550
|
-
|
|
9551
|
-
if (sug.metaTitle || sug.metaDescription) {
|
|
9552
|
-
patch.meta = { title: sug.metaTitle, description: sug.metaDescription };
|
|
9553
|
-
}
|
|
9554
|
-
if (sug.focusKeyword && sug.focusKeyword !== data.current?.focusKeyword) {
|
|
9555
|
-
patch.focusKeyword = sug.focusKeyword;
|
|
9556
|
-
}
|
|
9557
|
-
if (Object.keys(patch).length > 0) {
|
|
9558
|
-
await fetch(`/api/${collection}/${id}`, {
|
|
9559
|
-
method: "PATCH",
|
|
9560
|
-
headers: { "Content-Type": "application/json" },
|
|
9561
|
-
credentials: "include",
|
|
9562
|
-
body: JSON.stringify(patch)
|
|
9563
|
-
});
|
|
9564
|
-
}
|
|
9565
|
-
} catch {
|
|
9555
|
+
const changed = (data.results || []).filter((r) => r.changed && !r.error);
|
|
9556
|
+
setBulkPreview({ results: changed, capped: !!data.capped, total: data.processed || 0 });
|
|
9566
9557
|
}
|
|
9558
|
+
} catch {
|
|
9567
9559
|
}
|
|
9568
9560
|
setBulkOptimizing(false);
|
|
9561
|
+
}, [selectedIds]);
|
|
9562
|
+
const handleBulkApplyPreview = useCallback(async () => {
|
|
9563
|
+
if (!bulkPreview || bulkPreview.results.length === 0) return;
|
|
9564
|
+
setBulkApplying(true);
|
|
9565
|
+
const corrections = bulkPreview.results.map((r) => ({
|
|
9566
|
+
collection: r.collection,
|
|
9567
|
+
id: r.id,
|
|
9568
|
+
metaTitle: r.after.metaTitle,
|
|
9569
|
+
metaDescription: r.after.metaDescription,
|
|
9570
|
+
focusKeyword: r.after.focusKeyword
|
|
9571
|
+
}));
|
|
9572
|
+
try {
|
|
9573
|
+
await fetch("/api/seo-plugin/ai-optimize-bulk", {
|
|
9574
|
+
method: "POST",
|
|
9575
|
+
headers: { "Content-Type": "application/json" },
|
|
9576
|
+
credentials: "include",
|
|
9577
|
+
body: JSON.stringify({ corrections, apply: true, limit: 100 })
|
|
9578
|
+
});
|
|
9579
|
+
} catch {
|
|
9580
|
+
}
|
|
9581
|
+
setBulkApplying(false);
|
|
9582
|
+
setBulkPreview(null);
|
|
9569
9583
|
setSelectedIds(/* @__PURE__ */ new Set());
|
|
9570
9584
|
fetchAudit();
|
|
9571
|
-
}, [
|
|
9585
|
+
}, [bulkPreview, fetchAudit]);
|
|
9586
|
+
const handleExportBulkPreviewCsv = useCallback(() => {
|
|
9587
|
+
if (!bulkPreview) return;
|
|
9588
|
+
const header = [
|
|
9589
|
+
"collection",
|
|
9590
|
+
"id",
|
|
9591
|
+
"title",
|
|
9592
|
+
"before_title",
|
|
9593
|
+
"after_title",
|
|
9594
|
+
"before_description",
|
|
9595
|
+
"after_description",
|
|
9596
|
+
"before_keyword",
|
|
9597
|
+
"after_keyword"
|
|
9598
|
+
];
|
|
9599
|
+
const rows = bulkPreview.results.map((r) => [
|
|
9600
|
+
r.collection,
|
|
9601
|
+
r.id,
|
|
9602
|
+
r.title,
|
|
9603
|
+
r.before.metaTitle,
|
|
9604
|
+
r.after.metaTitle,
|
|
9605
|
+
r.before.metaDescription,
|
|
9606
|
+
r.after.metaDescription,
|
|
9607
|
+
r.before.focusKeyword,
|
|
9608
|
+
r.after.focusKeyword
|
|
9609
|
+
]);
|
|
9610
|
+
const csv = [header, ...rows].map((row) => row.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
|
|
9611
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
9612
|
+
const url = URL.createObjectURL(blob);
|
|
9613
|
+
const a = document.createElement("a");
|
|
9614
|
+
a.href = url;
|
|
9615
|
+
a.download = `seo-bulk-optimize-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
9616
|
+
a.click();
|
|
9617
|
+
URL.revokeObjectURL(url);
|
|
9618
|
+
}, [bulkPreview]);
|
|
9572
9619
|
const handleInlineSave = useCallback(
|
|
9573
9620
|
async (item, metaTitle, metaDescription) => {
|
|
9574
9621
|
setSaving(true);
|
|
@@ -10321,6 +10368,96 @@ function SeoView() {
|
|
|
10321
10368
|
optimizing: bulkOptimizing,
|
|
10322
10369
|
t
|
|
10323
10370
|
}
|
|
10371
|
+
),
|
|
10372
|
+
bulkPreview && /* @__PURE__ */ jsx(
|
|
10373
|
+
"div",
|
|
10374
|
+
{
|
|
10375
|
+
style: {
|
|
10376
|
+
position: "fixed",
|
|
10377
|
+
inset: 0,
|
|
10378
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
10379
|
+
zIndex: 100,
|
|
10380
|
+
display: "flex",
|
|
10381
|
+
alignItems: "center",
|
|
10382
|
+
justifyContent: "center",
|
|
10383
|
+
padding: 24,
|
|
10384
|
+
fontFamily: "var(--font-body, system-ui)"
|
|
10385
|
+
},
|
|
10386
|
+
onClick: () => {
|
|
10387
|
+
if (!bulkApplying) setBulkPreview(null);
|
|
10388
|
+
},
|
|
10389
|
+
children: /* @__PURE__ */ jsxs(
|
|
10390
|
+
"div",
|
|
10391
|
+
{
|
|
10392
|
+
onClick: (e) => e.stopPropagation(),
|
|
10393
|
+
style: {
|
|
10394
|
+
width: "min(900px, 100%)",
|
|
10395
|
+
maxHeight: "85vh",
|
|
10396
|
+
display: "flex",
|
|
10397
|
+
flexDirection: "column",
|
|
10398
|
+
backgroundColor: V2.bgCard,
|
|
10399
|
+
border: `1px solid ${V2.border}`,
|
|
10400
|
+
borderRadius: 12,
|
|
10401
|
+
overflow: "hidden"
|
|
10402
|
+
},
|
|
10403
|
+
children: [
|
|
10404
|
+
/* @__PURE__ */ jsxs("div", { style: { padding: "16px 20px", borderBottom: `1px solid ${V2.border}` }, children: [
|
|
10405
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 16, fontWeight: 800, color: V2.text }, children: t.seoView.bulkPreviewTitle }),
|
|
10406
|
+
/* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: V2.textSecondary, marginTop: 4 }, children: [
|
|
10407
|
+
bulkPreview.results.length,
|
|
10408
|
+
" / ",
|
|
10409
|
+
bulkPreview.total,
|
|
10410
|
+
bulkPreview.capped ? ` \xB7 ${t.seoView.bulkCappedNote}` : ""
|
|
10411
|
+
] })
|
|
10412
|
+
] }),
|
|
10413
|
+
/* @__PURE__ */ jsxs("div", { style: { overflowY: "auto", padding: "8px 20px", flex: 1 }, children: [
|
|
10414
|
+
bulkPreview.results.length === 0 && /* @__PURE__ */ jsx("div", { style: { padding: 24, textAlign: "center", color: V2.textSecondary, fontSize: 14 }, children: t.seoView.bulkNoChanges }),
|
|
10415
|
+
bulkPreview.results.map((r) => {
|
|
10416
|
+
const fields = [];
|
|
10417
|
+
if (r.before.metaTitle !== r.after.metaTitle) fields.push({ label: "Title", before: r.before.metaTitle, after: r.after.metaTitle });
|
|
10418
|
+
if (r.before.metaDescription !== r.after.metaDescription) fields.push({ label: "Description", before: r.before.metaDescription, after: r.after.metaDescription });
|
|
10419
|
+
if (r.before.focusKeyword !== r.after.focusKeyword) fields.push({ label: "Keyword", before: r.before.focusKeyword, after: r.after.focusKeyword });
|
|
10420
|
+
return /* @__PURE__ */ jsxs("div", { style: { padding: "10px 0", borderBottom: `1px solid ${V2.border}` }, children: [
|
|
10421
|
+
/* @__PURE__ */ jsxs("div", { style: { fontSize: 13, fontWeight: 700, color: V2.text, marginBottom: 6 }, children: [
|
|
10422
|
+
r.title || `${r.collection}/${r.id}`,
|
|
10423
|
+
" ",
|
|
10424
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: V2.textSecondary, fontWeight: 400 }, children: r.collection })
|
|
10425
|
+
] }),
|
|
10426
|
+
fields.map((f, i) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: 4, fontSize: 12 }, children: [
|
|
10427
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 10, fontWeight: 700, color: V2.textSecondary, textTransform: "uppercase" }, children: f.label }),
|
|
10428
|
+
/* @__PURE__ */ jsx("div", { style: { color: V2.red, textDecoration: f.before ? "line-through" : "none", opacity: 0.75 }, children: f.before || t.seoAnalyzer.emptyValue }),
|
|
10429
|
+
/* @__PURE__ */ jsx("div", { style: { color: V2.green, fontWeight: 600 }, children: f.after || t.seoAnalyzer.emptyValue })
|
|
10430
|
+
] }, i))
|
|
10431
|
+
] }, `${r.collection}::${r.id}`);
|
|
10432
|
+
})
|
|
10433
|
+
] }),
|
|
10434
|
+
/* @__PURE__ */ jsxs("div", { style: { padding: "14px 20px", borderTop: `1px solid ${V2.border}`, display: "flex", justifyContent: "space-between", gap: 8 }, children: [
|
|
10435
|
+
/* @__PURE__ */ jsx(
|
|
10436
|
+
"button",
|
|
10437
|
+
{
|
|
10438
|
+
onClick: handleExportBulkPreviewCsv,
|
|
10439
|
+
disabled: bulkPreview.results.length === 0,
|
|
10440
|
+
style: { ...btnBase, backgroundColor: V2.cyan, color: "#000", opacity: bulkPreview.results.length === 0 ? 0.5 : 1 },
|
|
10441
|
+
children: t.seoView.bulkExport
|
|
10442
|
+
}
|
|
10443
|
+
),
|
|
10444
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
|
|
10445
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setBulkPreview(null), disabled: bulkApplying, style: { ...btnBase, backgroundColor: V2.bg, color: V2.text }, children: t.seoView.bulkCancel }),
|
|
10446
|
+
/* @__PURE__ */ jsx(
|
|
10447
|
+
"button",
|
|
10448
|
+
{
|
|
10449
|
+
onClick: handleBulkApplyPreview,
|
|
10450
|
+
disabled: bulkApplying || bulkPreview.results.length === 0,
|
|
10451
|
+
style: { ...btnBase, backgroundColor: "#7c3aed", color: "#fff", opacity: bulkApplying || bulkPreview.results.length === 0 ? 0.6 : 1 },
|
|
10452
|
+
children: bulkApplying ? t.seoView.bulkApplying : `${t.seoView.bulkApply} (${bulkPreview.results.length})`
|
|
10453
|
+
}
|
|
10454
|
+
)
|
|
10455
|
+
] })
|
|
10456
|
+
] })
|
|
10457
|
+
]
|
|
10458
|
+
}
|
|
10459
|
+
)
|
|
10460
|
+
}
|
|
10324
10461
|
)
|
|
10325
10462
|
]
|
|
10326
10463
|
}
|