refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,444 @@
// Google Search Console API Data Collector
// Sammelt SEO-Daten und speichert sie für Dashboard
const { google } = require('googleapis');
const fs = require('fs').promises;
const path = require('path');
// Konfiguration
const SITE_URL = 'https://memoro.ai/';
const CREDENTIALS_FILE = './credentials.json'; // Service Account JSON
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'
];
// Wichtige Seiten zum Tracken
const TRACKED_PAGES = [
'/de/meeting-protokoll-software',
'/de/',
'/de/features',
'/de/pricing',
'/de/blog'
];
class SEOTracker {
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'],
});
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 });
}
// 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 || [];
}
// 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'
}
});
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);
}
}
return results;
}
// 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 response.data.rows || [];
}
// 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}`);
}
// 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;
console.log(`📊 Sammle SEO-Daten für ${startDate}...`);
try {
// Sammle verschiedene Metriken
const [keywordData, pageData, topQueries] = await Promise.all([
this.getKeywordPerformance(startDate, endDate),
this.getPagePerformance(startDate, endDate),
this.getTopQueries(startDate, endDate)
]);
// 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)
};
// Speichere Tages-Snapshot
await this.saveData(dashboardData, `seo-data-${startDate}.json`);
// Update aggregierte Daten
await this.updateAggregatedData(dashboardData);
return dashboardData;
} catch (error) {
console.error('❌ Fehler beim Sammeln der Daten:', error);
throw error;
}
}
// 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);
});
// 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);
}
// 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
};
}
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: [] });
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
};
});
}
// 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
}
// 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
}))
});
// Behalte nur die letzten 90 Tage
if (aggregated.history.length > 90) {
aggregated.history = aggregated.history.slice(-90);
}
// 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
});
// 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 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(`
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);
}
module.exports = SEOTracker;

View file

