Auto-sync from website-creator

This commit is contained in:
Kunthawat Greethong
2026-03-08 23:03:19 +07:00
commit 9be686f587
117 changed files with 24737 additions and 0 deletions

350
skills/umami/SKILL.md Normal file
View File

@@ -0,0 +1,350 @@
---
name: umami
description: Self-hosted Umami Analytics integration with username/password authentication. Use to create websites, get tracking codes, and fetch analytics data.
---
# 📊 Umami Analytics Skill
**Skill Name:** `umami`
**Category:** `quick`
**Load Skills:** `[]`
---
## 🚀 Purpose
Integrate with self-hosted Umami Analytics using username/password authentication (like Easypanel):
-**Auto-login** - Get bearer token from credentials
-**Create websites** - Auto-create Umami website for new projects
-**Get tracking code** - Retrieve script URL for website integration
-**Fetch analytics** - Get pageviews, visitors, bounce rate
-**List websites** - Get all websites in Umami instance
**Use Cases:**
1. Auto-create Umami website when generating new website
2. Add tracking code to Astro website automatically
3. Fetch analytics data for SEO analysis
4. Manage multiple Umami websites
---
## 📋 Pre-Flight Questions
**MUST ask before using:**
1. **Umami Instance URL:**
- What's your Umami URL? (e.g., https://analytics.moreminimore.com)
2. **Authentication:**
- Username/email
- Password
3. **For Website Creation:**
- Website name
- Website domain
4. **For Existing Website:**
- Website name or domain (to find in Umami)
---
## 🔄 Workflows
### **Workflow 1: Auto-Login (First Step for All Operations)**
```python
Input: Umami URL, username, password
Process:
1. POST /api/auth/login
2. Get bearer token
3. Save token for subsequent requests
Output: Bearer token + user info
```
### **Workflow 2: Create Umami Website**
```python
Input: Website name, domain
Process:
1. Login (get token)
2. POST /api/websites
3. Get website ID
Output: Website ID, name, domain, tracking URL
```
### **Workflow 3: Get Tracking Code**
```python
Input: Website ID or domain
Process:
1. Get website ID
2. Generate tracking script URL
Output: Script tag or URL
```
### **Workflow 4: Add Tracking to Website**
```python
Input: Website repo path, Umami website ID
Process:
1. Get tracking code
2. Find Astro root layout
3. Add script to <head>
4. Save file
Output: Updated layout file
```
### **Workflow 5: Fetch Analytics**
```python
Input: Website ID, date range
Process:
1. GET /api/websites/:id/stats
2. Parse response
Output: Pageviews, visitors, bounce rate, etc.
```
---
## 🔧 Technical Implementation
### **Authentication:**
```python
POST {umami_url}/api/auth/login
Content-Type: application/json
{
"username": "your-username",
"password": "your-password"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"username": "admin",
"isAdmin": true
}
}
```
### **Create Website:**
```python
POST {umami_url}/api/websites
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "My Website",
"domain": "example.com"
}
Response:
{
"id": "website-uuid",
"name": "My Website",
"domain": "example.com",
"createdAt": "2026-03-08T..."
}
```
### **Get Tracking Code:**
```javascript
// Script URL format
<script defer src="{umami_url}/script.js" data-website-id="{website_id}"></script>
// Or for Fathom-style (if enabled)
<script defer src="{umami_url}/script.js" data-site-id="{website_id}"></script>
```
### **Get Stats:**
```python
GET {umami_url}/api/websites/{website_id}/stats
?startAt={timestamp}
&endAt={timestamp}
Authorization: Bearer {token}
Response:
{
"pageviews": 1234,
"uniques": 567,
"bounces": 89,
"totaltime": 12345
}
```
---
## 📁 Commands
### **Create Umami Website:**
```bash
python3 skills/umami/scripts/umami_client.py \
--action create-website \
--umami-url "https://analytics.moreminimore.com" \
--username "admin" \
--password "your-password" \
--website-name "My Website" \
--website-domain "example.com"
```
### **Get Tracking Code:**
```bash
python3 skills/umami/scripts/umami_client.py \
--action get-tracking \
--umami-url "https://analytics.moreminimore.com" \
--username "admin" \
--password "your-password" \
--website-id "website-uuid"
```
### **Add Tracking to Website:**
```bash
python3 skills/umami/scripts/umami_client.py \
--action add-tracking \
--umami-url "https://analytics.moreminimore.com" \
--username "admin" \
--password "your-password" \
--website-name "My Website" \
--website-repo "/path/to/astro-website"
```
### **Fetch Analytics:**
```bash
python3 skills/umami/scripts/umami_client.py \
--action get-stats \
--umami-url "https://analytics.moreminimore.com" \
--username "admin" \
--password "your-password" \
--website-id "website-uuid" \
--days 30
```
---
## ⚙️ Environment Variables
**Updated for username/password auth:**
```bash
# Umami Analytics (Self-Hosted)
UMAMI_URL=https://analytics.yoursite.com
UMAMI_USERNAME=admin
UMAMI_PASSWORD=your-password
```
**Note:** Changed from API key to username/password like Easypanel
---
## 📊 Output Examples
### **Create Website Output:**
```json
{
"success": true,
"website_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "My Website",
"domain": "example.com",
"tracking_url": "https://analytics.moreminimore.com/script.js",
"tracking_script": "<script defer src=\"https://analytics.moreminimore.com/script.js\" data-website-id=\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"></script>",
"created_at": "2026-03-08T16:00:00.000Z"
}
```
### **Stats Output:**
```json
{
"success": true,
"website_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"period": "last_30_days",
"stats": {
"pageviews": 12500,
"uniques": 8900,
"bounces": 1200,
"totaltime": 245000,
"avg_session_duration": 27.5,
"bounce_rate": 13.5
}
}
```
---
## 🔄 Integration with Other Skills
### **website-creator Integration:**
```python
# After creating Astro website
umami_result = create_umami_website(
umami_url, username, password,
website_name, website_domain
)
if umami_result['success']:
# Add tracking to Astro layout
add_tracking_to_astro(
website_repo,
umami_result['tracking_script']
)
```
### **seo-data Integration:**
```python
# Replace umami_connector.py stub
from umami import UmamiClient
umami = UmamiClient(umami_url, username, password)
stats = umami.get_page_data(website_id, days=30)
```
---
## ✅ Success Criteria
- [ ] Can login with username/password
- [ ] Can create new Umami website
- [ ] Can get tracking code
- [ ] Can add tracking to Astro website
- [ ] Can fetch analytics data
- [ ] Token cached for subsequent requests
---
## ⚠️ Important Notes
1. **Self-Hosted Only:** This skill is for self-hosted Umami instances
2. **Username/Password:** Uses login API, not API keys (Umami Cloud uses API keys)
3. **Token Caching:** Bearer token should be cached to avoid repeated logins
4. **Website Domain:** Must be full domain (https://example.com)
5. **Script URL:** Depends on Umami instance URL
---
## 📖 API Reference
- **Login:** POST /api/auth/login
- **Create Website:** POST /api/websites
- **Get Website:** GET /api/websites/:id
- **Get Stats:** GET /api/websites/:id/stats
- **List Websites:** GET /api/websites
Full docs: https://umami.is/docs/api
---
**Use this skill when you need to integrate with self-hosted Umami Analytics using username/password authentication.**

View File

@@ -0,0 +1,6 @@
# Umami Analytics (Self-Hosted)
# Get credentials from your Umami instance admin
UMAMI_URL=https://analytics.yoursite.com
UMAMI_USERNAME=admin
UMAMI_PASSWORD=your-password

View File

@@ -0,0 +1,4 @@
# Umami Analytics Client
requests>=2.31.0
python-dotenv>=1.0.0

View File

@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""
Umami Analytics Client
Self-hosted Umami integration with username/password authentication.
Creates websites, gets tracking codes, and fetches analytics data.
"""
import os
import sys
import requests
import argparse
from datetime import datetime, timedelta
from typing import Dict, Optional, List
from pathlib import Path
class UmamiClient:
"""Umami Analytics API client with username/password auth"""
def __init__(self, umami_url: str, username: str = None, password: str = None, token: str = None):
"""
Initialize Umami client
Args:
umami_url: Umami instance URL (e.g., https://analytics.example.com)
username: Umami username/email (for self-hosted)
password: Umami password (for self-hosted)
token: Bearer token (optional, if already have)
"""
self.umami_url = umami_url.rstrip('/')
self.api_url = f"{self.umami_url}/api"
self.username = username
self.password = password
self.token = token
self.user_id = None
# Auto-login if credentials provided
if username and password and not token:
self.login()
def login(self) -> Dict:
"""Login to Umami and get bearer token"""
try:
url = f"{self.api_url}/auth/login"
data = {
'username': self.username,
'password': self.password
}
response = requests.post(url, json=data)
response.raise_for_status()
result = response.json()
if 'token' in result:
self.token = result['token']
self.user_id = result.get('user', {}).get('id')
return {
'success': True,
'token': self.token,
'user_id': self.user_id,
'username': result.get('user', {}).get('username')
}
else:
return {'success': False, 'error': 'No token in response'}
except Exception as e:
return {'success': False, 'error': str(e)}
def _get_headers(self) -> Dict:
"""Get request headers with auth"""
if not self.token:
if self.username and self.password:
self.login()
return {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
def create_website(self, name: str, domain: str) -> Dict:
"""
Create new Umami website
Args:
name: Website name
domain: Website domain (full URL)
Returns:
Website creation result
"""
try:
url = f"{self.api_url}/websites"
data = {
'name': name,
'domain': domain
}
response = requests.post(url, json=data, headers=self._get_headers())
response.raise_for_status()
result = response.json()
return {
'success': True,
'website_id': result.get('id'),
'name': result.get('name'),
'domain': result.get('domain'),
'created_at': result.get('createdAt'),
'tracking_url': f"{self.umami_url}/script.js",
'tracking_script': self._get_tracking_script(result.get('id'))
}
except Exception as e:
return {'success': False, 'error': str(e)}
def get_website_by_domain(self, domain: str) -> Optional[Dict]:
"""Find website by domain"""
try:
websites = self.list_websites()
for site in websites:
if domain in site.get('domain', ''):
return site
return None
except:
return None
def list_websites(self) -> List[Dict]:
"""Get all websites"""
try:
url = f"{self.api_url}/websites"
response = requests.get(url, headers=self._get_headers())
response.raise_for_status()
result = response.json()
# Handle both array and paginated response
if isinstance(result, list):
return result
elif 'data' in result:
return result['data']
else:
return []
except Exception as e:
print(f"Error listing websites: {e}")
return []
def get_stats(self, website_id: str, days: int = 30) -> Dict:
"""
Get website statistics
Args:
website_id: Umami website ID
days: Number of days to look back
Returns:
Analytics stats
"""
try:
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
url = f"{self.api_url}/websites/{website_id}/stats"
params = {
'startAt': int(start_date.timestamp() * 1000),
'endAt': int(end_date.timestamp() * 1000)
}
response = requests.get(url, headers=self._get_headers(), params=params)
response.raise_for_status()
stats = response.json()
return {
'success': True,
'website_id': website_id,
'period': f'last_{days}_days',
'pageviews': stats.get('pageviews', 0),
'uniques': stats.get('uniques', 0),
'bounces': stats.get('bounces', 0),
'totaltime': stats.get('totaltime', 0),
'avg_session_duration': stats.get('totaltime', 0) / max(stats.get('visits', 1), 1),
'bounce_rate': stats.get('bounces', 0) / max(stats.get('visits', 1), 1) * 100
}
except Exception as e:
return {'success': False, 'error': str(e)}
def _get_tracking_script(self, website_id: str) -> str:
"""Generate tracking script HTML"""
return f'<script defer src="{self.umami_url}/script.js" data-website-id="{website_id}"></script>'
def add_tracking_to_astro(self, website_repo: str, website_id: str) -> Dict:
"""
Add Umami tracking to Astro website
Args:
website_repo: Path to Astro website repo
website_id: Umami website ID
Returns:
Result of adding tracking
"""
try:
tracking_script = self._get_tracking_script(website_id)
# Find Astro layout file
layout_paths = [
os.path.join(website_repo, 'src/layouts/Layout.astro'),
os.path.join(website_repo, 'src/layouts/BaseHead.astro'),
os.path.join(website_repo, 'src/pages/_document.tsx'),
os.path.join(website_repo, 'src/app.html')
]
layout_file = None
for path in layout_paths:
if os.path.exists(path):
layout_file = path
break
if not layout_file:
# Try to find any .astro file in src/layouts
layouts_dir = os.path.join(website_repo, 'src/layouts')
if os.path.exists(layouts_dir):
for f in os.listdir(layouts_dir):
if f.endswith('.astro'):
layout_file = os.path.join(layouts_dir, f)
break
if not layout_file:
return {'success': False, 'error': 'No Astro layout file found'}
# Read layout file
with open(layout_file, 'r', encoding='utf-8') as f:
content = f.read()
# Add tracking before </head>
if '</head>' in content:
content = content.replace('</head>', f' {tracking_script}\n </head>')
else:
# If no </head>, add at end of file
content += f'\n{tracking_script}\n'
# Write back
with open(layout_file, 'w', encoding='utf-8') as f:
f.write(content)
return {
'success': True,
'layout_file': layout_file,
'tracking_added': True
}
except Exception as e:
return {'success': False, 'error': str(e)}
def main():
"""Main CLI entry point"""
parser = argparse.ArgumentParser(description='Umami Analytics Client')
parser.add_argument('--action', required=True,
choices=['create-website', 'get-tracking', 'add-tracking', 'get-stats', 'list-websites'])
parser.add_argument('--umami-url', required=True, help='Umami instance URL')
parser.add_argument('--username', help='Umami username')
parser.add_argument('--password', help='Umami password')
parser.add_argument('--website-name', help='Website name (for create)')
parser.add_argument('--website-domain', help='Website domain (for create/find)')
parser.add_argument('--website-id', help='Website ID (for stats)')
parser.add_argument('--website-repo', help='Path to website repo (for add-tracking)')
parser.add_argument('--days', type=int, default=30, help='Days for stats')
args = parser.parse_args()
print(f"\n📊 Umami Analytics Client")
print(f"URL: {args.umami_url}\n")
# Initialize client
client = UmamiClient(args.umami_url, args.username, args.password)
if args.action == 'create-website':
if not args.website_name or not args.website_domain:
print("Error: --website-name and --website-domain required")
return
print(f"Creating website: {args.website_name} ({args.website_domain})")
result = client.create_website(args.website_name, args.website_domain)
if result['success']:
print(f"\n✅ Website created!")
print(f" ID: {result['website_id']}")
print(f" Name: {result['name']}")
print(f" Domain: {result['domain']}")
print(f" Tracking: {result['tracking_url']}")
print(f"\nScript:\n{result['tracking_script']}")
else:
print(f"\n❌ Failed: {result['error']}")
elif args.action == 'get-tracking':
if not args.website_id:
print("Error: --website-id required")
return
script = client._get_tracking_script(args.website_id)
print(f"\nTracking script for {args.website_id}:")
print(script)
elif args.action == 'add-tracking':
if not args.website_id or not args.website_repo:
print("Error: --website-id and --website-repo required")
return
print(f"Adding tracking to: {args.website_repo}")
result = client.add_tracking_to_astro(args.website_repo, args.website_id)
if result['success']:
print(f"\n✅ Tracking added!")
print(f" Layout: {result['layout_file']}")
else:
print(f"\n❌ Failed: {result['error']}")
elif args.action == 'get-stats':
if not args.website_id:
print("Error: --website-id required")
return
print(f"Getting stats for last {args.days} days...")
stats = client.get_stats(args.website_id, args.days)
if stats['success']:
print(f"\n📊 Analytics ({stats['period']}):")
print(f" Pageviews: {stats['pageviews']:,}")
print(f" Unique visitors: {stats['uniques']:,}")
print(f" Bounces: {stats['bounces']:,}")
print(f" Bounce rate: {stats['bounce_rate']:.1f}%")
print(f" Avg session: {stats['avg_session_duration']:.1f}s")
else:
print(f"\n❌ Failed: {stats['error']}")
elif args.action == 'list-websites':
print("Listing websites...")
websites = client.list_websites()
print(f"\nFound {len(websites)} websites:")
for site in websites:
print(f"{site.get('name')} - {site.get('domain')}")
if __name__ == '__main__':
main()