158 lines
No EOL
5.6 KiB
TypeScript
158 lines
No EOL
5.6 KiB
TypeScript
import PocketBase from 'pocketbase';
|
||
|
||
const PB_URL = 'http://127.0.0.1:8090';
|
||
const pb = new PocketBase(PB_URL);
|
||
|
||
interface Post {
|
||
id: string;
|
||
title: string;
|
||
content: string;
|
||
slug: string;
|
||
category: string;
|
||
draft: boolean;
|
||
}
|
||
|
||
interface AnalysisResult {
|
||
title: string;
|
||
slug: string;
|
||
category: string;
|
||
hasClaim: boolean;
|
||
hasTakeaway: boolean;
|
||
hasPronouns: boolean;
|
||
hasProof: boolean;
|
||
h2Issues: string[];
|
||
problems: string[];
|
||
}
|
||
|
||
function analyzePost(post: Post): AnalysisResult {
|
||
const result: AnalysisResult = {
|
||
title: post.title,
|
||
slug: post.slug,
|
||
category: post.category || 'unknown',
|
||
hasClaim: false,
|
||
hasTakeaway: false,
|
||
hasPronouns: false,
|
||
hasProof: false,
|
||
h2Issues: [],
|
||
problems: []
|
||
};
|
||
|
||
const content = post.content || '';
|
||
|
||
// Check for Claim patterns directly in content (including HTML)
|
||
// Look for new first paragraph that starts with Claim-like content
|
||
// Check first 1500 chars to catch posts where Claim may have been inserted
|
||
const claimPatterns = [
|
||
{ pattern: /За\s+\d+/i, name: 'За год' },
|
||
{ pattern: /В\s+\d+\s+(год|месяц)/i, name: 'В год/месяц' },
|
||
{ pattern: /\d+%/i, name: 'Процент' },
|
||
{ pattern: /Вы можете/i, name: 'Вы можете' },
|
||
];
|
||
|
||
const prefix = content.substring(0, 3000);
|
||
result.hasClaim = claimPatterns.some(p => p.pattern.test(prefix));
|
||
|
||
// Proof: contains numbers
|
||
const numberPatterns = [/\d+%/g, /\d+\s+(лет|месяцев|дней|тысяч|рублей)/gi];
|
||
result.hasProof = numberPatterns.some(p => p.test(content));
|
||
|
||
// Pronouns check
|
||
const pronouns = /\b(мы|наш|наша|наши|их|он|она|оно|этот|эта|эти)\b/gi;
|
||
result.hasPronouns = pronouns.test(content);
|
||
|
||
// Takeaway check
|
||
const takeawayPatterns = [
|
||
/что\s+делать/i,
|
||
/обращайтесь/i,
|
||
/позвоните/i,
|
||
/закажите/i,
|
||
/получите/i,
|
||
/напишите/i,
|
||
/свяжитесь/i
|
||
];
|
||
result.hasTakeaway = takeawayPatterns.some(p => p.test(content));
|
||
|
||
// H2 analysis - check all H2s in content
|
||
const h2Matches = content.match(/<h2[^>]*>[^<]+<\/h2>/gi) || [];
|
||
const badH2s = ['проблема', 'подход', 'ситуация', 'что было', 'решение', 'как помочь', 'поможет'];
|
||
h2Matches.forEach(h2 => {
|
||
const h2Text = h2.toLowerCase();
|
||
if (badH2s.some(b => h2Text.includes(b))) {
|
||
result.h2Issues.push(h2.replace(/<[^>]+>/g, ''));
|
||
}
|
||
});
|
||
|
||
// Collect problems
|
||
if (!result.hasClaim) result.problems.push('Нет Claim в первом абзаце');
|
||
if (!result.hasTakeaway) result.problems.push('Нет Takeaway');
|
||
if (result.hasPronouns) result.problems.push('Есть местоимения');
|
||
if (!result.hasProof) result.problems.push('Нет конкретных цифр');
|
||
if (result.h2Issues.length > 0) result.problems.push(`Размытые H2: ${result.h2Issues.join(', ')}`);
|
||
|
||
return result;
|
||
}
|
||
|
||
async function main() {
|
||
console.log('📊 Анализ постов блога по методологии Answer Unit\n');
|
||
console.log('='.repeat(60));
|
||
|
||
try {
|
||
const postsResult = await pb.collection('posts').getList(1, 500);
|
||
const posts = postsResult.items as unknown as Post[];
|
||
|
||
console.log(`Найдено постов: ${posts.length}\n`);
|
||
|
||
// Filter for non-draft posts
|
||
const activePosts = posts.filter(p => !p.draft);
|
||
console.log(`Опубликованных постов: ${activePosts.length}\n`);
|
||
|
||
const results = activePosts.map(analyzePost);
|
||
|
||
// Stats
|
||
const withClaim = results.filter(r => r.hasClaim).length;
|
||
const withTakeaway = results.filter(r => r.hasTakeaway).length;
|
||
const withPronouns = results.filter(r => r.hasPronouns).length;
|
||
const withProof = results.filter(r => r.hasProof).length;
|
||
const withH2Issues = results.filter(r => r.h2Issues.length > 0).length;
|
||
const problemCount = results.filter(r => r.problems.length > 0).length;
|
||
|
||
const n = activePosts.length;
|
||
console.log('📈 ОБЩАЯ СТАТИСТИКА:');
|
||
console.log('-'.repeat(40));
|
||
console.log(` ✓ Есть Claim: ${withClaim}/${n} (${n > 0 ? Math.round(withClaim/n*100) : 0}%)`);
|
||
console.log(` ✓ Есть Takeaway: ${withTakeaway}/${n} (${n > 0 ? Math.round(withTakeaway/n*100) : 0}%)`);
|
||
console.log(` ✗ Есть местоимения: ${withPronouns}/${n} (${n > 0 ? Math.round(withPronouns/n*100) : 0}%)`);
|
||
console.log(` ✓ Есть Proof (цифры): ${withProof}/${n} (${n > 0 ? Math.round(withProof/n*100) : 0}%)`);
|
||
console.log(` ✗ Проблемы с H2: ${withH2Issues}/${n}`);
|
||
console.log(` ⚠️ Постов с проблемами: ${problemCount}/${n}\n`);
|
||
|
||
// Problem posts
|
||
const problemPosts = results.filter(r => r.problems.length > 0);
|
||
|
||
if (problemPosts.length > 0) {
|
||
console.log('❌ ПОСТЫ С ПРОБЛЕМАМИ:');
|
||
console.log('-'.repeat(40));
|
||
problemPosts.forEach((r, i) => {
|
||
console.log(`\n${i+1}. ${r.title}`);
|
||
console.log(` slug: ${r.slug}`);
|
||
console.log(` category: ${r.category}`);
|
||
r.problems.forEach(p => console.log(` - ${p}`));
|
||
});
|
||
}
|
||
|
||
// Good posts
|
||
const goodPosts = results.filter(r => r.problems.length === 0);
|
||
if (goodPosts.length > 0) {
|
||
console.log('\n\n✅ ПОСТЫ БЕЗ ПРОБЛЕМ:');
|
||
console.log('-'.repeat(40));
|
||
goodPosts.forEach(r => {
|
||
console.log(` • ${r.title}`);
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
}
|
||
}
|
||
|
||
main(); |