@rankcli/agent-runtime 0.0.15 → 0.0.16

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.
Files changed (202) hide show
  1. package/dist/index.d.mts +43 -393
  2. package/dist/index.d.ts +43 -393
  3. package/dist/index.js +306 -6526
  4. package/dist/index.mjs +202 -6412
  5. package/package.json +6 -1
  6. package/scripts/build-deno.ts +0 -134
  7. package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
  8. package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
  9. package/src/analyzers/geo-analyzer.test.ts +0 -310
  10. package/src/analyzers/geo-analyzer.ts +0 -814
  11. package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
  12. package/src/analyzers/image-optimization-analyzer.ts +0 -348
  13. package/src/analyzers/index.ts +0 -233
  14. package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
  15. package/src/analyzers/internal-linking-analyzer.ts +0 -419
  16. package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
  17. package/src/analyzers/mobile-seo-analyzer.ts +0 -455
  18. package/src/analyzers/security-headers-analyzer.test.ts +0 -115
  19. package/src/analyzers/security-headers-analyzer.ts +0 -318
  20. package/src/analyzers/structured-data-analyzer.test.ts +0 -210
  21. package/src/analyzers/structured-data-analyzer.ts +0 -590
  22. package/src/audit/ai/analyzer.ts +0 -347
  23. package/src/audit/ai/index.ts +0 -29
  24. package/src/audit/ai/prompts/content-analysis.ts +0 -271
  25. package/src/audit/ai/types.ts +0 -179
  26. package/src/audit/checks/additional-checks.ts +0 -439
  27. package/src/audit/checks/agent-experience.ts +0 -108
  28. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  29. package/src/audit/checks/ai-content-structure.ts +0 -325
  30. package/src/audit/checks/ai-readiness.ts +0 -406
  31. package/src/audit/checks/anchor-text.ts +0 -179
  32. package/src/audit/checks/answer-conciseness.ts +0 -322
  33. package/src/audit/checks/asset-minification.ts +0 -270
  34. package/src/audit/checks/bing-optimization.ts +0 -206
  35. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  36. package/src/audit/checks/caching-headers.ts +0 -305
  37. package/src/audit/checks/canonical-advanced.ts +0 -150
  38. package/src/audit/checks/canonical-domain.ts +0 -196
  39. package/src/audit/checks/citation-quality.ts +0 -358
  40. package/src/audit/checks/client-rendering.ts +0 -542
  41. package/src/audit/checks/color-contrast.ts +0 -342
  42. package/src/audit/checks/content-freshness.ts +0 -170
  43. package/src/audit/checks/content-science.ts +0 -589
  44. package/src/audit/checks/conversion-elements.ts +0 -526
  45. package/src/audit/checks/crawlability.ts +0 -220
  46. package/src/audit/checks/directory-listing.ts +0 -172
  47. package/src/audit/checks/dom-analysis.ts +0 -191
  48. package/src/audit/checks/dom-size.ts +0 -246
  49. package/src/audit/checks/duplicate-content.ts +0 -194
  50. package/src/audit/checks/eeat-signals.ts +0 -990
  51. package/src/audit/checks/entity-seo.ts +0 -396
  52. package/src/audit/checks/featured-snippet.ts +0 -473
  53. package/src/audit/checks/freshness-signals.ts +0 -443
  54. package/src/audit/checks/funnel-intent.ts +0 -463
  55. package/src/audit/checks/hreflang.ts +0 -174
  56. package/src/audit/checks/html-compliance.ts +0 -302
  57. package/src/audit/checks/image-dimensions.ts +0 -167
  58. package/src/audit/checks/images.ts +0 -160
  59. package/src/audit/checks/indexnow.ts +0 -275
  60. package/src/audit/checks/interactive-tools.ts +0 -475
  61. package/src/audit/checks/internal-link-graph.ts +0 -436
  62. package/src/audit/checks/keyword-analysis.ts +0 -239
  63. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  64. package/src/audit/checks/keyword-placement.ts +0 -471
  65. package/src/audit/checks/links.ts +0 -273
  66. package/src/audit/checks/llms-txt.ts +0 -224
  67. package/src/audit/checks/local-seo.ts +0 -296
  68. package/src/audit/checks/mobile.ts +0 -167
  69. package/src/audit/checks/modern-images.ts +0 -226
  70. package/src/audit/checks/navboost-signals.ts +0 -395
  71. package/src/audit/checks/on-page.ts +0 -209
  72. package/src/audit/checks/page-resources.ts +0 -285
  73. package/src/audit/checks/pagination.ts +0 -180
  74. package/src/audit/checks/performance.ts +0 -153
  75. package/src/audit/checks/platform-presence.ts +0 -580
  76. package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
  77. package/src/audit/checks/rag-chunk-readiness.ts +0 -163
  78. package/src/audit/checks/redirect-analysis.ts +0 -153
  79. package/src/audit/checks/redirect-chain.ts +0 -389
  80. package/src/audit/checks/resource-hints.ts +0 -420
  81. package/src/audit/checks/responsive-css.ts +0 -247
  82. package/src/audit/checks/responsive-images.ts +0 -396
  83. package/src/audit/checks/review-ecosystem.ts +0 -415
  84. package/src/audit/checks/robots-validation.ts +0 -373
  85. package/src/audit/checks/security-headers.ts +0 -188
  86. package/src/audit/checks/security.ts +0 -144
  87. package/src/audit/checks/serp-preview.ts +0 -251
  88. package/src/audit/checks/site-maturity.ts +0 -453
  89. package/src/audit/checks/social-meta.test.ts +0 -275
  90. package/src/audit/checks/social-meta.ts +0 -134
  91. package/src/audit/checks/soft-404.ts +0 -151
  92. package/src/audit/checks/structured-data.ts +0 -238
  93. package/src/audit/checks/tech-detection.ts +0 -496
  94. package/src/audit/checks/topical-clusters.ts +0 -435
  95. package/src/audit/checks/tracker-bloat.ts +0 -462
  96. package/src/audit/checks/tracking-verification.test.ts +0 -371
  97. package/src/audit/checks/tracking-verification.ts +0 -636
  98. package/src/audit/checks/url-safety.ts +0 -682
  99. package/src/audit/deno-entry.ts +0 -66
  100. package/src/audit/discovery/index.ts +0 -15
  101. package/src/audit/discovery/link-crawler.ts +0 -233
  102. package/src/audit/discovery/repo-routes.ts +0 -347
  103. package/src/audit/engine.ts +0 -633
  104. package/src/audit/fixes/index.ts +0 -209
  105. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  106. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  107. package/src/audit/index.ts +0 -74
  108. package/src/audit/runner.test.ts +0 -299
  109. package/src/audit/runner.ts +0 -130
  110. package/src/audit/types.ts +0 -1987
  111. package/src/content/featured-snippet.ts +0 -367
  112. package/src/content/generator.test.ts +0 -534
  113. package/src/content/generator.ts +0 -501
  114. package/src/content/headline.ts +0 -317
  115. package/src/content/index.ts +0 -62
  116. package/src/content/intent.ts +0 -258
  117. package/src/content/keyword-density.ts +0 -349
  118. package/src/content/readability.ts +0 -262
  119. package/src/executor.ts +0 -336
  120. package/src/fixer/framework-fixes.test.ts +0 -489
  121. package/src/fixer/framework-fixes.ts +0 -5287
  122. package/src/fixer/index.ts +0 -6
  123. package/src/fixer/schemas.ts +0 -971
  124. package/src/fixer.ts +0 -642
  125. package/src/frameworks/detector.test.ts +0 -248
  126. package/src/frameworks/detector.ts +0 -899
  127. package/src/frameworks/index.ts +0 -68
  128. package/src/frameworks/recipes/angular.yaml +0 -171
  129. package/src/frameworks/recipes/astro.yaml +0 -206
  130. package/src/frameworks/recipes/django.yaml +0 -180
  131. package/src/frameworks/recipes/laravel.yaml +0 -137
  132. package/src/frameworks/recipes/nextjs.yaml +0 -268
  133. package/src/frameworks/recipes/nuxt.yaml +0 -175
  134. package/src/frameworks/recipes/rails.yaml +0 -188
  135. package/src/frameworks/recipes/react.yaml +0 -202
  136. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  137. package/src/frameworks/recipes/vue.yaml +0 -137
  138. package/src/frameworks/recipes/wordpress.yaml +0 -209
  139. package/src/frameworks/suggestion-engine.ts +0 -357
  140. package/src/geo/geo-content.test.ts +0 -305
  141. package/src/geo/geo-content.ts +0 -266
  142. package/src/geo/geo-history.test.ts +0 -473
  143. package/src/geo/geo-history.ts +0 -433
  144. package/src/geo/geo-tracker.test.ts +0 -359
  145. package/src/geo/geo-tracker.ts +0 -411
  146. package/src/geo/index.ts +0 -11
  147. package/src/geo/llm-citation-checker.ts +0 -188
  148. package/src/git/commit-helper.test.ts +0 -261
  149. package/src/git/commit-helper.ts +0 -329
  150. package/src/git/index.ts +0 -12
  151. package/src/git/pr-helper.test.ts +0 -284
  152. package/src/git/pr-helper.ts +0 -307
  153. package/src/index.ts +0 -76
  154. package/src/keywords/ai-keyword-engine.ts +0 -1062
  155. package/src/keywords/ai-summarizer.ts +0 -387
  156. package/src/keywords/ci-mode.ts +0 -555
  157. package/src/keywords/engine.ts +0 -359
  158. package/src/keywords/index.ts +0 -151
  159. package/src/keywords/llm-judge.ts +0 -357
  160. package/src/keywords/nlp-analysis.ts +0 -706
  161. package/src/keywords/prioritizer.ts +0 -295
  162. package/src/keywords/site-crawler.ts +0 -342
  163. package/src/keywords/sources/autocomplete.ts +0 -139
  164. package/src/keywords/sources/competitive-search.ts +0 -450
  165. package/src/keywords/sources/competitor-analysis.ts +0 -374
  166. package/src/keywords/sources/dataforseo.ts +0 -206
  167. package/src/keywords/sources/free-sources.ts +0 -294
  168. package/src/keywords/sources/gsc.ts +0 -123
  169. package/src/keywords/topic-grouping.ts +0 -327
  170. package/src/keywords/types.ts +0 -144
  171. package/src/keywords/wizard.ts +0 -457
  172. package/src/loader.ts +0 -40
  173. package/src/ranking/index.ts +0 -5
  174. package/src/ranking/serp-client.ts +0 -348
  175. package/src/ranking/tracker.ts +0 -380
  176. package/src/ranking/types.ts +0 -123
  177. package/src/reports/index.ts +0 -7
  178. package/src/reports/report-generator.test.ts +0 -293
  179. package/src/reports/report-generator.ts +0 -713
  180. package/src/scheduler/alerts.test.ts +0 -458
  181. package/src/scheduler/alerts.ts +0 -328
  182. package/src/scheduler/index.ts +0 -8
  183. package/src/scheduler/scheduled-audit.test.ts +0 -377
  184. package/src/scheduler/scheduled-audit.ts +0 -149
  185. package/src/test/integration-test.ts +0 -325
  186. package/src/tools/analyzer.ts +0 -373
  187. package/src/tools/crawl.ts +0 -293
  188. package/src/tools/files.ts +0 -301
  189. package/src/tools/h1-fixer.ts +0 -249
  190. package/src/tools/index.ts +0 -67
  191. package/src/tracking/github-action.ts +0 -326
  192. package/src/tracking/google-analytics.ts +0 -265
  193. package/src/tracking/index.ts +0 -45
  194. package/src/tracking/report-generator.ts +0 -386
  195. package/src/tracking/search-console.ts +0 -335
  196. package/src/types.ts +0 -148
  197. package/src/utils/http.ts +0 -302
  198. package/src/wasm-adapter.ts +0 -297
  199. package/src/wasm-entry.ts +0 -14
  200. package/tsconfig.json +0 -17
  201. package/tsup.wasm.config.ts +0 -26
  202. package/vitest.config.ts +0 -15
