@consilioweb/payload-seo-analyzer 1.8.0 → 1.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/README.md +33 -1
- package/dist/client.cjs +232 -6
- package/dist/client.js +232 -6
- package/dist/index.cjs +393 -120
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +393 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -637,7 +637,9 @@ All endpoints are prefixed with the configured `endpointBasePath` (default: `/se
|
|
|
637
637
|
| `POST` | `/suggest-links` | Internal link suggestions for a page |
|
|
638
638
|
| `POST` | `/create-redirect` | Create a single redirect entry |
|
|
639
639
|
| `GET` `POST` `PATCH` `DELETE` | `/redirects` | Full CRUD for redirect management |
|
|
640
|
-
| `POST` | `/ai-generate` |
|
|
640
|
+
| `POST` | `/ai-generate` | Heuristic meta title/description generation (no API key needed) |
|
|
641
|
+
| `POST` | `/ai-rewrite` | Rewrite a single meta field (Claude if `ANTHROPIC_API_KEY` set, else heuristic) |
|
|
642
|
+
| `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 |
|
|
641
643
|
| `GET` | `/cannibalization` | Detect keyword cannibalization |
|
|
642
644
|
| `POST` | `/external-links` | Check external link status (live HTTP checks with SSRF protection) |
|
|
643
645
|
| `GET` | `/sitemap-config` | Sitemap configuration data |
|
|
@@ -647,6 +649,36 @@ All endpoints are prefixed with the configured `endpointBasePath` (default: `/se
|
|
|
647
649
|
| `GET` | `/link-graph` | Internal link graph data |
|
|
648
650
|
| `GET` `POST` `DELETE` | `/seo-logs` | 404 log management (POST supports secret-header auth) |
|
|
649
651
|
|
|
652
|
+
### AI SEO Optimize (`/ai-optimize`)
|
|
653
|
+
|
|
654
|
+
One-click **"scan → propose → apply"** assistant in the editor sidebar (button **“Optimiser avec l'IA”**):
|
|
655
|
+
|
|
656
|
+
1. Runs the real SEO engine on the current page and collects the failing/warning meta checks.
|
|
657
|
+
2. Sends the content + detected issues to Claude, which proposes an optimized **meta title**, **meta description** and (only when missing) a **focus keyword**, applying the same SEO 2026 rules the engine enforces.
|
|
658
|
+
3. The editor reviews the *current → suggested* diff + rationale and **applies in one click** — the fields are filled, then saved as usual.
|
|
659
|
+
|
|
660
|
+
Scope is **meta only** by design: the SEO 2026 analysis classifies mass AI-generated body content as a spam/penalty risk, so the feature never rewrites page content.
|
|
661
|
+
|
|
662
|
+
**Setup (opt-in, billed to your own Anthropic key):**
|
|
663
|
+
|
|
664
|
+
| Env var | Default | Purpose |
|
|
665
|
+
|---|---|---|
|
|
666
|
+
| `ANTHROPIC_API_KEY` | — | Your Anthropic API key, read **only** server-side (never from the client). **Without it, the feature falls back to the built-in heuristic generators.** |
|
|
667
|
+
| `SEO_AI_MODEL` | `claude-opus-4-8` | Override the Claude model (e.g. `claude-haiku-4-5` for a cheaper/faster option). |
|
|
668
|
+
|
|
669
|
+
The endpoint is gated behind `features.aiFeatures` (default `true`). Suggestions are **validated/clamped server-side** (title ≤ 70, description ≤ 160, focus keyword only filled when empty) so what gets applied is always rule-compliant, regardless of the model's output.
|
|
670
|
+
|
|
671
|
+
### Low-memory hosting (e.g. Infomaniak)
|
|
672
|
+
|
|
673
|
+
The site-wide audit (`/admin/seo` dashboard) is the heaviest operation. On constrained shared Node hosting it is built **single-flight, in the background**: the first uncached load returns `202` and the dashboard shows a “generating…” state while it computes, then polls until ready — it never blocks or OOM-kills the process. Tune it via env vars if needed:
|
|
674
|
+
|
|
675
|
+
| Env var | Default | Purpose |
|
|
676
|
+
|---|---|---|
|
|
677
|
+
| `SEO_AUDIT_BATCH_SIZE` | `15` | Documents processed per batch. Lower it on very small hosts. |
|
|
678
|
+
| `SEO_AUDIT_MAX_DOCS` | `1500` | Hard cap on documents audited. |
|
|
679
|
+
|
|
680
|
+
For the lowest-memory tiers you can also set `features: { warmCache: false }` to skip the startup/hourly pre-load entirely.
|
|
681
|
+
|
|
650
682
|
<img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
|
|
651
683
|
|
|
652
684
|
## SEO Rules Reference
|
package/dist/client.cjs
CHANGED
|
@@ -1058,6 +1058,8 @@ var fr = {
|
|
|
1058
1058
|
},
|
|
1059
1059
|
seoView: {
|
|
1060
1060
|
loadingAudit: "Chargement de l'audit SEO...",
|
|
1061
|
+
buildingAudit: "G\xE9n\xE9ration de l'audit SEO en cours\u2026 (calcul\xE9 en arri\xE8re-plan pour ne pas surcharger le serveur, cela peut prendre un moment sur un gros site)",
|
|
1062
|
+
buildTimeout: "La g\xE9n\xE9ration de l'audit prend plus de temps que pr\xE9vu. R\xE9essayez dans quelques instants.",
|
|
1061
1063
|
errorSaving: "Erreur lors de la sauvegarde",
|
|
1062
1064
|
auditTitle: "Audit SEO",
|
|
1063
1065
|
pagesAnalyzed: "pages analys\xE9es",
|
|
@@ -1500,7 +1502,19 @@ var fr = {
|
|
|
1500
1502
|
generateMeta: "G\xE9n\xE9rer les meta",
|
|
1501
1503
|
metaTitle: "Meta Title",
|
|
1502
1504
|
metaDescription: "Meta Description",
|
|
1503
|
-
emptyValue: "(vide)"
|
|
1505
|
+
emptyValue: "(vide)",
|
|
1506
|
+
optimizeWithAi: "Optimiser avec l'IA",
|
|
1507
|
+
optimizeIntro: "L'IA analyse la page et propose des meta optimis\xE9es (titre, description, mot-cl\xE9). V\xE9rifiez puis appliquez.",
|
|
1508
|
+
optimizeRunning: "Analyse en cours\u2026",
|
|
1509
|
+
applyAll: "Appliquer",
|
|
1510
|
+
applied: "Appliqu\xE9",
|
|
1511
|
+
whyChanges: "Pourquoi ces changements",
|
|
1512
|
+
labelCurrent: "Actuel",
|
|
1513
|
+
labelSuggested: "Sugg\xE9r\xE9",
|
|
1514
|
+
labelFocusKeyword: "Mot-cl\xE9 cible",
|
|
1515
|
+
heuristicNote: "Suggestions heuristiques (cl\xE9 API Claude non configur\xE9e).",
|
|
1516
|
+
applySaveHint: "Champs remplis \u2014 pensez \xE0 enregistrer le document.",
|
|
1517
|
+
noMetaChange: "Aucun changement propos\xE9."
|
|
1504
1518
|
},
|
|
1505
1519
|
scoreHistory: {
|
|
1506
1520
|
loading: "Chargement de l'historique...",
|
|
@@ -1636,6 +1650,8 @@ var en = {
|
|
|
1636
1650
|
},
|
|
1637
1651
|
seoView: {
|
|
1638
1652
|
loadingAudit: "Loading SEO audit...",
|
|
1653
|
+
buildingAudit: "Building the SEO audit\u2026 (computed in the background to avoid overloading the server \u2014 this can take a moment on a large site)",
|
|
1654
|
+
buildTimeout: "The audit is taking longer than expected. Please try again in a moment.",
|
|
1639
1655
|
errorSaving: "Error during save",
|
|
1640
1656
|
auditTitle: "SEO Audit",
|
|
1641
1657
|
pagesAnalyzed: "pages analyzed",
|
|
@@ -2078,7 +2094,19 @@ var en = {
|
|
|
2078
2094
|
generateMeta: "Generate meta",
|
|
2079
2095
|
metaTitle: "Meta Title",
|
|
2080
2096
|
metaDescription: "Meta Description",
|
|
2081
|
-
emptyValue: "(empty)"
|
|
2097
|
+
emptyValue: "(empty)",
|
|
2098
|
+
optimizeWithAi: "Optimize with AI",
|
|
2099
|
+
optimizeIntro: "AI analyzes the page and proposes optimized meta tags (title, description, keyword). Review, then apply.",
|
|
2100
|
+
optimizeRunning: "Analyzing\u2026",
|
|
2101
|
+
applyAll: "Apply",
|
|
2102
|
+
applied: "Applied",
|
|
2103
|
+
whyChanges: "Why these changes",
|
|
2104
|
+
labelCurrent: "Current",
|
|
2105
|
+
labelSuggested: "Suggested",
|
|
2106
|
+
labelFocusKeyword: "Focus keyword",
|
|
2107
|
+
heuristicNote: "Heuristic suggestions (Claude API key not configured).",
|
|
2108
|
+
applySaveHint: "Fields filled \u2014 remember to save the document.",
|
|
2109
|
+
noMetaChange: "No changes proposed."
|
|
2082
2110
|
},
|
|
2083
2111
|
scoreHistory: {
|
|
2084
2112
|
loading: "Loading history...",
|
|
@@ -6484,9 +6512,11 @@ var C2 = {
|
|
|
6484
6512
|
white: "#fff",
|
|
6485
6513
|
green: "#22c55e",
|
|
6486
6514
|
red: "#ef4444",
|
|
6515
|
+
bg: "#fafafa",
|
|
6487
6516
|
textPrimary: "var(--theme-text, #1a1a1a)",
|
|
6488
6517
|
textSecondary: "var(--theme-elevation-600, #6b7280)",
|
|
6489
6518
|
border: "var(--theme-border-color, #000)",
|
|
6519
|
+
inputBg: "var(--theme-input-bg, #fff)",
|
|
6490
6520
|
surfaceBg: "var(--theme-elevation-0, #fff)",
|
|
6491
6521
|
surface50: "var(--theme-elevation-50, #f9fafb)"
|
|
6492
6522
|
};
|
|
@@ -6986,10 +7016,48 @@ function useInternalLinkSuggestions(documentId, collection, textContent) {
|
|
|
6986
7016
|
}, [textContent, documentId, collection]);
|
|
6987
7017
|
return { suggestions, loading };
|
|
6988
7018
|
}
|
|
7019
|
+
function AiDiffRow({
|
|
7020
|
+
label,
|
|
7021
|
+
current,
|
|
7022
|
+
suggested,
|
|
7023
|
+
labelCurrent,
|
|
7024
|
+
labelSuggested,
|
|
7025
|
+
emptyValue,
|
|
7026
|
+
C: colors
|
|
7027
|
+
}) {
|
|
7028
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: 10 }, children: [
|
|
7029
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 10, fontWeight: 700, color: colors.textSecondary, textTransform: "uppercase", marginBottom: 4 }, children: label }),
|
|
7030
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 11, color: colors.textSecondary, marginBottom: 3 }, children: [
|
|
7031
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 700 }, children: [
|
|
7032
|
+
labelCurrent,
|
|
7033
|
+
": "
|
|
7034
|
+
] }),
|
|
7035
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { textDecoration: current ? "line-through" : "none", opacity: 0.7 }, children: current || emptyValue })
|
|
7036
|
+
] }),
|
|
7037
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
7038
|
+
"div",
|
|
7039
|
+
{
|
|
7040
|
+
style: {
|
|
7041
|
+
padding: "6px 10px",
|
|
7042
|
+
borderRadius: 6,
|
|
7043
|
+
border: `1px solid var(--theme-elevation-200, #e5e7eb)`,
|
|
7044
|
+
backgroundColor: colors.surface50,
|
|
7045
|
+
fontSize: 12,
|
|
7046
|
+
color: colors.textPrimary,
|
|
7047
|
+
lineHeight: 1.5
|
|
7048
|
+
},
|
|
7049
|
+
children: [
|
|
7050
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 10, fontWeight: 700, color: colors.green, textTransform: "uppercase", marginRight: 6 }, children: labelSuggested }),
|
|
7051
|
+
suggested || emptyValue
|
|
7052
|
+
]
|
|
7053
|
+
}
|
|
7054
|
+
)
|
|
7055
|
+
] });
|
|
7056
|
+
}
|
|
6989
7057
|
var SeoAnalyzer = () => {
|
|
6990
7058
|
const locale = useSeoLocale();
|
|
6991
7059
|
const t = getDashboardT(locale);
|
|
6992
|
-
const [formFields] = ui.useAllFormFields();
|
|
7060
|
+
const [formFields, dispatchFields] = ui.useAllFormFields();
|
|
6993
7061
|
const initialScoreRef = React4.useRef(null);
|
|
6994
7062
|
const [suggestionsOpen, setSuggestionsOpen] = React4.useState(true);
|
|
6995
7063
|
const [cannibalizationExpanded, setCannibalizationExpanded] = React4.useState(false);
|
|
@@ -6998,6 +7066,10 @@ var SeoAnalyzer = () => {
|
|
|
6998
7066
|
const [aiGenerating, setAiGenerating] = React4.useState(false);
|
|
6999
7067
|
const [aiResult, setAiResult] = React4.useState(null);
|
|
7000
7068
|
const [aiCopied, setAiCopied] = React4.useState(null);
|
|
7069
|
+
const [aiOptimizing, setAiOptimizing] = React4.useState(false);
|
|
7070
|
+
const [aiOptimizeResult, setAiOptimizeResult] = React4.useState(null);
|
|
7071
|
+
const [aiOptimizeApplied, setAiOptimizeApplied] = React4.useState(false);
|
|
7072
|
+
const [aiOptimizeError, setAiOptimizeError] = React4.useState(false);
|
|
7001
7073
|
const getFieldValue = React4.useCallback(
|
|
7002
7074
|
(path) => {
|
|
7003
7075
|
if (!formFields) return void 0;
|
|
@@ -7725,6 +7797,145 @@ var SeoAnalyzer = () => {
|
|
|
7725
7797
|
}
|
|
7726
7798
|
)
|
|
7727
7799
|
] }),
|
|
7800
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: 12 }, children: [
|
|
7801
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7802
|
+
"button",
|
|
7803
|
+
{
|
|
7804
|
+
type: "button",
|
|
7805
|
+
disabled: aiOptimizing || !documentId,
|
|
7806
|
+
title: !documentId ? t.seoAnalyzer.applySaveHint : void 0,
|
|
7807
|
+
onClick: async () => {
|
|
7808
|
+
setAiOptimizing(true);
|
|
7809
|
+
setAiOptimizeResult(null);
|
|
7810
|
+
setAiOptimizeApplied(false);
|
|
7811
|
+
setAiOptimizeError(false);
|
|
7812
|
+
try {
|
|
7813
|
+
const res = await fetch("/api/seo-plugin/ai-optimize", {
|
|
7814
|
+
method: "POST",
|
|
7815
|
+
credentials: "include",
|
|
7816
|
+
headers: { "Content-Type": "application/json" },
|
|
7817
|
+
body: JSON.stringify({ collection: currentCollection, id: documentId })
|
|
7818
|
+
});
|
|
7819
|
+
if (res.ok) {
|
|
7820
|
+
setAiOptimizeResult(await res.json());
|
|
7821
|
+
} else {
|
|
7822
|
+
setAiOptimizeError(true);
|
|
7823
|
+
}
|
|
7824
|
+
} catch {
|
|
7825
|
+
setAiOptimizeError(true);
|
|
7826
|
+
}
|
|
7827
|
+
setAiOptimizing(false);
|
|
7828
|
+
},
|
|
7829
|
+
style: {
|
|
7830
|
+
display: "flex",
|
|
7831
|
+
alignItems: "center",
|
|
7832
|
+
gap: 6,
|
|
7833
|
+
width: "100%",
|
|
7834
|
+
padding: "10px 14px",
|
|
7835
|
+
borderRadius: 8,
|
|
7836
|
+
border: `2px solid ${C2.border}`,
|
|
7837
|
+
backgroundColor: "#7c3aed",
|
|
7838
|
+
color: "#fff",
|
|
7839
|
+
fontWeight: 800,
|
|
7840
|
+
fontSize: 12,
|
|
7841
|
+
cursor: aiOptimizing || !documentId ? "not-allowed" : "pointer",
|
|
7842
|
+
opacity: aiOptimizing || !documentId ? 0.6 : 1,
|
|
7843
|
+
textTransform: "uppercase",
|
|
7844
|
+
letterSpacing: "0.04em",
|
|
7845
|
+
justifyContent: "center",
|
|
7846
|
+
boxShadow: "2px 2px 0 0 var(--theme-border-color, rgba(0,0,0,1))"
|
|
7847
|
+
},
|
|
7848
|
+
children: aiOptimizing ? t.seoAnalyzer.optimizeRunning : `\u2728 ${t.seoAnalyzer.optimizeWithAi}`
|
|
7849
|
+
}
|
|
7850
|
+
),
|
|
7851
|
+
aiOptimizeError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 8, padding: "8px 12px", borderRadius: 6, fontSize: 11, color: C2.white, backgroundColor: C2.red }, children: t.common.loadingError }),
|
|
7852
|
+
aiOptimizeResult && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7853
|
+
"div",
|
|
7854
|
+
{
|
|
7855
|
+
style: {
|
|
7856
|
+
marginTop: 8,
|
|
7857
|
+
padding: "12px 14px",
|
|
7858
|
+
borderRadius: 8,
|
|
7859
|
+
border: `2px solid ${C2.border}`,
|
|
7860
|
+
backgroundColor: C2.surfaceBg
|
|
7861
|
+
},
|
|
7862
|
+
children: [
|
|
7863
|
+
aiOptimizeResult.method === "heuristic" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 10, color: C2.textSecondary, marginBottom: 8, fontStyle: "italic" }, children: t.seoAnalyzer.heuristicNote }),
|
|
7864
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7865
|
+
AiDiffRow,
|
|
7866
|
+
{
|
|
7867
|
+
label: t.seoAnalyzer.metaTitle,
|
|
7868
|
+
current: aiOptimizeResult.current.metaTitle,
|
|
7869
|
+
suggested: aiOptimizeResult.suggestions.metaTitle,
|
|
7870
|
+
labelCurrent: t.seoAnalyzer.labelCurrent,
|
|
7871
|
+
labelSuggested: t.seoAnalyzer.labelSuggested,
|
|
7872
|
+
emptyValue: t.seoAnalyzer.emptyValue,
|
|
7873
|
+
C: C2
|
|
7874
|
+
}
|
|
7875
|
+
),
|
|
7876
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7877
|
+
AiDiffRow,
|
|
7878
|
+
{
|
|
7879
|
+
label: t.seoAnalyzer.metaDescription,
|
|
7880
|
+
current: aiOptimizeResult.current.metaDescription,
|
|
7881
|
+
suggested: aiOptimizeResult.suggestions.metaDescription,
|
|
7882
|
+
labelCurrent: t.seoAnalyzer.labelCurrent,
|
|
7883
|
+
labelSuggested: t.seoAnalyzer.labelSuggested,
|
|
7884
|
+
emptyValue: t.seoAnalyzer.emptyValue,
|
|
7885
|
+
C: C2
|
|
7886
|
+
}
|
|
7887
|
+
),
|
|
7888
|
+
aiOptimizeResult.suggestions.focusKeyword && aiOptimizeResult.suggestions.focusKeyword !== aiOptimizeResult.current.focusKeyword && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7889
|
+
AiDiffRow,
|
|
7890
|
+
{
|
|
7891
|
+
label: t.seoAnalyzer.labelFocusKeyword,
|
|
7892
|
+
current: aiOptimizeResult.current.focusKeyword,
|
|
7893
|
+
suggested: aiOptimizeResult.suggestions.focusKeyword,
|
|
7894
|
+
labelCurrent: t.seoAnalyzer.labelCurrent,
|
|
7895
|
+
labelSuggested: t.seoAnalyzer.labelSuggested,
|
|
7896
|
+
emptyValue: t.seoAnalyzer.emptyValue,
|
|
7897
|
+
C: C2
|
|
7898
|
+
}
|
|
7899
|
+
),
|
|
7900
|
+
aiOptimizeResult.suggestions.rationale.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 4, marginBottom: 10 }, children: [
|
|
7901
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 10, fontWeight: 700, color: C2.textSecondary, textTransform: "uppercase", marginBottom: 4 }, children: t.seoAnalyzer.whyChanges }),
|
|
7902
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { style: { margin: 0, paddingLeft: 16, fontSize: 11, color: C2.textPrimary, lineHeight: 1.5 }, children: aiOptimizeResult.suggestions.rationale.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: r }, i)) })
|
|
7903
|
+
] }),
|
|
7904
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7905
|
+
"button",
|
|
7906
|
+
{
|
|
7907
|
+
type: "button",
|
|
7908
|
+
disabled: aiOptimizeApplied,
|
|
7909
|
+
onClick: () => {
|
|
7910
|
+
const sug = aiOptimizeResult.suggestions;
|
|
7911
|
+
if (sug.metaTitle) dispatchFields({ type: "UPDATE", path: "meta.title", value: sug.metaTitle });
|
|
7912
|
+
if (sug.metaDescription) dispatchFields({ type: "UPDATE", path: "meta.description", value: sug.metaDescription });
|
|
7913
|
+
if (sug.focusKeyword && sug.focusKeyword !== aiOptimizeResult.current.focusKeyword) {
|
|
7914
|
+
dispatchFields({ type: "UPDATE", path: "focusKeyword", value: sug.focusKeyword });
|
|
7915
|
+
}
|
|
7916
|
+
setAiOptimizeApplied(true);
|
|
7917
|
+
},
|
|
7918
|
+
style: {
|
|
7919
|
+
width: "100%",
|
|
7920
|
+
padding: "9px 14px",
|
|
7921
|
+
borderRadius: 6,
|
|
7922
|
+
border: `2px solid ${C2.border}`,
|
|
7923
|
+
backgroundColor: aiOptimizeApplied ? C2.green : C2.cyan,
|
|
7924
|
+
color: aiOptimizeApplied ? C2.white : C2.black,
|
|
7925
|
+
fontWeight: 800,
|
|
7926
|
+
fontSize: 11,
|
|
7927
|
+
textTransform: "uppercase",
|
|
7928
|
+
letterSpacing: "0.04em",
|
|
7929
|
+
cursor: aiOptimizeApplied ? "default" : "pointer"
|
|
7930
|
+
},
|
|
7931
|
+
children: aiOptimizeApplied ? `\u2713 ${t.seoAnalyzer.applied}` : t.seoAnalyzer.applyAll
|
|
7932
|
+
}
|
|
7933
|
+
),
|
|
7934
|
+
aiOptimizeApplied && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 6, fontSize: 10, color: C2.textSecondary, textAlign: "center" }, children: t.seoAnalyzer.applySaveHint })
|
|
7935
|
+
]
|
|
7936
|
+
}
|
|
7937
|
+
)
|
|
7938
|
+
] }),
|
|
7728
7939
|
suggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7729
7940
|
"div",
|
|
7730
7941
|
{
|
|
@@ -9064,6 +9275,7 @@ function SeoView() {
|
|
|
9064
9275
|
const [items, setItems] = React4.useState([]);
|
|
9065
9276
|
const [stats, setStats] = React4.useState(null);
|
|
9066
9277
|
const [loading, setLoading] = React4.useState(true);
|
|
9278
|
+
const [building, setBuilding] = React4.useState(false);
|
|
9067
9279
|
const [error, setError] = React4.useState(null);
|
|
9068
9280
|
const [filter, setFilter] = React4.useState("all");
|
|
9069
9281
|
const [scoreFilter, setScoreFilter] = React4.useState("all");
|
|
@@ -9080,9 +9292,22 @@ function SeoView() {
|
|
|
9080
9292
|
const fetchAudit = React4.useCallback(async (forceRefresh = false) => {
|
|
9081
9293
|
setLoading(true);
|
|
9082
9294
|
setError(null);
|
|
9295
|
+
setBuilding(false);
|
|
9083
9296
|
try {
|
|
9084
|
-
const
|
|
9085
|
-
|
|
9297
|
+
const base = "/api/seo-plugin/audit";
|
|
9298
|
+
let res = await fetch(forceRefresh ? `${base}?nocache=1` : base, {
|
|
9299
|
+
credentials: "include",
|
|
9300
|
+
cache: "no-store"
|
|
9301
|
+
});
|
|
9302
|
+
let attempts = 0;
|
|
9303
|
+
const MAX_POLLS = 90;
|
|
9304
|
+
while (res.status === 202 && attempts < MAX_POLLS) {
|
|
9305
|
+
setBuilding(true);
|
|
9306
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
9307
|
+
res = await fetch(base, { credentials: "include", cache: "no-store" });
|
|
9308
|
+
attempts++;
|
|
9309
|
+
}
|
|
9310
|
+
if (res.status === 202) throw new Error(t.seoView.buildTimeout);
|
|
9086
9311
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
9087
9312
|
const data = await res.json();
|
|
9088
9313
|
setItems(data.results || []);
|
|
@@ -9090,6 +9315,7 @@ function SeoView() {
|
|
|
9090
9315
|
} catch (e) {
|
|
9091
9316
|
setError(e instanceof Error ? e.message : t.common.loadingError);
|
|
9092
9317
|
}
|
|
9318
|
+
setBuilding(false);
|
|
9093
9319
|
setLoading(false);
|
|
9094
9320
|
}, [t]);
|
|
9095
9321
|
React4.useEffect(() => {
|
|
@@ -9531,7 +9757,7 @@ function SeoView() {
|
|
|
9531
9757
|
fontSize: 14,
|
|
9532
9758
|
fontFamily: "var(--font-body, system-ui)"
|
|
9533
9759
|
},
|
|
9534
|
-
children: t.seoView.loadingAudit
|
|
9760
|
+
children: building ? t.seoView.buildingAudit : t.seoView.loadingAudit
|
|
9535
9761
|
}
|
|
9536
9762
|
);
|
|
9537
9763
|
}
|