@consilioweb/payload-seo-analyzer 1.15.0 → 1.16.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 +2 -0
- package/dist/index.cjs +120 -5
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +120 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -634,6 +634,8 @@ All endpoints are prefixed with the configured `endpointBasePath` (default: `/se
|
|
|
634
634
|
| `POST` `GET` | `/rank-snapshot` `/rank-history` | Rank tracking (opt-in `features.gscApi`) — daily GSC position snapshots + movement over time |
|
|
635
635
|
| `GET` `POST` | `/alerts-digest` `/alerts-run` | Monitoring digest (opt-in `features.alerts`) — score regressions, new 404s, ranking drops via webhook/email |
|
|
636
636
|
| `GET` | `/ctr-opportunities` | CTR opportunities (opt-in `features.gscApi`) — well-ranked pages with low CTR → targeted meta rewrite, one-click AI |
|
|
637
|
+
| `GET` | `/health` | Module health (admin) — integration/job status + actionable warnings |
|
|
638
|
+
| `GET` `POST` | `/indexnow-key.txt` `/indexnow-submit` | IndexNow (opt-in `features.indexNow`) — proactive indexing on publish + manual batch submit |
|
|
637
639
|
| `GET` | `/history` | Score history data for trend charts |
|
|
638
640
|
| `GET` | `/sitemap-audit` | Sitemap structure audit |
|
|
639
641
|
| `GET` | `/sitemap.xml` `/sitemap-news.xml` `/sitemap-images.xml` `/sitemap-video.xml` | XML sitemaps (public) — standard + Google News (48h) + image + video |
|
package/dist/index.cjs
CHANGED
|
@@ -7544,6 +7544,7 @@ function createSeoHealthHandler(basePath, seoConfig) {
|
|
|
7544
7544
|
gscEncryptionKey: !!process.env.SEO_GSC_ENCRYPTION_KEY,
|
|
7545
7545
|
alertWebhook: !!process.env.SEO_ALERT_WEBHOOK_URL,
|
|
7546
7546
|
alertEmail: !!process.env.SEO_ALERT_EMAIL,
|
|
7547
|
+
indexNowKey: !!process.env.SEO_INDEXNOW_KEY,
|
|
7547
7548
|
siteUrl: seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || null
|
|
7548
7549
|
};
|
|
7549
7550
|
const cacheStats = seoCache.stats();
|
|
@@ -7596,6 +7597,102 @@ function createSeoHealthHandler(basePath, seoConfig) {
|
|
|
7596
7597
|
};
|
|
7597
7598
|
}
|
|
7598
7599
|
|
|
7600
|
+
// src/endpoints/indexNow.ts
|
|
7601
|
+
function isAdmin9(user) {
|
|
7602
|
+
if (!user) return false;
|
|
7603
|
+
if (user.role === "admin") return true;
|
|
7604
|
+
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
7605
|
+
return false;
|
|
7606
|
+
}
|
|
7607
|
+
function docToUrl(slug, siteUrl) {
|
|
7608
|
+
const base = siteUrl.replace(/\/$/, "");
|
|
7609
|
+
if (!slug || slug === "home") return base;
|
|
7610
|
+
return `${base}/${slug}`;
|
|
7611
|
+
}
|
|
7612
|
+
async function submitToIndexNow(siteUrl, key, keyLocation, urls) {
|
|
7613
|
+
if (!key || urls.length === 0) return { ok: false, reason: "no_key_or_urls" };
|
|
7614
|
+
let host;
|
|
7615
|
+
try {
|
|
7616
|
+
host = new URL(siteUrl).host;
|
|
7617
|
+
} catch {
|
|
7618
|
+
return { ok: false, reason: "bad_site_url" };
|
|
7619
|
+
}
|
|
7620
|
+
try {
|
|
7621
|
+
const resp = await fetch("https://api.indexnow.org/indexnow", {
|
|
7622
|
+
method: "POST",
|
|
7623
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
7624
|
+
body: JSON.stringify({ host, key, keyLocation, urlList: urls.slice(0, 1e4) })
|
|
7625
|
+
});
|
|
7626
|
+
return { ok: resp.ok, status: resp.status };
|
|
7627
|
+
} catch (e) {
|
|
7628
|
+
return { ok: false, reason: e instanceof Error ? e.message : "fetch_error" };
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
function keyLocationFor(siteUrl, basePath) {
|
|
7632
|
+
return `${siteUrl.replace(/\/$/, "")}/api${basePath}/indexnow-key.txt`;
|
|
7633
|
+
}
|
|
7634
|
+
function createIndexNowKeyHandler() {
|
|
7635
|
+
return async () => {
|
|
7636
|
+
const key = process.env.SEO_INDEXNOW_KEY || "";
|
|
7637
|
+
if (!key) return new Response("IndexNow key not configured", { status: 404 });
|
|
7638
|
+
return new Response(key, {
|
|
7639
|
+
status: 200,
|
|
7640
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=86400" }
|
|
7641
|
+
});
|
|
7642
|
+
};
|
|
7643
|
+
}
|
|
7644
|
+
function createIndexNowSubmitHandler(basePath, targetCollections, seoConfig) {
|
|
7645
|
+
return async (req) => {
|
|
7646
|
+
try {
|
|
7647
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7648
|
+
const key = process.env.SEO_INDEXNOW_KEY;
|
|
7649
|
+
const siteUrl = resolveGscSiteUrl(seoConfig);
|
|
7650
|
+
if (!key) return Response.json({ error: "SEO_INDEXNOW_KEY not configured." }, { status: 400 });
|
|
7651
|
+
if (!siteUrl) return Response.json({ error: "siteUrl not configured." }, { status: 400 });
|
|
7652
|
+
const urls = [];
|
|
7653
|
+
for (const collection of targetCollections) {
|
|
7654
|
+
try {
|
|
7655
|
+
const res = await req.payload.find({ collection, limit: 1e3, depth: 0, overrideAccess: true });
|
|
7656
|
+
for (const d of res.docs) {
|
|
7657
|
+
if (d._status === "draft") continue;
|
|
7658
|
+
urls.push(docToUrl(d.slug || "", siteUrl));
|
|
7659
|
+
}
|
|
7660
|
+
} catch {
|
|
7661
|
+
}
|
|
7662
|
+
}
|
|
7663
|
+
const result = await submitToIndexNow(siteUrl, key, keyLocationFor(siteUrl, basePath), urls);
|
|
7664
|
+
return Response.json(
|
|
7665
|
+
{ submitted: urls.length, ...result },
|
|
7666
|
+
{ headers: { "Cache-Control": "no-store" } }
|
|
7667
|
+
);
|
|
7668
|
+
} catch (error) {
|
|
7669
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
7670
|
+
req.payload.logger.error(`[seo] indexnow-submit error: ${message}`);
|
|
7671
|
+
return Response.json({ error: message }, { status: 500 });
|
|
7672
|
+
}
|
|
7673
|
+
};
|
|
7674
|
+
}
|
|
7675
|
+
function createIndexNowHook(basePath, seoConfig) {
|
|
7676
|
+
return ({ doc, req }) => {
|
|
7677
|
+
try {
|
|
7678
|
+
const key = process.env.SEO_INDEXNOW_KEY;
|
|
7679
|
+
const siteUrl = resolveGscSiteUrl(seoConfig);
|
|
7680
|
+
if (!key || !siteUrl) return doc;
|
|
7681
|
+
const status = doc?._status;
|
|
7682
|
+
if (status && status !== "published") return doc;
|
|
7683
|
+
const slug = doc?.slug || "";
|
|
7684
|
+
const url = docToUrl(slug, siteUrl);
|
|
7685
|
+
void submitToIndexNow(siteUrl, key, keyLocationFor(siteUrl, basePath), [url]).then((r) => {
|
|
7686
|
+
if (!r.ok && r.reason !== "no_key_or_urls") {
|
|
7687
|
+
req?.payload?.logger?.warn(`[seo] IndexNow submit failed (${r.status || r.reason})`);
|
|
7688
|
+
}
|
|
7689
|
+
});
|
|
7690
|
+
} catch {
|
|
7691
|
+
}
|
|
7692
|
+
return doc;
|
|
7693
|
+
};
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7599
7696
|
// src/rateLimiter.ts
|
|
7600
7697
|
function createRateLimiter(maxRequests, windowMs) {
|
|
7601
7698
|
const store = /* @__PURE__ */ new Map();
|
|
@@ -7643,7 +7740,7 @@ function getClientIp(req) {
|
|
|
7643
7740
|
|
|
7644
7741
|
// src/endpoints/seoLogs.ts
|
|
7645
7742
|
var VALID_LOG_TYPES = ["404", "redirect", "error"];
|
|
7646
|
-
function
|
|
7743
|
+
function isAdmin10(user) {
|
|
7647
7744
|
if (!user) return false;
|
|
7648
7745
|
if (user.role === "admin") return true;
|
|
7649
7746
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7740,7 +7837,7 @@ function createSeoLogsHandler(seoLogsSecret) {
|
|
|
7740
7837
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
7741
7838
|
}
|
|
7742
7839
|
if (method === "DELETE") {
|
|
7743
|
-
if (!
|
|
7840
|
+
if (!isAdmin10(req.user)) {
|
|
7744
7841
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
7745
7842
|
}
|
|
7746
7843
|
try {
|
|
@@ -7982,7 +8079,7 @@ function stopRankTracker() {
|
|
|
7982
8079
|
}
|
|
7983
8080
|
|
|
7984
8081
|
// src/endpoints/alerts.ts
|
|
7985
|
-
function
|
|
8082
|
+
function isAdmin11(user) {
|
|
7986
8083
|
if (!user) return false;
|
|
7987
8084
|
if (user.role === "admin") return true;
|
|
7988
8085
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -8148,7 +8245,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
|
|
|
8148
8245
|
function createAlertsDigestHandler() {
|
|
8149
8246
|
return async (req) => {
|
|
8150
8247
|
try {
|
|
8151
|
-
if (!
|
|
8248
|
+
if (!isAdmin11(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
8152
8249
|
const cfg = getAlertConfig();
|
|
8153
8250
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
8154
8251
|
return Response.json(
|
|
@@ -8174,7 +8271,7 @@ function createAlertsDigestHandler() {
|
|
|
8174
8271
|
function createAlertsRunHandler(siteUrl) {
|
|
8175
8272
|
return async (req) => {
|
|
8176
8273
|
try {
|
|
8177
|
-
if (!
|
|
8274
|
+
if (!isAdmin11(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
8178
8275
|
const cfg = getAlertConfig();
|
|
8179
8276
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
8180
8277
|
const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
|
|
@@ -10436,6 +10533,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10436
10533
|
// disable on low-memory hosts to skip startup pre-loading
|
|
10437
10534
|
alerts: false,
|
|
10438
10535
|
// opt-in — requires SEO_ALERT_WEBHOOK_URL and/or SEO_ALERT_EMAIL
|
|
10536
|
+
indexNow: false,
|
|
10537
|
+
// opt-in — requires SEO_INDEXNOW_KEY
|
|
10439
10538
|
...pluginConfig.features
|
|
10440
10539
|
};
|
|
10441
10540
|
function hasExistingSeoMeta(fields) {
|
|
@@ -10536,6 +10635,16 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10536
10635
|
]
|
|
10537
10636
|
};
|
|
10538
10637
|
}
|
|
10638
|
+
if (features.indexNow) {
|
|
10639
|
+
const existingHooks = updated.hooks?.afterChange || [];
|
|
10640
|
+
updated.hooks = {
|
|
10641
|
+
...updated.hooks,
|
|
10642
|
+
afterChange: [
|
|
10643
|
+
...Array.isArray(existingHooks) ? existingHooks : [existingHooks],
|
|
10644
|
+
createIndexNowHook(basePath, seoConfig)
|
|
10645
|
+
]
|
|
10646
|
+
};
|
|
10647
|
+
}
|
|
10539
10648
|
return updated;
|
|
10540
10649
|
}
|
|
10541
10650
|
return collection;
|
|
@@ -10736,6 +10845,12 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10736
10845
|
{ path: `${basePath}/alerts-run`, method: "post", handler: withRateLimit(createAlertsRunHandler(resolveGscSiteUrl(seoConfig))) }
|
|
10737
10846
|
);
|
|
10738
10847
|
}
|
|
10848
|
+
if (features.indexNow) {
|
|
10849
|
+
pluginEndpoints.push(
|
|
10850
|
+
{ path: `${basePath}/indexnow-key.txt`, method: "get", handler: createIndexNowKeyHandler() },
|
|
10851
|
+
{ path: `${basePath}/indexnow-submit`, method: "post", handler: withRateLimit(createIndexNowSubmitHandler(basePath, targetCollections, seoConfig)) }
|
|
10852
|
+
);
|
|
10853
|
+
}
|
|
10739
10854
|
if (features.keywords) {
|
|
10740
10855
|
pluginEndpoints.push({
|
|
10741
10856
|
path: `${basePath}/keyword-research`,
|
package/dist/index.d.cts
CHANGED
|
@@ -184,6 +184,12 @@ interface SeoFeatures {
|
|
|
184
184
|
* SEO_ALERT_POSITION_DROP, SEO_ALERT_INTERVAL_HOURS). Email uses Payload's email adapter.
|
|
185
185
|
*/
|
|
186
186
|
alerts?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* IndexNow proactive indexing (opt-in, default false). Pings Bing/Yandex/etc. when content is
|
|
189
|
+
* published. Requires `SEO_INDEXNOW_KEY` (a key you generate once) — the plugin serves the
|
|
190
|
+
* verification file at `<basePath>/indexnow-key.txt`.
|
|
191
|
+
*/
|
|
192
|
+
indexNow?: boolean;
|
|
187
193
|
}
|
|
188
194
|
/** Pre-computed context shared across all rule modules to avoid redundant work */
|
|
189
195
|
interface AnalysisContext {
|
package/dist/index.d.ts
CHANGED
|
@@ -184,6 +184,12 @@ interface SeoFeatures {
|
|
|
184
184
|
* SEO_ALERT_POSITION_DROP, SEO_ALERT_INTERVAL_HOURS). Email uses Payload's email adapter.
|
|
185
185
|
*/
|
|
186
186
|
alerts?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* IndexNow proactive indexing (opt-in, default false). Pings Bing/Yandex/etc. when content is
|
|
189
|
+
* published. Requires `SEO_INDEXNOW_KEY` (a key you generate once) — the plugin serves the
|
|
190
|
+
* verification file at `<basePath>/indexnow-key.txt`.
|
|
191
|
+
*/
|
|
192
|
+
indexNow?: boolean;
|
|
187
193
|
}
|
|
188
194
|
/** Pre-computed context shared across all rule modules to avoid redundant work */
|
|
189
195
|
interface AnalysisContext {
|
package/dist/index.js
CHANGED
|
@@ -7542,6 +7542,7 @@ function createSeoHealthHandler(basePath, seoConfig) {
|
|
|
7542
7542
|
gscEncryptionKey: !!process.env.SEO_GSC_ENCRYPTION_KEY,
|
|
7543
7543
|
alertWebhook: !!process.env.SEO_ALERT_WEBHOOK_URL,
|
|
7544
7544
|
alertEmail: !!process.env.SEO_ALERT_EMAIL,
|
|
7545
|
+
indexNowKey: !!process.env.SEO_INDEXNOW_KEY,
|
|
7545
7546
|
siteUrl: seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || null
|
|
7546
7547
|
};
|
|
7547
7548
|
const cacheStats = seoCache.stats();
|
|
@@ -7594,6 +7595,102 @@ function createSeoHealthHandler(basePath, seoConfig) {
|
|
|
7594
7595
|
};
|
|
7595
7596
|
}
|
|
7596
7597
|
|
|
7598
|
+
// src/endpoints/indexNow.ts
|
|
7599
|
+
function isAdmin9(user) {
|
|
7600
|
+
if (!user) return false;
|
|
7601
|
+
if (user.role === "admin") return true;
|
|
7602
|
+
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
7603
|
+
return false;
|
|
7604
|
+
}
|
|
7605
|
+
function docToUrl(slug, siteUrl) {
|
|
7606
|
+
const base = siteUrl.replace(/\/$/, "");
|
|
7607
|
+
if (!slug || slug === "home") return base;
|
|
7608
|
+
return `${base}/${slug}`;
|
|
7609
|
+
}
|
|
7610
|
+
async function submitToIndexNow(siteUrl, key, keyLocation, urls) {
|
|
7611
|
+
if (!key || urls.length === 0) return { ok: false, reason: "no_key_or_urls" };
|
|
7612
|
+
let host;
|
|
7613
|
+
try {
|
|
7614
|
+
host = new URL(siteUrl).host;
|
|
7615
|
+
} catch {
|
|
7616
|
+
return { ok: false, reason: "bad_site_url" };
|
|
7617
|
+
}
|
|
7618
|
+
try {
|
|
7619
|
+
const resp = await fetch("https://api.indexnow.org/indexnow", {
|
|
7620
|
+
method: "POST",
|
|
7621
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
7622
|
+
body: JSON.stringify({ host, key, keyLocation, urlList: urls.slice(0, 1e4) })
|
|
7623
|
+
});
|
|
7624
|
+
return { ok: resp.ok, status: resp.status };
|
|
7625
|
+
} catch (e) {
|
|
7626
|
+
return { ok: false, reason: e instanceof Error ? e.message : "fetch_error" };
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
function keyLocationFor(siteUrl, basePath) {
|
|
7630
|
+
return `${siteUrl.replace(/\/$/, "")}/api${basePath}/indexnow-key.txt`;
|
|
7631
|
+
}
|
|
7632
|
+
function createIndexNowKeyHandler() {
|
|
7633
|
+
return async () => {
|
|
7634
|
+
const key = process.env.SEO_INDEXNOW_KEY || "";
|
|
7635
|
+
if (!key) return new Response("IndexNow key not configured", { status: 404 });
|
|
7636
|
+
return new Response(key, {
|
|
7637
|
+
status: 200,
|
|
7638
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=86400" }
|
|
7639
|
+
});
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7642
|
+
function createIndexNowSubmitHandler(basePath, targetCollections, seoConfig) {
|
|
7643
|
+
return async (req) => {
|
|
7644
|
+
try {
|
|
7645
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7646
|
+
const key = process.env.SEO_INDEXNOW_KEY;
|
|
7647
|
+
const siteUrl = resolveGscSiteUrl(seoConfig);
|
|
7648
|
+
if (!key) return Response.json({ error: "SEO_INDEXNOW_KEY not configured." }, { status: 400 });
|
|
7649
|
+
if (!siteUrl) return Response.json({ error: "siteUrl not configured." }, { status: 400 });
|
|
7650
|
+
const urls = [];
|
|
7651
|
+
for (const collection of targetCollections) {
|
|
7652
|
+
try {
|
|
7653
|
+
const res = await req.payload.find({ collection, limit: 1e3, depth: 0, overrideAccess: true });
|
|
7654
|
+
for (const d of res.docs) {
|
|
7655
|
+
if (d._status === "draft") continue;
|
|
7656
|
+
urls.push(docToUrl(d.slug || "", siteUrl));
|
|
7657
|
+
}
|
|
7658
|
+
} catch {
|
|
7659
|
+
}
|
|
7660
|
+
}
|
|
7661
|
+
const result = await submitToIndexNow(siteUrl, key, keyLocationFor(siteUrl, basePath), urls);
|
|
7662
|
+
return Response.json(
|
|
7663
|
+
{ submitted: urls.length, ...result },
|
|
7664
|
+
{ headers: { "Cache-Control": "no-store" } }
|
|
7665
|
+
);
|
|
7666
|
+
} catch (error) {
|
|
7667
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
7668
|
+
req.payload.logger.error(`[seo] indexnow-submit error: ${message}`);
|
|
7669
|
+
return Response.json({ error: message }, { status: 500 });
|
|
7670
|
+
}
|
|
7671
|
+
};
|
|
7672
|
+
}
|
|
7673
|
+
function createIndexNowHook(basePath, seoConfig) {
|
|
7674
|
+
return ({ doc, req }) => {
|
|
7675
|
+
try {
|
|
7676
|
+
const key = process.env.SEO_INDEXNOW_KEY;
|
|
7677
|
+
const siteUrl = resolveGscSiteUrl(seoConfig);
|
|
7678
|
+
if (!key || !siteUrl) return doc;
|
|
7679
|
+
const status = doc?._status;
|
|
7680
|
+
if (status && status !== "published") return doc;
|
|
7681
|
+
const slug = doc?.slug || "";
|
|
7682
|
+
const url = docToUrl(slug, siteUrl);
|
|
7683
|
+
void submitToIndexNow(siteUrl, key, keyLocationFor(siteUrl, basePath), [url]).then((r) => {
|
|
7684
|
+
if (!r.ok && r.reason !== "no_key_or_urls") {
|
|
7685
|
+
req?.payload?.logger?.warn(`[seo] IndexNow submit failed (${r.status || r.reason})`);
|
|
7686
|
+
}
|
|
7687
|
+
});
|
|
7688
|
+
} catch {
|
|
7689
|
+
}
|
|
7690
|
+
return doc;
|
|
7691
|
+
};
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7597
7694
|
// src/rateLimiter.ts
|
|
7598
7695
|
function createRateLimiter(maxRequests, windowMs) {
|
|
7599
7696
|
const store = /* @__PURE__ */ new Map();
|
|
@@ -7641,7 +7738,7 @@ function getClientIp(req) {
|
|
|
7641
7738
|
|
|
7642
7739
|
// src/endpoints/seoLogs.ts
|
|
7643
7740
|
var VALID_LOG_TYPES = ["404", "redirect", "error"];
|
|
7644
|
-
function
|
|
7741
|
+
function isAdmin10(user) {
|
|
7645
7742
|
if (!user) return false;
|
|
7646
7743
|
if (user.role === "admin") return true;
|
|
7647
7744
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7738,7 +7835,7 @@ function createSeoLogsHandler(seoLogsSecret) {
|
|
|
7738
7835
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
7739
7836
|
}
|
|
7740
7837
|
if (method === "DELETE") {
|
|
7741
|
-
if (!
|
|
7838
|
+
if (!isAdmin10(req.user)) {
|
|
7742
7839
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
7743
7840
|
}
|
|
7744
7841
|
try {
|
|
@@ -7980,7 +8077,7 @@ function stopRankTracker() {
|
|
|
7980
8077
|
}
|
|
7981
8078
|
|
|
7982
8079
|
// src/endpoints/alerts.ts
|
|
7983
|
-
function
|
|
8080
|
+
function isAdmin11(user) {
|
|
7984
8081
|
if (!user) return false;
|
|
7985
8082
|
if (user.role === "admin") return true;
|
|
7986
8083
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -8146,7 +8243,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
|
|
|
8146
8243
|
function createAlertsDigestHandler() {
|
|
8147
8244
|
return async (req) => {
|
|
8148
8245
|
try {
|
|
8149
|
-
if (!
|
|
8246
|
+
if (!isAdmin11(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
8150
8247
|
const cfg = getAlertConfig();
|
|
8151
8248
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
8152
8249
|
return Response.json(
|
|
@@ -8172,7 +8269,7 @@ function createAlertsDigestHandler() {
|
|
|
8172
8269
|
function createAlertsRunHandler(siteUrl) {
|
|
8173
8270
|
return async (req) => {
|
|
8174
8271
|
try {
|
|
8175
|
-
if (!
|
|
8272
|
+
if (!isAdmin11(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
8176
8273
|
const cfg = getAlertConfig();
|
|
8177
8274
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
8178
8275
|
const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
|
|
@@ -10434,6 +10531,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10434
10531
|
// disable on low-memory hosts to skip startup pre-loading
|
|
10435
10532
|
alerts: false,
|
|
10436
10533
|
// opt-in — requires SEO_ALERT_WEBHOOK_URL and/or SEO_ALERT_EMAIL
|
|
10534
|
+
indexNow: false,
|
|
10535
|
+
// opt-in — requires SEO_INDEXNOW_KEY
|
|
10437
10536
|
...pluginConfig.features
|
|
10438
10537
|
};
|
|
10439
10538
|
function hasExistingSeoMeta(fields) {
|
|
@@ -10534,6 +10633,16 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10534
10633
|
]
|
|
10535
10634
|
};
|
|
10536
10635
|
}
|
|
10636
|
+
if (features.indexNow) {
|
|
10637
|
+
const existingHooks = updated.hooks?.afterChange || [];
|
|
10638
|
+
updated.hooks = {
|
|
10639
|
+
...updated.hooks,
|
|
10640
|
+
afterChange: [
|
|
10641
|
+
...Array.isArray(existingHooks) ? existingHooks : [existingHooks],
|
|
10642
|
+
createIndexNowHook(basePath, seoConfig)
|
|
10643
|
+
]
|
|
10644
|
+
};
|
|
10645
|
+
}
|
|
10537
10646
|
return updated;
|
|
10538
10647
|
}
|
|
10539
10648
|
return collection;
|
|
@@ -10734,6 +10843,12 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10734
10843
|
{ path: `${basePath}/alerts-run`, method: "post", handler: withRateLimit(createAlertsRunHandler(resolveGscSiteUrl(seoConfig))) }
|
|
10735
10844
|
);
|
|
10736
10845
|
}
|
|
10846
|
+
if (features.indexNow) {
|
|
10847
|
+
pluginEndpoints.push(
|
|
10848
|
+
{ path: `${basePath}/indexnow-key.txt`, method: "get", handler: createIndexNowKeyHandler() },
|
|
10849
|
+
{ path: `${basePath}/indexnow-submit`, method: "post", handler: withRateLimit(createIndexNowSubmitHandler(basePath, targetCollections, seoConfig)) }
|
|
10850
|
+
);
|
|
10851
|
+
}
|
|
10737
10852
|
if (features.keywords) {
|
|
10738
10853
|
pluginEndpoints.push({
|
|
10739
10854
|
path: `${basePath}/keyword-research`,
|
package/package.json
CHANGED