@@ -1,990 +0,0 @@
1
- // E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) Signals Detection
2
- // Based on Google's Quality Rater Guidelines and advanced SEO research
3
-
4
- import * as cheerio from 'cheerio';
5
- import type { AuditIssue } from '../types.js';
6
-
7
- export interface AuthorInfo {
8
- name: string | null;
9
- bio: string | null;
10
- credentials: string[];
11
- socialLinks: string[];
12
- hasSchema: boolean;
13
- photoPresent: boolean;
14
- }
15
-
16
- export interface OrganizationInfo {
17
- name: string | null;
18
- type: string | null;
19
- hasSchema: boolean;
20
- hasContactInfo: boolean;
21
- hasSocialProfiles: boolean;
22
- trustBadges: string[];
23
- }
24
-
25
- export interface ReviewInfo {
26
- hasReviewer: boolean;
27
- reviewerName: string | null;
28
- reviewerCredentials: string[];
29
- reviewDate: string | null;
30
- nextReviewDate: string | null;
31
- reviewType: 'medical' | 'expert' | 'fact-check' | 'editorial' | null;
32
- }
33
-
34
- export interface TrustBadgeInfo {
35
- pressFeatures: string[]; // "As seen in Forbes", "Featured in..."
36
- certifications: string[]; // "Clinically studied", "FDA approved"
37
- awards: string[];
38
- partnerships: string[];
39
- }
40
-
41
- export interface EEATData {
42
- author: AuthorInfo;
43
- organization: OrganizationInfo;
44
- reviewer: ReviewInfo;
45
- trustBadges: TrustBadgeInfo;
46
- experienceSignals: string[];
47
- expertiseSignals: string[];
48
- authoritySignals: string[];
49
- trustSignals: string[];
50
- citationCount: number;
51
- authoritativeSources: string[];
52
- contentDate: {
53
- published: string | null;
54
- modified: string | null;
55
- };
56
- hasVideoContent: boolean;
57
- videoExperienceSignals: string[];
58
- isYMYL: boolean;
59
- }
60
-
61
- // Credential patterns that indicate expertise
62
- const CREDENTIAL_PATTERNS = [
63
- /\b(MD|M\.D\.|Dr\.|Doctor)\b/i,
64
- /\b(PhD|Ph\.D\.|Doctorate)\b/i,
65
- /\b(JD|J\.D\.|Attorney|Lawyer|Esq\.)\b/i,
66
- /\b(CPA|Certified Public Accountant)\b/i,
67
- /\b(CFP|Certified Financial Planner)\b/i,
68
- /\b(RN|Registered Nurse)\b/i,
69
- /\b(PE|Professional Engineer)\b/i,
70
- /\b(MBA|Master of Business)\b/i,
71
- /\b(Professor|Prof\.)\b/i,
72
- /\b(Licensed|Certified|Accredited)\b/i,
73
- /\b(\d+\+?\s*years?\s*(of\s+)?experience)\b/i,
74
- ];
75
-
76
- // Trust badge patterns
77
- const TRUST_BADGE_PATTERNS = [
78
- /BBB\s*(Accredited|A\+|Rating)/i,
79
- /Norton\s*Secure/i,
80
- /McAfee\s*Secure/i,
81
- /TRUSTe/i,
82
- /SSL\s*(Secure|Certified)/i,
83
- /Verified\s*(by|Business)/i,
84
- /Award[\-\s]*(Winning|Winner)/i,
85
- /ISO\s*\d+/i,
86
- /HIPAA\s*Compliant/i,
87
- /GDPR\s*Compliant/i,
88
- /PCI[\-\s]*(DSS|Compliant)/i,
89
- /SOC\s*2/i,
90
- ];
91
-
92
- // Authoritative source domains
93
- const AUTHORITATIVE_DOMAINS = [
94
- 'wikipedia.org',
95
- 'gov',
96
- 'edu',
97
- 'nytimes.com',
98
- 'wsj.com',
99
- 'bbc.com',
100
- 'reuters.com',
101
- 'nature.com',
102
- 'science.org',
103
- 'pubmed.ncbi.nlm.nih.gov',
104
- 'who.int',
105
- 'cdc.gov',
106
- 'nih.gov',
107
- 'harvard.edu',
108
- 'stanford.edu',
109
- 'mit.edu',
110
- ];
111
-
112
- // YMYL (Your Money Your Life) topic indicators
113
- const YMYL_PATTERNS = [
114
- /\b(medical|health|disease|treatment|symptom|diagnosis|medication|drug)\b/i,
115
- /\b(financial|investment|tax|insurance|mortgage|credit|loan|retirement)\b/i,
116
- /\b(legal|attorney|lawyer|lawsuit|court|divorce|custody)\b/i,
117
- /\b(safety|emergency|danger|warning|crisis)\b/i,
118
- /\b(government|voting|election|law|regulation)\b/i,
119
- /\b(religion|ethnic|race|gender|sexuality)\b/i,
120
- /\b(buy|purchase|price|cost|shop)\b/i,
121
- ];
122
-
123
- /**
124
- * Extract author information from the page
125
- */
126
- export function extractAuthorInfo(html: string): AuthorInfo {
127
- const $ = cheerio.load(html);
128
- const info: AuthorInfo = {
129
- name: null,
130
- bio: null,
131
- credentials: [],
132
- socialLinks: [],
133
- hasSchema: false,
134
- photoPresent: false,
135
- };
136
-
137
- // Check for author schema
138
- $('script[type="application/ld+json"]').each((_, el) => {
139
- try {
140
- const data = JSON.parse($(el).html() || '');
141
- const checkAuthor = (obj: Record<string, unknown>) => {
142
- if (obj['@type'] === 'Person' || obj.author) {
143
- info.hasSchema = true;
144
- const author = obj.author || obj;
145
- if (typeof author === 'object' && author !== null) {
146
- info.name = (author as Record<string, string>).name || info.name;
147
- }
148
- }
149
- };
150
-
151
- if (Array.isArray(data)) {
152
- data.forEach(checkAuthor);
153
- } else {
154
- checkAuthor(data);
155
- }
156
- } catch {
157
- // Invalid JSON
158
- }
159
- });
160
-
161
- // Check for author meta tags
162
- const authorMeta = $('meta[name="author"]').attr('content');
163
- if (authorMeta) {
164
- info.name = info.name || authorMeta;
165
- }
166
-
167
- // Check for author link
168
- const authorLink = $('link[rel="author"]').attr('href');
169
- if (authorLink) {
170
- info.socialLinks.push(authorLink);
171
- }
172
-
173
- // Look for author byline patterns
174
- const bylineSelectors = [
175
- '.author',
176
- '.byline',
177
- '[rel="author"]',
178
- '.post-author',
179
- '.article-author',
180
- '.author-name',
181
- '.written-by',
182
- '[itemprop="author"]',
183
- ];
184
-
185
- for (const selector of bylineSelectors) {
186
- const el = $(selector).first();
187
- if (el.length) {
188
- const text = el.text().trim();
189
- if (text && text.length < 100) {
190
- info.name = info.name || text.replace(/^(by|written by|author:)\s*/i, '');
191
- }
192
- break;
193
- }
194
- }
195
-
196
- // Look for author bio
197
- const bioSelectors = ['.author-bio', '.author-description', '.about-author', '[itemprop="description"]'];
198
- for (const selector of bioSelectors) {
199
- const el = $(selector).first();
200
- if (el.length) {
201
- info.bio = el.text().trim().substring(0, 500);
202
- break;
203
- }
204
- }
205
-
206
- // Check for author photo
207
- info.photoPresent =
208
- $('img.author-photo, img.author-image, .author img, [itemprop="image"]').length > 0 ||
209
- $('img[alt*="author"], img[alt*="Author"]').length > 0;
210
-
211
- // Extract credentials from author name/bio
212
- const textToCheck = `${info.name || ''} ${info.bio || ''}`;
213
- for (const pattern of CREDENTIAL_PATTERNS) {
214
- const match = textToCheck.match(pattern);
215
- if (match) {
216
- info.credentials.push(match[0]);
217
- }
218
- }
219
-
220
- // Check for social profile links near author info
221
- $('.author a, .byline a, .author-social a').each((_, el) => {
222
- const href = $(el).attr('href');
223
- if (
224
- href &&
225
- (href.includes('linkedin.com') ||
226
- href.includes('twitter.com') ||
227
- href.includes('github.com') ||
228
- href.includes('medium.com'))
229
- ) {
230
- info.socialLinks.push(href);
231
- }
232
- });
233
-
234
- return info;
235
- }
236
-
237
- /**
238
- * Extract organization information
239
- */
240
- export function extractOrganizationInfo(html: string): OrganizationInfo {
241
- const $ = cheerio.load(html);
242
- const info: OrganizationInfo = {
243
- name: null,
244
- type: null,
245
- hasSchema: false,
246
- hasContactInfo: false,
247
- hasSocialProfiles: false,
248
- trustBadges: [],
249
- };
250
-
251
- // Check for organization schema
252
- $('script[type="application/ld+json"]').each((_, el) => {
253
- try {
254
- const data = JSON.parse($(el).html() || '');
255
- const checkOrg = (obj: Record<string, unknown>) => {
256
- const type = obj['@type'] as string;
257
- if (type === 'Organization' || type === 'LocalBusiness' || type === 'Corporation') {
258
- info.hasSchema = true;
259
- info.name = obj.name as string || info.name;
260
- info.type = type;
261
- if (obj.contactPoint || obj.telephone || obj.email) {
262
- info.hasContactInfo = true;
263
- }
264
- if (obj.sameAs && Array.isArray(obj.sameAs) && (obj.sameAs as string[]).length > 0) {
265
- info.hasSocialProfiles = true;
266
- }
267
- }
268
- };
269
-
270
- if (Array.isArray(data)) {
271
- data.forEach(checkOrg);
272
- } else {
273
- checkOrg(data);
274
- }
275
- } catch {
276
- // Invalid JSON
277
- }
278
- });
279
-
280
- // Check for contact information
281
- const hasContact =
282
- $('a[href^="tel:"]').length > 0 ||
283
- $('a[href^="mailto:"]').length > 0 ||
284
- $('.contact, #contact, [class*="contact"]').length > 0;
285
- info.hasContactInfo = info.hasContactInfo || hasContact;
286
-
287
- // Check for social profile links
288
- const socialLinks = $('a[href*="facebook.com"], a[href*="twitter.com"], a[href*="linkedin.com"], a[href*="instagram.com"]');
289
- info.hasSocialProfiles = info.hasSocialProfiles || socialLinks.length > 0;
290
-
291
- // Look for trust badges
292
- const pageText = $.text();
293
- for (const pattern of TRUST_BADGE_PATTERNS) {
294
- const match = pageText.match(pattern);
295
- if (match) {
296
- info.trustBadges.push(match[0]);
297
- }
298
- }
299
-
300
- return info;
301
- }
302
-
303
- /**
304
- * Detect experience signals (first-hand experience indicators)
305
- */
306
- export function detectExperienceSignals(html: string): string[] {
307
- const $ = cheerio.load(html);
308
- $('script, style').remove();
309
- const text = $('body').text();
310
- const signals: string[] = [];
311
-
312
- const experiencePatterns = [
313
- /\bI (tested|tried|used|reviewed|experienced|personally)\b/i,
314
- /\bin my experience\b/i,
315
- /\bwe (tested|tried|used|reviewed|found)\b/i,
316
- /\bfirst-hand\b/i,
317
- /\bhands-on\b/i,
318
- /\bI've been (using|doing|working)\b/i,
319
- /\bfor (\d+) years? I've\b/i,
320
- /\bour team (tested|reviewed|evaluated)\b/i,
321
- ];
322
-
323
- for (const pattern of experiencePatterns) {
324
- if (pattern.test(text)) {
325
- const match = text.match(pattern);
326
- if (match) {
327
- signals.push(match[0]);
328
- }
329
- }
330
- }
331
-
332
- // Check for original images (simplified - checking for non-stock patterns)
333
- const hasOriginalImages =
334
- $('img[src*="author"], img[src*="team"], img[src*="review"], img[src*="test"]').length > 0;
335
- if (hasOriginalImages) {
336
- signals.push('Original images present');
337
- }
338
-
339
- // Check for user reviews/testimonials
340
- const hasReviews =
341
- $('[class*="review"], [class*="testimonial"], [itemtype*="Review"]').length > 0;
342
- if (hasReviews) {
343
- signals.push('User reviews/testimonials present');
344
- }
345
-
346
- return signals;
347
- }
348
-
349
- /**
350
- * Detect expertise signals
351
- */
352
- export function detectExpertiseSignals(html: string): string[] {
353
- const $ = cheerio.load(html);
354
- const text = $('body').text();
355
- const signals: string[] = [];
356
-
357
- // Check for technical depth
358
- const technicalPatterns = [
359
- /\b(methodology|analysis|research|study|data)\b/i,
360
- /\b(according to|based on|supported by)\b/i,
361
- /\b(statistics|metrics|measurements)\b/i,
362
- ];
363
-
364
- for (const pattern of technicalPatterns) {
365
- if (pattern.test(text)) {
366
- signals.push('Technical/research depth');
367
- break;
368
- }
369
- }
370
-
371
- // Check for credentials in content
372
- for (const pattern of CREDENTIAL_PATTERNS) {
373
- if (pattern.test(text)) {
374
- const match = text.match(pattern);
375
- if (match) {
376
- signals.push(`Credential: ${match[0]}`);
377
- }
378
- }
379
- }
380
-
381
- // Check for "reviewed by" or "medically reviewed"
382
- if (/\b(reviewed by|fact-checked by|medically reviewed|expert reviewed)\b/i.test(text)) {
383
- signals.push('Expert review indicated');
384
- }
385
-
386
- return signals;
387
- }
388
-
389
- /**
390
- * Count and categorize external citations
391
- */
392
- export function analyzeCitations(html: string): { count: number; authoritativeSources: string[] } {
393
- const $ = cheerio.load(html);
394
- const authoritativeSources: string[] = [];
395
- let citationCount = 0;
396
-
397
- $('a[href^="http"]').each((_, el) => {
398
- const href = $(el).attr('href') || '';
399
- try {
400
- const url = new URL(href);
401
- const hostname = url.hostname.toLowerCase();
402
-
403
- // Check if it's an authoritative source
404
- for (const domain of AUTHORITATIVE_DOMAINS) {
405
- if (hostname.includes(domain) || hostname.endsWith(`.${domain}`)) {
406
- authoritativeSources.push(hostname);
407
- citationCount++;
408
- break;
409
- }
410
- }
411
- } catch {
412
- // Invalid URL
413
- }
414
- });
415
-
416
- return { count: citationCount, authoritativeSources: [...new Set(authoritativeSources)] };
417
- }
418
-
419
- /**
420
- * Extract reviewer information (medically reviewed, fact-checked, etc.)
421
- */
422
- export function extractReviewerInfo(html: string): ReviewInfo {
423
- const $ = cheerio.load(html);
424
- const text = $('body').text();
425
- const info: ReviewInfo = {
426
- hasReviewer: false,
427
- reviewerName: null,
428
- reviewerCredentials: [],
429
- reviewDate: null,
430
- nextReviewDate: null,
431
- reviewType: null,
432
- };
433
-
434
- // Check for "reviewed by" patterns
435
- const reviewPatterns = [
436
- /medically reviewed by\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i,
437
- /reviewed by\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i,
438
- /fact[- ]checked by\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i,
439
- /verified by\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i,
440
- /expert reviewed by\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i,
441
- ];
442
-
443
- for (const pattern of reviewPatterns) {
444
- const match = text.match(pattern);
445
- if (match) {
446
- info.hasReviewer = true;
447
- info.reviewerName = match[1];
448
-
449
- // Determine review type
450
- if (/medical/i.test(match[0])) {
451
- info.reviewType = 'medical';
452
- } else if (/fact[- ]check/i.test(match[0])) {
453
- info.reviewType = 'fact-check';
454
- } else if (/expert/i.test(match[0])) {
455
- info.reviewType = 'expert';
456
- } else {
457
- info.reviewType = 'editorial';
458
- }
459
-
460
- // Extract credentials if present near the name
461
- for (const credPattern of CREDENTIAL_PATTERNS) {
462
- const credMatch = text.substring(match.index || 0, (match.index || 0) + 200).match(credPattern);
463
- if (credMatch) {
464
- info.reviewerCredentials.push(credMatch[0]);
465
- }
466
- }
467
- break;
468
- }
469
- }
470
-
471
- // Check for review date patterns
472
- const reviewDatePatterns = [
473
- /last reviewed[:\s]+(\w+\s+\d{1,2},?\s+\d{4})/i,
474
- /reviewed on[:\s]+(\w+\s+\d{1,2},?\s+\d{4})/i,
475
- /review date[:\s]+(\d{4}-\d{2}-\d{2})/i,
476
- ];
477
-
478
- for (const pattern of reviewDatePatterns) {
479
- const match = text.match(pattern);
480
- if (match) {
481
- info.reviewDate = match[1];
482
- break;
483
- }
484
- }
485
-
486
- // Check for next review date (important for health content freshness)
487
- const nextReviewPatterns = [
488
- /next review[:\s]+(\w+\s+\d{4})/i,
489
- /due for review[:\s]+(\w+\s+\d{4})/i,
490
- /review due[:\s]+(\w+\s+\d{4})/i,
491
- ];
492
-
493
- for (const pattern of nextReviewPatterns) {
494
- const match = text.match(pattern);
495
- if (match) {
496
- info.nextReviewDate = match[1];
497
- break;
498
- }
499
- }
500
-
501
- // Check for reviewer elements
502
- const reviewerSelectors = [
503
- '.reviewed-by', '.fact-checker', '.medical-reviewer',
504
- '[class*="reviewer"]', '[class*="fact-check"]',
505
- ];
506
- for (const selector of reviewerSelectors) {
507
- const el = $(selector).first();
508
- if (el.length) {
509
- info.hasReviewer = true;
510
- const elText = el.text().trim();
511
- if (elText.length < 100 && !info.reviewerName) {
512
- info.reviewerName = elText.replace(/^(reviewed by|fact-checked by|verified by)\s*/i, '');
513
- }
514
- break;
515
- }
516
- }
517
-
518
- return info;
519
- }
520
-
521
- /**
522
- * Extract trust badges ("As seen in", "Clinically studied", awards, etc.)
523
- */
524
- export function extractTrustBadges(html: string): TrustBadgeInfo {
525
- const $ = cheerio.load(html);
526
- const text = $('body').text().toLowerCase();
527
- const htmlLower = html.toLowerCase();
528
-
529
- const info: TrustBadgeInfo = {
530
- pressFeatures: [],
531
- certifications: [],
532
- awards: [],
533
- partnerships: [],
534
- };
535
-
536
- // Press/media features ("As seen in", "Featured in")
537
- const pressPatterns = [
538
- /as seen (?:in|on)\s+([^.]+)/gi,
539
- /featured (?:in|on)\s+([^.]+)/gi,
540
- /covered by\s+([^.]+)/gi,
541
- /mentioned (?:in|by)\s+([^.]+)/gi,
542
- ];
543
-
544
- for (const pattern of pressPatterns) {
545
- let match;
546
- while ((match = pattern.exec(text)) !== null) {
547
- // Extract publication names
548
- const publications = match[1]
549
- .split(/,|\band\b/)
550
- .map(p => p.trim())
551
- .filter(p => p.length > 2 && p.length < 50);
552
- info.pressFeatures.push(...publications);
553
- }
554
- }
555
-
556
- // Check for press logo sections
557
- const pressSelectors = [
558
- '.as-seen-in', '.featured-in', '.press-logos', '.media-logos',
559
- '[class*="press"]', '[class*="featured"]', '.trust-badges',
560
- ];
561
- for (const selector of pressSelectors) {
562
- if ($(selector).length > 0) {
563
- // Check for common press outlet names in images
564
- $(`${selector} img`).each((_, el) => {
565
- const alt = $(el).attr('alt') || '';
566
- const src = $(el).attr('src') || '';
567
- const combined = (alt + src).toLowerCase();
568
- const outlets = ['forbes', 'nytimes', 'wsj', 'bbc', 'cnn', 'techcrunch', 'wired', 'bloomberg'];
569
- for (const outlet of outlets) {
570
- if (combined.includes(outlet) && !info.pressFeatures.includes(outlet)) {
571
- info.pressFeatures.push(outlet);
572
- }
573
- }
574
- });
575
- }
576
- }
577
-
578
- // Certifications and research backing
579
- const certPatterns = [
580
- { pattern: /clinically (?:studied|proven|tested)/gi, type: 'Clinically studied' },
581
- { pattern: /fda[- ](?:approved|cleared|registered)/gi, type: 'FDA approved' },
582
- { pattern: /dermatologist[- ](?:tested|recommended)/gi, type: 'Dermatologist tested' },
583
- { pattern: /doctor[- ](?:recommended|approved)/gi, type: 'Doctor recommended' },
584
- { pattern: /lab[- ]tested/gi, type: 'Lab tested' },
585
- { pattern: /third[- ]party[- ]tested/gi, type: 'Third-party tested' },
586
- { pattern: /scientifically[- ](?:proven|backed)/gi, type: 'Scientifically proven' },
587
- { pattern: /peer[- ]reviewed/gi, type: 'Peer reviewed' },
588
- { pattern: /iso[- ]\d+[- ]certified/gi, type: 'ISO certified' },
589
- { pattern: /organic[- ]certified/gi, type: 'Organic certified' },
590
- { pattern: /usda[- ]organic/gi, type: 'USDA Organic' },
591
- ];
592
-
593
- for (const { pattern, type } of certPatterns) {
594
- if (pattern.test(text)) {
595
- info.certifications.push(type);
596
- }
597
- }
598
-
599
- // Awards
600
- const awardPatterns = [
601
- /(?:won|received|awarded)\s+(?:the\s+)?([^.]+award)/gi,
602
- /award[- ]winning/gi,
603
- /best (?:of|in) \d{4}/gi,
604
- /editor'?s? choice/gi,
605
- /top[- ]rated/gi,
606
- /#1 (?:rated|ranked|best)/gi,
607
- ];
608
-
609
- for (const pattern of awardPatterns) {
610
- const match = text.match(pattern);
611
- if (match) {
612
- info.awards.push(match[0]);
613
- }
614
- }
615
-
616
- // Partnerships ("Trusted by", "Used by")
617
- const partnerPatterns = [
618
- /trusted by\s+([^.]+)/gi,
619
- /used by\s+([^.]+)/gi,
620
- /partnered? with\s+([^.]+)/gi,
621
- /official partner/gi,
622
- ];
623
-
624
- for (const pattern of partnerPatterns) {
625
- const match = text.match(pattern);
626
- if (match) {
627
- info.partnerships.push(match[0].substring(0, 100));
628
- }
629
- }
630
-
631
- // Deduplicate
632
- info.pressFeatures = [...new Set(info.pressFeatures)].slice(0, 10);
633
- info.certifications = [...new Set(info.certifications)];
634
- info.awards = [...new Set(info.awards)].slice(0, 5);
635
- info.partnerships = [...new Set(info.partnerships)].slice(0, 5);
636
-
637
- return info;
638
- }
639
-
640
- /**
641
- * Detect video content and video experience signals
642
- */
643
- export function detectVideoExperienceSignals(html: string): { hasVideo: boolean; signals: string[] } {
644
- const $ = cheerio.load(html);
645
- const signals: string[] = [];
646
-
647
- // Check for video elements
648
- const hasVideoElement = $('video').length > 0;
649
- const hasYouTubeEmbed = $('iframe[src*="youtube"], iframe[src*="youtu.be"]').length > 0;
650
- const hasVimeoEmbed = $('iframe[src*="vimeo"]').length > 0;
651
- const hasWistiaEmbed = $('script[src*="wistia"], [class*="wistia"]').length > 0;
652
- const hasVideoSchema = html.includes('"@type":"VideoObject"') || html.includes('"@type": "VideoObject"');
653
-
654
- const hasVideo = hasVideoElement || hasYouTubeEmbed || hasVimeoEmbed || hasWistiaEmbed;
655
-
656
- if (hasVideo) {
657
- signals.push('Video content present');
658
-
659
- if (hasVideoSchema) {
660
- signals.push('VideoObject schema present');
661
- }
662
-
663
- // Check for video context suggesting experience
664
- const videoContextSelectors = [
665
- '.video-demo', '.product-video', '.review-video', '.tutorial-video',
666
- '[class*="demo"]', '[class*="walkthrough"]', '[class*="how-to"]',
667
- ];
668
-
669
- for (const selector of videoContextSelectors) {
670
- if ($(selector).length > 0) {
671
- signals.push('Demonstration/tutorial video present');
672
- break;
673
- }
674
- }
675
-
676
- // Check video titles/descriptions for experience indicators
677
- const videoTitles = $('video, iframe').map((_, el) => {
678
- return $(el).attr('title') || $(el).attr('aria-label') || '';
679
- }).get().join(' ').toLowerCase();
680
-
681
- if (/review|test|demo|hands-on|unboxing|how-to|tutorial/.test(videoTitles)) {
682
- signals.push('Experience-based video content');
683
- }
684
- }
685
-
686
- // Check for video testimonials
687
- if ($('[class*="video-testimonial"], [class*="video-review"]').length > 0) {
688
- signals.push('Video testimonials present');
689
- }
690
-
691
- return { hasVideo, signals };
692
- }
693
-
694
- /**
695
- * Extract content dates
696
- */
697
- export function extractContentDates(html: string): { published: string | null; modified: string | null } {
698
- const $ = cheerio.load(html);
699
- let published: string | null = null;
700
- let modified: string | null = null;
701
-
702
- // Check schema
703
- $('script[type="application/ld+json"]').each((_, el) => {
704
- try {
705
- const data = JSON.parse($(el).html() || '');
706
- const checkDates = (obj: Record<string, unknown>) => {
707
- published = published || (obj.datePublished as string) || null;
708
- modified = modified || (obj.dateModified as string) || null;
709
- };
710
-
711
- if (Array.isArray(data)) {
712
- data.forEach(checkDates);
713
- } else {
714
- checkDates(data);
715
- }
716
- } catch {
717
- // Invalid JSON
718
- }
719
- });
720
-
721
- // Check meta tags
722
- published = published || $('meta[property="article:published_time"]').attr('content') || null;
723
- modified = modified || $('meta[property="article:modified_time"]').attr('content') || null;
724
-
725
- // Check time elements
726
- if (!published) {
727
- const timeEl = $('time[datetime]').first();
728
- published = timeEl.attr('datetime') || null;
729
- }
730
-
731
- return { published, modified };
732
- }
733
-
734
- /**
735
- * Detect if content is YMYL (Your Money Your Life)
736
- */
737
- export function detectYMYL(html: string, url: string): boolean {
738
- const $ = cheerio.load(html);
739
- const text = ($('title').text() + ' ' + $('h1').text() + ' ' + $('body').text()).toLowerCase();
740
-
741
- for (const pattern of YMYL_PATTERNS) {
742
- if (pattern.test(text)) {
743
- return true;
744
- }
745
- }
746
-
747
- // Check URL path for YMYL indicators
748
- const urlLower = url.toLowerCase();
749
- if (
750
- urlLower.includes('/health') ||
751
- urlLower.includes('/finance') ||
752
- urlLower.includes('/legal') ||
753
- urlLower.includes('/medical') ||
754
- urlLower.includes('/money')
755
- ) {
756
- return true;
757
- }
758
-
759
- return false;
760
- }
761
-
762
- /**
763
- * Main function: Analyze E-E-A-T signals
764
- */
765
- export function analyzeEEATSignals(
766
- html: string,
767
- url: string
768
- ): { issues: AuditIssue[]; data: EEATData } {
769
- const issues: AuditIssue[] = [];
770
-
771
- const author = extractAuthorInfo(html);
772
- const organization = extractOrganizationInfo(html);
773
- const reviewer = extractReviewerInfo(html);
774
- const trustBadges = extractTrustBadges(html);
775
- const experienceSignals = detectExperienceSignals(html);
776
- const expertiseSignals = detectExpertiseSignals(html);
777
- const citations = analyzeCitations(html);
778
- const contentDate = extractContentDates(html);
779
- const videoExperience = detectVideoExperienceSignals(html);
780
- const isYMYL = detectYMYL(html, url);
781
-
782
- // Build authority signals list
783
- const authoritySignals: string[] = [];
784
- if (organization.hasSchema) authoritySignals.push('Organization schema present');
785
- if (author.hasSchema) authoritySignals.push('Author schema present');
786
- if (citations.authoritativeSources.length > 0) {
787
- authoritySignals.push(`Citations to ${citations.authoritativeSources.length} authoritative sources`);
788
- }
789
-
790
- // Build trust signals list
791
- const trustSignals: string[] = [];
792
- if (organization.hasContactInfo) trustSignals.push('Contact information present');
793
- if (organization.trustBadges.length > 0) {
794
- trustSignals.push(`Trust badges: ${organization.trustBadges.join(', ')}`);
795
- }
796
- if (url.startsWith('https://')) trustSignals.push('HTTPS enabled');
797
-
798
- // Generate issues for YMYL content with missing E-E-A-T signals
799
- if (isYMYL) {
800
- if (!author.name && !author.hasSchema) {
801
- issues.push({
802
- code: 'YMYL_NO_AUTHOR',
803
- severity: 'warning',
804
- category: 'content',
805
- title: 'YMYL content without author information',
806
- description: 'This appears to be YMYL (Your Money Your Life) content but has no visible author.',
807
- impact: 'Google prioritizes E-E-A-T signals heavily for YMYL topics.',
808
- howToFix: 'Add author byline with credentials, bio, and author schema.',
809
- affectedUrls: [url],
810
- });
811
- }
812
-
813
- if (author.credentials.length === 0 && expertiseSignals.length === 0) {
814
- issues.push({
815
- code: 'YMYL_NO_EXPERTISE',
816
- severity: 'warning',
817
- category: 'content',
818
- title: 'YMYL content without expertise signals',
819
- description: 'YMYL content lacks visible expertise indicators (credentials, qualifications).',
820
- impact: 'Expertise is crucial for YMYL content ranking.',
821
- howToFix: 'Include author credentials, "reviewed by" statements, or cite expert sources.',
822
- affectedUrls: [url],
823
- });
824
- }
825
-
826
- if (citations.authoritativeSources.length === 0) {
827
- issues.push({
828
- code: 'YMYL_NO_CITATIONS',
829
- severity: 'warning',
830
- category: 'content',
831
- title: 'YMYL content without authoritative citations',
832
- description: 'YMYL content has no links to authoritative sources (.gov, .edu, etc.).',
833
- impact: 'Citing authoritative sources builds trust for sensitive topics.',
834
- howToFix: 'Add citations to government, academic, or recognized industry sources.',
835
- affectedUrls: [url],
836
- });
837
- }
838
- }
839
-
840
- // General E-E-A-T recommendations
841
- if (!author.name && !organization.name) {
842
- issues.push({
843
- code: 'NO_ENTITY_IDENTIFIED',
844
- severity: 'notice',
845
- category: 'content',
846
- title: 'No author or organization identified',
847
- description: 'Page lacks clear attribution to an author or organization.',
848
- impact: 'Anonymous content may be seen as less trustworthy.',
849
- howToFix: 'Add author byline or organization information with structured data.',
850
- affectedUrls: [url],
851
- });
852
- }
853
-
854
- if (!organization.hasSchema) {
855
- issues.push({
856
- code: 'NO_ORGANIZATION_SCHEMA',
857
- severity: 'notice',
858
- category: 'structured-data',
859
- title: 'Missing Organization schema',
860
- description: 'No Organization structured data found.',
861
- impact: 'Missed opportunity to establish organizational identity for E-E-A-T.',
862
- howToFix: 'Add Organization schema with name, logo, contact, and social profiles.',
863
- affectedUrls: [url],
864
- });
865
- }
866
-
867
- if (!author.hasSchema && author.name) {
868
- issues.push({
869
- code: 'AUTHOR_NO_SCHEMA',
870
- severity: 'notice',
871
- category: 'structured-data',
872
- title: 'Author without Person schema',
873
- description: 'Author name is present but no Person structured data.',
874
- impact: 'Person schema helps Google understand author entity relationships.',
875
- howToFix: 'Add Person schema with sameAs links to author profiles.',
876
- affectedUrls: [url],
877
- });
878
- }
879
-
880
- if (!contentDate.modified && contentDate.published) {
881
- issues.push({
882
- code: 'NO_MODIFIED_DATE',
883
- severity: 'notice',
884
- category: 'content',
885
- title: 'No content modification date',
886
- description: 'Content has published date but no last-modified date.',
887
- impact: 'Modified dates signal content freshness to search engines.',
888
- howToFix: 'Add article:modified_time meta tag or dateModified in schema.',
889
- affectedUrls: [url],
890
- });
891
- }
892
-
893
- if (experienceSignals.length === 0 && !isYMYL) {
894
- issues.push({
895
- code: 'NO_EXPERIENCE_SIGNALS',
896
- severity: 'notice',
897
- category: 'content',
898
- title: 'No first-hand experience signals detected',
899
- description: 'Content lacks indicators of personal/first-hand experience.',
900
- impact: 'The "Experience" in E-E-A-T values authentic, first-hand knowledge.',
901
- howToFix: 'Add personal insights, original photos, or hands-on testing details.',
902
- affectedUrls: [url],
903
- });
904
- }
905
-
906
- // Additional issues for enhanced E-E-A-T signals (2025 best practices)
907
-
908
- // YMYL content without medical/expert reviewer
909
- if (isYMYL && !reviewer.hasReviewer) {
910
- const $ = cheerio.load(html);
911
- const isHealthContent = /health|medical|disease|symptom|treatment/i.test($('title').text() + $('h1').text());
912
-
913
- if (isHealthContent) {
914
- issues.push({
915
- code: 'YMYL_NO_MEDICAL_REVIEWER',
916
- severity: 'warning',
917
- category: 'content',
918
- title: 'Health content without medical reviewer',
919
- description: 'Health-related content lacks "medically reviewed by" or expert reviewer information.',
920
- impact: 'Google strongly prefers health content reviewed by qualified medical professionals.',
921
- howToFix: 'Add "Medically reviewed by [Name, Credentials]" with reviewer bio and review date.',
922
- affectedUrls: [url],
923
- });
924
- }
925
- }
926
-
927
- // Content with reviewer but no review date
928
- if (reviewer.hasReviewer && !reviewer.reviewDate) {
929
- issues.push({
930
- code: 'REVIEWER_NO_DATE',
931
- severity: 'notice',
932
- category: 'content',
933
- title: 'Reviewer present but no review date',
934
- description: 'Content shows a reviewer but does not display when the review was conducted.',
935
- impact: 'Review dates help establish content freshness and credibility.',
936
- howToFix: 'Add "Last reviewed: [Date]" near the reviewer attribution.',
937
- affectedUrls: [url],
938
- });
939
- }
940
-
941
- // No press/authority badges for commercial content
942
- const isCommercial = /buy|price|shop|product|service/i.test(html);
943
- if (isCommercial && trustBadges.pressFeatures.length === 0 && trustBadges.certifications.length === 0) {
944
- issues.push({
945
- code: 'COMMERCIAL_NO_TRUST_BADGES',
946
- severity: 'notice',
947
- category: 'content',
948
- title: 'Commercial content without trust signals',
949
- description: 'Product/service page lacks "As seen in", certifications, or trust badges.',
950
- impact: 'Trust badges and press features build credibility and influence AI recommendations.',
951
- howToFix: 'Add "As seen in [Press]" logos, certifications, or customer testimonials with ratings.',
952
- affectedUrls: [url],
953
- });
954
- }
955
-
956
- // Video experience for product/review content
957
- const isReviewContent = /review|comparison|best|top \d+/i.test(html);
958
- if (isReviewContent && !videoExperience.hasVideo) {
959
- issues.push({
960
- code: 'REVIEW_NO_VIDEO',
961
- severity: 'notice',
962
- category: 'content',
963
- title: 'Review content without video',
964
- description: 'Product review or comparison content lacks video demonstrations.',
965
- impact: 'Video content signals first-hand experience, highly valued by Google and AI tools.',
966
- howToFix: 'Add hands-on video reviews showing actual product testing and demonstration.',
967
- affectedUrls: [url],
968
- });
969
- }
970
-
971
- return {
972
- issues,
973
- data: {
974
- author,
975
- organization,
976
- reviewer,
977
- trustBadges,
978
- experienceSignals: [...experienceSignals, ...videoExperience.signals],
979
- expertiseSignals,
980
- authoritySignals,
981
- trustSignals,
982
- citationCount: citations.count,
983
- authoritativeSources: citations.authoritativeSources,
984
- contentDate,
985
- hasVideoContent: videoExperience.hasVideo,
986
- videoExperienceSignals: videoExperience.signals,
987
- isYMYL,
988
- },
989
- };
990
- }