@lynxflow/seo-engine 1.4.1 → 1.5.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 +84 -339
- package/TUTORIEL_INTEGRATION_SITE.md +283 -0
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/dist/ad-intelligence-cro.d.ts +58 -0
- package/dist/ai-bots-log-analyzer.d.ts +36 -0
- package/dist/analytics-client.d.ts +48 -11
- package/dist/auth-key.d.ts +9 -0
- package/dist/brand-dna-calendar.d.ts +35 -0
- package/dist/copy-frameworks-master.d.ts +56 -0
- package/dist/cro-copywriting-engine.d.ts +46 -0
- package/dist/crosslink-scorer.d.ts +33 -0
- package/dist/embeddable-seo-widget.d.ts +12 -0
- package/dist/engine.d.ts +29 -1
- package/dist/engine.test.d.ts +1 -0
- package/dist/extended-schemas.d.ts +106 -0
- package/dist/geo-mesh-linking.d.ts +33 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +4173 -128
- package/dist/index.mjs +4101 -98
- package/dist/instant-matrix-search.d.ts +19 -0
- package/dist/isr-cache-manager.d.ts +35 -0
- package/dist/knowledge-graph-linker.d.ts +29 -0
- package/dist/legal-disclaimers.d.ts +46 -0
- package/dist/llm-content-cleaner.d.ts +16 -0
- package/dist/llm-prompt.d.ts +13 -0
- package/dist/master-marketing-engine.d.ts +114 -0
- package/dist/matrix-engine.d.ts +201 -0
- package/dist/mcp-seo-server.d.ts +21 -0
- package/dist/ngram-density-analyzer.d.ts +35 -0
- package/dist/rank-math-parity.d.ts +58 -0
- package/dist/real-reviews-sync.d.ts +73 -0
- package/dist/rss-syndication-feed.d.ts +25 -0
- package/dist/schema-builder.d.ts +6 -0
- package/dist/seo-opportunities-decay.d.ts +54 -0
- package/dist/serp-history-alerts.d.ts +27 -0
- package/dist/slug-engine.d.ts +32 -0
- package/dist/social-ads-seo.d.ts +77 -0
- package/dist/social-growth-suite.d.ts +125 -0
- package/dist/social-trend-seo.d.ts +38 -0
- package/dist/social-video-seo.d.ts +62 -0
- package/dist/team-rbac.d.ts +17 -0
- package/dist/technical-rules-auditor.d.ts +50 -0
- package/dist/types.d.ts +4 -44
- package/dist/yoast-parity.d.ts +45 -0
- package/package.json +8 -5
- package/src/ad-intelligence-cro.ts +140 -0
- package/src/ai-bots-log-analyzer.ts +127 -0
- package/src/analytics-client.ts +222 -48
- package/src/auth-key.ts +59 -0
- package/src/brand-dna-calendar.ts +120 -0
- package/src/copy-frameworks-master.ts +89 -0
- package/src/cro-copywriting-engine.ts +105 -0
- package/src/crosslink-scorer.ts +101 -0
- package/src/embeddable-seo-widget.ts +57 -0
- package/src/engine.test.ts +136 -0
- package/src/engine.ts +218 -27
- package/src/extended-schemas.ts +279 -0
- package/src/geo-mesh-linking.ts +93 -0
- package/src/index.ts +89 -0
- package/src/instant-matrix-search.ts +48 -0
- package/src/isr-cache-manager.ts +78 -0
- package/src/knowledge-graph-linker.ts +81 -0
- package/src/legal-disclaimers.ts +407 -0
- package/src/llm-content-cleaner.ts +64 -0
- package/src/llm-prompt.ts +81 -0
- package/src/master-marketing-engine.ts +314 -0
- package/src/matrix-engine.ts +818 -0
- package/src/mcp-seo-server.ts +108 -0
- package/src/ngram-density-analyzer.ts +108 -0
- package/src/rank-math-parity.ts +235 -0
- package/src/real-reviews-sync.ts +207 -0
- package/src/rss-syndication-feed.ts +56 -0
- package/src/schema-builder.ts +71 -24
- package/src/seo-opportunities-decay.ts +154 -0
- package/src/serp-history-alerts.ts +75 -0
- package/src/slug-engine.ts +183 -0
- package/src/social-ads-seo.ts +211 -0
- package/src/social-growth-suite.ts +310 -0
- package/src/social-trend-seo.ts +103 -0
- package/src/social-video-seo.ts +100 -0
- package/src/team-rbac.ts +61 -0
- package/src/technical-rules-auditor.ts +171 -0
- package/src/types.ts +22 -47
- package/src/yoast-parity.ts +161 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🎬 Video & Social SEO Analytics Engine (YouTube, TikTok, Instagram Reels)
|
|
3
|
+
*
|
|
4
|
+
* Tracks video rankings in Google Video Carousels and YouTube/TikTok Search,
|
|
5
|
+
* measures search attribution, and generates Google-compliant Schema.org `VideoObject` graphs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TrackedVideoRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
platform: "youtube" | "tiktok" | "instagram-reels";
|
|
11
|
+
title: string;
|
|
12
|
+
videoUrl: string;
|
|
13
|
+
thumbnailUrl: string;
|
|
14
|
+
views: number;
|
|
15
|
+
likes: number;
|
|
16
|
+
shares: number;
|
|
17
|
+
commentsCount: number;
|
|
18
|
+
durationSec: number;
|
|
19
|
+
publishedAt: string;
|
|
20
|
+
targetKeywords: string[];
|
|
21
|
+
googleVideoCarouselRank?: number; // e.g. #1 in Google Video Carousel
|
|
22
|
+
youtubeSearchRank?: number;
|
|
23
|
+
tiktokSearchRank?: number;
|
|
24
|
+
associatedLandingPageUrl?: string;
|
|
25
|
+
estimatedSearchVisitsGenerated: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface VideoSchemaOpts {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
thumbnailUrl: string;
|
|
32
|
+
uploadDate: string;
|
|
33
|
+
contentUrl: string;
|
|
34
|
+
embedUrl: string;
|
|
35
|
+
durationIso: string; // e.g. "PT1M45S"
|
|
36
|
+
transcript?: string;
|
|
37
|
+
chapters?: { name: string; startOffsetSeconds: number }[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SocialVideoSeoAnalyticsEngine {
|
|
41
|
+
/**
|
|
42
|
+
* Generates a Google-verified Schema.org `VideoObject` with chapters (Clip / SeekToAction).
|
|
43
|
+
*/
|
|
44
|
+
static buildVideoObjectSchema(opts: VideoSchemaOpts): Record<string, unknown> {
|
|
45
|
+
const schema: Record<string, unknown> = {
|
|
46
|
+
"@context": "https://schema.org",
|
|
47
|
+
"@type": "VideoObject",
|
|
48
|
+
name: opts.name,
|
|
49
|
+
description: opts.description,
|
|
50
|
+
thumbnailUrl: [opts.thumbnailUrl],
|
|
51
|
+
uploadDate: opts.uploadDate,
|
|
52
|
+
contentUrl: opts.contentUrl,
|
|
53
|
+
embedUrl: opts.embedUrl,
|
|
54
|
+
duration: opts.durationIso,
|
|
55
|
+
transcript: opts.transcript,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (opts.chapters && opts.chapters.length > 0) {
|
|
59
|
+
schema.hasPart = opts.chapters.map((ch, idx) => ({
|
|
60
|
+
"@type": "Clip",
|
|
61
|
+
name: ch.name,
|
|
62
|
+
startOffset: ch.startOffsetSeconds,
|
|
63
|
+
endOffset:
|
|
64
|
+
idx < opts.chapters!.length - 1
|
|
65
|
+
? opts.chapters![idx + 1].startOffsetSeconds
|
|
66
|
+
: ch.startOffsetSeconds + 30,
|
|
67
|
+
url: `${opts.contentUrl}?t=${ch.startOffsetSeconds}`,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return schema;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Analyzes an array of videos to calculate total social reach and search traffic attribution.
|
|
76
|
+
*/
|
|
77
|
+
static calculateVideoPortfolioStats(videos: TrackedVideoRecord[]) {
|
|
78
|
+
const totalViews = videos.reduce((acc, v) => acc + v.views, 0);
|
|
79
|
+
const totalEngagement = videos.reduce((acc, v) => acc + (v.likes + v.shares + v.commentsCount), 0);
|
|
80
|
+
const inGoogleCarouselCount = videos.filter((v) => Boolean(v.googleVideoCarouselRank)).length;
|
|
81
|
+
const totalSearchVisits = videos.reduce((acc, v) => acc + v.estimatedSearchVisitsGenerated, 0);
|
|
82
|
+
|
|
83
|
+
const platformBreakdown = {
|
|
84
|
+
youtube: videos.filter((v) => v.platform === "youtube").length,
|
|
85
|
+
tiktok: videos.filter((v) => v.platform === "tiktok").length,
|
|
86
|
+
instagram: videos.filter((v) => v.platform === "instagram-reels").length,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
totalVideosTracked: videos.length,
|
|
91
|
+
totalViews,
|
|
92
|
+
totalEngagement,
|
|
93
|
+
avgEngagementRate: totalViews > 0 ? Math.round((totalEngagement / totalViews) * 1000) / 10 : 0,
|
|
94
|
+
inGoogleCarouselCount,
|
|
95
|
+
totalSearchVisits,
|
|
96
|
+
platformBreakdown,
|
|
97
|
+
topPerformingVideos: [...videos].sort((a, b) => b.views - a.views).slice(0, 5),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/team-rbac.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 👥 Team IAM & Multi-User RBAC Engine (Issue #87)
|
|
3
|
+
*
|
|
4
|
+
* Provides granular permissions per team member across SEO projects:
|
|
5
|
+
* - `owner`: Full project control, billing, domain deletion.
|
|
6
|
+
* - `admin`: Add/remove tracked keywords, run crawls, edit AI prompts.
|
|
7
|
+
* - `editor`: View rankings, generate pSEO pages, run audits.
|
|
8
|
+
* - `viewer`: Read-only access to dashboards and CSV exports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type TeamRole = "owner" | "admin" | "editor" | "viewer";
|
|
12
|
+
|
|
13
|
+
export type PermissionAction =
|
|
14
|
+
| "project:delete"
|
|
15
|
+
| "project:update"
|
|
16
|
+
| "keywords:add"
|
|
17
|
+
| "keywords:delete"
|
|
18
|
+
| "keywords:export"
|
|
19
|
+
| "audit:run"
|
|
20
|
+
| "pseo:generate"
|
|
21
|
+
| "ai_copilot:execute";
|
|
22
|
+
|
|
23
|
+
const ROLE_PERMISSIONS: Record<TeamRole, PermissionAction[]> = {
|
|
24
|
+
owner: [
|
|
25
|
+
"project:delete",
|
|
26
|
+
"project:update",
|
|
27
|
+
"keywords:add",
|
|
28
|
+
"keywords:delete",
|
|
29
|
+
"keywords:export",
|
|
30
|
+
"audit:run",
|
|
31
|
+
"pseo:generate",
|
|
32
|
+
"ai_copilot:execute",
|
|
33
|
+
],
|
|
34
|
+
admin: [
|
|
35
|
+
"project:update",
|
|
36
|
+
"keywords:add",
|
|
37
|
+
"keywords:delete",
|
|
38
|
+
"keywords:export",
|
|
39
|
+
"audit:run",
|
|
40
|
+
"pseo:generate",
|
|
41
|
+
"ai_copilot:execute",
|
|
42
|
+
],
|
|
43
|
+
editor: [
|
|
44
|
+
"keywords:add",
|
|
45
|
+
"keywords:export",
|
|
46
|
+
"audit:run",
|
|
47
|
+
"pseo:generate",
|
|
48
|
+
"ai_copilot:execute",
|
|
49
|
+
],
|
|
50
|
+
viewer: ["keywords:export"],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class TeamRbacEngine {
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a user role has permission to perform an action.
|
|
56
|
+
*/
|
|
57
|
+
static can(role: TeamRole, action: PermissionAction): boolean {
|
|
58
|
+
const permissions = ROLE_PERMISSIONS[role] || [];
|
|
59
|
+
return permissions.includes(action);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔬 Technical SEO Rules Auditor (Inspiration: seonaut/internal/issues/)
|
|
3
|
+
*
|
|
4
|
+
* Implements strict, production-grade automated checks:
|
|
5
|
+
* 1. Heading Hierarchy Integrity (Missing H1, Multiple H1s, Hierarchy Gaps like H1->H3)
|
|
6
|
+
* 2. Redirect Chain & Loop Detection (301/302 > 1 hop)
|
|
7
|
+
* 3. Thin Content Detection (< 300 words)
|
|
8
|
+
* 4. Canonical URL Auditing (Missing, relative, mismatched)
|
|
9
|
+
* 5. Hreflang Symmetrical Validation (Missing return tags)
|
|
10
|
+
* 6. Image Optimization (Missing Alt text, heavy assets > 100KB)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PageAuditInput {
|
|
14
|
+
url: string;
|
|
15
|
+
statusCode: number;
|
|
16
|
+
headings: { tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; text: string }[];
|
|
17
|
+
wordCount: number;
|
|
18
|
+
canonicalUrl?: string;
|
|
19
|
+
hreflangs?: { lang: string; url: string }[];
|
|
20
|
+
images?: { src: string; alt?: string; sizeBytes?: number }[];
|
|
21
|
+
redirectChain?: { url: string; statusCode: number }[];
|
|
22
|
+
timeToFirstByteMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type IssueSeverity = "critical" | "warning" | "info";
|
|
26
|
+
|
|
27
|
+
export interface TechnicalIssue {
|
|
28
|
+
code: string;
|
|
29
|
+
category: "headings" | "redirects" | "content" | "canonical" | "hreflang" | "images" | "performance";
|
|
30
|
+
severity: IssueSeverity;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
recommendation: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TechnicalRulesAuditor {
|
|
37
|
+
/**
|
|
38
|
+
* Audits a single page payload across all technical rules.
|
|
39
|
+
*/
|
|
40
|
+
static auditPage(page: PageAuditInput): TechnicalIssue[] {
|
|
41
|
+
const issues: TechnicalIssue[] = [];
|
|
42
|
+
|
|
43
|
+
// 1. Heading Hierarchy Audit
|
|
44
|
+
const h1s = page.headings.filter((h) => h.tag === "h1");
|
|
45
|
+
if (h1s.length === 0) {
|
|
46
|
+
issues.push({
|
|
47
|
+
code: "HEADING_H1_MISSING",
|
|
48
|
+
category: "headings",
|
|
49
|
+
severity: "critical",
|
|
50
|
+
title: "Balise <h1> manquante",
|
|
51
|
+
description: `Aucune balise <h1> trouvée sur ${page.url}.`,
|
|
52
|
+
recommendation: "Ajoutez exactement une balise <h1> contenant votre mot-clé principal.",
|
|
53
|
+
});
|
|
54
|
+
} else if (h1s.length > 1) {
|
|
55
|
+
issues.push({
|
|
56
|
+
code: "HEADING_H1_MULTIPLE",
|
|
57
|
+
category: "headings",
|
|
58
|
+
severity: "warning",
|
|
59
|
+
title: "Multiples balises <h1> détectées",
|
|
60
|
+
description: `La page contient ${h1s.length} balises <h1>.`,
|
|
61
|
+
recommendation: "Conservez une seule balise <h1> principale et convertissez les autres en <h2>.",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check heading hierarchy gaps (e.g. H1 followed immediately by H3 without H2)
|
|
66
|
+
for (let i = 0; i < page.headings.length - 1; i++) {
|
|
67
|
+
const currentLevel = parseInt(page.headings[i].tag.replace("h", ""), 10);
|
|
68
|
+
const nextLevel = parseInt(page.headings[i + 1].tag.replace("h", ""), 10);
|
|
69
|
+
if (nextLevel - currentLevel > 1) {
|
|
70
|
+
issues.push({
|
|
71
|
+
code: "HEADING_HIERARCHY_GAP",
|
|
72
|
+
category: "headings",
|
|
73
|
+
severity: "warning",
|
|
74
|
+
title: "Saut dans la hiérarchie des titres",
|
|
75
|
+
description: `Passage direct de <${page.headings[i].tag}> à <${page.headings[i + 1].tag}> sans niveau intermédiaire.`,
|
|
76
|
+
recommendation: "Respectez l'arborescence logique des titres (H1 ➔ H2 ➔ H3).",
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Redirect Chain & Loop Audit
|
|
83
|
+
if (page.redirectChain && page.redirectChain.length > 1) {
|
|
84
|
+
const isLoop = new Set(page.redirectChain.map((r) => r.url)).size < page.redirectChain.length;
|
|
85
|
+
if (isLoop) {
|
|
86
|
+
issues.push({
|
|
87
|
+
code: "REDIRECT_LOOP",
|
|
88
|
+
category: "redirects",
|
|
89
|
+
severity: "critical",
|
|
90
|
+
title: "Boucle de redirection infinie",
|
|
91
|
+
description: `Boucle de redirection détectée sur ${page.url}.`,
|
|
92
|
+
recommendation: "Corrigez la configuration de redirection pour pointer directement vers l'URL finale.",
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
issues.push({
|
|
96
|
+
code: "REDIRECT_CHAIN",
|
|
97
|
+
category: "redirects",
|
|
98
|
+
severity: "warning",
|
|
99
|
+
title: "Chaîne de redirection (> 1 saut)",
|
|
100
|
+
description: `La redirection traverse ${page.redirectChain.length} étapes successives.`,
|
|
101
|
+
recommendation: "Redirigez directement de l'URL initiale vers la destination finale en 1 seul saut 301.",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Thin Content Detection (< 300 words)
|
|
107
|
+
if (page.wordCount < 300 && page.statusCode === 200) {
|
|
108
|
+
issues.push({
|
|
109
|
+
code: "CONTENT_THIN",
|
|
110
|
+
category: "content",
|
|
111
|
+
severity: "critical",
|
|
112
|
+
title: "Contenu trop faible (Thin Content)",
|
|
113
|
+
description: `La page ne contient que ${page.wordCount} mots (seuil minimum recommandé : 300 mots).`,
|
|
114
|
+
recommendation: "Enrichissez la page avec du contenu textuel informatif, FAQs et preuves sociales.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Canonical Auditing
|
|
119
|
+
if (!page.canonicalUrl && page.statusCode === 200) {
|
|
120
|
+
issues.push({
|
|
121
|
+
code: "CANONICAL_MISSING",
|
|
122
|
+
category: "canonical",
|
|
123
|
+
severity: "warning",
|
|
124
|
+
title: "Balise canonique manquante",
|
|
125
|
+
description: `Aucune balise <link rel="canonical"> n'est déclarée.`,
|
|
126
|
+
recommendation: "Ajoutez une URL canonique absolue pointant vers la version officielle de la page.",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 5. Image Optimization & Alt Attributes
|
|
131
|
+
if (page.images) {
|
|
132
|
+
const missingAlts = page.images.filter((img) => !img.alt || img.alt.trim() === "");
|
|
133
|
+
if (missingAlts.length > 0) {
|
|
134
|
+
issues.push({
|
|
135
|
+
code: "IMAGES_ALT_MISSING",
|
|
136
|
+
category: "images",
|
|
137
|
+
severity: "warning",
|
|
138
|
+
title: "Images sans texte alternatif (alt)",
|
|
139
|
+
description: `${missingAlts.length} image(s) n'ont pas d'attribut alt renseigné.`,
|
|
140
|
+
recommendation: "Renseignez des descriptions textuelles précises pour l'accessibilité et Google Images.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const heavyImages = page.images.filter((img) => (img.sizeBytes || 0) > 100_000);
|
|
145
|
+
if (heavyImages.length > 0) {
|
|
146
|
+
issues.push({
|
|
147
|
+
code: "IMAGES_HEAVY",
|
|
148
|
+
category: "images",
|
|
149
|
+
severity: "info",
|
|
150
|
+
title: "Images trop lourdes (> 100 KB)",
|
|
151
|
+
description: `${heavyImages.length} image(s) dépassent 100 KB.`,
|
|
152
|
+
recommendation: "Convertissez vos images au format moderne WebP / AVIF et compressez-les.",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 6. Time To First Byte (TTFB)
|
|
158
|
+
if (page.timeToFirstByteMs && page.timeToFirstByteMs > 500) {
|
|
159
|
+
issues.push({
|
|
160
|
+
code: "PERF_HIGH_TTFB",
|
|
161
|
+
category: "performance",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
title: "Temps de réponse serveur élevé (TTFB > 500ms)",
|
|
164
|
+
description: `Le TTFB mesuré est de ${page.timeToFirstByteMs}ms.`,
|
|
165
|
+
recommendation: "Activez la mise en cache Edge CDN (Cloudflare) ou l'ISR Next.js pour passer sous 50ms.",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return issues;
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -22,54 +22,27 @@ export interface LynxServiceDefinition {
|
|
|
22
22
|
faqs?: { question: string; answer: string }[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
conversionBoost?: string;
|
|
36
|
-
}
|
|
25
|
+
import type {
|
|
26
|
+
PseoIndustry,
|
|
27
|
+
PseoCompetitor,
|
|
28
|
+
PseoIntegration,
|
|
29
|
+
PseoUseCase,
|
|
30
|
+
PseoPersona,
|
|
31
|
+
PseoService,
|
|
32
|
+
PseoLocation,
|
|
33
|
+
PseoCalculator,
|
|
34
|
+
} from "./matrix-engine";
|
|
37
35
|
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface PseoIntegration {
|
|
50
|
-
slug: string;
|
|
51
|
-
name: string;
|
|
52
|
-
category: string;
|
|
53
|
-
badge?: string;
|
|
54
|
-
syncFeatures?: string[];
|
|
55
|
-
setupTimeMinutes?: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface PseoUseCase {
|
|
59
|
-
slug: string;
|
|
60
|
-
title: string;
|
|
61
|
-
problem: string;
|
|
62
|
-
aiSolution: string;
|
|
63
|
-
measurableOutcome?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface PseoPersona {
|
|
67
|
-
slug: string;
|
|
68
|
-
role: string;
|
|
69
|
-
department: string;
|
|
70
|
-
topResponsibilities?: string[];
|
|
71
|
-
timeWastersAutomated?: string[];
|
|
72
|
-
}
|
|
36
|
+
export type {
|
|
37
|
+
PseoIndustry,
|
|
38
|
+
PseoCompetitor,
|
|
39
|
+
PseoIntegration,
|
|
40
|
+
PseoUseCase,
|
|
41
|
+
PseoPersona,
|
|
42
|
+
PseoService,
|
|
43
|
+
PseoLocation,
|
|
44
|
+
PseoCalculator,
|
|
45
|
+
};
|
|
73
46
|
|
|
74
47
|
export interface PseoCity {
|
|
75
48
|
slug: string;
|
|
@@ -106,6 +79,8 @@ export interface LynxSeoConfig {
|
|
|
106
79
|
personas?: PseoPersona[];
|
|
107
80
|
useCases?: PseoUseCase[];
|
|
108
81
|
cities?: PseoCity[];
|
|
82
|
+
locations?: PseoLocation[];
|
|
83
|
+
phone?: string;
|
|
109
84
|
}
|
|
110
85
|
|
|
111
86
|
export interface LynxResolvedPage {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🟢 Yoast SEO Parity & Real-Time Readability Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements the iconic Yoast SEO features (13M+ installs):
|
|
5
|
+
* 1. Readability Analysis (Analyse de Lisibilité aux pastilles Vertes/Oranges/Rouges) :
|
|
6
|
+
* - Flesch Reading Ease Formula (Score de lisibilité)
|
|
7
|
+
* - Transition Words Detection (Mots de transition > 30%)
|
|
8
|
+
* - Passive Voice Detection (Voix passive < 10%)
|
|
9
|
+
* - Sentence Length Distribution (Phrases > 20 mots < 25%)
|
|
10
|
+
* - Paragraph Length (< 150 mots par paragraphe)
|
|
11
|
+
* - Subheading Distribution (< 300 mots sous un sous-titre)
|
|
12
|
+
* 2. Google SERP Pixel Width Calculator (Titre max 600px, Description max 960px)
|
|
13
|
+
* 3. Cornerstone Content (Contenu Pilier & Priorisation du Maillage)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface YoastReadabilityBullet {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
status: "green" | "orange" | "red"; // Pastille verte, orange ou rouge
|
|
20
|
+
scoreValue: number;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface YoastAnalysisResult {
|
|
25
|
+
overallReadability: "green" | "orange" | "red";
|
|
26
|
+
fleschReadingScore: number; // 0 to 100 (60-70 = standard/facile)
|
|
27
|
+
fleschGrade: string;
|
|
28
|
+
transitionWordsPercent: number;
|
|
29
|
+
passiveVoicePercent: number;
|
|
30
|
+
longSentencesPercent: number;
|
|
31
|
+
bullets: YoastReadabilityBullet[];
|
|
32
|
+
serpPreview: {
|
|
33
|
+
titlePixelWidth: number;
|
|
34
|
+
titlePixelLimit: 600;
|
|
35
|
+
titleIsTruncated: boolean;
|
|
36
|
+
descPixelWidth: number;
|
|
37
|
+
descPixelLimit: 960;
|
|
38
|
+
descIsTruncated: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class YoastParityEngine {
|
|
43
|
+
private static frenchTransitionWords = [
|
|
44
|
+
"en effet", "de plus", "par conséquent", "cependant", "ainsi", "notamment", "en outre",
|
|
45
|
+
"d'ailleurs", "toutefois", "en revanche", "néanmoins", "c'est pourquoi", "en conclusion",
|
|
46
|
+
"par exemple", "premièrement", "deuxièmement", "en fait", "grâce à", "donc", "car", "puisque"
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 1. Full Yoast-Style Readability Analysis.
|
|
51
|
+
*/
|
|
52
|
+
static analyzeReadability(text: string, title = "", description = ""): YoastAnalysisResult {
|
|
53
|
+
const plainText = text.replace(/<[^>]+>/g, " ").replace(/[#*`_~]/g, " ").trim();
|
|
54
|
+
const sentences = plainText.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
55
|
+
const words = plainText.split(/\s+/).filter((w) => w.length > 0);
|
|
56
|
+
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
57
|
+
|
|
58
|
+
const totalWords = Math.max(1, words.length);
|
|
59
|
+
const totalSentences = Math.max(1, sentences.length);
|
|
60
|
+
|
|
61
|
+
// 1. Flesch Reading Ease (Formule adaptée au français : 207 - (1.015 * ASL) - (84.6 * ASW))
|
|
62
|
+
const avgSentenceLength = totalWords / totalSentences;
|
|
63
|
+
// Estimation des syllabes (~1.4 syllabes par mot en français moyen)
|
|
64
|
+
const estimatedSyllables = words.reduce((acc, word) => acc + Math.max(1, word.length / 3), 0);
|
|
65
|
+
const avgSyllablesPerWord = estimatedSyllables / totalWords;
|
|
66
|
+
const fleschScore = Math.max(0, Math.min(100, Math.round(207 - (1.015 * avgSentenceLength) - (84.6 * avgSyllablesPerWord))));
|
|
67
|
+
|
|
68
|
+
let fleschGrade = "Très facile à lire";
|
|
69
|
+
if (fleschScore < 30) fleschGrade = "Très difficile (académique)";
|
|
70
|
+
else if (fleschScore < 50) fleschGrade = "Difficile";
|
|
71
|
+
else if (fleschScore < 60) fleschGrade = "Moyen";
|
|
72
|
+
else if (fleschScore < 70) fleschGrade = "Facile (Idéal pour le web)";
|
|
73
|
+
|
|
74
|
+
// 2. Transition Words Detection (> 30% des phrases)
|
|
75
|
+
let transitionCount = 0;
|
|
76
|
+
for (const sentence of sentences) {
|
|
77
|
+
const lower = sentence.toLowerCase();
|
|
78
|
+
if (this.frenchTransitionWords.some((tw) => lower.includes(tw))) {
|
|
79
|
+
transitionCount++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const transitionPercent = Math.round((transitionCount / totalSentences) * 100);
|
|
83
|
+
|
|
84
|
+
// 3. Long Sentences (> 20 mots)
|
|
85
|
+
const longSentences = sentences.filter((s) => s.split(/\s+/).length > 20).length;
|
|
86
|
+
const longSentencesPercent = Math.round((longSentences / totalSentences) * 100);
|
|
87
|
+
|
|
88
|
+
// 4. Passive Voice Approximation (formules passives en français : être + participe)
|
|
89
|
+
const passiveRegex = /\b(est|sont|été|fut|furent|sera|seront|était|étaient)\s+([a-zéèêëàâîïôûüç]+(é|és|ée|ées|i|is|ie|ies|u|us|ue|ues))\b/gi;
|
|
90
|
+
const passiveMatches = plainText.match(passiveRegex) || [];
|
|
91
|
+
const passivePercent = Math.round((passiveMatches.length / totalSentences) * 100);
|
|
92
|
+
|
|
93
|
+
// Bullets checklist (Pastilles Yoast)
|
|
94
|
+
const bullets: YoastReadabilityBullet[] = [];
|
|
95
|
+
|
|
96
|
+
// Bullet Flesch
|
|
97
|
+
bullets.push({
|
|
98
|
+
id: "flesch_reading_ease",
|
|
99
|
+
label: "Facilité de lecture Flesch",
|
|
100
|
+
status: fleschScore >= 55 ? "green" : fleschScore >= 45 ? "orange" : "red",
|
|
101
|
+
scoreValue: fleschScore,
|
|
102
|
+
message: `Score Flesch : ${fleschScore}/100 (${fleschGrade}).`,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Bullet Transitions
|
|
106
|
+
bullets.push({
|
|
107
|
+
id: "transition_words",
|
|
108
|
+
label: "Mots de transition",
|
|
109
|
+
status: transitionPercent >= 30 ? "green" : transitionPercent >= 20 ? "orange" : "red",
|
|
110
|
+
scoreValue: transitionPercent,
|
|
111
|
+
message: `${transitionPercent}% des phrases contiennent des mots de transition (Recommandé : > 30%).`,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Bullet Sentence Length
|
|
115
|
+
bullets.push({
|
|
116
|
+
id: "sentence_length",
|
|
117
|
+
label: "Longueur des phrases",
|
|
118
|
+
status: longSentencesPercent <= 25 ? "green" : longSentencesPercent <= 35 ? "orange" : "red",
|
|
119
|
+
scoreValue: longSentencesPercent,
|
|
120
|
+
message: `${longSentencesPercent}% des phrases dépassent 20 mots (Recommandé : < 25%).`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Bullet Passive Voice
|
|
124
|
+
bullets.push({
|
|
125
|
+
id: "passive_voice",
|
|
126
|
+
label: "Voix passive",
|
|
127
|
+
status: passivePercent <= 10 ? "green" : passivePercent <= 18 ? "orange" : "red",
|
|
128
|
+
scoreValue: passivePercent,
|
|
129
|
+
message: `${passivePercent}% de voix passive (Recommandé : < 10%).`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Overall status
|
|
133
|
+
const redCount = bullets.filter((b) => b.status === "red").length;
|
|
134
|
+
const orangeCount = bullets.filter((b) => b.status === "orange").length;
|
|
135
|
+
let overallReadability: "green" | "orange" | "red" = "green";
|
|
136
|
+
if (redCount >= 2) overallReadability = "red";
|
|
137
|
+
else if (redCount === 1 || orangeCount >= 2) overallReadability = "orange";
|
|
138
|
+
|
|
139
|
+
// 5. SERP Pixel Width (Approximation : ~9.5px par caractère pour le titre Arial 18px)
|
|
140
|
+
const titlePixelWidth = Math.round(title.length * 9.2);
|
|
141
|
+
const descPixelWidth = Math.round(description.length * 5.8);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
overallReadability,
|
|
145
|
+
fleschReadingScore: fleschScore,
|
|
146
|
+
fleschGrade,
|
|
147
|
+
transitionWordsPercent: transitionPercent,
|
|
148
|
+
passiveVoicePercent: passivePercent,
|
|
149
|
+
longSentencesPercent: longSentencesPercent,
|
|
150
|
+
bullets,
|
|
151
|
+
serpPreview: {
|
|
152
|
+
titlePixelWidth,
|
|
153
|
+
titlePixelLimit: 600,
|
|
154
|
+
titleIsTruncated: titlePixelWidth > 600,
|
|
155
|
+
descPixelWidth,
|
|
156
|
+
descPixelLimit: 960,
|
|
157
|
+
descIsTruncated: descPixelWidth > 960,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|