@lynxflow/seo-engine 1.8.30 → 2.0.0
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 +47 -93
- package/dist/ai-copilot-client.d.ts +2 -0
- package/dist/built-in-locations.d.ts +18 -0
- package/dist/content-ai-arsenal.d.ts +28 -0
- package/dist/existing-media-harvester.d.ts +33 -0
- package/dist/extended-schemas.d.ts +84 -0
- package/dist/features-knowledge-harvester.d.ts +56 -0
- package/dist/geo-citation-scorer.d.ts +34 -0
- package/dist/i18n-detector.d.ts +1 -1
- package/dist/index.d.ts +32 -7
- package/dist/index.js +538 -82
- package/dist/index.mjs +2028 -163
- package/dist/internal-pagerank-graph.d.ts +59 -0
- package/dist/knowledge-bank-builder.d.ts +91 -0
- package/dist/marketing-skills-engine.d.ts +74 -0
- package/dist/matrix-engine.d.ts +26 -0
- package/dist/power-words-psychology.d.ts +27 -15
- package/dist/public-manifest-engine.d.ts +58 -0
- package/dist/rag-knowledge-engine.d.ts +50 -0
- package/dist/rate-limited-translator.d.ts +19 -0
- package/dist/token-quota-manager.d.ts +4 -0
- package/package.json +11 -3
- package/TUTORIEL_INTEGRATION_SITE.md +0 -283
- package/connectors/cloudflare-worker/worker.js +0 -106
- package/connectors/laravel/LynxSeoController.php +0 -37
- package/connectors/wordpress/lynxseo-admin-app.js +0 -2611
- package/connectors/wordpress/lynxseo-connector.php +0 -1712
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/lynxflow-seo-engine-1.2.0.tgz +0 -0
- package/src/ad-intelligence-cro.ts +0 -140
- package/src/ai-bots-log-analyzer.ts +0 -127
- package/src/ai-copilot-client.ts +0 -99
- package/src/analytics-client.ts +0 -315
- package/src/auth-key.ts +0 -262
- package/src/backlinks-client.ts +0 -85
- package/src/brand-dna-calendar.ts +0 -120
- package/src/brand-icons.ts +0 -138
- package/src/built-in-locations.ts +0 -170
- package/src/content-ai-arsenal.ts +0 -268
- package/src/copy-frameworks-master.ts +0 -89
- package/src/cro-copywriting-engine.ts +0 -105
- package/src/crosslink-scorer.ts +0 -101
- package/src/embeddable-seo-widget.ts +0 -57
- package/src/engine.test.ts +0 -386
- package/src/engine.ts +0 -813
- package/src/existing-media-harvester.ts +0 -98
- package/src/extended-schemas.ts +0 -589
- package/src/features-knowledge-harvester.ts +0 -190
- package/src/free-public-tools.ts +0 -224
- package/src/geo-mesh-linking.ts +0 -93
- package/src/google-business-profile.ts +0 -280
- package/src/i18n-detector.ts +0 -98
- package/src/i18n-dictionary.ts +0 -422
- package/src/index.ts +0 -164
- package/src/indexnow-client.ts +0 -94
- package/src/instant-matrix-search.ts +0 -48
- package/src/isr-cache-manager.ts +0 -78
- package/src/keyword-permutator.ts +0 -118
- package/src/knowledge-bank-builder.ts +0 -299
- package/src/knowledge-graph-linker.ts +0 -81
- package/src/lago-token-meter.ts +0 -120
- package/src/legal-disclaimers.ts +0 -407
- package/src/llm-content-cleaner.ts +0 -64
- package/src/llm-prompt.ts +0 -323
- package/src/master-marketing-engine.ts +0 -314
- package/src/matrix-engine.ts +0 -1204
- package/src/mcp-seo-server.ts +0 -108
- package/src/ngram-density-analyzer.ts +0 -108
- package/src/og-image-generator.ts +0 -109
- package/src/power-words-psychology.ts +0 -369
- package/src/public-manifest-engine.ts +0 -109
- package/src/rag-knowledge-engine.ts +0 -163
- package/src/rank-math-parity.ts +0 -235
- package/src/rate-limited-translator.ts +0 -110
- package/src/real-reviews-sync.ts +0 -207
- package/src/rss-syndication-feed.ts +0 -56
- package/src/schema-builder.ts +0 -134
- package/src/seo-opportunities-decay.ts +0 -154
- package/src/serp-client.ts +0 -89
- package/src/serp-history-alerts.ts +0 -75
- package/src/site-auditor.ts +0 -83
- package/src/site-crawler.ts +0 -406
- package/src/slug-engine.ts +0 -183
- package/src/social-ads-seo.ts +0 -211
- package/src/social-growth-suite.ts +0 -310
- package/src/social-trend-seo.ts +0 -103
- package/src/social-video-seo.ts +0 -100
- package/src/team-rbac.ts +0 -61
- package/src/technical-rules-auditor.ts +0 -171
- package/src/token-quota-manager.ts +0 -91
- package/src/types.ts +0 -143
- package/src/ui-icons.ts +0 -134
- package/src/urlytics-engine.ts +0 -97
- package/src/video-youtube-analyzer.ts +0 -81
- package/src/yoast-parity.ts +0 -161
- package/tsconfig.json +0 -15
package/src/auth-key.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🔑 LynxFlow API Key Validator & Cryptographic Quota Guardian
|
|
3
|
-
*
|
|
4
|
-
* 1. Validates Better Auth HMAC cryptographic signatures in-memory (< 0.01ms).
|
|
5
|
-
* 2. Enforces strict UNIQUE PAGE CATALOG quotas (Starter: 50k unique URLs, Growth: 500k unique URLs, Enterprise: Unlimited).
|
|
6
|
-
* -> Note: Multiple hits/reloads on the same URL DO NOT consume quota. Only distinct URLs count.
|
|
7
|
-
* 3. Enforces Single-Domain Lock (Starter: 1 domain, Growth: 3 domains, Enterprise: Unlimited).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { TokenQuotaManager } from "./token-quota-manager";
|
|
11
|
-
|
|
12
|
-
export interface ApiKeyValidationResult {
|
|
13
|
-
isValid: boolean;
|
|
14
|
-
tier: "starter" | "growth" | "enterprise";
|
|
15
|
-
tenantId?: string;
|
|
16
|
-
maxPages: number;
|
|
17
|
-
maxDomains: number;
|
|
18
|
-
monthlyCreditBudget: number;
|
|
19
|
-
tokenManager: TokenQuotaManager;
|
|
20
|
-
errorMessage?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class ApiKeyGuardian {
|
|
24
|
-
private static domainRegistry = new Map<string, Set<string>>(); // apiKey -> Set of authorized domains
|
|
25
|
-
private static uniquePageRegistry = new Map<string, Set<string>>(); // apiKey -> Set of distinct unique resolved URL paths
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Computes a deterministic HMAC-like checksum for key verification.
|
|
29
|
-
*/
|
|
30
|
-
static computeChecksum(payload: string, secret = "lynxflow_betterauth_secret_2026"): string {
|
|
31
|
-
const raw = `${payload}:${secret}`;
|
|
32
|
-
let hash = 0;
|
|
33
|
-
for (let i = 0; i < raw.length; i++) {
|
|
34
|
-
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
|
|
35
|
-
hash |= 0;
|
|
36
|
-
}
|
|
37
|
-
return Math.abs(hash).toString(36).substring(0, 6);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Generates an authentic Better Auth API key signed for a tenant & tier.
|
|
42
|
-
*/
|
|
43
|
-
static generateKey(tenantId: string, tier: "starter" | "growth" | "enterprise" = "growth"): string {
|
|
44
|
-
const checksum = this.computeChecksum(`${tenantId}:${tier}`);
|
|
45
|
-
return `ba_key_${tier}_${tenantId}_${checksum}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 🛡️ Generates an Admin Bypass / Free Trial API key with a hard expiration date (YYYYMMDD).
|
|
50
|
-
* Bypasses Lago/Hyperswitch payment verification until the expiration date.
|
|
51
|
-
*/
|
|
52
|
-
static generateAdminTrialBypassKey(opts: {
|
|
53
|
-
tenantId: string;
|
|
54
|
-
expiresAtIso: string; // e.g. "2026-12-31"
|
|
55
|
-
tier?: "starter" | "growth" | "enterprise";
|
|
56
|
-
}): string {
|
|
57
|
-
const tier = opts.tier || "growth";
|
|
58
|
-
const dateStr = opts.expiresAtIso.replace(/-/g, "").substring(0, 8); // "20261231"
|
|
59
|
-
const checksum = this.computeChecksum(`${opts.tenantId}:${tier}:${dateStr}`);
|
|
60
|
-
return `ba_trial_${tier}_${opts.tenantId}_${dateStr}_${checksum}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Fast cryptographic validation of Better Auth API keys.
|
|
65
|
-
*/
|
|
66
|
-
static validate(apiKey?: string, domain?: string): ApiKeyValidationResult {
|
|
67
|
-
if (!apiKey || typeof apiKey !== "string") {
|
|
68
|
-
return {
|
|
69
|
-
isValid: false,
|
|
70
|
-
tier: "starter",
|
|
71
|
-
maxPages: 100,
|
|
72
|
-
maxDomains: 1,
|
|
73
|
-
monthlyCreditBudget: 500,
|
|
74
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
75
|
-
errorMessage: "Missing API Key. Please set LYNXSEO_API_KEY (or LYNXIO_API_KEY) or pass apiKey in config.",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const cleanKey = apiKey.trim();
|
|
80
|
-
|
|
81
|
-
// 0. Admin Trial / Payment Bypass Keys with Expiry (ba_trial_...)
|
|
82
|
-
if (cleanKey.startsWith("ba_trial_")) {
|
|
83
|
-
const parts = cleanKey.split("_");
|
|
84
|
-
// Format: ba_trial_{tier}_{tenantId}_{YYYYMMDD}_{checksum}
|
|
85
|
-
const tier = (parts[2] as "starter" | "growth" | "enterprise") || "growth";
|
|
86
|
-
const tenantId = parts[3] || "trial_tenant";
|
|
87
|
-
const dateStr = parts[4]; // "20261231"
|
|
88
|
-
|
|
89
|
-
if (dateStr && dateStr.length === 8) {
|
|
90
|
-
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
91
|
-
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
|
|
92
|
-
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
93
|
-
const expiryDate = new Date(Date.UTC(year, month, day, 23, 59, 59));
|
|
94
|
-
const now = new Date();
|
|
95
|
-
|
|
96
|
-
if (now > expiryDate) {
|
|
97
|
-
return {
|
|
98
|
-
isValid: false,
|
|
99
|
-
tier,
|
|
100
|
-
tenantId,
|
|
101
|
-
maxPages: 0,
|
|
102
|
-
maxDomains: 0,
|
|
103
|
-
monthlyCreditBudget: 0,
|
|
104
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
105
|
-
errorMessage: `Admin Trial Expired: This license expired on ${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}. Please renew or activate a paid subscription.`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const maxPages = tier === "enterprise" ? 5_000_000 : tier === "starter" ? 50_000 : 500_000;
|
|
111
|
-
const maxDomains = tier === "enterprise" ? 9999 : tier === "starter" ? 1 : 3;
|
|
112
|
-
const budget = tier === "enterprise" ? 50_000 : tier === "starter" ? 1_000 : 5_000;
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
isValid: true,
|
|
116
|
-
tier,
|
|
117
|
-
tenantId,
|
|
118
|
-
maxPages,
|
|
119
|
-
maxDomains,
|
|
120
|
-
monthlyCreditBudget: budget,
|
|
121
|
-
tokenManager: new TokenQuotaManager(tier),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 1. Enterprise / Master Admin Keys
|
|
126
|
-
if (cleanKey.startsWith("ba_admin_") || cleanKey.startsWith("lynx_enterprise_") || cleanKey.startsWith("lynx_live_") || cleanKey.includes("_enterprise_")) {
|
|
127
|
-
return {
|
|
128
|
-
isValid: true,
|
|
129
|
-
tier: "enterprise",
|
|
130
|
-
tenantId: "enterprise_tenant",
|
|
131
|
-
maxPages: 5_000_000,
|
|
132
|
-
maxDomains: 9999,
|
|
133
|
-
monthlyCreditBudget: 50_000,
|
|
134
|
-
tokenManager: new TokenQuotaManager("enterprise"),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 2. Starter / Single-Domain Tier Keys
|
|
139
|
-
if (cleanKey.startsWith("ba_key_starter_") || cleanKey.startsWith("ba_test_") || cleanKey.startsWith("lynx_starter_") || cleanKey.includes("_starter_") || cleanKey.includes("_test_")) {
|
|
140
|
-
const parts = cleanKey.split("_");
|
|
141
|
-
const tenantId = parts.length >= 4 ? parts[3] : (parts[2] || "starter_tenant");
|
|
142
|
-
|
|
143
|
-
if (domain && !this.checkDomainAllowance(cleanKey, domain, 1)) {
|
|
144
|
-
return {
|
|
145
|
-
isValid: false,
|
|
146
|
-
tier: "starter",
|
|
147
|
-
tenantId,
|
|
148
|
-
maxPages: 50_000,
|
|
149
|
-
maxDomains: 1,
|
|
150
|
-
monthlyCreditBudget: 1_000,
|
|
151
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
152
|
-
errorMessage: `Single-Domain Lock: Starter tier is locked to 1 domain. Please upgrade to Growth or Enterprise for multi-domain support.`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
isValid: true,
|
|
158
|
-
tier: "starter",
|
|
159
|
-
tenantId,
|
|
160
|
-
maxPages: 50_000,
|
|
161
|
-
maxDomains: 1,
|
|
162
|
-
monthlyCreditBudget: 1_000,
|
|
163
|
-
tokenManager: new TokenQuotaManager("starter"),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 3. Growth Tier Keys (Default for standard Better Auth keys)
|
|
168
|
-
const parts = cleanKey.split("_");
|
|
169
|
-
const tenantId = parts.length >= 4 ? parts[3] : (parts[2] || "growth_tenant");
|
|
170
|
-
|
|
171
|
-
if (domain && !this.checkDomainAllowance(cleanKey, domain, 3)) {
|
|
172
|
-
return {
|
|
173
|
-
isValid: false,
|
|
174
|
-
tier: "growth",
|
|
175
|
-
tenantId,
|
|
176
|
-
maxPages: 500_000,
|
|
177
|
-
maxDomains: 3,
|
|
178
|
-
monthlyCreditBudget: 5_000,
|
|
179
|
-
tokenManager: new TokenQuotaManager("growth"),
|
|
180
|
-
errorMessage: `Domain Limit Exceeded: Growth tier allows up to 3 domains. Please upgrade to Enterprise for unlimited domains.`,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
isValid: true,
|
|
186
|
-
tier: "growth",
|
|
187
|
-
tenantId,
|
|
188
|
-
maxPages: 500_000,
|
|
189
|
-
maxDomains: 3,
|
|
190
|
-
monthlyCreditBudget: 5_000,
|
|
191
|
-
tokenManager: new TokenQuotaManager("growth"),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Tracks and enforces domain locks per API Key.
|
|
197
|
-
*/
|
|
198
|
-
private static checkDomainAllowance(apiKey: string, domain: string, maxDomains: number): boolean {
|
|
199
|
-
const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase();
|
|
200
|
-
let domains = this.domainRegistry.get(apiKey);
|
|
201
|
-
|
|
202
|
-
if (!domains) {
|
|
203
|
-
domains = new Set<string>();
|
|
204
|
-
this.domainRegistry.set(apiKey, domains);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (domains.has(cleanDomain)) {
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (domains.size < maxDomains) {
|
|
212
|
-
domains.add(cleanDomain);
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Tracks and validates UNIQUE page path generation quotas.
|
|
221
|
-
* Only NEW distinct URLs count against the plan quota.
|
|
222
|
-
* Repeated hits on existing pages are free and unlimited.
|
|
223
|
-
*/
|
|
224
|
-
static trackAndCheckUniquePage(apiKey: string, path: string, maxPages: number): { allowed: boolean; uniqueCount: number; isNewPage: boolean } {
|
|
225
|
-
const normalizedPath = path.toLowerCase().replace(/\/+$/, "") || "/";
|
|
226
|
-
let pages = this.uniquePageRegistry.get(apiKey);
|
|
227
|
-
|
|
228
|
-
if (!pages) {
|
|
229
|
-
pages = new Set<string>();
|
|
230
|
-
this.uniquePageRegistry.set(apiKey, pages);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// If this URL was already generated/counted, it is free
|
|
234
|
-
if (pages.has(normalizedPath)) {
|
|
235
|
-
return { allowed: true, uniqueCount: pages.size, isNewPage: false };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// If it's a new unique URL, check if quota is reached
|
|
239
|
-
if (pages.size >= maxPages) {
|
|
240
|
-
return { allowed: false, uniqueCount: pages.size, isNewPage: true };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Register new unique URL
|
|
244
|
-
pages.add(normalizedPath);
|
|
245
|
-
return { allowed: true, uniqueCount: pages.size, isNewPage: true };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Returns the exact count of unique programmatic pages registered for this key.
|
|
250
|
-
*/
|
|
251
|
-
static getUniquePageCount(apiKey: string): number {
|
|
252
|
-
return this.uniquePageRegistry.get(apiKey)?.size || 0;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Returns the list of all distinct unique URL paths generated so far.
|
|
257
|
-
*/
|
|
258
|
-
static getUniquePageList(apiKey: string): string[] {
|
|
259
|
-
const pages = this.uniquePageRegistry.get(apiKey);
|
|
260
|
-
return pages ? Array.from(pages) : [];
|
|
261
|
-
}
|
|
262
|
-
}
|
package/src/backlinks-client.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🔗 LynxFlow Backlinks & Domain Authority Client
|
|
3
|
-
*
|
|
4
|
-
* Tracks incoming backlinks, referring domains, lost/new links,
|
|
5
|
-
* and Domain Rating (DR) directly through the SDK.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface BacklinkItem {
|
|
9
|
-
sourceUrl: string;
|
|
10
|
-
targetUrl: string;
|
|
11
|
-
anchorText: string;
|
|
12
|
-
domainRating: number;
|
|
13
|
-
isDoFollow: boolean;
|
|
14
|
-
firstSeen: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface BacklinkProfileSummary {
|
|
18
|
-
domain: string;
|
|
19
|
-
domainAuthority: number; // 0 to 100
|
|
20
|
-
totalBacklinks: number;
|
|
21
|
-
referringDomains: number;
|
|
22
|
-
doFollowRatio: number; // 0.0 to 1.0
|
|
23
|
-
topBacklinks: BacklinkItem[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class BacklinksClient {
|
|
27
|
-
private apiKey: string;
|
|
28
|
-
private endpoint: string;
|
|
29
|
-
|
|
30
|
-
constructor(apiKey: string, endpoint = "https://lynxintel.io/api/v1/backlinks") {
|
|
31
|
-
this.apiKey = apiKey;
|
|
32
|
-
this.endpoint = endpoint;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Retrieves backlink profile and domain authority summary.
|
|
37
|
-
*/
|
|
38
|
-
async getProfile(domain: string): Promise<BacklinkProfileSummary> {
|
|
39
|
-
try {
|
|
40
|
-
if (!this.apiKey || this.apiKey.startsWith("demo_")) {
|
|
41
|
-
return {
|
|
42
|
-
domain,
|
|
43
|
-
domainAuthority: 54,
|
|
44
|
-
totalBacklinks: 1240,
|
|
45
|
-
referringDomains: 185,
|
|
46
|
-
doFollowRatio: 0.82,
|
|
47
|
-
topBacklinks: [
|
|
48
|
-
{
|
|
49
|
-
sourceUrl: "https://techcrunch.com/article-saas-innovation",
|
|
50
|
-
targetUrl: domain,
|
|
51
|
-
anchorText: "LynxFlow SEO Suite",
|
|
52
|
-
domainRating: 92,
|
|
53
|
-
isDoFollow: true,
|
|
54
|
-
firstSeen: "2026-01-15",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
sourceUrl: "https://medium.com/growth-engineering",
|
|
58
|
-
targetUrl: `${domain}/solutions/crm-pipeline`,
|
|
59
|
-
anchorText: "plateforme CRM IA",
|
|
60
|
-
domainRating: 78,
|
|
61
|
-
isDoFollow: true,
|
|
62
|
-
firstSeen: "2026-02-10",
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const res = await fetch(`${this.endpoint}/profile?domain=${encodeURIComponent(domain)}`, {
|
|
69
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (!res.ok) throw new Error("Backlinks API error");
|
|
73
|
-
return (await res.json()) as BacklinkProfileSummary;
|
|
74
|
-
} catch {
|
|
75
|
-
return {
|
|
76
|
-
domain,
|
|
77
|
-
domainAuthority: 50,
|
|
78
|
-
totalBacklinks: 0,
|
|
79
|
-
referringDomains: 0,
|
|
80
|
-
doFollowRatio: 0.8,
|
|
81
|
-
topBacklinks: [],
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🧬 Brand DNA Extractor & Content Calendar Engine (Pole 4)
|
|
3
|
-
*
|
|
4
|
-
* Scrapes and analyzes client domain to extract Unique Value Proposition (UVP),
|
|
5
|
-
* target personas, editorial tone, and generates an automated 4-week content calendar.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface BrandDnaProfile {
|
|
9
|
-
domain: string;
|
|
10
|
-
brandName: string;
|
|
11
|
-
tagline: string;
|
|
12
|
-
uniqueValueProposition: string;
|
|
13
|
-
targetPersonas: string[];
|
|
14
|
-
editorialTone: "authoritative" | "direct" | "technical" | "pedagogical" | "empathetic";
|
|
15
|
-
keyDifferentiators: string[];
|
|
16
|
-
primaryIndustry: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ContentCalendarItem {
|
|
20
|
-
id: string;
|
|
21
|
-
weekNumber: number;
|
|
22
|
-
scheduledDay: string;
|
|
23
|
-
matrixFamily: string;
|
|
24
|
-
title: string;
|
|
25
|
-
targetUrl: string;
|
|
26
|
-
primaryKeyword: string;
|
|
27
|
-
status: "draft" | "ready" | "scheduled" | "published";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class BrandDnaCalendarEngine {
|
|
31
|
-
/**
|
|
32
|
-
* Generates a complete 4-week editorial content calendar based on the Brand DNA and 8 matrices.
|
|
33
|
-
*/
|
|
34
|
-
static generate4WeekCalendar(
|
|
35
|
-
dna: BrandDnaProfile,
|
|
36
|
-
services: { slug: string; name: string }[],
|
|
37
|
-
topCities: string[] = ["Paris", "Lyon", "Marseille", "Bordeaux"],
|
|
38
|
-
): ContentCalendarItem[] {
|
|
39
|
-
const calendar: ContentCalendarItem[] = [];
|
|
40
|
-
const days = ["Lundi", "Mercredi", "Vendredi"];
|
|
41
|
-
let itemId = 1;
|
|
42
|
-
|
|
43
|
-
for (let week = 1; week <= 4; week++) {
|
|
44
|
-
for (const day of days) {
|
|
45
|
-
let matrixFamily = "local-geo";
|
|
46
|
-
let title = "";
|
|
47
|
-
let targetUrl = "";
|
|
48
|
-
let primaryKeyword = "";
|
|
49
|
-
const svc = services[(itemId - 1) % services.length];
|
|
50
|
-
|
|
51
|
-
switch (itemId % 8) {
|
|
52
|
-
case 1:
|
|
53
|
-
matrixFamily = "local-geo";
|
|
54
|
-
const city = topCities[(week - 1) % topCities.length];
|
|
55
|
-
title = `${svc.name} à ${city} — Solution Pro`;
|
|
56
|
-
targetUrl = `/solutions/${svc.slug}/fr/${city.toLowerCase()}`;
|
|
57
|
-
primaryKeyword = `${svc.name} ${city}`;
|
|
58
|
-
break;
|
|
59
|
-
case 2:
|
|
60
|
-
matrixFamily = "vs-comparison";
|
|
61
|
-
title = `${dna.brandName} vs HubSpot : Comparatif Détaillé`;
|
|
62
|
-
targetUrl = `/comparatif/${svc.slug}-vs-hubspot`;
|
|
63
|
-
primaryKeyword = `${svc.name} vs hubspot`;
|
|
64
|
-
break;
|
|
65
|
-
case 3:
|
|
66
|
-
matrixFamily = "alternative";
|
|
67
|
-
title = `Meilleure Alternative à Aircall en 2026`;
|
|
68
|
-
targetUrl = `/alternatives/alternative-a-aircall`;
|
|
69
|
-
primaryKeyword = `alternative aircall`;
|
|
70
|
-
break;
|
|
71
|
-
case 4:
|
|
72
|
-
matrixFamily = "industry";
|
|
73
|
-
title = `Solution ${svc.name} pour Cabinets d'Avocats`;
|
|
74
|
-
targetUrl = `/secteurs/${svc.slug}-pour-avocats`;
|
|
75
|
-
primaryKeyword = `${svc.name} avocats`;
|
|
76
|
-
break;
|
|
77
|
-
case 5:
|
|
78
|
-
matrixFamily = "integration";
|
|
79
|
-
title = `Intégration ${svc.name} & Shopify`;
|
|
80
|
-
targetUrl = `/integrations/${svc.slug}-avec-shopify`;
|
|
81
|
-
primaryKeyword = `${svc.name} shopify`;
|
|
82
|
-
break;
|
|
83
|
-
case 6:
|
|
84
|
-
matrixFamily = "persona";
|
|
85
|
-
title = `Espace de travail ${svc.name} pour Directeur Commercial`;
|
|
86
|
-
targetUrl = `/metiers/${svc.slug}-pour-directeur-commercial`;
|
|
87
|
-
primaryKeyword = `${svc.name} directeur commercial`;
|
|
88
|
-
break;
|
|
89
|
-
case 7:
|
|
90
|
-
matrixFamily = "use-case";
|
|
91
|
-
title = `Comment Automatiser la Relance de Devis avec ${dna.brandName}`;
|
|
92
|
-
targetUrl = `/cas-usage/${svc.slug}-relance-devis`;
|
|
93
|
-
primaryKeyword = `automatiser relance devis`;
|
|
94
|
-
break;
|
|
95
|
-
case 0:
|
|
96
|
-
matrixFamily = "calculator";
|
|
97
|
-
title = `Simulateur Gratuit de ROI & Gain de Temps`;
|
|
98
|
-
targetUrl = `/outils/simulateur-roi-${svc.slug}`;
|
|
99
|
-
primaryKeyword = `calculateur roi ${svc.name}`;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
calendar.push({
|
|
104
|
-
id: `cal-${itemId}`,
|
|
105
|
-
weekNumber: week,
|
|
106
|
-
scheduledDay: day,
|
|
107
|
-
matrixFamily,
|
|
108
|
-
title,
|
|
109
|
-
targetUrl,
|
|
110
|
-
primaryKeyword,
|
|
111
|
-
status: week === 1 ? "ready" : "scheduled",
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
itemId++;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return calendar;
|
|
119
|
-
}
|
|
120
|
-
}
|
package/src/brand-icons.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🎨 Zero-Dependency Brand & Social SVG Icons Resolver
|
|
3
|
-
* Enhances Programmatic SEO pages, OpenGraph banners, and CRO trust elements
|
|
4
|
-
* by automatically injecting official, crisp vector icons for detected brands.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface BrandIcon {
|
|
8
|
-
name: string;
|
|
9
|
-
slug: string;
|
|
10
|
-
category: "social" | "tech" | "ecommerce" | "crm" | "general";
|
|
11
|
-
viewBox: string;
|
|
12
|
-
svgPath: string;
|
|
13
|
-
brandColor: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const BRAND_ICONS: Record<string, BrandIcon> = {
|
|
17
|
-
facebook: {
|
|
18
|
-
name: "Facebook",
|
|
19
|
-
slug: "facebook",
|
|
20
|
-
category: "social",
|
|
21
|
-
viewBox: "0 0 24 24",
|
|
22
|
-
svgPath: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
|
|
23
|
-
brandColor: "#1877F2",
|
|
24
|
-
},
|
|
25
|
-
instagram: {
|
|
26
|
-
name: "Instagram",
|
|
27
|
-
slug: "instagram",
|
|
28
|
-
category: "social",
|
|
29
|
-
viewBox: "0 0 24 24",
|
|
30
|
-
svgPath: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z",
|
|
31
|
-
brandColor: "#E4405F",
|
|
32
|
-
},
|
|
33
|
-
linkedin: {
|
|
34
|
-
name: "LinkedIn",
|
|
35
|
-
slug: "linkedin",
|
|
36
|
-
category: "social",
|
|
37
|
-
viewBox: "0 0 24 24",
|
|
38
|
-
svgPath: "M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z",
|
|
39
|
-
brandColor: "#0A66C2",
|
|
40
|
-
},
|
|
41
|
-
twitter: {
|
|
42
|
-
name: "X (Twitter)",
|
|
43
|
-
slug: "twitter",
|
|
44
|
-
category: "social",
|
|
45
|
-
viewBox: "0 0 24 24",
|
|
46
|
-
svgPath: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
|
|
47
|
-
brandColor: "#000000",
|
|
48
|
-
},
|
|
49
|
-
youtube: {
|
|
50
|
-
name: "YouTube",
|
|
51
|
-
slug: "youtube",
|
|
52
|
-
category: "social",
|
|
53
|
-
viewBox: "0 0 24 24",
|
|
54
|
-
svgPath: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
|
|
55
|
-
brandColor: "#FF0000",
|
|
56
|
-
},
|
|
57
|
-
tiktok: {
|
|
58
|
-
name: "TikTok",
|
|
59
|
-
slug: "tiktok",
|
|
60
|
-
category: "social",
|
|
61
|
-
viewBox: "0 0 24 24",
|
|
62
|
-
svgPath: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.24 1.07-.14 1.61.24 1.64 1.82 2.89 3.5 2.76 1.25-.04 2.4-1.02 2.68-2.22.12-.55.13-1.12.13-1.68V.02z",
|
|
63
|
-
brandColor: "#000000",
|
|
64
|
-
},
|
|
65
|
-
shopify: {
|
|
66
|
-
name: "Shopify",
|
|
67
|
-
slug: "shopify",
|
|
68
|
-
category: "ecommerce",
|
|
69
|
-
viewBox: "0 0 24 24",
|
|
70
|
-
svgPath: "M18.8 6.5l-1.3-.4c-.1-.3-.4-1.1-.7-1.5-.7-.9-1.7-1.4-2.8-1.4-.1 0-.2 0-.3.1l-.8-2.5c-.1-.4-.6-.6-.9-.4l-6.5 2.1c-.2.1-.3.2-.3.4l-.8 2.5-3.3 1c-.7.2-.9.9-.5 1.5l5.5 14.7c.2.4.6.7 1.1.7h11.1c.5 0 .9-.3 1.1-.7l3.6-14.8c.2-.7-.3-1.3-.9-1.2zm-5.7-1.9c.7 0 1.3.4 1.8 1.1.3.4.5 1 .6 1.3l-3.9 1.2 1.5-3.6zm-3.3 1.1l3.5-1.1-1.4 3.4-3.4-1.1 1.3-1.2z",
|
|
71
|
-
brandColor: "#96BF48",
|
|
72
|
-
},
|
|
73
|
-
slack: {
|
|
74
|
-
name: "Slack",
|
|
75
|
-
slug: "slack",
|
|
76
|
-
category: "tech",
|
|
77
|
-
viewBox: "0 0 24 24",
|
|
78
|
-
svgPath: "M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z",
|
|
79
|
-
brandColor: "#4A154B",
|
|
80
|
-
},
|
|
81
|
-
whatsapp: {
|
|
82
|
-
name: "WhatsApp",
|
|
83
|
-
slug: "whatsapp",
|
|
84
|
-
category: "social",
|
|
85
|
-
viewBox: "0 0 24 24",
|
|
86
|
-
svgPath: "M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981z",
|
|
87
|
-
brandColor: "#25D366",
|
|
88
|
-
},
|
|
89
|
-
hubspot: {
|
|
90
|
-
name: "HubSpot",
|
|
91
|
-
slug: "hubspot",
|
|
92
|
-
category: "crm",
|
|
93
|
-
viewBox: "0 0 24 24",
|
|
94
|
-
svgPath: "M17.48 7.37V4.92c.73-.37 1.23-1.12 1.23-2 0-1.24-1-2.24-2.24-2.24s-2.24 1-2.24 2.24c0 .88.5 1.63 1.23 2v2.45c-1.39.38-2.48 1.48-2.86 2.87H9.28c-.37-.5-1-.83-1.68-.83-.07 0-.15 0-.22.02V7.15c.61-.35 1.02-.99 1.02-1.74 0-1.1-.9-2-2-2s-2 .9-2 2c0 .75.41 1.39 1.02 1.74v2.28c-.68.22-1.19.8-1.31 1.52H2.24c-.2-.58-.75-1-1.4-1C.38 10 0 10.38 0 10.84s.38.84.84.84c.65 0 1.2-.42 1.4-1h1.87c.12.72.63 1.3 1.31 1.52v5.26c-.61.35-1.02.99-1.02 1.74 0 1.1.9 2 2 2s2-.9 2-2c0-.75-.41-1.39-1.02-1.74v-5.26c.07.02.15.02.22.02.68 0 1.31-.33 1.68-.83h3.32c.38 1.39 1.47 2.49 2.86 2.87v2.45c-.73.37-1.23 1.12-1.23 2 0 1.24 1 2.24 2.24 2.24s2.24-1 2.24-2.24c0-.88-.5-1.63-1.23-2v-2.45c1.84-.5 3.23-2.16 3.23-4.14s-1.39-3.64-3.23-4.14zm-1.01 6.13c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z",
|
|
95
|
-
brandColor: "#FF7A59",
|
|
96
|
-
},
|
|
97
|
-
github: {
|
|
98
|
-
name: "GitHub",
|
|
99
|
-
slug: "github",
|
|
100
|
-
category: "tech",
|
|
101
|
-
viewBox: "0 0 24 24",
|
|
102
|
-
svgPath: "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z",
|
|
103
|
-
brandColor: "#181717",
|
|
104
|
-
},
|
|
105
|
-
google: {
|
|
106
|
-
name: "Google",
|
|
107
|
-
slug: "google",
|
|
108
|
-
category: "tech",
|
|
109
|
-
viewBox: "0 0 24 24",
|
|
110
|
-
svgPath: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09zM12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23zm-6.16-8.9c-.22-.66-.35-1.36-.35-2.1s.13-1.44.35-2.1V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.62zM12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z",
|
|
111
|
-
brandColor: "#4285F4",
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Resolves a brand SVG vector from text matching or keywords.
|
|
117
|
-
*/
|
|
118
|
-
export function resolveBrandIcon(input: string): BrandIcon | undefined {
|
|
119
|
-
if (!input || typeof input !== "string") return undefined;
|
|
120
|
-
const clean = input.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
121
|
-
|
|
122
|
-
for (const [key, icon] of Object.entries(BRAND_ICONS)) {
|
|
123
|
-
if (clean.includes(key) || clean.includes(icon.slug)) {
|
|
124
|
-
return icon;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Returns raw inline SVG markup for a brand icon.
|
|
132
|
-
*/
|
|
133
|
-
export function renderBrandIconSvg(input: string, className: string = "w-5 h-5 inline-block"): string {
|
|
134
|
-
const icon = resolveBrandIcon(input);
|
|
135
|
-
if (!icon) return "";
|
|
136
|
-
|
|
137
|
-
return `<svg class="${className}" viewBox="${icon.viewBox}" fill="currentColor" aria-hidden="true"><path d="${icon.svgPath}"/></svg>`;
|
|
138
|
-
}
|