mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 20:26:40 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
444
apps/memoro/apps/landing/scripts/seo-tracker.js
Normal file
444
apps/memoro/apps/landing/scripts/seo-tracker.js
Normal 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;
|
||||
296
apps/memoro/apps/landing/scripts/setup-seo-tracking.md
Normal file
296
apps/memoro/apps/landing/scripts/setup-seo-tracking.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue