style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -12,433 +12,460 @@ const DATA_DIR = './data/seo';
// Wichtige Keywords zum Tracken
const TRACKED_KEYWORDS = [
'meeting protokoll software',
'ki transkription',
'automatische protokollierung',
'gesprächsprotokoll app',
'meeting dokumentation',
'spracherkennung meetings',
'protokoll automatisch erstellen',
'memoro'
'meeting protokoll software',
'ki transkription',
'automatische protokollierung',
'gesprächsprotokoll app',
'meeting dokumentation',
'spracherkennung meetings',
'protokoll automatisch erstellen',
'memoro',
];
// Wichtige Seiten zum Tracken
const TRACKED_PAGES = [
'/de/meeting-protokoll-software',
'/de/',
'/de/features',
'/de/pricing',
'/de/blog'
'/de/meeting-protokoll-software',
'/de/',
'/de/features',
'/de/pricing',
'/de/blog',
];
class SEOTracker {
constructor() {
this.auth = null;
this.searchConsole = null;
}
constructor() {
this.auth = null;
this.searchConsole = null;
}
async initialize() {
// Auth Setup
const auth = new google.auth.GoogleAuth({
keyFile: CREDENTIALS_FILE,
scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
});
async initialize() {
// Auth Setup
const auth = new google.auth.GoogleAuth({
keyFile: CREDENTIALS_FILE,
scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
});
this.auth = await auth.getClient();
this.searchConsole = google.searchconsole({
version: 'v1',
auth: this.auth,
});
this.auth = await auth.getClient();
this.searchConsole = google.searchconsole({
version: 'v1',
auth: this.auth,
});
// Erstelle Data Directory wenn nicht vorhanden
await fs.mkdir(DATA_DIR, { recursive: true });
}
// Erstelle Data Directory wenn nicht vorhanden
await fs.mkdir(DATA_DIR, { recursive: true });
}
// Hole Performance-Daten für Keywords
async getKeywordPerformance(startDate, endDate) {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['query', 'page', 'country', 'device'],
dimensionFilterGroups: [{
filters: [{
dimension: 'query',
operator: 'contains',
expression: 'protokoll'
}]
}],
rowLimit: 1000,
dataState: 'final'
}
});
// Hole Performance-Daten für Keywords
async getKeywordPerformance(startDate, endDate) {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['query', 'page', 'country', 'device'],
dimensionFilterGroups: [
{
filters: [
{
dimension: 'query',
operator: 'contains',
expression: 'protokoll',
},
],
},
],
rowLimit: 1000,
dataState: 'final',
},
});
return response.data.rows || [];
}
return response.data.rows || [];
}
// Hole Performance-Daten für spezifische Seiten
async getPagePerformance(startDate, endDate) {
const results = [];
for (const page of TRACKED_PAGES) {
try {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['page', 'query'],
dimensionFilterGroups: [{
filters: [{
dimension: 'page',
operator: 'equals',
expression: SITE_URL + page.substring(1)
}]
}],
rowLimit: 100,
dataState: 'final'
}
});
// Hole Performance-Daten für spezifische Seiten
async getPagePerformance(startDate, endDate) {
const results = [];
if (response.data.rows) {
results.push({
page,
data: response.data.rows,
totals: {
clicks: response.data.rows.reduce((sum, row) => sum + row.clicks, 0),
impressions: response.data.rows.reduce((sum, row) => sum + row.impressions, 0),
ctr: response.data.rows.reduce((sum, row) => sum + row.ctr, 0) / response.data.rows.length,
position: response.data.rows.reduce((sum, row) => sum + row.position, 0) / response.data.rows.length
}
});
}
} catch (error) {
console.error(`Error fetching data for ${page}:`, error.message);
}
}
for (const page of TRACKED_PAGES) {
try {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['page', 'query'],
dimensionFilterGroups: [
{
filters: [
{
dimension: 'page',
operator: 'equals',
expression: SITE_URL + page.substring(1),
},
],
},
],
rowLimit: 100,
dataState: 'final',
},
});
return results;
}
if (response.data.rows) {
results.push({
page,
data: response.data.rows,
totals: {
clicks: response.data.rows.reduce((sum, row) => sum + row.clicks, 0),
impressions: response.data.rows.reduce((sum, row) => sum + row.impressions, 0),
ctr:
response.data.rows.reduce((sum, row) => sum + row.ctr, 0) /
response.data.rows.length,
position:
response.data.rows.reduce((sum, row) => sum + row.position, 0) /
response.data.rows.length,
},
});
}
} catch (error) {
console.error(`Error fetching data for ${page}:`, error.message);
}
}
// Hole Top-Suchanfragen
async getTopQueries(startDate, endDate, limit = 50) {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['query'],
rowLimit: limit,
dataState: 'final'
}
});
return results;
}
return response.data.rows || [];
}
// Hole Top-Suchanfragen
async getTopQueries(startDate, endDate, limit = 50) {
const response = await this.searchConsole.searchanalytics.query({
siteUrl: SITE_URL,
requestBody: {
startDate,
endDate,
dimensions: ['query'],
rowLimit: limit,
dataState: 'final',
},
});
// Speichere Daten als JSON
async saveData(data, filename) {
const filepath = path.join(DATA_DIR, filename);
await fs.writeFile(filepath, JSON.stringify(data, null, 2));
console.log(`✅ Daten gespeichert: ${filepath}`);
}
return response.data.rows || [];
}
// Hauptfunktion zum Sammeln aller Daten
async collectDailyData() {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const startDate = yesterday.toISOString().split('T')[0];
const endDate = startDate;
// Speichere Daten als JSON
async saveData(data, filename) {
const filepath = path.join(DATA_DIR, filename);
await fs.writeFile(filepath, JSON.stringify(data, null, 2));
console.log(`✅ Daten gespeichert: ${filepath}`);
}
console.log(`📊 Sammle SEO-Daten für ${startDate}...`);
// Hauptfunktion zum Sammeln aller Daten
async collectDailyData() {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
try {
// Sammle verschiedene Metriken
const [keywordData, pageData, topQueries] = await Promise.all([
this.getKeywordPerformance(startDate, endDate),
this.getPagePerformance(startDate, endDate),
this.getTopQueries(startDate, endDate)
]);
const startDate = yesterday.toISOString().split('T')[0];
const endDate = startDate;
// Strukturiere Daten für Dashboard
const dashboardData = {
date: startDate,
timestamp: new Date().toISOString(),
summary: {
totalClicks: topQueries.reduce((sum, q) => sum + q.clicks, 0),
totalImpressions: topQueries.reduce((sum, q) => sum + q.impressions, 0),
avgCTR: topQueries.reduce((sum, q) => sum + q.ctr, 0) / topQueries.length,
avgPosition: topQueries.reduce((sum, q) => sum + q.position, 0) / topQueries.length
},
keywords: this.processKeywordData(keywordData),
pages: pageData,
topQueries: topQueries.slice(0, 20),
trackedKeywords: this.filterTrackedKeywords(keywordData)
};
console.log(`📊 Sammle SEO-Daten für ${startDate}...`);
// Speichere Tages-Snapshot
await this.saveData(dashboardData, `seo-data-${startDate}.json`);
try {
// Sammle verschiedene Metriken
const [keywordData, pageData, topQueries] = await Promise.all([
this.getKeywordPerformance(startDate, endDate),
this.getPagePerformance(startDate, endDate),
this.getTopQueries(startDate, endDate),
]);
// Update aggregierte Daten
await this.updateAggregatedData(dashboardData);
// Strukturiere Daten für Dashboard
const dashboardData = {
date: startDate,
timestamp: new Date().toISOString(),
summary: {
totalClicks: topQueries.reduce((sum, q) => sum + q.clicks, 0),
totalImpressions: topQueries.reduce((sum, q) => sum + q.impressions, 0),
avgCTR: topQueries.reduce((sum, q) => sum + q.ctr, 0) / topQueries.length,
avgPosition: topQueries.reduce((sum, q) => sum + q.position, 0) / topQueries.length,
},
keywords: this.processKeywordData(keywordData),
pages: pageData,
topQueries: topQueries.slice(0, 20),
trackedKeywords: this.filterTrackedKeywords(keywordData),
};
return dashboardData;
} catch (error) {
console.error('❌ Fehler beim Sammeln der Daten:', error);
throw error;
}
}
// Speichere Tages-Snapshot
await this.saveData(dashboardData, `seo-data-${startDate}.json`);
// Filtere und strukturiere Keyword-Daten
processKeywordData(data) {
const keywordMap = {};
data.forEach(row => {
const query = row.keys[0];
if (!keywordMap[query]) {
keywordMap[query] = {
query,
clicks: 0,
impressions: 0,
positions: [],
devices: {},
pages: new Set()
};
}
keywordMap[query].clicks += row.clicks;
keywordMap[query].impressions += row.impressions;
keywordMap[query].positions.push(row.position);
const device = row.keys[3];
keywordMap[query].devices[device] = (keywordMap[query].devices[device] || 0) + row.clicks;
const page = row.keys[1];
keywordMap[query].pages.add(page);
});
// Update aggregierte Daten
await this.updateAggregatedData(dashboardData);
// Berechne Durchschnittswerte
return Object.values(keywordMap).map(kw => ({
query: kw.query,
clicks: kw.clicks,
impressions: kw.impressions,
ctr: kw.clicks / kw.impressions,
avgPosition: kw.positions.reduce((a, b) => a + b, 0) / kw.positions.length,
devices: kw.devices,
pageCount: kw.pages.size
})).sort((a, b) => b.clicks - a.clicks);
}
return dashboardData;
} catch (error) {
console.error('❌ Fehler beim Sammeln der Daten:', error);
throw error;
}
}
// Filtere tracked Keywords
filterTrackedKeywords(data) {
return TRACKED_KEYWORDS.map(keyword => {
const matches = data.filter(row =>
row.keys[0].toLowerCase().includes(keyword.toLowerCase())
);
if (matches.length === 0) {
return {
keyword,
status: 'not_ranking',
clicks: 0,
impressions: 0,
position: null
};
}
// Filtere und strukturiere Keyword-Daten
processKeywordData(data) {
const keywordMap = {};
const totals = matches.reduce((acc, row) => ({
clicks: acc.clicks + row.clicks,
impressions: acc.impressions + row.impressions,
positions: [...acc.positions, row.position]
}), { clicks: 0, impressions: 0, positions: [] });
data.forEach((row) => {
const query = row.keys[0];
if (!keywordMap[query]) {
keywordMap[query] = {
query,
clicks: 0,
impressions: 0,
positions: [],
devices: {},
pages: new Set(),
};
}
return {
keyword,
status: 'ranking',
clicks: totals.clicks,
impressions: totals.impressions,
ctr: totals.clicks / totals.impressions,
position: totals.positions.reduce((a, b) => a + b, 0) / totals.positions.length,
trend: null // Wird später berechnet
};
});
}
keywordMap[query].clicks += row.clicks;
keywordMap[query].impressions += row.impressions;
keywordMap[query].positions.push(row.position);
// Update aggregierte Daten für Trends
async updateAggregatedData(newData) {
const aggregatedFile = path.join(DATA_DIR, 'aggregated-seo-data.json');
let aggregated = { history: [], keywords: {} };
try {
const existing = await fs.readFile(aggregatedFile, 'utf-8');
aggregated = JSON.parse(existing);
} catch (error) {
// File existiert noch nicht
}
const device = row.keys[3];
keywordMap[query].devices[device] = (keywordMap[query].devices[device] || 0) + row.clicks;
// Füge neue Daten zur Historie hinzu
aggregated.history.push({
date: newData.date,
summary: newData.summary,
topKeywords: newData.keywords.slice(0, 10).map(k => ({
query: k.query,
clicks: k.clicks,
position: k.avgPosition
}))
});
const page = row.keys[1];
keywordMap[query].pages.add(page);
});
// Behalte nur die letzten 90 Tage
if (aggregated.history.length > 90) {
aggregated.history = aggregated.history.slice(-90);
}
// Berechne Durchschnittswerte
return Object.values(keywordMap)
.map((kw) => ({
query: kw.query,
clicks: kw.clicks,
impressions: kw.impressions,
ctr: kw.clicks / kw.impressions,
avgPosition: kw.positions.reduce((a, b) => a + b, 0) / kw.positions.length,
devices: kw.devices,
pageCount: kw.pages.size,
}))
.sort((a, b) => b.clicks - a.clicks);
}
// Update Keyword-Trends
newData.trackedKeywords.forEach(kw => {
if (!aggregated.keywords[kw.keyword]) {
aggregated.keywords[kw.keyword] = [];
}
aggregated.keywords[kw.keyword].push({
date: newData.date,
position: kw.position,
clicks: kw.clicks,
impressions: kw.impressions
});
// Filtere tracked Keywords
filterTrackedKeywords(data) {
return TRACKED_KEYWORDS.map((keyword) => {
const matches = data.filter((row) =>
row.keys[0].toLowerCase().includes(keyword.toLowerCase())
);
// Behalte nur die letzten 30 Datenpunkte pro Keyword
if (aggregated.keywords[kw.keyword].length > 30) {
aggregated.keywords[kw.keyword] = aggregated.keywords[kw.keyword].slice(-30);
}
});
if (matches.length === 0) {
return {
keyword,
status: 'not_ranking',
clicks: 0,
impressions: 0,
position: null,
};
}
await this.saveData(aggregated, 'aggregated-seo-data.json');
}
const totals = matches.reduce(
(acc, row) => ({
clicks: acc.clicks + row.clicks,
impressions: acc.impressions + row.impressions,
positions: [...acc.positions, row.position],
}),
{ clicks: 0, impressions: 0, positions: [] }
);
// Generiere Wochenbericht
async generateWeeklyReport() {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
return {
keyword,
status: 'ranking',
clicks: totals.clicks,
impressions: totals.impressions,
ctr: totals.clicks / totals.impressions,
position: totals.positions.reduce((a, b) => a + b, 0) / totals.positions.length,
trend: null, // Wird später berechnet
};
});
}
const start = startDate.toISOString().split('T')[0];
const end = endDate.toISOString().split('T')[0];
// Update aggregierte Daten für Trends
async updateAggregatedData(newData) {
const aggregatedFile = path.join(DATA_DIR, 'aggregated-seo-data.json');
console.log(`📈 Generiere Wochenbericht ${start} bis ${end}...`);
let aggregated = { history: [], keywords: {} };
const [keywordData, pageData, topQueries] = await Promise.all([
this.getKeywordPerformance(start, end),
this.getPagePerformance(start, end),
this.getTopQueries(start, end, 100)
]);
try {
const existing = await fs.readFile(aggregatedFile, 'utf-8');
aggregated = JSON.parse(existing);
} catch (error) {
// File existiert noch nicht
}
const report = {
period: { start, end },
generated: new Date().toISOString(),
summary: {
totalClicks: topQueries.reduce((sum, q) => sum + q.clicks, 0),
totalImpressions: topQueries.reduce((sum, q) => sum + q.impressions, 0),
avgCTR: (topQueries.reduce((sum, q) => sum + q.ctr, 0) / topQueries.length * 100).toFixed(2) + '%',
avgPosition: (topQueries.reduce((sum, q) => sum + q.position, 0) / topQueries.length).toFixed(1)
},
topPerformingQueries: topQueries.slice(0, 20),
pagePerformance: pageData,
newKeywords: this.findNewKeywords(keywordData),
positionChanges: await this.calculatePositionChanges()
};
// Füge neue Daten zur Historie hinzu
aggregated.history.push({
date: newData.date,
summary: newData.summary,
topKeywords: newData.keywords.slice(0, 10).map((k) => ({
query: k.query,
clicks: k.clicks,
position: k.avgPosition,
})),
});
await this.saveData(report, `weekly-report-${end}.json`);
return report;
}
// Behalte nur die letzten 90 Tage
if (aggregated.history.length > 90) {
aggregated.history = aggregated.history.slice(-90);
}
// Finde neue Keywords
findNewKeywords(currentData) {
// Hier würdest du mit historischen Daten vergleichen
return currentData
.filter(row => row.impressions > 10 && row.position < 50)
.map(row => ({
query: row.keys[0],
impressions: row.impressions,
position: row.position,
opportunity: row.position > 10 ? 'high' : 'medium'
}))
.slice(0, 20);
}
// Update Keyword-Trends
newData.trackedKeywords.forEach((kw) => {
if (!aggregated.keywords[kw.keyword]) {
aggregated.keywords[kw.keyword] = [];
}
// Berechne Positionsänderungen
async calculatePositionChanges() {
const aggregatedFile = path.join(DATA_DIR, 'aggregated-seo-data.json');
try {
const data = await fs.readFile(aggregatedFile, 'utf-8');
const aggregated = JSON.parse(data);
return Object.entries(aggregated.keywords).map(([keyword, history]) => {
if (history.length < 2) return null;
const current = history[history.length - 1];
const previous = history[history.length - 7] || history[0];
return {
keyword,
currentPosition: current.position,
previousPosition: previous.position,
change: previous.position - current.position,
trend: current.position < previous.position ? 'improving' :
current.position > previous.position ? 'declining' : 'stable'
};
}).filter(Boolean);
} catch (error) {
return [];
}
}
aggregated.keywords[kw.keyword].push({
date: newData.date,
position: kw.position,
clicks: kw.clicks,
impressions: kw.impressions,
});
// Behalte nur die letzten 30 Datenpunkte pro Keyword
if (aggregated.keywords[kw.keyword].length > 30) {
aggregated.keywords[kw.keyword] = aggregated.keywords[kw.keyword].slice(-30);
}
});
await this.saveData(aggregated, 'aggregated-seo-data.json');
}
// Generiere Wochenbericht
async generateWeeklyReport() {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const start = startDate.toISOString().split('T')[0];
const end = endDate.toISOString().split('T')[0];
console.log(`📈 Generiere Wochenbericht ${start} bis ${end}...`);
const [keywordData, pageData, topQueries] = await Promise.all([
this.getKeywordPerformance(start, end),
this.getPagePerformance(start, end),
this.getTopQueries(start, end, 100),
]);
const report = {
period: { start, end },
generated: new Date().toISOString(),
summary: {
totalClicks: topQueries.reduce((sum, q) => sum + q.clicks, 0),
totalImpressions: topQueries.reduce((sum, q) => sum + q.impressions, 0),
avgCTR:
((topQueries.reduce((sum, q) => sum + q.ctr, 0) / topQueries.length) * 100).toFixed(2) +
'%',
avgPosition: (
topQueries.reduce((sum, q) => sum + q.position, 0) / topQueries.length
).toFixed(1),
},
topPerformingQueries: topQueries.slice(0, 20),
pagePerformance: pageData,
newKeywords: this.findNewKeywords(keywordData),
positionChanges: await this.calculatePositionChanges(),
};
await this.saveData(report, `weekly-report-${end}.json`);
return report;
}
// Finde neue Keywords
findNewKeywords(currentData) {
// Hier würdest du mit historischen Daten vergleichen
return currentData
.filter((row) => row.impressions > 10 && row.position < 50)
.map((row) => ({
query: row.keys[0],
impressions: row.impressions,
position: row.position,
opportunity: row.position > 10 ? 'high' : 'medium',
}))
.slice(0, 20);
}
// Berechne Positionsänderungen
async calculatePositionChanges() {
const aggregatedFile = path.join(DATA_DIR, 'aggregated-seo-data.json');
try {
const data = await fs.readFile(aggregatedFile, 'utf-8');
const aggregated = JSON.parse(data);
return Object.entries(aggregated.keywords)
.map(([keyword, history]) => {
if (history.length < 2) return null;
const current = history[history.length - 1];
const previous = history[history.length - 7] || history[0];
return {
keyword,
currentPosition: current.position,
previousPosition: previous.position,
change: previous.position - current.position,
trend:
current.position < previous.position
? 'improving'
: current.position > previous.position
? 'declining'
: 'stable',
};
})
.filter(Boolean);
} catch (error) {
return [];
}
}
}
// CLI Interface
async function main() {
const tracker = new SEOTracker();
await tracker.initialize();
const tracker = new SEOTracker();
await tracker.initialize();
const command = process.argv[2];
const command = process.argv[2];
switch (command) {
case 'daily':
await tracker.collectDailyData();
break;
case 'weekly':
await tracker.generateWeeklyReport();
break;
case 'test':
// Test mit den letzten 7 Tagen
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const start = startDate.toISOString().split('T')[0];
console.log(`Test: Hole Daten von ${start} bis ${endDate}`);
const data = await tracker.getTopQueries(start, endDate, 10);
console.log('Top Queries:', data);
break;
default:
console.log(`
switch (command) {
case 'daily':
await tracker.collectDailyData();
break;
case 'weekly':
await tracker.generateWeeklyReport();
break;
case 'test':
// Test mit den letzten 7 Tagen
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const start = startDate.toISOString().split('T')[0];
console.log(`Test: Hole Daten von ${start} bis ${endDate}`);
const data = await tracker.getTopQueries(start, endDate, 10);
console.log('Top Queries:', data);
break;
default:
console.log(`
SEO Tracker - Verwendung:
node seo-tracker.js daily - Sammle tägliche Daten
node seo-tracker.js weekly - Generiere Wochenbericht
node seo-tracker.js test - Teste API-Verbindung
`);
}
}
}
// Führe aus wenn direkt aufgerufen
if (require.main === module) {
main().catch(console.error);
main().catch(console.error);
}
module.exports = SEOTracker;
module.exports = SEOTracker;