@orion-studios/payload-seo-audit 1.0.0 → 1.1.1
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 +48 -45
- package/dist/api/cron.d.ts.map +1 -1
- package/dist/api/cron.js +3 -23
- package/dist/api/run-stream.d.ts.map +1 -1
- package/dist/api/run-stream.js +19 -215
- package/dist/api/run.d.ts.map +1 -1
- package/dist/api/run.js +4 -25
- package/dist/collections/SeoDashboardView.d.ts +3 -0
- package/dist/collections/SeoDashboardView.d.ts.map +1 -0
- package/dist/collections/SeoDashboardView.js +30 -0
- package/dist/collections/SeoSnapshots.d.ts.map +1 -1
- package/dist/collections/SeoSnapshots.js +26 -0
- package/dist/components/layout/SeoReportShell.js +2 -2
- package/dist/components/types.d.ts +10 -0
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/views/SeoDashboard.d.ts.map +1 -1
- package/dist/components/views/SeoDashboard.js +68 -53
- package/dist/components/views/SeoPageReport.d.ts.map +1 -1
- package/dist/components/views/SeoPageReport.js +21 -10
- package/dist/components/views/SeoSnapshotReport.d.ts.map +1 -1
- package/dist/components/views/SeoSnapshotReport.js +21 -10
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/globals/SeoDashboard.js +1 -1
- package/dist/globals/SeoIntegrations.d.ts +3 -0
- package/dist/globals/SeoIntegrations.d.ts.map +1 -0
- package/dist/globals/SeoIntegrations.js +305 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/utilities/crux.d.ts +6 -0
- package/dist/utilities/crux.d.ts.map +1 -0
- package/dist/utilities/crux.js +244 -0
- package/dist/utilities/dataforseo.d.ts +12 -0
- package/dist/utilities/dataforseo.d.ts.map +1 -0
- package/dist/utilities/dataforseo.js +169 -0
- package/dist/utilities/gsc.d.ts +4 -11
- package/dist/utilities/gsc.d.ts.map +1 -1
- package/dist/utilities/gsc.js +58 -22
- package/dist/utilities/integrationSettings.d.ts +10 -0
- package/dist/utilities/integrationSettings.d.ts.map +1 -0
- package/dist/utilities/integrationSettings.js +198 -0
- package/dist/utilities/opsWebhook.d.ts +15 -0
- package/dist/utilities/opsWebhook.d.ts.map +1 -0
- package/dist/utilities/opsWebhook.js +130 -0
- package/dist/utilities/pagespeed.d.ts +1 -1
- package/dist/utilities/pagespeed.d.ts.map +1 -1
- package/dist/utilities/pagespeed.js +11 -5
- package/dist/utilities/providers.d.ts +2 -2
- package/dist/utilities/providers.d.ts.map +1 -1
- package/dist/utilities/providers.js +12 -7
- package/dist/utilities/runAudit.d.ts +4 -1
- package/dist/utilities/runAudit.d.ts.map +1 -1
- package/dist/utilities/runAudit.js +112 -11
- package/dist/utilities/secrets.d.ts +23 -0
- package/dist/utilities/secrets.d.ts.map +1 -0
- package/dist/utilities/secrets.js +108 -0
- package/dist/utilities/types.d.ts +85 -0
- package/dist/utilities/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldRunAuthoritySync = exports.syncDataForSEOAuthoritySnapshot = void 0;
|
|
4
|
+
const normalizeBaseURL = (value) => value.replace(/\/+$/, '') || 'https://api.dataforseo.com';
|
|
5
|
+
const resolveTarget = (site) => {
|
|
6
|
+
if (site.domain?.trim())
|
|
7
|
+
return site.domain.trim();
|
|
8
|
+
if (!site.canonicalHost?.trim())
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return new URL(site.canonicalHost).hostname;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return site.canonicalHost;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const mapLinkType = (value) => {
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return 'unknown';
|
|
20
|
+
const normalized = value.toLowerCase();
|
|
21
|
+
if (normalized.includes('nofollow'))
|
|
22
|
+
return 'nofollow';
|
|
23
|
+
if (normalized.includes('dofollow') || normalized.includes('follow'))
|
|
24
|
+
return 'follow';
|
|
25
|
+
return 'unknown';
|
|
26
|
+
};
|
|
27
|
+
const extractSourceDomain = (sourceURL) => {
|
|
28
|
+
if (!sourceURL)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
return new URL(sourceURL).hostname;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const postDataForSEO = async ({ baseURL, login, password, endpoint, payload, }) => {
|
|
38
|
+
const auth = Buffer.from(`${login}:${password}`).toString('base64');
|
|
39
|
+
const response = await fetch(`${baseURL}${endpoint}`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
cache: 'no-store',
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: `Basic ${auth}`,
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify([payload]),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`DataForSEO request failed with status ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
return (await response.json());
|
|
52
|
+
};
|
|
53
|
+
const syncDataForSEOAuthoritySnapshot = async ({ payload, site, settings, }) => {
|
|
54
|
+
if (!settings.authority.enabled || settings.authority.provider !== 'dataforseo') {
|
|
55
|
+
return {
|
|
56
|
+
enabled: false,
|
|
57
|
+
provider: settings.authority.provider,
|
|
58
|
+
imported: 0,
|
|
59
|
+
skipped: 'disabled',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const login = settings.authority.dataForSeoLogin.trim();
|
|
63
|
+
const password = settings.authority.dataForSeoPassword.trim();
|
|
64
|
+
const target = resolveTarget(site);
|
|
65
|
+
if (!login || !password || !target) {
|
|
66
|
+
return {
|
|
67
|
+
enabled: true,
|
|
68
|
+
provider: 'dataforseo',
|
|
69
|
+
imported: 0,
|
|
70
|
+
skipped: 'missing-config',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const baseURL = normalizeBaseURL(settings.authority.dataForSeoBaseURL.trim());
|
|
74
|
+
try {
|
|
75
|
+
const summaryResponse = await postDataForSEO({
|
|
76
|
+
baseURL,
|
|
77
|
+
login,
|
|
78
|
+
password,
|
|
79
|
+
endpoint: '/v3/backlinks/summary/live',
|
|
80
|
+
payload: {
|
|
81
|
+
target,
|
|
82
|
+
include_subdomains: true,
|
|
83
|
+
exclude_internal_backlinks: true,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const backlinksResponse = await postDataForSEO({
|
|
87
|
+
baseURL,
|
|
88
|
+
login,
|
|
89
|
+
password,
|
|
90
|
+
endpoint: '/v3/backlinks/backlinks/live',
|
|
91
|
+
payload: {
|
|
92
|
+
target,
|
|
93
|
+
include_subdomains: true,
|
|
94
|
+
exclude_internal_backlinks: true,
|
|
95
|
+
limit: settings.authority.captureTopBacklinksLimit,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const summaryTask = summaryResponse?.tasks?.[0];
|
|
99
|
+
const backlinksTask = backlinksResponse?.tasks?.[0];
|
|
100
|
+
const summaryResult = summaryTask?.result?.[0] || {};
|
|
101
|
+
const backlinkItems = backlinksTask?.result?.[0]?.items || [];
|
|
102
|
+
const backlinks = backlinkItems.map((item) => ({
|
|
103
|
+
sourceURL: item?.source_url || undefined,
|
|
104
|
+
targetURL: item?.target_url || undefined,
|
|
105
|
+
anchorText: item?.anchor || undefined,
|
|
106
|
+
firstSeen: item?.first_seen || undefined,
|
|
107
|
+
lastSeen: item?.last_visited || undefined,
|
|
108
|
+
linkType: mapLinkType(item?.link_attribute),
|
|
109
|
+
}));
|
|
110
|
+
const uniqueDomains = new Set(backlinks
|
|
111
|
+
.map((item) => extractSourceDomain(item.sourceURL))
|
|
112
|
+
.filter((value) => Boolean(value)));
|
|
113
|
+
const totalBacklinks = Math.max(Number(summaryResult?.total_count || 0), Number(summaryResult?.backlinks || 0), backlinks.length);
|
|
114
|
+
const referringDomains = Math.max(Number(summaryResult?.referring_domains || 0), uniqueDomains.size);
|
|
115
|
+
const domainAuthorityProxy = Math.max(1, Math.min(100, Number(summaryResult?.domain_rank || Math.round(Math.log10(referringDomains + 1) * 30))));
|
|
116
|
+
const summaryCost = Number(summaryTask?.cost || 0);
|
|
117
|
+
const backlinksCost = Number(backlinksTask?.cost || 0);
|
|
118
|
+
const estimatedCostUSD = Number((summaryCost + backlinksCost).toFixed(4));
|
|
119
|
+
const snapshot = await payload.create({
|
|
120
|
+
collection: 'seo-authority-snapshots',
|
|
121
|
+
data: {
|
|
122
|
+
label: `DataForSEO Snapshot ${new Date().toISOString()}`,
|
|
123
|
+
capturedAt: new Date().toISOString(),
|
|
124
|
+
source: 'provider',
|
|
125
|
+
totalBacklinks,
|
|
126
|
+
referringDomains,
|
|
127
|
+
domainAuthorityProxy,
|
|
128
|
+
backlinks,
|
|
129
|
+
providerMetadata: {
|
|
130
|
+
provider: 'dataforseo',
|
|
131
|
+
target,
|
|
132
|
+
summaryCost,
|
|
133
|
+
backlinksCost,
|
|
134
|
+
estimatedCostUSD,
|
|
135
|
+
summaryStatusCode: summaryTask?.status_code,
|
|
136
|
+
backlinksStatusCode: backlinksTask?.status_code,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
overrideAccess: true,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
enabled: true,
|
|
143
|
+
provider: 'dataforseo',
|
|
144
|
+
imported: backlinks.length,
|
|
145
|
+
snapshotID: snapshot.id,
|
|
146
|
+
estimatedCostUSD,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
enabled: true,
|
|
152
|
+
provider: 'dataforseo',
|
|
153
|
+
imported: 0,
|
|
154
|
+
skipped: 'request-failed',
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
exports.syncDataForSEOAuthoritySnapshot = syncDataForSEOAuthoritySnapshot;
|
|
160
|
+
const shouldRunAuthoritySync = ({ runType, settings, }) => {
|
|
161
|
+
if (!settings.authority.enabled || settings.authority.provider === 'none')
|
|
162
|
+
return false;
|
|
163
|
+
if (settings.authority.runPolicy === 'all')
|
|
164
|
+
return true;
|
|
165
|
+
if (settings.authority.runPolicy === 'scheduled-only')
|
|
166
|
+
return runType === 'scheduled';
|
|
167
|
+
return runType === 'manual' || runType === 'scheduled';
|
|
168
|
+
};
|
|
169
|
+
exports.shouldRunAuthoritySync = shouldRunAuthoritySync;
|
package/dist/utilities/gsc.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { Payload } from 'payload';
|
|
2
|
-
import type { SEOSiteRecord } from '../utilities/types';
|
|
3
|
-
export declare const syncGSCKeywordVisibility: ({ payload, site, }: {
|
|
2
|
+
import type { GSCSyncResult, IntegrationSettings, SEOSiteRecord } from '../utilities/types';
|
|
3
|
+
export declare const syncGSCKeywordVisibility: ({ payload, site, settings, }: {
|
|
4
4
|
payload: Payload;
|
|
5
5
|
site: SEOSiteRecord;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
enabled: boolean;
|
|
9
|
-
skipped?: undefined;
|
|
10
|
-
} | {
|
|
11
|
-
imported: number;
|
|
12
|
-
enabled: boolean;
|
|
13
|
-
skipped: string;
|
|
14
|
-
}>;
|
|
6
|
+
settings: IntegrationSettings;
|
|
7
|
+
}) => Promise<GSCSyncResult>;
|
|
15
8
|
//# sourceMappingURL=gsc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gsc.d.ts","sourceRoot":"","sources":["../../src/utilities/gsc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"gsc.d.ts","sourceRoot":"","sources":["../../src/utilities/gsc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAyC3F,eAAO,MAAM,wBAAwB,GAAU,8BAI5C;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,mBAAmB,CAAA;CAC9B,KAAG,OAAO,CAAC,aAAa,CA2ExB,CAAA"}
|
package/dist/utilities/gsc.js
CHANGED
|
@@ -2,37 +2,68 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.syncGSCKeywordVisibility = void 0;
|
|
4
4
|
const googleapis_1 = require("googleapis");
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
const toISODate = (value) => value.toISOString().slice(0, 10);
|
|
6
|
+
const resolveSiteURL = (site, settings) => {
|
|
7
|
+
const override = settings.gsc.siteUrlOverride.trim();
|
|
8
|
+
if (override)
|
|
9
|
+
return override;
|
|
10
|
+
return (site.canonicalHost || '').trim();
|
|
11
|
+
};
|
|
12
|
+
const createOAuthClient = (settings) => {
|
|
13
|
+
const clientID = settings.gsc.oauthClientID.trim();
|
|
14
|
+
const clientSecret = settings.gsc.oauthClientSecret.trim();
|
|
15
|
+
const redirectURI = settings.gsc.oauthRedirectURI.trim();
|
|
16
|
+
const refreshToken = settings.gsc.oauthRefreshToken.trim();
|
|
17
|
+
if (!clientID || !clientSecret || !redirectURI || !refreshToken) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const oauth2Client = new googleapis_1.google.auth.OAuth2(clientID, clientSecret, redirectURI);
|
|
21
|
+
oauth2Client.setCredentials({
|
|
22
|
+
refresh_token: refreshToken,
|
|
23
|
+
});
|
|
24
|
+
return oauth2Client;
|
|
25
|
+
};
|
|
26
|
+
const createServiceAccountClient = (settings) => {
|
|
27
|
+
const email = settings.gsc.serviceAccountEmail.trim();
|
|
28
|
+
const privateKey = settings.gsc.serviceAccountPrivateKey.trim();
|
|
29
|
+
if (!email || !privateKey)
|
|
30
|
+
return null;
|
|
31
|
+
return new googleapis_1.google.auth.JWT({
|
|
32
|
+
email,
|
|
33
|
+
key: privateKey,
|
|
34
|
+
scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const syncGSCKeywordVisibility = async ({ payload, site, settings, }) => {
|
|
38
|
+
if (!settings.gsc.enabled) {
|
|
39
|
+
return { imported: 0, enabled: false, skipped: 'disabled' };
|
|
40
|
+
}
|
|
41
|
+
const siteURL = resolveSiteURL(site, settings);
|
|
42
|
+
if (!siteURL) {
|
|
43
|
+
return { imported: 0, enabled: true, skipped: 'missing-site-url' };
|
|
14
44
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
45
|
+
const authClient = settings.gsc.authMode === 'service-account'
|
|
46
|
+
? createServiceAccountClient(settings)
|
|
47
|
+
: createOAuthClient(settings);
|
|
48
|
+
if (!authClient) {
|
|
49
|
+
return {
|
|
50
|
+
imported: 0,
|
|
51
|
+
enabled: true,
|
|
52
|
+
skipped: `missing-${settings.gsc.authMode}-credentials`,
|
|
53
|
+
};
|
|
18
54
|
}
|
|
19
55
|
try {
|
|
20
|
-
const
|
|
21
|
-
oauth2Client.setCredentials({
|
|
22
|
-
refresh_token: env.refreshToken,
|
|
23
|
-
});
|
|
24
|
-
const searchConsole = googleapis_1.google.searchconsole({ version: 'v1', auth: oauth2Client });
|
|
56
|
+
const searchConsole = googleapis_1.google.searchconsole({ version: 'v1', auth: authClient });
|
|
25
57
|
const endDate = new Date();
|
|
26
58
|
const startDate = new Date();
|
|
27
|
-
startDate.setDate(endDate.getDate() -
|
|
28
|
-
const toISODate = (value) => value.toISOString().slice(0, 10);
|
|
59
|
+
startDate.setDate(endDate.getDate() - settings.gsc.lookbackDays);
|
|
29
60
|
const response = await searchConsole.searchanalytics.query({
|
|
30
|
-
siteUrl:
|
|
61
|
+
siteUrl: siteURL,
|
|
31
62
|
requestBody: {
|
|
32
63
|
startDate: toISODate(startDate),
|
|
33
64
|
endDate: toISODate(endDate),
|
|
34
65
|
dimensions: ['query', 'page'],
|
|
35
|
-
rowLimit:
|
|
66
|
+
rowLimit: settings.gsc.rowLimit,
|
|
36
67
|
},
|
|
37
68
|
});
|
|
38
69
|
const rows = response.data.rows || [];
|
|
@@ -63,7 +94,12 @@ const syncGSCKeywordVisibility = async ({ payload, site, }) => {
|
|
|
63
94
|
}
|
|
64
95
|
catch (error) {
|
|
65
96
|
console.warn('GSC sync failed:', error);
|
|
66
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
imported: 0,
|
|
99
|
+
enabled: true,
|
|
100
|
+
skipped: 'request-failed',
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
};
|
|
67
103
|
}
|
|
68
104
|
};
|
|
69
105
|
exports.syncGSCKeywordVisibility = syncGSCKeywordVisibility;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { NormalizedSeoConfig } from '../config';
|
|
3
|
+
import type { IntegrationSettings, SEOSiteRecord } from '../utilities/types';
|
|
4
|
+
export declare const siteFromSeoConfig: (seoConfig: NormalizedSeoConfig) => SEOSiteRecord;
|
|
5
|
+
export declare const resolveIntegrationSettings: ({ payload, site, seoConfig, }: {
|
|
6
|
+
payload: Payload;
|
|
7
|
+
site: SEOSiteRecord;
|
|
8
|
+
seoConfig: NormalizedSeoConfig;
|
|
9
|
+
}) => Promise<IntegrationSettings>;
|
|
10
|
+
//# sourceMappingURL=integrationSettings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrationSettings.d.ts","sourceRoot":"","sources":["../../src/utilities/integrationSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,KAAK,EAMV,mBAAmB,EACnB,aAAa,EACd,MAAM,oBAAoB,CAAA;AA6C3B,eAAO,MAAM,iBAAiB,GAAI,WAAW,mBAAmB,KAAG,aAuBjE,CAAA;AA0DF,eAAO,MAAM,0BAA0B,GAAU,+BAI9C;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,aAAa,CAAA;IACnB,SAAS,EAAE,mBAAmB,CAAA;CAC/B,KAAG,OAAO,CAAC,mBAAmB,CA2F9B,CAAA"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveIntegrationSettings = exports.siteFromSeoConfig = void 0;
|
|
4
|
+
const secrets_1 = require("../utilities/secrets");
|
|
5
|
+
const defaultAuthorityPolicy = 'manual+scheduled';
|
|
6
|
+
const asBoolean = (value, fallback) => typeof value === 'boolean' ? value : fallback;
|
|
7
|
+
const asString = (value, fallback = '') => typeof value === 'string' ? value : fallback;
|
|
8
|
+
const asNumber = (value, fallback) => {
|
|
9
|
+
const numeric = Number(value);
|
|
10
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
11
|
+
};
|
|
12
|
+
const normalizeCountryCode = (value) => value.trim().toUpperCase().slice(0, 2);
|
|
13
|
+
const normalizeRunPolicy = (value) => {
|
|
14
|
+
if (value === 'scheduled-only' || value === 'all')
|
|
15
|
+
return value;
|
|
16
|
+
return defaultAuthorityPolicy;
|
|
17
|
+
};
|
|
18
|
+
const normalizeProvider = (value) => {
|
|
19
|
+
if (value === 'dataforseo')
|
|
20
|
+
return 'dataforseo';
|
|
21
|
+
return 'none';
|
|
22
|
+
};
|
|
23
|
+
const normalizeFormFactor = (value) => {
|
|
24
|
+
if (value === 'PHONE' || value === 'DESKTOP')
|
|
25
|
+
return value;
|
|
26
|
+
return 'ALL_FORM_FACTORS';
|
|
27
|
+
};
|
|
28
|
+
const normalizeScope = (value) => {
|
|
29
|
+
if (value === 'origin-only' || value === 'key-urls-only')
|
|
30
|
+
return value;
|
|
31
|
+
return 'origin+key-urls';
|
|
32
|
+
};
|
|
33
|
+
const normalizeAuthMode = (value) => {
|
|
34
|
+
if (value === 'service-account')
|
|
35
|
+
return 'service-account';
|
|
36
|
+
return 'oauth';
|
|
37
|
+
};
|
|
38
|
+
const normalizePrivateKey = (value) => value.replace(/\\n/g, '\n');
|
|
39
|
+
const siteFromSeoConfig = (seoConfig) => ({
|
|
40
|
+
id: 1,
|
|
41
|
+
name: seoConfig.site.name,
|
|
42
|
+
domain: seoConfig.site.domain,
|
|
43
|
+
canonicalHost: seoConfig.site.canonicalHost,
|
|
44
|
+
sitemapURL: seoConfig.site.sitemapURL,
|
|
45
|
+
keyURLs: seoConfig.site.keyURLs.map((url) => ({ url })),
|
|
46
|
+
crawlSettings: {
|
|
47
|
+
maxPages: seoConfig.crawl.maxPages,
|
|
48
|
+
maxDepth: seoConfig.crawl.maxDepth,
|
|
49
|
+
requestTimeoutMs: seoConfig.crawl.requestTimeoutMs,
|
|
50
|
+
includePatterns: seoConfig.crawl.includePatterns.map((pattern) => ({ pattern })),
|
|
51
|
+
excludePatterns: seoConfig.crawl.excludePatterns.map((pattern) => ({ pattern })),
|
|
52
|
+
},
|
|
53
|
+
integrations: {
|
|
54
|
+
enablePageSpeed: seoConfig.integrations.enablePageSpeed,
|
|
55
|
+
enableSearchConsole: seoConfig.integrations.enableSearchConsole,
|
|
56
|
+
enableCrUX: seoConfig.integrations.enableCrUX,
|
|
57
|
+
},
|
|
58
|
+
thresholds: {
|
|
59
|
+
targetLCPMs: seoConfig.thresholds.targetLCPMs,
|
|
60
|
+
targetCLS: seoConfig.thresholds.targetCLS,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
exports.siteFromSeoConfig = siteFromSeoConfig;
|
|
64
|
+
const readEnvDefaults = ({ seoConfig, site, }) => {
|
|
65
|
+
const canonicalHost = site.canonicalHost || seoConfig.site.canonicalHost;
|
|
66
|
+
return {
|
|
67
|
+
source: 'env',
|
|
68
|
+
gsc: {
|
|
69
|
+
enabled: seoConfig.integrations.enableSearchConsole,
|
|
70
|
+
authMode: 'oauth',
|
|
71
|
+
siteUrlOverride: canonicalHost,
|
|
72
|
+
rowLimit: 250,
|
|
73
|
+
lookbackDays: 7,
|
|
74
|
+
oauthClientID: process.env.SEO_GSC_CLIENT_ID || '',
|
|
75
|
+
oauthClientSecret: process.env.SEO_GSC_CLIENT_SECRET || '',
|
|
76
|
+
oauthRedirectURI: process.env.SEO_GSC_REDIRECT_URI || '',
|
|
77
|
+
oauthRefreshToken: process.env.SEO_GSC_REFRESH_TOKEN || '',
|
|
78
|
+
serviceAccountEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL || '',
|
|
79
|
+
serviceAccountPrivateKey: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY || '',
|
|
80
|
+
},
|
|
81
|
+
pageSpeed: {
|
|
82
|
+
enabled: seoConfig.integrations.enablePageSpeed,
|
|
83
|
+
apiKey: process.env.SEO_PAGESPEED_API_KEY || process.env.GOOGLE_PAGESPEED_API_KEY || '',
|
|
84
|
+
},
|
|
85
|
+
crux: {
|
|
86
|
+
enabled: seoConfig.integrations.enableCrUX,
|
|
87
|
+
apiKey: process.env.SEO_CRUX_API_KEY || '',
|
|
88
|
+
queryScope: 'origin+key-urls',
|
|
89
|
+
formFactor: normalizeFormFactor(process.env.SEO_CRUX_FORM_FACTOR),
|
|
90
|
+
countryCode: normalizeCountryCode(process.env.SEO_CRUX_COUNTRY || ''),
|
|
91
|
+
lookbackWindowDays: 28,
|
|
92
|
+
},
|
|
93
|
+
authority: {
|
|
94
|
+
enabled: Boolean(process.env.SEO_PROVIDER && process.env.SEO_PROVIDER_API_KEY),
|
|
95
|
+
provider: normalizeProvider((process.env.SEO_PROVIDER || '').toLowerCase()),
|
|
96
|
+
captureTopBacklinksLimit: asNumber(process.env.SEO_DATAFORSEO_LIMIT, 100),
|
|
97
|
+
runPolicy: defaultAuthorityPolicy,
|
|
98
|
+
dataForSeoLogin: process.env.SEO_DATAFORSEO_LOGIN || '',
|
|
99
|
+
dataForSeoPassword: process.env.SEO_DATAFORSEO_PASSWORD || '',
|
|
100
|
+
dataForSeoBaseURL: process.env.SEO_DATAFORSEO_BASE_URL || 'https://api.dataforseo.com',
|
|
101
|
+
},
|
|
102
|
+
ops: {
|
|
103
|
+
enabled: Boolean(process.env.SEO_OPS_WEBHOOK_URL),
|
|
104
|
+
webhookURL: process.env.SEO_OPS_WEBHOOK_URL || '',
|
|
105
|
+
webhookSecret: process.env.SEO_OPS_WEBHOOK_SECRET || '',
|
|
106
|
+
sendPerRun: true,
|
|
107
|
+
sendMonthlyRollup: true,
|
|
108
|
+
lastRollupMonth: process.env.SEO_OPS_LAST_ROLLUP_MONTH || '',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const resolveIntegrationSettings = async ({ payload, site, seoConfig, }) => {
|
|
113
|
+
const envDefaults = readEnvDefaults({ seoConfig, site });
|
|
114
|
+
let globalDoc = null;
|
|
115
|
+
try {
|
|
116
|
+
const doc = (await payload.findGlobal({
|
|
117
|
+
slug: 'seo-integrations',
|
|
118
|
+
depth: 0,
|
|
119
|
+
overrideAccess: true,
|
|
120
|
+
req: {
|
|
121
|
+
context: {
|
|
122
|
+
seoAuditInternal: true,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
globalDoc = (0, secrets_1.decryptSecretPaths)({
|
|
127
|
+
data: doc,
|
|
128
|
+
secretPaths: [
|
|
129
|
+
'gsc.oauthClientSecret',
|
|
130
|
+
'gsc.oauthRefreshToken',
|
|
131
|
+
'gsc.serviceAccountPrivateKey',
|
|
132
|
+
'pageSpeed.apiKey',
|
|
133
|
+
'crux.apiKey',
|
|
134
|
+
'authority.dataForSeoLogin',
|
|
135
|
+
'authority.dataForSeoPassword',
|
|
136
|
+
'ops.webhookSecret',
|
|
137
|
+
],
|
|
138
|
+
payload,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
globalDoc = null;
|
|
143
|
+
}
|
|
144
|
+
if (!globalDoc) {
|
|
145
|
+
return envDefaults;
|
|
146
|
+
}
|
|
147
|
+
const gsc = (globalDoc.gsc || {});
|
|
148
|
+
const pageSpeed = (globalDoc.pageSpeed || {});
|
|
149
|
+
const crux = (globalDoc.crux || {});
|
|
150
|
+
const authority = (globalDoc.authority || {});
|
|
151
|
+
const ops = (globalDoc.ops || {});
|
|
152
|
+
return {
|
|
153
|
+
source: 'global',
|
|
154
|
+
gsc: {
|
|
155
|
+
enabled: asBoolean(gsc.enabled, envDefaults.gsc.enabled),
|
|
156
|
+
authMode: normalizeAuthMode(gsc.authMode || envDefaults.gsc.authMode),
|
|
157
|
+
siteUrlOverride: asString(gsc.siteUrlOverride, envDefaults.gsc.siteUrlOverride),
|
|
158
|
+
rowLimit: Math.max(25, Math.min(25000, asNumber(gsc.rowLimit, envDefaults.gsc.rowLimit))),
|
|
159
|
+
lookbackDays: Math.max(1, Math.min(90, asNumber(gsc.lookbackDays, envDefaults.gsc.lookbackDays))),
|
|
160
|
+
oauthClientID: asString(gsc.oauthClientID, envDefaults.gsc.oauthClientID),
|
|
161
|
+
oauthClientSecret: asString(gsc.oauthClientSecret, envDefaults.gsc.oauthClientSecret),
|
|
162
|
+
oauthRedirectURI: asString(gsc.oauthRedirectURI, envDefaults.gsc.oauthRedirectURI),
|
|
163
|
+
oauthRefreshToken: asString(gsc.oauthRefreshToken, envDefaults.gsc.oauthRefreshToken),
|
|
164
|
+
serviceAccountEmail: asString(gsc.serviceAccountEmail, envDefaults.gsc.serviceAccountEmail),
|
|
165
|
+
serviceAccountPrivateKey: normalizePrivateKey(asString(gsc.serviceAccountPrivateKey, envDefaults.gsc.serviceAccountPrivateKey)),
|
|
166
|
+
},
|
|
167
|
+
pageSpeed: {
|
|
168
|
+
enabled: asBoolean(pageSpeed.enabled, envDefaults.pageSpeed.enabled),
|
|
169
|
+
apiKey: asString(pageSpeed.apiKey, envDefaults.pageSpeed.apiKey),
|
|
170
|
+
},
|
|
171
|
+
crux: {
|
|
172
|
+
enabled: asBoolean(crux.enabled, envDefaults.crux.enabled),
|
|
173
|
+
apiKey: asString(crux.apiKey, envDefaults.crux.apiKey),
|
|
174
|
+
queryScope: normalizeScope(crux.queryScope),
|
|
175
|
+
formFactor: normalizeFormFactor(crux.formFactor),
|
|
176
|
+
countryCode: normalizeCountryCode(asString(crux.countryCode, envDefaults.crux.countryCode)),
|
|
177
|
+
lookbackWindowDays: Math.max(1, Math.min(180, asNumber(crux.lookbackWindowDays, envDefaults.crux.lookbackWindowDays))),
|
|
178
|
+
},
|
|
179
|
+
authority: {
|
|
180
|
+
enabled: asBoolean(authority.enabled, envDefaults.authority.enabled),
|
|
181
|
+
provider: normalizeProvider(authority.provider),
|
|
182
|
+
captureTopBacklinksLimit: Math.max(10, Math.min(1000, asNumber(authority.captureTopBacklinksLimit, envDefaults.authority.captureTopBacklinksLimit))),
|
|
183
|
+
runPolicy: normalizeRunPolicy(authority.runPolicy),
|
|
184
|
+
dataForSeoLogin: asString(authority.dataForSeoLogin, envDefaults.authority.dataForSeoLogin),
|
|
185
|
+
dataForSeoPassword: asString(authority.dataForSeoPassword, envDefaults.authority.dataForSeoPassword),
|
|
186
|
+
dataForSeoBaseURL: asString(authority.dataForSeoBaseURL, envDefaults.authority.dataForSeoBaseURL),
|
|
187
|
+
},
|
|
188
|
+
ops: {
|
|
189
|
+
enabled: asBoolean(ops.enabled, envDefaults.ops.enabled),
|
|
190
|
+
webhookURL: asString(ops.webhookURL, envDefaults.ops.webhookURL),
|
|
191
|
+
webhookSecret: asString(ops.webhookSecret, envDefaults.ops.webhookSecret),
|
|
192
|
+
sendPerRun: asBoolean(ops.sendPerRun, true),
|
|
193
|
+
sendMonthlyRollup: asBoolean(ops.sendMonthlyRollup, true),
|
|
194
|
+
lastRollupMonth: asString(ops.lastRollupMonth, envDefaults.ops.lastRollupMonth),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
exports.resolveIntegrationSettings = resolveIntegrationSettings;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { AuthoritySyncResult, CrUXSyncResult, GSCSyncResult, IntegrationSettings } from '../utilities/types';
|
|
3
|
+
export declare const notifyOpsForRun: ({ payload, settings, snapshotID, runType, siteName, pagesChecked, overallScore, gsc, crux, authority, }: {
|
|
4
|
+
payload: Payload;
|
|
5
|
+
settings: IntegrationSettings;
|
|
6
|
+
snapshotID: number | string;
|
|
7
|
+
runType: "manual" | "scheduled" | "publish-triggered";
|
|
8
|
+
siteName: string;
|
|
9
|
+
pagesChecked: number;
|
|
10
|
+
overallScore: number;
|
|
11
|
+
gsc: GSCSyncResult;
|
|
12
|
+
crux: CrUXSyncResult;
|
|
13
|
+
authority: AuthoritySyncResult;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
//# sourceMappingURL=opsWebhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opsWebhook.d.ts","sourceRoot":"","sources":["../../src/utilities/opsWebhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AA0CjH,eAAO,MAAM,eAAe,GAAU,yGAWnC;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,OAAO,EAAE,QAAQ,GAAG,WAAW,GAAG,mBAAmB,CAAA;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,aAAa,CAAA;IAClB,IAAI,EAAE,cAAc,CAAA;IACpB,SAAS,EAAE,mBAAmB,CAAA;CAC/B,kBAqHA,CAAA"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.notifyOpsForRun = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const signPayload = (secret, payload) => crypto_1.default.createHmac('sha256', secret).update(payload).digest('hex');
|
|
9
|
+
const sendWebhook = async ({ url, secret, body, }) => {
|
|
10
|
+
const rawBody = JSON.stringify(body);
|
|
11
|
+
const headers = {
|
|
12
|
+
'content-type': 'application/json',
|
|
13
|
+
};
|
|
14
|
+
if (secret?.trim()) {
|
|
15
|
+
headers['x-seo-ops-signature'] = signPayload(secret, rawBody);
|
|
16
|
+
}
|
|
17
|
+
await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
cache: 'no-store',
|
|
20
|
+
headers,
|
|
21
|
+
body: rawBody,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const startOfMonth = (date) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
|
|
25
|
+
const endOfMonth = (date) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1));
|
|
26
|
+
const monthKey = (date) => `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
27
|
+
const notifyOpsForRun = async ({ payload, settings, snapshotID, runType, siteName, pagesChecked, overallScore, gsc, crux, authority, }) => {
|
|
28
|
+
if (!settings.ops.enabled || !settings.ops.webhookURL.trim())
|
|
29
|
+
return;
|
|
30
|
+
const webhookURL = settings.ops.webhookURL.trim();
|
|
31
|
+
const webhookSecret = settings.ops.webhookSecret.trim();
|
|
32
|
+
if (settings.ops.sendPerRun) {
|
|
33
|
+
await sendWebhook({
|
|
34
|
+
url: webhookURL,
|
|
35
|
+
secret: webhookSecret,
|
|
36
|
+
body: {
|
|
37
|
+
event: 'seo.run.completed',
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
data: {
|
|
40
|
+
snapshotID,
|
|
41
|
+
runType,
|
|
42
|
+
siteName,
|
|
43
|
+
pagesChecked,
|
|
44
|
+
overallScore,
|
|
45
|
+
gsc,
|
|
46
|
+
crux,
|
|
47
|
+
authority,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!settings.ops.sendMonthlyRollup)
|
|
53
|
+
return;
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const currentMonthKey = monthKey(now);
|
|
56
|
+
if (settings.ops.lastRollupMonth === currentMonthKey)
|
|
57
|
+
return;
|
|
58
|
+
const monthStart = startOfMonth(now);
|
|
59
|
+
const monthEnd = endOfMonth(now);
|
|
60
|
+
const snapshots = await payload.find({
|
|
61
|
+
collection: 'seo-snapshots',
|
|
62
|
+
where: {
|
|
63
|
+
and: [
|
|
64
|
+
{
|
|
65
|
+
startedAt: {
|
|
66
|
+
greater_than_equal: monthStart.toISOString(),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
startedAt: {
|
|
71
|
+
less_than: monthEnd.toISOString(),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
status: {
|
|
76
|
+
equals: 'completed',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
limit: 500,
|
|
82
|
+
depth: 0,
|
|
83
|
+
overrideAccess: true,
|
|
84
|
+
});
|
|
85
|
+
const docs = snapshots.docs;
|
|
86
|
+
const totalRuns = docs.length;
|
|
87
|
+
const totalPagesChecked = docs.reduce((sum, snapshot) => sum + Number(snapshot?.metrics?.pagesChecked || 0), 0);
|
|
88
|
+
const avgOverallScore = totalRuns > 0
|
|
89
|
+
? Number((docs.reduce((sum, snapshot) => sum + Number(snapshot?.scores?.overall || 0), 0) / totalRuns).toFixed(2))
|
|
90
|
+
: 0;
|
|
91
|
+
await sendWebhook({
|
|
92
|
+
url: webhookURL,
|
|
93
|
+
secret: webhookSecret,
|
|
94
|
+
body: {
|
|
95
|
+
event: 'seo.monthly.rollup',
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
data: {
|
|
98
|
+
month: currentMonthKey,
|
|
99
|
+
totalRuns,
|
|
100
|
+
totalPagesChecked,
|
|
101
|
+
avgOverallScore,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
const integrationsDoc = (await payload.findGlobal({
|
|
107
|
+
slug: 'seo-integrations',
|
|
108
|
+
depth: 0,
|
|
109
|
+
overrideAccess: true,
|
|
110
|
+
req: {
|
|
111
|
+
context: {
|
|
112
|
+
seoAuditInternal: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
await payload.updateGlobal({
|
|
117
|
+
slug: 'seo-integrations',
|
|
118
|
+
data: {
|
|
119
|
+
ops: {
|
|
120
|
+
...(integrationsDoc?.ops || {}),
|
|
121
|
+
lastRollupMonth: currentMonthKey,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
overrideAccess: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
exports.notifyOpsForRun = notifyOpsForRun;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SEOPageSpeedSummary } from '../utilities/types';
|
|
2
|
-
export declare const getPageSpeedSummary: (urls: string[]) => Promise<SEOPageSpeedSummary | null>;
|
|
2
|
+
export declare const getPageSpeedSummary: (urls: string[], explicitApiKey?: string) => Promise<SEOPageSpeedSummary | null>;
|
|
3
3
|
//# sourceMappingURL=pagespeed.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagespeed.d.ts","sourceRoot":"","sources":["../../src/utilities/pagespeed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AA2B7D,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"pagespeed.d.ts","sourceRoot":"","sources":["../../src/utilities/pagespeed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AA2B7D,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EAAE,EACd,iBAAiB,MAAM,KACtB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAuCpC,CAAA"}
|