110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
export type CopyAnalysis = {
|
|
length: number;
|
|
words: number;
|
|
sentences: number;
|
|
avgWordsPerSentence: number;
|
|
readability: number; // 0..100, higher = easier
|
|
keywordMatches: string[];
|
|
missingKeywords: string[];
|
|
seoScore: number; // 0..100
|
|
geoScore: number; // 0..100
|
|
seoTips: string[];
|
|
geoTips: string[];
|
|
};
|
|
|
|
const SYLLABLE_VOWELS = /[aeiouàèéìòùAEIOUÀÈÉÌÒÙ]+/g;
|
|
|
|
function countSyllables(word: string): number {
|
|
const matches = word.match(SYLLABLE_VOWELS);
|
|
return matches ? matches.length : 1;
|
|
}
|
|
|
|
export function analyzeCopy(text: string, primaryKeywords: string[] = []): CopyAnalysis {
|
|
const clean = text.trim();
|
|
const length = clean.length;
|
|
const wordList = clean.split(/\s+/).filter(Boolean);
|
|
const words = wordList.length;
|
|
const sentences = Math.max(1, (clean.match(/[.!?]+/g) ?? []).length);
|
|
const avgWordsPerSentence = words / sentences;
|
|
|
|
// Flesch reading ease approximation
|
|
const syllables = wordList.reduce((n, w) => n + countSyllables(w), 0);
|
|
const asl = words ? words / sentences : 0;
|
|
const asw = words ? syllables / words : 0;
|
|
const readability = Math.max(0, Math.min(100, Math.round(206.835 - 1.015 * asl - 84.6 * asw)));
|
|
|
|
const lower = clean.toLowerCase();
|
|
const keywordMatches: string[] = [];
|
|
const missingKeywords: string[] = [];
|
|
for (const kw of primaryKeywords) {
|
|
if (!kw) continue;
|
|
if (lower.includes(kw.toLowerCase())) keywordMatches.push(kw);
|
|
else missingKeywords.push(kw);
|
|
}
|
|
|
|
// SEO tips
|
|
const seoTips: string[] = [];
|
|
if (length === 0) seoTips.push('Il testo è vuoto.');
|
|
if (length > 0 && length < 40) seoTips.push('Testo molto breve: difficile farsi indicizzare.');
|
|
if (length > 320) seoTips.push('Testo lungo: valuta di spezzarlo in paragrafi o bullet.');
|
|
if (avgWordsPerSentence > 24) seoTips.push('Frasi troppo lunghe: scendi sotto le 24 parole per frase.');
|
|
if (readability < 45) seoTips.push('Leggibilità bassa: semplifica lessico e sintassi.');
|
|
if (missingKeywords.length && words > 4) {
|
|
seoTips.push(`Manca una keyword primaria: ${missingKeywords.slice(0, 2).join(', ')}.`);
|
|
}
|
|
if (/[A-Z]{6,}/.test(clean)) seoTips.push('Evita testo tutto maiuscolo: penalizza la leggibilità.');
|
|
|
|
// GEO tips (Generative Engine Optimization)
|
|
const geoTips: string[] = [];
|
|
const questionMarks = (clean.match(/\?/g) ?? []).length;
|
|
const hasNumbers = /\d/.test(clean);
|
|
const hasEntities = /\b(sassari|sardegna|coni|cammino di santiago|giampy)\b/i.test(clean);
|
|
const firstPerson = /\b(io|mio|mia|miei|mie|sono|faccio|porto|prendo)\b/i.test(clean);
|
|
const declarative = sentences >= 1 && /[.!]/.test(clean);
|
|
|
|
if (words >= 20 && questionMarks === 0)
|
|
geoTips.push('Aggiungi una domanda esplicita per farti citare come risposta diretta.');
|
|
if (!hasNumbers && words >= 20)
|
|
geoTips.push('Inserisci dati concreti (anni, numero clienti, distanze) per aumentare la citabilità.');
|
|
if (!hasEntities && words >= 15)
|
|
geoTips.push('Nomina entità specifiche (luogo, certificazione, brand) per il grounding generativo.');
|
|
if (!firstPerson && words >= 20)
|
|
geoTips.push('Usa la prima persona: i motori generativi estraggono più facilmente testimonianze autoriali.');
|
|
if (!declarative && words >= 10)
|
|
geoTips.push('Termina con punto fermo: le frasi complete vengono citate più spesso.');
|
|
|
|
// Scores
|
|
let seoScore = 100;
|
|
seoScore -= seoTips.length * 12;
|
|
if (keywordMatches.length === 0 && primaryKeywords.length && words > 4) seoScore -= 10;
|
|
if (readability >= 60) seoScore += 5;
|
|
seoScore = Math.max(0, Math.min(100, seoScore));
|
|
|
|
let geoScore = 100;
|
|
geoScore -= geoTips.length * 14;
|
|
if (hasEntities) geoScore += 5;
|
|
if (hasNumbers) geoScore += 3;
|
|
geoScore = Math.max(0, Math.min(100, geoScore));
|
|
|
|
return {
|
|
length,
|
|
words,
|
|
sentences,
|
|
avgWordsPerSentence: Math.round(avgWordsPerSentence * 10) / 10,
|
|
readability,
|
|
keywordMatches,
|
|
missingKeywords,
|
|
seoScore,
|
|
geoScore,
|
|
seoTips,
|
|
geoTips
|
|
};
|
|
}
|
|
|
|
export function aggregateScore(analyses: CopyAnalysis[]): { seo: number; geo: number } {
|
|
if (!analyses.length) return { seo: 0, geo: 0 };
|
|
const seo = Math.round(analyses.reduce((a, x) => a + x.seoScore, 0) / analyses.length);
|
|
const geo = Math.round(analyses.reduce((a, x) => a + x.geoScore, 0) / analyses.length);
|
|
return { seo, geo };
|
|
}
|