@ -0,0 +1,296 @@
# SEO Tracking Setup Guide
## 1. Google Cloud Setup
### Schritt 1: API aktivieren
1. Gehe zu [Google Cloud Console](https://console.cloud.google.com/)
2. Neues Projekt erstellen: "Memoro SEO Tracker"
3. APIs & Services → Bibliothek
4. Suche "Google Search Console API"
5. Aktivieren
### Schritt 2: Service Account erstellen
1. APIs & Services → Anmeldedaten
2. "+ Anmeldedaten erstellen" → Service-Konto
3. Name: "memoro-seo-tracker"
4. Rolle: "Viewer"
5. JSON-Schlüssel erstellen und herunterladen
6. Speichere als `credentials.json` im scripts Ordner
### Schritt 3: Search Console Zugriff
1. Gehe zu [Google Search Console](https://search.google.com/search-console)
2. Einstellungen → Nutzer und Berechtigungen
3. Nutzer hinzufügen: [service-account-email]@[project-id].iam.gserviceaccount.com
4. Berechtigung: "Eingeschränkt"
## 2. Dependencies installieren
```bash
cd scripts
npm init -y
npm install googleapis node-cron dotenv
```
## 3. Cron Job einrichten
### Option A: Node.js Cron (Entwicklung)
Erstelle `scripts/cron-runner.js`:
```javascript
const cron = require('node-cron');
const SEOTracker = require('./seo-tracker');
const tracker = new SEOTracker();
// Täglich um 6 Uhr morgens
cron.schedule('0 6 * * *', async () => {
console.log('Running daily SEO data collection...');
await tracker.initialize();
await tracker.collectDailyData();
});
// Wöchentlich Montags um 9 Uhr
cron.schedule('0 9 * * 1', async () => {
console.log('Generating weekly report...');
await tracker.initialize();
await tracker.generateWeeklyReport();
});
console.log('SEO Tracker Cron Jobs started');
```
Starten mit: `node cron-runner.js`
### Option B: GitHub Actions (Production)
Erstelle `.github/workflows/seo-tracker.yml`:
```yaml
name: SEO Data Collection
on:
schedule:
# Täglich um 6:00 UTC
- cron: '0 6 * * *'
workflow_dispatch: # Manueller Trigger
jobs:
collect-seo-data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
cd scripts
npm ci
- name: Run SEO Tracker
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
run: |
cd scripts
echo "$GOOGLE_CREDENTIALS" > credentials.json
node seo-tracker.js daily
- name: Upload data artifacts
uses: actions/upload-artifact@v3
with:
name: seo-data
path: scripts/data/seo/
- name: Commit data (optional)
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add scripts/data/seo/
git commit -m "Update SEO data $(date +'%Y-%m-%d')" || exit 0
git push
```
### Option C: Vercel Cron (wenn du Vercel nutzt)
In `vercel.json`:
```json
{
"crons": [
{
"path": "/api/seo-collect",
"schedule": "0 6 * * *"
}
]
}
```
## 4. Dashboard Integration
### Statische Seite (Astro)
Erstelle `/src/pages/de/seo-dashboard.astro`:
```astro
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import SEODashboard from "../../components/SEODashboard.astro";
// Lade die neuesten Daten
import fs from 'fs';
import path from 'path';
let seoData = null;
try {
const dataPath = path.join(process.cwd(), 'scripts/data/seo/aggregated-seo-data.json');
const rawData = fs.readFileSync(dataPath, 'utf-8');
seoData = JSON.parse(rawData);
} catch (error) {
console.error('Could not load SEO data:', error);
}
---
<BaseLayout title="SEO Dashboard">
<div class="container mx-auto px-4 py-8">
<SEODashboard data={seoData} />
</div>
</BaseLayout>
```
### Live Dashboard (mit API)
Erstelle `/api/seo-data.js`:
```javascript
// API Endpoint für Live-Daten
import { google } from 'googleapis';
export async function GET(request) {
const url = new URL(request.url);
const range = url.searchParams.get('range') || '7d';
// Initialisiere Google Auth
const auth = new google.auth.GoogleAuth({
credentials: JSON.parse(process.env.GOOGLE_CREDENTIALS),
scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
});
const searchConsole = google.searchconsole({
version: 'v1',
auth: await auth.getClient(),
});
// Hole Daten
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const response = await searchConsole.searchanalytics.query({
siteUrl: 'https://memoro.ai/',
requestBody: {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
dimensions: ['query', 'page'],
rowLimit: 100
}
});
return new Response(JSON.stringify(response.data), {
headers: { 'Content-Type': 'application/json' }
});
}
```
## 5. Monitoring & Alerts
### Email-Benachrichtigungen
Erweitere `seo-tracker.js`:
```javascript
async sendAlert(subject, message) {
// Mit Sendgrid, Postmark oder ähnlichem
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
await sgMail.send({
to: 'team@memoro.ai',
from: 'seo-alerts@memoro.ai',
subject: subject,
text: message,
});
}
// In collectDailyData():
if (dashboardData.summary.avgPosition < 10) {
await this.sendAlert(
'🎉 SEO Meilenstein erreicht!',
`Durchschnittliche Position unter 10: ${dashboardData.summary.avgPosition}`
);
}
```
### Slack Integration
```javascript
async notifySlack(message) {
const webhook = process.env.SLACK_WEBHOOK_URL;
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
```
## 6. Testen
```bash
# Test API Verbindung
node scripts/seo-tracker.js test
# Sammle Daten manuell
node scripts/seo-tracker.js daily
# Generiere Report
node scripts/seo-tracker.js weekly
```
## Nächste Schritte
1. ✅ Google Cloud Projekt erstellen
2. ✅ Service Account anlegen
3. ✅ Search Console Zugriff geben
4. ✅ Dependencies installieren
5. ✅ Ersten Test durchführen
6. ✅ Cron Job aktivieren
7. ✅ Dashboard deployen
## Wichtige Metriken zum Tracken
- **Primäre KPIs:**
- Ranking für "meeting protokoll software"
- CTR der Landing Pages
- Neue rankende Keywords
- **Sekundäre KPIs:**
- Seitengeschwindigkeit (Core Web Vitals)
- Crawl-Fehler
- Mobile Usability
## Troubleshooting
**Error: "User does not have sufficient permission"**
→ Service Account Email in Search Console hinzufügen
**Error: "API not enabled"**
→ Google Search Console API im Cloud Console aktivieren
**Keine Daten verfügbar**
→ Search Console braucht 2-3 Tage für neue Properties