@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,580 @@
|
|
|
1
|
+
// Platform Presence Check - Search Everywhere Optimization
|
|
2
|
+
// Reference: "The New Rules of SEO (2026)" by Neil Patel
|
|
3
|
+
// "It's not just about Google anymore"
|
|
4
|
+
// "Search Everywhere Optimization - You need to be on TikTok, Reddit, Amazon, YouTube, ChatGPT"
|
|
5
|
+
// "Cross-platform trust network - being mentioned on multiple platforms validates your brand"
|
|
6
|
+
// "RICE framework: Reach, Impact, Confidence, Effort for platform prioritization"
|
|
7
|
+
|
|
8
|
+
import * as cheerio from 'cheerio';
|
|
9
|
+
import type { AuditIssue } from '../types.js';
|
|
10
|
+
|
|
11
|
+
export interface PlatformLink {
|
|
12
|
+
platform: string;
|
|
13
|
+
url: string;
|
|
14
|
+
type: 'profile' | 'content' | 'mention' | 'embed';
|
|
15
|
+
location: 'header' | 'footer' | 'content' | 'sidebar' | 'schema';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlatformPresenceData {
|
|
19
|
+
detectedPlatforms: string[];
|
|
20
|
+
platformLinks: PlatformLink[];
|
|
21
|
+
socialProfiles: {
|
|
22
|
+
platform: string;
|
|
23
|
+
url: string;
|
|
24
|
+
hasSchemaMarkup: boolean;
|
|
25
|
+
}[];
|
|
26
|
+
embeddedContent: {
|
|
27
|
+
platform: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}[];
|
|
30
|
+
metrics: {
|
|
31
|
+
totalPlatforms: number;
|
|
32
|
+
hasYouTube: boolean;
|
|
33
|
+
hasTikTok: boolean;
|
|
34
|
+
hasTwitter: boolean;
|
|
35
|
+
hasLinkedIn: boolean;
|
|
36
|
+
hasReddit: boolean;
|
|
37
|
+
hasFacebook: boolean;
|
|
38
|
+
hasInstagram: boolean;
|
|
39
|
+
hasPinterest: boolean;
|
|
40
|
+
hasPodcast: boolean;
|
|
41
|
+
hasGitHub: boolean;
|
|
42
|
+
};
|
|
43
|
+
schemaPresence: {
|
|
44
|
+
hasSameAs: boolean;
|
|
45
|
+
sameAsUrls: string[];
|
|
46
|
+
hasOrganizationSchema: boolean;
|
|
47
|
+
hasPersonSchema: boolean;
|
|
48
|
+
};
|
|
49
|
+
crossPlatformScore: number; // 0-100
|
|
50
|
+
recommendations: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Platform detection patterns
|
|
54
|
+
const PLATFORM_PATTERNS: {
|
|
55
|
+
name: string;
|
|
56
|
+
urlPatterns: RegExp[];
|
|
57
|
+
embedPatterns?: RegExp[];
|
|
58
|
+
priority: 'high' | 'medium' | 'low'; // RICE-based priority
|
|
59
|
+
}[] = [
|
|
60
|
+
{
|
|
61
|
+
name: 'YouTube',
|
|
62
|
+
urlPatterns: [/youtube\.com/, /youtu\.be/],
|
|
63
|
+
embedPatterns: [/youtube\.com\/embed/, /youtube-nocookie\.com/],
|
|
64
|
+
priority: 'high',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'TikTok',
|
|
68
|
+
urlPatterns: [/tiktok\.com/],
|
|
69
|
+
embedPatterns: [/tiktok\.com\/embed/],
|
|
70
|
+
priority: 'high',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Twitter/X',
|
|
74
|
+
urlPatterns: [/twitter\.com/, /x\.com/],
|
|
75
|
+
embedPatterns: [/platform\.twitter\.com/],
|
|
76
|
+
priority: 'high',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'LinkedIn',
|
|
80
|
+
urlPatterns: [/linkedin\.com/],
|
|
81
|
+
priority: 'high',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Reddit',
|
|
85
|
+
urlPatterns: [/reddit\.com/, /redd\.it/],
|
|
86
|
+
priority: 'high',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Facebook',
|
|
90
|
+
urlPatterns: [/facebook\.com/, /fb\.com/, /fb\.me/],
|
|
91
|
+
embedPatterns: [/facebook\.com\/plugins/],
|
|
92
|
+
priority: 'medium',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Instagram',
|
|
96
|
+
urlPatterns: [/instagram\.com/, /instagr\.am/],
|
|
97
|
+
embedPatterns: [/instagram\.com\/embed/],
|
|
98
|
+
priority: 'medium',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'Pinterest',
|
|
102
|
+
urlPatterns: [/pinterest\.com/, /pin\.it/],
|
|
103
|
+
priority: 'medium',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'GitHub',
|
|
107
|
+
urlPatterns: [/github\.com/],
|
|
108
|
+
priority: 'medium',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'Medium',
|
|
112
|
+
urlPatterns: [/medium\.com/],
|
|
113
|
+
priority: 'low',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'Spotify',
|
|
117
|
+
urlPatterns: [/spotify\.com/, /open\.spotify\.com/],
|
|
118
|
+
embedPatterns: [/open\.spotify\.com\/embed/],
|
|
119
|
+
priority: 'medium',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Apple Podcasts',
|
|
123
|
+
urlPatterns: [/podcasts\.apple\.com/],
|
|
124
|
+
priority: 'medium',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Discord',
|
|
128
|
+
urlPatterns: [/discord\.gg/, /discord\.com/],
|
|
129
|
+
priority: 'low',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Twitch',
|
|
133
|
+
urlPatterns: [/twitch\.tv/],
|
|
134
|
+
embedPatterns: [/player\.twitch\.tv/],
|
|
135
|
+
priority: 'low',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Vimeo',
|
|
139
|
+
urlPatterns: [/vimeo\.com/],
|
|
140
|
+
embedPatterns: [/player\.vimeo\.com/],
|
|
141
|
+
priority: 'low',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'ProductHunt',
|
|
145
|
+
urlPatterns: [/producthunt\.com/],
|
|
146
|
+
priority: 'medium',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Substack',
|
|
150
|
+
urlPatterns: [/substack\.com/],
|
|
151
|
+
priority: 'low',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Amazon',
|
|
155
|
+
urlPatterns: [/amazon\.com/, /amzn\.to/],
|
|
156
|
+
priority: 'medium',
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Determine link location on page
|
|
162
|
+
*/
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
function getLinkLocation($: cheerio.CheerioAPI, element: cheerio.Cheerio<any>): PlatformLink['location'] {
|
|
165
|
+
const parents = element.parents().toArray();
|
|
166
|
+
|
|
167
|
+
for (const parent of parents) {
|
|
168
|
+
const tagName = parent.tagName?.toLowerCase();
|
|
169
|
+
const className = ($(parent).attr('class') || '').toLowerCase();
|
|
170
|
+
|
|
171
|
+
if (tagName === 'header' || className.includes('header')) return 'header';
|
|
172
|
+
if (tagName === 'footer' || className.includes('footer')) return 'footer';
|
|
173
|
+
if (tagName === 'aside' || className.includes('sidebar')) return 'sidebar';
|
|
174
|
+
if (tagName === 'main' || tagName === 'article' || className.includes('content')) return 'content';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return 'content';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detect platform from URL
|
|
182
|
+
*/
|
|
183
|
+
function detectPlatform(url: string): string | null {
|
|
184
|
+
const urlLower = url.toLowerCase();
|
|
185
|
+
|
|
186
|
+
for (const platform of PLATFORM_PATTERNS) {
|
|
187
|
+
if (platform.urlPatterns.some(p => p.test(urlLower))) {
|
|
188
|
+
return platform.name;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Determine link type (profile, content, embed)
|
|
197
|
+
*/
|
|
198
|
+
function getLinkType(url: string, platform: string): PlatformLink['type'] {
|
|
199
|
+
const urlLower = url.toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Profile patterns
|
|
202
|
+
const profilePatterns = [
|
|
203
|
+
/youtube\.com\/@/,
|
|
204
|
+
/youtube\.com\/c\//,
|
|
205
|
+
/youtube\.com\/channel\//,
|
|
206
|
+
/twitter\.com\/[^/]+$/,
|
|
207
|
+
/x\.com\/[^/]+$/,
|
|
208
|
+
/linkedin\.com\/company\//,
|
|
209
|
+
/linkedin\.com\/in\//,
|
|
210
|
+
/instagram\.com\/[^/]+\/?$/,
|
|
211
|
+
/tiktok\.com\/@/,
|
|
212
|
+
/facebook\.com\/[^/]+\/?$/,
|
|
213
|
+
/github\.com\/[^/]+\/?$/,
|
|
214
|
+
/reddit\.com\/user\//,
|
|
215
|
+
/reddit\.com\/r\/[^/]+\/?$/,
|
|
216
|
+
/pinterest\.com\/[^/]+\/?$/,
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
if (profilePatterns.some(p => p.test(urlLower))) {
|
|
220
|
+
return 'profile';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Embed detection
|
|
224
|
+
const embedPatterns = [
|
|
225
|
+
/embed/,
|
|
226
|
+
/player\./,
|
|
227
|
+
/plugins/,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
if (embedPatterns.some(p => p.test(urlLower))) {
|
|
231
|
+
return 'embed';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return 'content';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract platform links from HTML
|
|
239
|
+
*/
|
|
240
|
+
export function extractPlatformLinks(html: string): PlatformLink[] {
|
|
241
|
+
const $ = cheerio.load(html);
|
|
242
|
+
const links: PlatformLink[] = [];
|
|
243
|
+
|
|
244
|
+
$('a[href]').each((_, element) => {
|
|
245
|
+
const $el = $(element);
|
|
246
|
+
const href = $el.attr('href') || '';
|
|
247
|
+
const platform = detectPlatform(href);
|
|
248
|
+
|
|
249
|
+
if (platform) {
|
|
250
|
+
links.push({
|
|
251
|
+
platform,
|
|
252
|
+
url: href,
|
|
253
|
+
type: getLinkType(href, platform),
|
|
254
|
+
location: getLinkLocation($, $el),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return links;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Detect embedded content (iframes, embeds)
|
|
264
|
+
*/
|
|
265
|
+
export function detectEmbeddedContent(html: string): { platform: string; count: number }[] {
|
|
266
|
+
const $ = cheerio.load(html);
|
|
267
|
+
const embeds: Map<string, number> = new Map();
|
|
268
|
+
|
|
269
|
+
// Check iframes
|
|
270
|
+
$('iframe[src]').each((_, element) => {
|
|
271
|
+
const src = $(element).attr('src') || '';
|
|
272
|
+
const platform = detectPlatform(src);
|
|
273
|
+
if (platform) {
|
|
274
|
+
embeds.set(platform, (embeds.get(platform) || 0) + 1);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Check embed tags
|
|
279
|
+
$('embed[src]').each((_, element) => {
|
|
280
|
+
const src = $(element).attr('src') || '';
|
|
281
|
+
const platform = detectPlatform(src);
|
|
282
|
+
if (platform) {
|
|
283
|
+
embeds.set(platform, (embeds.get(platform) || 0) + 1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Check Twitter/X widgets
|
|
288
|
+
if (html.includes('twitter-tweet') || html.includes('twitter-timeline')) {
|
|
289
|
+
embeds.set('Twitter/X', (embeds.get('Twitter/X') || 0) + 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check TikTok embeds
|
|
293
|
+
if (html.includes('tiktok-embed')) {
|
|
294
|
+
embeds.set('TikTok', (embeds.get('TikTok') || 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return Array.from(embeds.entries()).map(([platform, count]) => ({ platform, count }));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Extract schema.org social profiles
|
|
302
|
+
*/
|
|
303
|
+
export function extractSchemaProfiles(html: string): {
|
|
304
|
+
sameAsUrls: string[];
|
|
305
|
+
hasOrganizationSchema: boolean;
|
|
306
|
+
hasPersonSchema: boolean;
|
|
307
|
+
} {
|
|
308
|
+
const $ = cheerio.load(html);
|
|
309
|
+
const sameAsUrls: string[] = [];
|
|
310
|
+
let hasOrganizationSchema = false;
|
|
311
|
+
let hasPersonSchema = false;
|
|
312
|
+
|
|
313
|
+
// Find JSON-LD scripts
|
|
314
|
+
$('script[type="application/ld+json"]').each((_, element) => {
|
|
315
|
+
try {
|
|
316
|
+
const content = $(element).html() || '';
|
|
317
|
+
const jsonData = JSON.parse(content);
|
|
318
|
+
|
|
319
|
+
const processSchema = (schema: Record<string, unknown>) => {
|
|
320
|
+
if (schema['@type'] === 'Organization') {
|
|
321
|
+
hasOrganizationSchema = true;
|
|
322
|
+
}
|
|
323
|
+
if (schema['@type'] === 'Person') {
|
|
324
|
+
hasPersonSchema = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Extract sameAs URLs
|
|
328
|
+
if (schema.sameAs) {
|
|
329
|
+
if (Array.isArray(schema.sameAs)) {
|
|
330
|
+
sameAsUrls.push(...(schema.sameAs as string[]));
|
|
331
|
+
} else if (typeof schema.sameAs === 'string') {
|
|
332
|
+
sameAsUrls.push(schema.sameAs);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (Array.isArray(jsonData)) {
|
|
338
|
+
jsonData.forEach(item => {
|
|
339
|
+
if (typeof item === 'object' && item !== null) {
|
|
340
|
+
processSchema(item as Record<string, unknown>);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} else if (typeof jsonData === 'object' && jsonData !== null) {
|
|
344
|
+
processSchema(jsonData);
|
|
345
|
+
|
|
346
|
+
// Check @graph
|
|
347
|
+
if (Array.isArray(jsonData['@graph'])) {
|
|
348
|
+
jsonData['@graph'].forEach((item: Record<string, unknown>) => {
|
|
349
|
+
if (typeof item === 'object' && item !== null) {
|
|
350
|
+
processSchema(item);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
// Invalid JSON, skip
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return { sameAsUrls, hasOrganizationSchema, hasPersonSchema };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Calculate cross-platform score
|
|
365
|
+
*/
|
|
366
|
+
function calculateCrossPlatformScore(
|
|
367
|
+
platformCount: number,
|
|
368
|
+
hasHighPriorityPlatforms: boolean,
|
|
369
|
+
hasSchemaProfiles: boolean,
|
|
370
|
+
hasEmbeddedContent: boolean
|
|
371
|
+
): number {
|
|
372
|
+
let score = 0;
|
|
373
|
+
|
|
374
|
+
// Platform count (max 50 points)
|
|
375
|
+
score += Math.min(platformCount * 10, 50);
|
|
376
|
+
|
|
377
|
+
// High priority platforms (max 30 points)
|
|
378
|
+
if (hasHighPriorityPlatforms) {
|
|
379
|
+
score += 30;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Schema markup (10 points)
|
|
383
|
+
if (hasSchemaProfiles) {
|
|
384
|
+
score += 10;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Embedded content (10 points)
|
|
388
|
+
if (hasEmbeddedContent) {
|
|
389
|
+
score += 10;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return Math.min(score, 100);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Main function: Analyze platform presence
|
|
397
|
+
*/
|
|
398
|
+
export function analyzePlatformPresence(
|
|
399
|
+
html: string,
|
|
400
|
+
url: string
|
|
401
|
+
): { issues: AuditIssue[]; data: PlatformPresenceData } {
|
|
402
|
+
const issues: AuditIssue[] = [];
|
|
403
|
+
|
|
404
|
+
// Extract platform links
|
|
405
|
+
const platformLinks = extractPlatformLinks(html);
|
|
406
|
+
|
|
407
|
+
// Get unique platforms
|
|
408
|
+
const detectedPlatforms = [...new Set(platformLinks.map(l => l.platform))];
|
|
409
|
+
|
|
410
|
+
// Extract social profiles (profile type links)
|
|
411
|
+
const socialProfiles = platformLinks
|
|
412
|
+
.filter(l => l.type === 'profile')
|
|
413
|
+
.map(l => ({
|
|
414
|
+
platform: l.platform,
|
|
415
|
+
url: l.url,
|
|
416
|
+
hasSchemaMarkup: false, // Updated below
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
// Detect embedded content
|
|
420
|
+
const embeddedContent = detectEmbeddedContent(html);
|
|
421
|
+
|
|
422
|
+
// Extract schema profiles
|
|
423
|
+
const schemaData = extractSchemaProfiles(html);
|
|
424
|
+
|
|
425
|
+
// Mark profiles with schema markup
|
|
426
|
+
socialProfiles.forEach(profile => {
|
|
427
|
+
profile.hasSchemaMarkup = schemaData.sameAsUrls.some(schemaUrl =>
|
|
428
|
+
schemaUrl.toLowerCase().includes(profile.platform.toLowerCase())
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Calculate metrics
|
|
433
|
+
const hasHighPriority = (name: string) => detectedPlatforms.includes(name);
|
|
434
|
+
|
|
435
|
+
const metrics = {
|
|
436
|
+
totalPlatforms: detectedPlatforms.length,
|
|
437
|
+
hasYouTube: hasHighPriority('YouTube'),
|
|
438
|
+
hasTikTok: hasHighPriority('TikTok'),
|
|
439
|
+
hasTwitter: hasHighPriority('Twitter/X'),
|
|
440
|
+
hasLinkedIn: hasHighPriority('LinkedIn'),
|
|
441
|
+
hasReddit: hasHighPriority('Reddit'),
|
|
442
|
+
hasFacebook: hasHighPriority('Facebook'),
|
|
443
|
+
hasInstagram: hasHighPriority('Instagram'),
|
|
444
|
+
hasPinterest: hasHighPriority('Pinterest'),
|
|
445
|
+
hasPodcast: hasHighPriority('Spotify') || hasHighPriority('Apple Podcasts'),
|
|
446
|
+
hasGitHub: hasHighPriority('GitHub'),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// High priority platforms present
|
|
450
|
+
const highPriorityPlatforms = PLATFORM_PATTERNS
|
|
451
|
+
.filter(p => p.priority === 'high')
|
|
452
|
+
.map(p => p.name);
|
|
453
|
+
const hasHighPriorityPlatforms = highPriorityPlatforms.some(p => detectedPlatforms.includes(p));
|
|
454
|
+
|
|
455
|
+
// Calculate cross-platform score
|
|
456
|
+
const crossPlatformScore = calculateCrossPlatformScore(
|
|
457
|
+
detectedPlatforms.length,
|
|
458
|
+
hasHighPriorityPlatforms,
|
|
459
|
+
schemaData.sameAsUrls.length > 0,
|
|
460
|
+
embeddedContent.length > 0
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Generate recommendations
|
|
464
|
+
const recommendations: string[] = [];
|
|
465
|
+
|
|
466
|
+
if (!metrics.hasYouTube) {
|
|
467
|
+
recommendations.push('Add YouTube presence - video content ranks on both YouTube and Google');
|
|
468
|
+
}
|
|
469
|
+
if (!metrics.hasTikTok) {
|
|
470
|
+
recommendations.push('Consider TikTok for short-form video discovery (high reach for B2C)');
|
|
471
|
+
}
|
|
472
|
+
if (!metrics.hasTwitter) {
|
|
473
|
+
recommendations.push('Create Twitter/X presence for real-time engagement and brand visibility');
|
|
474
|
+
}
|
|
475
|
+
if (!metrics.hasLinkedIn) {
|
|
476
|
+
recommendations.push('LinkedIn is essential for B2B - add company page link');
|
|
477
|
+
}
|
|
478
|
+
if (schemaData.sameAsUrls.length === 0) {
|
|
479
|
+
recommendations.push('Add sameAs property to your Organization schema with social profile URLs');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Generate issues
|
|
483
|
+
|
|
484
|
+
// No social profiles detected
|
|
485
|
+
if (socialProfiles.length === 0) {
|
|
486
|
+
issues.push({
|
|
487
|
+
code: 'NO_SOCIAL_PROFILES',
|
|
488
|
+
severity: 'warning',
|
|
489
|
+
category: 'social',
|
|
490
|
+
title: 'No social media profile links detected',
|
|
491
|
+
description: 'Page has no visible links to social media profiles.',
|
|
492
|
+
impact: 'Cross-platform presence builds brand trust and provides additional discovery channels.',
|
|
493
|
+
howToFix: 'Add links to your social media profiles in the header, footer, or contact section.',
|
|
494
|
+
affectedUrls: [url],
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// No high-priority platforms
|
|
499
|
+
if (!hasHighPriorityPlatforms && detectedPlatforms.length > 0) {
|
|
500
|
+
issues.push({
|
|
501
|
+
code: 'NO_HIGH_PRIORITY_PLATFORMS',
|
|
502
|
+
severity: 'notice',
|
|
503
|
+
category: 'social',
|
|
504
|
+
title: 'Missing high-priority platform links',
|
|
505
|
+
description: 'No links to YouTube, TikTok, Twitter/X, LinkedIn, or Reddit detected.',
|
|
506
|
+
impact: 'These platforms have the highest reach and impact for SEO diversification.',
|
|
507
|
+
howToFix: 'Prioritize creating presence on YouTube, Twitter/X, and LinkedIn (using RICE framework).',
|
|
508
|
+
affectedUrls: [url],
|
|
509
|
+
details: {
|
|
510
|
+
currentPlatforms: detectedPlatforms,
|
|
511
|
+
recommendedPlatforms: highPriorityPlatforms,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// No schema sameAs
|
|
517
|
+
if (schemaData.sameAsUrls.length === 0 && socialProfiles.length > 0) {
|
|
518
|
+
issues.push({
|
|
519
|
+
code: 'NO_SCHEMA_SAMEAS',
|
|
520
|
+
severity: 'notice',
|
|
521
|
+
category: 'social',
|
|
522
|
+
title: 'Social profiles not in schema.org markup',
|
|
523
|
+
description: 'Social media links found but not included in Organization/Person schema.',
|
|
524
|
+
impact: 'Schema sameAs helps search engines understand your brand\'s official social presence.',
|
|
525
|
+
howToFix: 'Add sameAs array to your Organization or Person schema with all social profile URLs.',
|
|
526
|
+
affectedUrls: [url],
|
|
527
|
+
details: {
|
|
528
|
+
profilesFound: socialProfiles.map(p => p.platform),
|
|
529
|
+
schemaUrls: schemaData.sameAsUrls,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// No YouTube (critical for video SEO)
|
|
535
|
+
if (!metrics.hasYouTube && detectedPlatforms.length >= 2) {
|
|
536
|
+
issues.push({
|
|
537
|
+
code: 'NO_YOUTUBE_PRESENCE',
|
|
538
|
+
severity: 'notice',
|
|
539
|
+
category: 'social',
|
|
540
|
+
title: 'No YouTube channel linked',
|
|
541
|
+
description: 'Multiple social platforms linked but YouTube is missing.',
|
|
542
|
+
impact: 'YouTube is the second largest search engine. Video content ranks in both YouTube and Google.',
|
|
543
|
+
howToFix: 'Create a YouTube channel and link to it from your website.',
|
|
544
|
+
affectedUrls: [url],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// No embedded content
|
|
549
|
+
if (embeddedContent.length === 0 && detectedPlatforms.length > 0) {
|
|
550
|
+
issues.push({
|
|
551
|
+
code: 'NO_EMBEDDED_SOCIAL',
|
|
552
|
+
severity: 'notice',
|
|
553
|
+
category: 'social',
|
|
554
|
+
title: 'No embedded social content',
|
|
555
|
+
description: 'Social profiles linked but no embedded videos, tweets, or posts.',
|
|
556
|
+
impact: 'Embedded content increases engagement time and shows fresh, active social presence.',
|
|
557
|
+
howToFix: 'Embed relevant YouTube videos, Twitter feeds, or TikTok videos in your content.',
|
|
558
|
+
affectedUrls: [url],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
issues,
|
|
564
|
+
data: {
|
|
565
|
+
detectedPlatforms,
|
|
566
|
+
platformLinks,
|
|
567
|
+
socialProfiles,
|
|
568
|
+
embeddedContent,
|
|
569
|
+
metrics,
|
|
570
|
+
schemaPresence: {
|
|
571
|
+
hasSameAs: schemaData.sameAsUrls.length > 0,
|
|
572
|
+
sameAsUrls: schemaData.sameAsUrls,
|
|
573
|
+
hasOrganizationSchema: schemaData.hasOrganizationSchema,
|
|
574
|
+
hasPersonSchema: schemaData.hasPersonSchema,
|
|
575
|
+
},
|
|
576
|
+
crossPlatformScore,
|
|
577
|
+
recommendations,
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|