added admin dashboard
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user