@rankcli/agent-runtime 0.0.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 +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// Additional SEO Checks
|
|
2
|
+
// AMP detection, ads.txt, DMARC, email privacy, and more
|
|
3
|
+
|
|
4
|
+
import { httpGet } from '../../utils/http.js';
|
|
5
|
+
import * as cheerio from 'cheerio';
|
|
6
|
+
import type { AuditIssue } from '../types.js';
|
|
7
|
+
import { ISSUE_DEFINITIONS } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Isomorphic DNS TXT record lookup using DNS-over-HTTPS (Cloudflare)
|
|
11
|
+
* Works in both Node.js and Deno/browser environments
|
|
12
|
+
*/
|
|
13
|
+
async function resolveTxtDoH(domain: string): Promise<string[][]> {
|
|
14
|
+
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=TXT`;
|
|
15
|
+
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
headers: {
|
|
18
|
+
'Accept': 'application/dns-json'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`DNS lookup failed: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await response.json() as { Answer?: Array<{ data: string }> };
|
|
27
|
+
|
|
28
|
+
if (!data.Answer || data.Answer.length === 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse TXT records - they come with quotes that need to be removed
|
|
33
|
+
return data.Answer.map(record => {
|
|
34
|
+
const txt = record.data.replace(/^"|"$/g, '');
|
|
35
|
+
return [txt];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AdsTxtData {
|
|
40
|
+
exists: boolean;
|
|
41
|
+
valid: boolean;
|
|
42
|
+
entries: number;
|
|
43
|
+
errors: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DMARCData {
|
|
47
|
+
exists: boolean;
|
|
48
|
+
policy: string | null;
|
|
49
|
+
record: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SPFData {
|
|
53
|
+
exists: boolean;
|
|
54
|
+
record: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AMPData {
|
|
58
|
+
hasAmpLink: boolean;
|
|
59
|
+
ampUrl: string | null;
|
|
60
|
+
ampValid: boolean | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AdditionalChecksData {
|
|
64
|
+
adsTxt: AdsTxtData;
|
|
65
|
+
dmarc: DMARCData;
|
|
66
|
+
spf: SPFData;
|
|
67
|
+
amp: AMPData;
|
|
68
|
+
plaintextEmails: string[];
|
|
69
|
+
hasAppleTouchIcon: boolean;
|
|
70
|
+
faviconFormat: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check for ads.txt file
|
|
75
|
+
*/
|
|
76
|
+
export async function checkAdsTxt(baseUrl: string): Promise<{ issues: AuditIssue[]; data: AdsTxtData }> {
|
|
77
|
+
const issues: AuditIssue[] = [];
|
|
78
|
+
const url = new URL('/ads.txt', baseUrl).href;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await httpGet<string>(url, {
|
|
82
|
+
|
|
83
|
+
timeout: 10000,
|
|
84
|
+
validateStatus: () => true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (response.status === 404 || response.status >= 400) {
|
|
88
|
+
// Not an error for most sites - only relevant for publishers
|
|
89
|
+
return {
|
|
90
|
+
issues,
|
|
91
|
+
data: { exists: false, valid: false, entries: 0, errors: [] },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = response.data as string;
|
|
96
|
+
const lines = content.split('\n').filter((line) => line.trim() && !line.trim().startsWith('#'));
|
|
97
|
+
|
|
98
|
+
// Basic validation
|
|
99
|
+
const errors: string[] = [];
|
|
100
|
+
let validEntries = 0;
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
// ads.txt format: domain, publisher-account-id, account-type, certification-authority-id
|
|
104
|
+
const parts = line.split(',').map((p) => p.trim());
|
|
105
|
+
if (parts.length >= 3) {
|
|
106
|
+
validEntries++;
|
|
107
|
+
} else if (line.trim()) {
|
|
108
|
+
errors.push(`Invalid line format: ${line.substring(0, 50)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const valid = errors.length === 0 && validEntries > 0;
|
|
113
|
+
|
|
114
|
+
if (!valid && validEntries > 0) {
|
|
115
|
+
issues.push({
|
|
116
|
+
...ISSUE_DEFINITIONS.ADS_TXT_INVALID,
|
|
117
|
+
affectedUrls: [url],
|
|
118
|
+
details: { errors, validEntries },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
issues,
|
|
124
|
+
data: { exists: true, valid, entries: validEntries, errors },
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return {
|
|
128
|
+
issues,
|
|
129
|
+
data: { exists: false, valid: false, entries: 0, errors: ['Failed to fetch'] },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check DMARC DNS record
|
|
136
|
+
*/
|
|
137
|
+
export async function checkDMARC(domain: string): Promise<{ issues: AuditIssue[]; data: DMARCData }> {
|
|
138
|
+
const issues: AuditIssue[] = [];
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Extract domain from URL if needed
|
|
142
|
+
let cleanDomain = domain;
|
|
143
|
+
try {
|
|
144
|
+
cleanDomain = new URL(domain).hostname;
|
|
145
|
+
} catch {
|
|
146
|
+
// Already a domain
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Remove www prefix
|
|
150
|
+
cleanDomain = cleanDomain.replace(/^www\./, '');
|
|
151
|
+
|
|
152
|
+
// Query DMARC record using DNS-over-HTTPS
|
|
153
|
+
const dmarcDomain = `_dmarc.${cleanDomain}`;
|
|
154
|
+
const records = await resolveTxtDoH(dmarcDomain);
|
|
155
|
+
|
|
156
|
+
// Find DMARC record
|
|
157
|
+
const dmarcRecord = records.flat().find((r) => r.startsWith('v=DMARC1'));
|
|
158
|
+
|
|
159
|
+
if (dmarcRecord) {
|
|
160
|
+
// Parse policy
|
|
161
|
+
const policyMatch = dmarcRecord.match(/p=(\w+)/);
|
|
162
|
+
const policy = policyMatch ? policyMatch[1] : null;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
issues,
|
|
166
|
+
data: { exists: true, policy, record: dmarcRecord },
|
|
167
|
+
};
|
|
168
|
+
} else {
|
|
169
|
+
issues.push({
|
|
170
|
+
...ISSUE_DEFINITIONS.DMARC_MISSING,
|
|
171
|
+
affectedUrls: [domain],
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
issues,
|
|
175
|
+
data: { exists: false, policy: null, record: null },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
issues.push({
|
|
180
|
+
...ISSUE_DEFINITIONS.DMARC_MISSING,
|
|
181
|
+
affectedUrls: [domain],
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
issues,
|
|
185
|
+
data: { exists: false, policy: null, record: null },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check SPF DNS record
|
|
192
|
+
*/
|
|
193
|
+
export async function checkSPF(domain: string): Promise<{ issues: AuditIssue[]; data: SPFData }> {
|
|
194
|
+
const issues: AuditIssue[] = [];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
let cleanDomain = domain;
|
|
198
|
+
try {
|
|
199
|
+
cleanDomain = new URL(domain).hostname;
|
|
200
|
+
} catch {
|
|
201
|
+
// Already a domain
|
|
202
|
+
}
|
|
203
|
+
cleanDomain = cleanDomain.replace(/^www\./, '');
|
|
204
|
+
|
|
205
|
+
const records = await resolveTxtDoH(cleanDomain);
|
|
206
|
+
const spfRecord = records.flat().find((r) => r.startsWith('v=spf1'));
|
|
207
|
+
|
|
208
|
+
if (spfRecord) {
|
|
209
|
+
return {
|
|
210
|
+
issues,
|
|
211
|
+
data: { exists: true, record: spfRecord },
|
|
212
|
+
};
|
|
213
|
+
} else {
|
|
214
|
+
issues.push({
|
|
215
|
+
...ISSUE_DEFINITIONS.SPF_MISSING,
|
|
216
|
+
affectedUrls: [domain],
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
issues,
|
|
220
|
+
data: { exists: false, record: null },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// DNS lookup failed - could be many reasons
|
|
225
|
+
return {
|
|
226
|
+
issues,
|
|
227
|
+
data: { exists: false, record: null },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check for AMP version
|
|
234
|
+
*/
|
|
235
|
+
export function checkAMP(html: string, url: string): { issues: AuditIssue[]; data: AMPData } {
|
|
236
|
+
const issues: AuditIssue[] = [];
|
|
237
|
+
const $ = cheerio.load(html);
|
|
238
|
+
|
|
239
|
+
// Check for amphtml link
|
|
240
|
+
const ampLink = $('link[rel="amphtml"]').attr('href');
|
|
241
|
+
|
|
242
|
+
if (ampLink) {
|
|
243
|
+
return {
|
|
244
|
+
issues,
|
|
245
|
+
data: {
|
|
246
|
+
hasAmpLink: true,
|
|
247
|
+
ampUrl: ampLink,
|
|
248
|
+
ampValid: null, // Would need to validate the AMP page
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if current page is AMP
|
|
254
|
+
const isAmpPage = $('html[amp]').length > 0 || $('html[⚡]').length > 0;
|
|
255
|
+
|
|
256
|
+
if (isAmpPage) {
|
|
257
|
+
return {
|
|
258
|
+
issues,
|
|
259
|
+
data: {
|
|
260
|
+
hasAmpLink: false,
|
|
261
|
+
ampUrl: url,
|
|
262
|
+
ampValid: null,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Only flag AMP missing for content-heavy pages (articles, news)
|
|
268
|
+
const isArticle =
|
|
269
|
+
$('article').length > 0 ||
|
|
270
|
+
$('meta[property="og:type"][content="article"]').length > 0 ||
|
|
271
|
+
html.toLowerCase().includes('blog') ||
|
|
272
|
+
html.toLowerCase().includes('news');
|
|
273
|
+
|
|
274
|
+
if (isArticle) {
|
|
275
|
+
issues.push({
|
|
276
|
+
...ISSUE_DEFINITIONS.AMP_MISSING,
|
|
277
|
+
affectedUrls: [url],
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
issues,
|
|
283
|
+
data: {
|
|
284
|
+
hasAmpLink: false,
|
|
285
|
+
ampUrl: null,
|
|
286
|
+
ampValid: null,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check for plaintext email addresses
|
|
293
|
+
*/
|
|
294
|
+
export function checkPlaintextEmails(html: string, url: string): { issues: AuditIssue[]; emails: string[] } {
|
|
295
|
+
const issues: AuditIssue[] = [];
|
|
296
|
+
const $ = cheerio.load(html);
|
|
297
|
+
|
|
298
|
+
// Remove script and style content
|
|
299
|
+
$('script, style').remove();
|
|
300
|
+
const text = $('body').text();
|
|
301
|
+
|
|
302
|
+
// Email regex
|
|
303
|
+
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
304
|
+
const emails = text.match(emailRegex) || [];
|
|
305
|
+
|
|
306
|
+
// Filter out common false positives
|
|
307
|
+
const filteredEmails = emails.filter((email) => {
|
|
308
|
+
const lowerEmail = email.toLowerCase();
|
|
309
|
+
return (
|
|
310
|
+
!lowerEmail.includes('example.com') &&
|
|
311
|
+
!lowerEmail.includes('yoursite.com') &&
|
|
312
|
+
!lowerEmail.includes('yourdomain') &&
|
|
313
|
+
!lowerEmail.endsWith('.png') &&
|
|
314
|
+
!lowerEmail.endsWith('.jpg')
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Deduplicate
|
|
319
|
+
const uniqueEmails = [...new Set(filteredEmails)];
|
|
320
|
+
|
|
321
|
+
if (uniqueEmails.length > 0) {
|
|
322
|
+
issues.push({
|
|
323
|
+
...ISSUE_DEFINITIONS.PLAINTEXT_EMAIL,
|
|
324
|
+
affectedUrls: [url],
|
|
325
|
+
details: {
|
|
326
|
+
emails: uniqueEmails,
|
|
327
|
+
count: uniqueEmails.length,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { issues, emails: uniqueEmails };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check for Apple Touch Icon
|
|
337
|
+
*/
|
|
338
|
+
export function checkAppleTouchIcon(html: string, url: string): { issues: AuditIssue[]; exists: boolean } {
|
|
339
|
+
const issues: AuditIssue[] = [];
|
|
340
|
+
const $ = cheerio.load(html);
|
|
341
|
+
|
|
342
|
+
const hasAppleTouchIcon =
|
|
343
|
+
$('link[rel="apple-touch-icon"]').length > 0 || $('link[rel="apple-touch-icon-precomposed"]').length > 0;
|
|
344
|
+
|
|
345
|
+
if (!hasAppleTouchIcon) {
|
|
346
|
+
issues.push({
|
|
347
|
+
...ISSUE_DEFINITIONS.APPLE_TOUCH_ICON_MISSING,
|
|
348
|
+
affectedUrls: [url],
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { issues, exists: hasAppleTouchIcon };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check favicon format
|
|
357
|
+
*/
|
|
358
|
+
export function checkFaviconFormat(html: string, url: string): { issues: AuditIssue[]; format: string | null } {
|
|
359
|
+
const issues: AuditIssue[] = [];
|
|
360
|
+
const $ = cheerio.load(html);
|
|
361
|
+
|
|
362
|
+
// Check for favicon links
|
|
363
|
+
const faviconLink =
|
|
364
|
+
$('link[rel="icon"]').attr('href') ||
|
|
365
|
+
$('link[rel="shortcut icon"]').attr('href') ||
|
|
366
|
+
null;
|
|
367
|
+
|
|
368
|
+
if (!faviconLink) {
|
|
369
|
+
return { issues, format: null };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Determine format
|
|
373
|
+
let format: string | null = null;
|
|
374
|
+
const lowerHref = faviconLink.toLowerCase();
|
|
375
|
+
|
|
376
|
+
if (lowerHref.endsWith('.ico')) {
|
|
377
|
+
format = 'ico';
|
|
378
|
+
} else if (lowerHref.endsWith('.png')) {
|
|
379
|
+
format = 'png';
|
|
380
|
+
} else if (lowerHref.endsWith('.svg')) {
|
|
381
|
+
format = 'svg';
|
|
382
|
+
} else if (lowerHref.includes('data:image/')) {
|
|
383
|
+
format = 'inline';
|
|
384
|
+
} else {
|
|
385
|
+
format = 'unknown';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Note: Not having .ico is just a notice, not a critical issue
|
|
389
|
+
// Modern browsers support PNG/SVG favicons well
|
|
390
|
+
|
|
391
|
+
return { issues, format };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Run all additional checks
|
|
396
|
+
*/
|
|
397
|
+
export async function runAdditionalChecks(
|
|
398
|
+
url: string,
|
|
399
|
+
html: string
|
|
400
|
+
): Promise<{ issues: AuditIssue[]; data: AdditionalChecksData }> {
|
|
401
|
+
const allIssues: AuditIssue[] = [];
|
|
402
|
+
|
|
403
|
+
// Run parallel checks
|
|
404
|
+
const [adsTxtResult, dmarcResult, spfResult] = await Promise.all([
|
|
405
|
+
checkAdsTxt(url),
|
|
406
|
+
checkDMARC(url),
|
|
407
|
+
checkSPF(url),
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
allIssues.push(...adsTxtResult.issues);
|
|
411
|
+
allIssues.push(...dmarcResult.issues);
|
|
412
|
+
allIssues.push(...spfResult.issues);
|
|
413
|
+
|
|
414
|
+
// Run HTML-based checks
|
|
415
|
+
const ampResult = checkAMP(html, url);
|
|
416
|
+
allIssues.push(...ampResult.issues);
|
|
417
|
+
|
|
418
|
+
const emailResult = checkPlaintextEmails(html, url);
|
|
419
|
+
allIssues.push(...emailResult.issues);
|
|
420
|
+
|
|
421
|
+
const touchIconResult = checkAppleTouchIcon(html, url);
|
|
422
|
+
allIssues.push(...touchIconResult.issues);
|
|
423
|
+
|
|
424
|
+
const faviconResult = checkFaviconFormat(html, url);
|
|
425
|
+
allIssues.push(...faviconResult.issues);
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
issues: allIssues,
|
|
429
|
+
data: {
|
|
430
|
+
adsTxt: adsTxtResult.data,
|
|
431
|
+
dmarc: dmarcResult.data,
|
|
432
|
+
spf: spfResult.data,
|
|
433
|
+
amp: ampResult.data,
|
|
434
|
+
plaintextEmails: emailResult.emails,
|
|
435
|
+
hasAppleTouchIcon: touchIconResult.exists,
|
|
436
|
+
faviconFormat: faviconResult.format,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|