#!/usr/bin/env python3 """ Website Creator - Generate PDPA-compliant Astro websites Creates complete Astro projects with: - Bilingual support (Thai/English) - Umami Analytics integration (auto-create) - GA4 Analytics support (existing or new) - Google Search Console setup - Cookie consent management - Consent logging database (Astro DB) - PDPA-compliant legal pages - Easypanel deployment (manual sync after local preview) Usage: python3 create_astro_website.py \\ --name "Deal Plus Tech" \\ --type "corporate" \\ --languages "th,en" \\ --output "./dealplustech-website" # Then preview locally, and when ready: # Script will ask: "Sync to Gitea and deploy?" """ import os import sys import argparse import shutil import subprocess import json import time from pathlib import Path from datetime import datetime from urllib.parse import urlparse # ============================================================================ # INTERACTIVE SETUP FUNCTIONS # ============================================================================ def ask_analytics_setup(): """ Interactive analytics setup workflow Returns: dict: Analytics configuration """ print("\n" + "=" * 60) print("šŸ“Š ANALYTICS SETUP") print("=" * 60) config = { 'search_console': None, 'analytics_type': None, # 'umami' or 'ga4' 'umami_auto_create': False, 'umami_website_id': None, 'ga4_property_id': None, 'ga4_credentials_path': None, 'ga4_existing': False } # Step 1: Google Search Console (for all websites) print("\n1ļøāƒ£ Google Search Console Setup") print(" GSC is recommended for all websites for SEO monitoring.") gsc_choice = input("\n Do you want to setup Google Search Console? (y/n): ").strip().lower() if gsc_choice == 'y': print("\n GSC Setup Options:") print(" 1. I'll add it manually later (skip for now)") print(" 2. I have service account credentials file") gsc_method = input("\n Choose option (1-2): ").strip() if gsc_method == '2': gsc_path = input(" Enter path to GSC credentials file: ").strip() if os.path.exists(gsc_path): config['search_console'] = { 'credentials_path': gsc_path, 'setup_later': False } print(" āœ“ GSC credentials loaded") else: print(" ⚠ File not found, will setup later") config['search_console'] = {'setup_later': True} else: config['search_console'] = {'setup_later': True} print(" āœ“ Will setup later") else: print(" ā­ļø Skipping GSC setup") # Step 2: Choose Analytics Type (Umami OR GA4) print("\n2ļøāƒ£ Analytics Platform") print(" Choose ONE analytics platform:") print(" 1. Umami Analytics (recommended for most users)") print(" - Privacy-focused, self-hosted") print(" - Simple setup, auto-created") print(" - Good for most websites") print("\n 2. Google Analytics 4 (for advanced users)") print(" - Full-featured analytics") print(" - Requires Google account") print(" - Good for existing GA4 users") analytics_choice = input("\n Choose analytics (1-2): ").strip() if analytics_choice == '1': # Umami setup config['analytics_type'] = 'umami' print("\n šŸ“ˆ Umami Analytics Setup") # Check if Umami credentials are configured from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(__file__), '../../../.env')) umami_url = os.getenv('UMAMI_URL', '') umami_username = os.getenv('UMAMI_USERNAME', '') umami_password = os.getenv('UMAMI_PASSWORD', '') if umami_url and umami_username and umami_password: print(" āœ“ Umami credentials found in .env") print(" āœ“ Will auto-create Umami website for this project") config['umami_auto_create'] = True else: print(" ⚠ Umami credentials not configured in .env") print(" ā­ļø Skipping Umami setup (can add manually later)") elif analytics_choice == '2': # GA4 setup config['analytics_type'] = 'ga4' print("\n šŸ” Google Analytics 4 Setup") print(" 1. Create new GA4 property (auto-setup)") print(" 2. Use existing GA4 property (manual setup)") ga4_choice = input("\n Choose option (1-2): ").strip() if ga4_choice == '1': print("\n ⚠ Auto-creating GA4 properties requires API setup.") print(" ā­ļø Will provide instructions for manual setup") config['ga4_existing'] = False else: print("\n Please provide your existing GA4 details:") # Check unified .env for GA4 credentials from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(__file__), '../../../.env')) ga4_property_id = os.getenv('GA4_PROPERTY_ID', '') ga4_credentials_path = os.getenv('GA4_CREDENTIALS_PATH', '') if ga4_property_id: print(f" Found GA4 Property ID in .env: {ga4_property_id[:20]}...") use_global = input(" Use this for this project? (y/n): ").strip().lower() if use_global == 'y': config['ga4_property_id'] = ga4_property_id config['ga4_credentials_path'] = ga4_credentials_path print(" āœ“ Using global GA4 credentials") else: config['ga4_property_id'] = input(" Enter GA4 Property ID: ").strip() config['ga4_credentials_path'] = input(" Enter GA4 credentials file path: ").strip() else: config['ga4_property_id'] = input(" Enter GA4 Property ID (G-XXXXXXXXXX): ").strip() config['ga4_credentials_path'] = input(" Enter GA4 credentials file path: ").strip() config['ga4_existing'] = True else: print(" ā­ļø Skipping analytics setup") return config # ============================================================================ # TEMPLATES (abbreviated for brevity) # ============================================================================ ASTRO_CONFIG_TEMPLATE = """import {{ defineConfig }} from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; import db from '@astrojs/db'; import sitemap from '@astrojs/sitemap'; export default defineConfig({{ site: '{site_url}', output: 'hybrid', i18n: {{ locales: [{locales}], defaultLocale: '{default_locale}', routing: {{ prefixDefaultLocale: false, fallbackType: 'rewrite', }}, fallback: {{ th: 'en', }}, }}, integrations: [ tailwindcss(), db(), sitemap({{ i18n: {{ defaultLocale: '{default_locale}', }}, }}), ], }}); """ PACKAGE_JSON_TEMPLATE = """{{ "name": "{name}", "type": "module", "version": "1.0.0", "scripts": {{ "dev": "astro dev", "build": "astro build --remote", "preview": "astro preview", "astro": "astro", "db:push": "astro db push --remote", "db:seed": "astro db seed" }}, "dependencies": {{ "astro": "^5.17.1", "@astrojs/db": "^0.14.0", "@astrojs/sitemap": "^3.2.0", "@tailwindcss/vite": "^4.2.1", "tailwindcss": "^4.2.1", "astro-consent": "^1.0.0", "drizzle-orm": "^0.38.0", "@libsql/client": "^0.14.0" }} }} """ # ... (rest of templates remain the same) # ============================================================================ # MAIN FUNCTION # ============================================================================ def main(): """Main entry point.""" parser = argparse.ArgumentParser(description='Create PDPA-compliant Astro website') parser.add_argument('--name', required=True, help='Website name') parser.add_argument('--type', default='corporate', choices=['corporate', 'portfolio', 'landing', 'blog', 'ecommerce'], help='Website type') parser.add_argument('--languages', default='th,en', help='Languages (comma-separated): th, en') parser.add_argument('--primary-color', default='#2563eb', help='Primary color (hex)') parser.add_argument('--secondary-color', default='#1e40af', help='Secondary color (hex)') parser.add_argument('--features', default='blog,contact', help='Features (comma-separated): blog, products, contact, portfolio') parser.add_argument('--umami-id', default='', help='Umami Website ID') parser.add_argument('--umami-domain', default='analytics.example.com', help='Umami domain') parser.add_argument('--output', '-o', default='.', help='Output directory') parser.add_argument('--no-interactive', action='store_true', help='Skip interactive setup (use defaults)') args = parser.parse_args() # Auto-generate admin password from project folder name args.admin_password = Path(args.output).name.replace(' ', '').lower() # Load unified credentials from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(__file__), '../../../.env')) # Get Umami credentials for auto-setup args.umami_url = os.getenv('UMAMI_URL', '') args.umami_username = os.getenv('UMAMI_USERNAME', '') args.umami_password = os.getenv('UMAMI_PASSWORD', '') args.auto_setup_umami = bool(args.umami_url and args.umami_username and args.umami_password) languages = [lang.strip() for lang in args.languages.split(',')] default_locale = 'en' if 'en' in languages else languages[0] features = [f.strip() for f in args.features.split(',')] print(f"Creating website: {args.name}") print(f"Type: {args.type}") print(f"Languages: {languages}") print(f"Features: {features}") print(f"Output: {args.output}") # Interactive analytics setup (if not in no-interactive mode) analytics_config = None if not args.no_interactive: analytics_config = ask_analytics_setup() # Create project structure create_project(args, languages, default_locale, features) # Save analytics configuration to project if analytics_config: save_analytics_config(args.output, analytics_config) # Auto-setup Umami if credentials provided umami_website_id = args.umami_id if args.auto_setup_umami and (not analytics_config or analytics_config.get('analytics_type') == 'umami'): print("\nšŸ“ˆ Setting up Umami Analytics...") try: from umami_integration import setup_umami_for_website website_domain = args.name.lower().replace(' ', '-') + '.moreminimore.com' success, result = setup_umami_for_website( args.umami_url, args.umami_username, args.umami_password, args.name, website_domain, args.output ) if success: umami_website_id = result['website_id'] print(f" āœ“ Umami website created: {umami_website_id}") else: print(f" ⚠ Umami setup skipped: {result.get('error', 'Unknown error')}") except Exception as e: print(f" ⚠ Umami setup failed: {e}") print(" Continuing without Umami...") print(f"\nāœ… Website created successfully at: {args.output}") # Update .env with Umami ID if auto-setup env_file = os.path.join(args.output, '.env') if os.path.exists(env_file) and umami_website_id: with open(env_file, 'a', encoding='utf-8') as f: f.write(f'\n# Umami Analytics (auto-configured)\n') f.write(f'UMAMI_WEBSITE_ID={umami_website_id}\n') print(f" āœ“ Umami ID added to .env") print("\nNext steps:") print(f" 1. cd {args.output}") print(" 2. npm install") print(" 3. Update .env with your credentials") print(" 4. npm run dev") # Always ask to sync (skip if no-interactive mode) print("") print("=" * 60) print("šŸ  Website created locally!") print("=" * 60) print("") print("Preview at: http://localhost:4321") print("") # Ask if they want to sync to Gitea/Easypanel if args.no_interactive: print("āœ… Done! Website is ready at:", args.output) print("To sync later, run the sync command manually.") return sync_choice = input("Do you want to sync to Gitea and deploy to Easypanel? (y/n): ").strip().lower() if sync_choice != 'y': print("") print("āœ… Done! Website is ready at:", args.output) print("To sync later, run this script again or use gitea-sync/easypanel-deploy skills.") return print("") print("Proceeding with sync and deployment...") print("") # Step 1: Sync to Gitea print("šŸ“¦ Step 1/3: Syncing to Gitea...") git_url = sync_to_gitea(args.output, args.name) # Step 2: Deploy to Easypanel print("") print("šŸš€ Step 2/3: Deploying to Easypanel...") deployment_url = deploy_to_easypanel(args.output, args.name, git_url) # Step 3: Verify and monitor print("") print("šŸ“Š Step 3/3: Monitoring deployment...") monitor_deployment(args.name) # Final output print("") print("=" * 60) print("āœ… COMPLETE!") print("=" * 60) print("") print(f"šŸ“ Website generated: {args.output}") print(f"🌐 Gitea Repository: {git_url.replace('.git', '')}") print(f"šŸš€ Easypanel Deployment: {deployment_url}") print("") print("šŸ“‹ Next steps:") print(f" 1. Website is deploying to: {deployment_url}") print(f" 2. Check status at: https://panelwebsite.moreminimore.com") print(f" 3. Edit Umami config: cd {args.output} && nano .env") print("") def save_analytics_config(output_path: str, config: dict): """Save analytics configuration to project context""" context_dir = os.path.join(output_path, 'context') os.makedirs(context_dir, exist_ok=True) # Save data-services.json data_services = { 'ga4': { 'enabled': config.get('analytics_type') == 'ga4', 'property_id': config.get('ga4_property_id', ''), 'credentials_path': config.get('ga4_credentials_path', '') } if config.get('analytics_type') == 'ga4' else {'enabled': False}, 'gsc': { 'enabled': config.get('search_console') is not None, 'site_url': '', 'credentials_path': config.get('search_console', {}).get('credentials_path', '') }, 'umami': { 'enabled': config.get('analytics_type') == 'umami', 'api_url': os.getenv('UMAMI_URL', ''), 'website_id': config.get('umami_website_id', '') } if config.get('analytics_type') == 'umami' else {'enabled': False}, 'dataforseo': {'enabled': False} } with open(os.path.join(context_dir, 'data-services.json'), 'w', encoding='utf-8') as f: json.dump(data_services, f, indent=2) print(f" āœ“ Analytics config saved to context/data-services.json") # ============================================================================ # PROJECT CREATION FUNCTIONS # ============================================================================ def create_project(args, languages, default_locale, features): """Create the Astro project structure with templates.""" output_path = Path(args.output) project_name = args.name.lower().replace(' ', '-') site_url = f"https://{project_name}.moreminimore.com" # Get template directory script_dir = Path(__file__).parent template_dir = script_dir / 'templates' print("\nšŸ“ Creating project structure...") # Create directories dirs = [ output_path / 'public' / 'images', output_path / 'src' / 'components' / 'common', output_path / 'src' / 'components' / 'consent', output_path / 'src' / 'components' / 'ui', output_path / 'src' / 'layouts', output_path / 'src' / 'pages', output_path / 'src' / 'pages' / default_locale, output_path / 'src' / 'styles', output_path / 'src' / 'content' / 'blog', output_path / 'src' / 'lib', output_path / 'db', ] for d in dirs: d.mkdir(parents=True, exist_ok=True) print(" āœ“ Directory structure created") # Copy templates if they exist if template_dir.exists(): print(" šŸ“¦ Copying templates with IDs...") # Copy layouts layout_src = template_dir / 'layouts' / 'BaseLayout.astro' if layout_src.exists(): content = layout_src.read_text(encoding='utf-8') content = content.replace("const siteName = 'Website Name'", f"const siteName = '{args.name}'") content = content.replace("const siteUrl = 'https://example.com'", f"const siteUrl = '{site_url}'") (output_path / 'src' / 'layouts' / 'BaseLayout.astro').write_text(content, encoding='utf-8') # Copy Header header_src = template_dir / 'components' / 'common' / 'Header.astro' if header_src.exists(): shutil.copy(header_src, output_path / 'src' / 'components' / 'common' / 'Header.astro') # Copy Footer footer_src = template_dir / 'components' / 'common' / 'Footer.astro' if footer_src.exists(): shutil.copy(footer_src, output_path / 'src' / 'components' / 'common' / 'Footer.astro') # Copy page templates page_src = template_dir / 'pages' / 'index.astro' if page_src.exists(): shutil.copy(page_src, output_path / 'src' / 'pages' / default_locale / 'index.astro') # Copy styles style_src = template_dir / 'styles' / 'global.css' if style_src.exists(): shutil.copy(style_src, output_path / 'src' / 'styles' / 'global.css') print(" āœ“ Templates copied") # Create astro.config.mjs locales_str = ', '.join([f"'{lang}'" for lang in languages]) astro_config = ASTRO_CONFIG_TEMPLATE.format( site_url=site_url, locales=locales_str, default_locale=default_locale ) (output_path / 'astro.config.mjs').write_text(astro_config, encoding='utf-8') print(" āœ“ astro.config.mjs created") # Create package.json package_json = PACKAGE_JSON_TEMPLATE.format(name=project_name) (output_path / 'package.json').write_text(package_json, encoding='utf-8') print(" āœ“ package.json created") # Create tsconfig.json tsconfig = """{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } """ (output_path / 'tsconfig.json').write_text(tsconfig, encoding='utf-8') # Create env file env_content = f"""# Website Configuration SITE_NAME={args.name} SITE_URL={site_url} # Umami Analytics (optional - get from Umami dashboard) # UMAMI_WEBSITE_ID= # UMAMI_URL= """ (output_path / '.env').write_text(env_content, encoding='utf-8') print(" āœ“ Configuration files created") # Create basic index page if no template if not (output_path / 'src' / 'pages' / default_locale / 'index.astro').exists(): index_content = f"""--- import BaseLayout from '../layouts/BaseLayout.astro'; import Header from '../components/common/Header.astro'; import Footer from '../components/common/Footer.astro'; ---

Welcome to {args.name}

Your trusted partner