Files
opencode-skill/skills/thai-frontend-dev/scripts/refactor_existing_website.py
Kunthawat Greethong 7edf5bc4d0 feat: Import 35+ skills, merge duplicates, add openclaw installer
Major updates:
- Added 35+ new skills from awesome-opencode-skills and antigravity repos
- Merged SEO skills into seo-master
- Merged architecture skills into architecture
- Merged security skills into security-auditor and security-coder
- Merged testing skills into testing-master and testing-patterns
- Merged pentesting skills into pentesting
- Renamed website-creator to thai-frontend-dev
- Replaced skill-creator with github version
- Removed Chutes references (use MiniMax API instead)
- Added install-openclaw-skills.sh for cross-platform installation
- Updated .env.example with MiniMax API credentials
2026-03-26 11:37:39 +07:00

1285 lines
49 KiB
Python

#!/usr/bin/env python3
"""
Website Refactoring Script - Update existing Astro websites to new PDPA-compliant standard
This script migrates existing Astro websites to the new standardized structure with:
- PDPA-compliant legal pages
- Cookie consent system
- Consent logging database
- i18n routing (Thai/English)
- Umami Analytics integration
Usage:
python3 refactor_existing_website.py \
--input "./existing-website" \
--output "./refactored-website" \
--languages "th,en"
"""
import os
import sys
import shutil
import argparse
from pathlib import Path
from datetime import datetime
def main():
parser = argparse.ArgumentParser(
description='Refactor existing Astro website to PDPA-compliant standard'
)
parser.add_argument('--input', '-i', required=True, help='Input directory (existing website)')
parser.add_argument('--output', '-o', required=True, help='Output directory (refactored website)')
parser.add_argument('--languages', default='th,en', help='Languages (comma-separated)')
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('--admin-password', default='changeme', help='Admin password')
parser.add_argument('--skip-backup', action='store_true', help='Skip backup creation')
args = parser.parse_args()
input_dir = Path(args.input)
output_dir = Path(args.output)
languages = [lang.strip() for lang in args.languages.split(',')]
if not input_dir.exists():
print(f"Error: Input directory '{input_dir}' does not exist")
sys.exit(1)
print(f"🔄 Refactoring website from: {input_dir}")
print(f"📁 Output directory: {output_dir}")
print(f"🌐 Languages: {languages}")
# Create backup
if not args.skip_backup:
create_backup(input_dir)
# Create new structure
output_dir.mkdir(parents=True, exist_ok=True)
# Migrate content
migrate_content(input_dir, output_dir, languages)
# Add new features
add_pdpa_features(output_dir, args, languages)
# Update configuration
update_configs(output_dir, args, languages)
print(f"\n✅ Refactoring complete!")
print(f"\n📁 Refactored website at: {output_dir}")
print("\n📋 Next steps:")
print(f" 1. cd {output_dir}")
print(" 2. Review changes")
print(" 3. npm install")
print(" 4. Update .env with your credentials")
print(" 5. npm run dev")
print("\n⚠️ Important:")
print(" - Review Privacy Policy content (update company info)")
print(" - Review Terms & Conditions (update service details)")
print(" - Change admin password in .env")
print(" - Test all features before deployment")
def create_backup(input_dir):
"""Create backup of existing website."""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_dir = input_dir.parent / f"{input_dir.name}_backup_{timestamp}"
print(f"💾 Creating backup: {backup_dir}")
shutil.copytree(input_dir, backup_dir)
print(f" ✓ Backup created")
def migrate_content(input_dir, output_dir, languages):
"""Migrate existing content to new structure."""
# Create new folder structure
create_new_structure(output_dir)
# Copy existing content
src_input = input_dir / 'src'
if src_input.exists():
# Copy components
components_input = src_input / 'components'
if components_input.exists():
print("📦 Migrating components...")
shutil.copytree(
components_input,
output_dir / 'src' / 'components' / 'migrated',
dirs_exist_ok=True
)
print(f" ✓ Components migrated")
# Copy layouts
layouts_input = src_input / 'layouts'
if layouts_input.exists():
print("📐 Migrating layouts...")
shutil.copytree(
layouts_input,
output_dir / 'src' / 'layouts' / 'migrated',
dirs_exist_ok=True
)
print(f" ✓ Layouts migrated")
# Copy content collections (blog, products)
content_input = src_input / 'content'
if content_input.exists():
print("📝 Migrating content collections...")
# Copy blog posts
blog_input = content_input / 'blog'
if blog_input.exists():
# Organize by language if possible
for lang in languages:
lang_dir = output_dir / 'src' / 'content' / 'blog' / f'({lang})'
lang_dir.mkdir(parents=True, exist_ok=True)
# Copy all posts to default language folder for now
default_lang = 'en' if 'en' in languages else languages[0]
default_dir = output_dir / 'src' / 'content' / 'blog' / f'({default_lang})'
for md_file in blog_input.glob('*.md'):
shutil.copy2(md_file, default_dir)
print(f" ✓ Copied: {md_file.name}")
# Copy static assets
public_input = input_dir / 'public'
if public_input.exists():
print("🖼️ Migrating static assets...")
shutil.copytree(
public_input,
output_dir / 'public',
dirs_exist_ok=True
)
print(f" ✓ Assets migrated")
# Copy images
for img_dir in src_input.glob('**/images'):
rel_path = img_dir.relative_to(src_input)
dest_dir = output_dir / 'src' / 'components' / rel_path
dest_dir.mkdir(parents=True, exist_ok=True)
for img_file in img_dir.glob('*'):
if img_file.suffix.lower() in ['.jpg', '.jpeg', '.png', '.svg', '.webp']:
shutil.copy2(img_file, dest_dir)
print(f" ✓ Content migration complete")
def create_new_structure(output_dir):
"""Create the new standardized folder structure."""
dirs = [
output_dir / 'public' / 'images',
output_dir / 'src' / 'components' / 'common',
output_dir / 'src' / 'components' / 'consent',
output_dir / 'src' / 'components' / 'ui',
output_dir / 'src' / 'components' / 'migrated',
output_dir / 'src' / 'layouts',
output_dir / 'src' / 'layouts' / 'migrated',
output_dir / 'src' / 'pages',
output_dir / 'src' / 'pages' / 'api' / 'consent',
output_dir / 'src' / 'pages' / 'admin',
output_dir / 'src' / 'styles',
output_dir / 'src' / 'content' / 'blog',
output_dir / 'src' / 'lib',
output_dir / 'db',
]
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
print("📁 Created new folder structure")
def add_pdpa_features(output_dir, args, languages):
"""Add PDPA compliance features."""
default_locale = 'en' if 'en' in languages else languages[0]
# Create Privacy Policy pages
print("📄 Creating Privacy Policy pages...")
for lang in languages:
lang_dir = output_dir / 'src' / 'pages' / lang
lang_dir.mkdir(parents=True, exist_ok=True)
privacy_policy = get_privacy_policy_template(lang, args)
(lang_dir / 'privacy-policy.astro').write_text(privacy_policy)
print(" ✓ Privacy Policy created")
# Create Terms & Conditions pages
print("📄 Creating Terms & Conditions pages...")
for lang in languages:
lang_dir = output_dir / 'src' / 'pages' / lang
terms = get_terms_template(lang)
(lang_dir / 'terms-and-conditions.astro').write_text(terms)
print(" ✓ Terms & Conditions created")
# Create consent components
print("🍪 Creating cookie consent components...")
cookie_banner = get_cookie_banner_template()
(output_dir / 'src' / 'components' / 'consent' / 'CookieBanner.astro').write_text(cookie_banner)
print(" ✓ Cookie banner created")
# Create admin dashboard
print("🔐 Creating admin dashboard...")
admin_page = get_admin_dashboard_template()
admin_dir = output_dir / 'src' / 'pages' / 'admin'
admin_dir.mkdir(parents=True, exist_ok=True)
(admin_dir / 'consent-logs.astro').write_text(admin_page)
print(" ✓ Admin dashboard created")
# Create database schema
print("💾 Creating database schema...")
db_config = get_db_config_template()
(output_dir / 'db' / 'config.ts').write_text(db_config)
db_seed = get_db_seed_template()
(output_dir / 'db' / 'seed.ts').write_text(db_seed)
print(" ✓ Database schema created")
# Create API endpoints
print("🔌 Creating API endpoints...")
create_api_endpoints(output_dir)
print(" ✓ API endpoints created")
# Create i18n lib
print("🌐 Creating i18n utilities...")
i18n_lib = get_i18n_lib_template(languages)
(output_dir / 'src' / 'lib' / 'i18n.ts').write_text(i18n_lib)
print(" ✓ i18n utilities created")
def update_configs(output_dir, args, languages):
"""Update configuration files."""
default_locale = 'en' if 'en' in languages else languages[0]
locales_str = ', '.join([f"'{lang}'" for lang in languages])
# astro.config.mjs
print("⚙️ Updating astro.config.mjs...")
astro_config = f"""import {{ defineConfig }} from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import db from '@astrojs/db';
import sitemap from '@astrojs/sitemap';
export default defineConfig({{
site: 'https://example.com',
output: 'hybrid',
i18n: {{
locales: [{locales_str}],
defaultLocale: '{default_locale}',
routing: {{
prefixDefaultLocale: false,
fallbackType: 'rewrite',
}},
fallback: {{
th: 'en',
}},
}},
integrations: [
tailwindcss(),
db(),
sitemap({{
i18n: {{
defaultLocale: '{default_locale}',
}},
}}),
],
}});
"""
(output_dir / 'astro.config.mjs').write_text(astro_config)
print(" ✓ astro.config.mjs updated")
# package.json (add dependencies)
print("📦 Updating package.json...")
package_json_path = output_dir / 'package.json'
if package_json_path.exists():
import json
with open(package_json_path, 'r') as f:
package_json = json.load(f)
# Add new dependencies
new_deps = {
"@astrojs/db": "^0.14.0",
"@astrojs/sitemap": "^3.2.0",
"drizzle-orm": "^0.38.0",
"@libsql/client": "^0.14.0",
}
if 'dependencies' not in package_json:
package_json['dependencies'] = {}
package_json['dependencies'].update(new_deps)
# Add new scripts
package_json['scripts']['db:push'] = 'astro db push --remote'
package_json['scripts']['db:seed'] = 'astro db seed'
with open(package_json_path, 'w') as f:
json.dump(package_json, f, indent=2)
print(" ✓ package.json updated")
# Create .env.example
print("🔐 Creating .env.example...")
env_example = f"""# Umami Analytics
UMAMI_WEBSITE_ID={args.umami_id or 'your-website-id-here'}
UMAMI_DOMAIN={args.umami_domain}
# Admin
ADMIN_PASSWORD={args.admin_password}
# Database (optional)
# ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
# ASTRO_DB_APP_TOKEN=your-turso-token
# Site Configuration
SITE_URL=https://example.com
SITE_NAME="Website Name"
"""
(output_dir / '.env.example').write_text(env_example)
# Update .gitignore
gitignore = """node_modules
dist
.env
.astro
*.db
*.log
.DS_Store
"""
(output_dir / '.gitignore').write_text(gitignore)
print(" ✓ .env.example created")
# Create/update Dockerfile
print("🐳 Creating Dockerfile...")
dockerfile = """FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/db ./db
RUN apk add --no-cache sqlite-libs
EXPOSE 80
ENV NODE_ENV=production
ENV ASTRO_DB_REMOTE_URL=file:/app/data/consent.db
CMD ["sh", "-c", "mkdir -p /app/data && npx astro preview --host 0.0.0.0 --port 80"]
"""
(output_dir / 'Dockerfile').write_text(dockerfile)
print(" ✓ Dockerfile created")
# Create MIGRATION.md
print("📝 Creating migration guide...")
migration_guide = f"""# Migration Guide
## What Changed
This website has been refactored to include:
- ✅ PDPA-compliant Privacy Policy
- ✅ PDPA-compliant Terms & Conditions
- ✅ Cookie consent system
- ✅ Consent logging database
- ✅ i18n routing ({', '.join(languages)})
- ✅ Umami Analytics integration
- ✅ Admin dashboard
## New Features
### 1. Privacy Policy
Location: `/privacy-policy`, `/th/privacy-policy`
- All 14 PDPA Section 36 disclosures
- Version tracking
- Last updated date
### 2. Cookie Consent
- Appears on first visit
- Opt-in model (required by PDPA)
- Granular choices (essential/analytics/marketing)
- Consent logged to database
### 3. Admin Dashboard
URL: `/admin/consent-logs`
Password: {args.admin_password} (**CHANGE THIS!**)
Features:
- View all consent records
- Filter by date/locale
- Delete records (right to be forgotten)
### 4. Database
Schema: `db/config.ts`
- Stores consent logs
- SQLite file (development)
- Turso ready (production)
### 5. i18n Routing
- Default locale: {default_locale}
- URL structure: `/about` ({default_locale}), `/th/about` (Thai)
- Fallback: Thai → English
## Migration Steps
### 1. Review Changes
```bash
# Check new files
ls -la src/pages/
ls -la src/components/consent/
ls -la db/
```
### 2. Update Content
- [ ] Edit Privacy Policy (add your company info)
- [ ] Edit Terms & Conditions (add service details)
- [ ] Update .env with your credentials
- [ ] Change admin password
### 3. Test Features
```bash
npm install
npm run dev
# Open http://localhost:4321
```
Checklist:
- [ ] Language switcher works
- [ ] Cookie consent appears
- [ ] Admin dashboard accessible
- [ ] Consent logging works
### 4. Deploy
```bash
npm run build
docker build -t website:latest .
# Deploy to Easypanel
```
## Content Migration
### Blog Posts
Your existing blog posts have been copied to:
`src/content/blog/(en)/` (or your default language)
To add bilingual content:
1. Translate posts
2. Copy to `src/content/blog/(th)/`
3. Update frontmatter with `locale: 'th'`
### Components
Migrated components are in:
`src/components/migrated/`
You can continue using them or migrate to new structure.
## Rollback
If you need to rollback:
1. Use the backup created at: `{args.input}_backup_TIMESTAMP`
2. Restore from backup
3. Revert deployment
## Support
See documentation:
- `SKILL.md` - Complete skill workflow
- `SPECIFICATION.md` - Technical details
- `IMPLEMENTATION_SUMMARY.md` - Feature summary
"""
(output_dir / 'MIGRATION.md').write_text(migration_guide)
print(" ✓ Migration guide created")
def get_privacy_policy_template(lang, args):
"""Generate Privacy Policy template."""
if lang == 'th':
return f"""---
import BaseLayout from '../../layouts/BaseLayout.astro';
const lastUpdated = new Date().toLocaleDateString('th-TH', {{
year: 'numeric',
month: 'long',
day: 'numeric'
}});
const version = '1.0.0';
---
<BaseLayout title="นโยบายความเป็นส่วนตัว | {args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}">
<section class="py-20 bg-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
นโยบายความเป็นส่วนตัว
</h1>
<p class="text-center text-gray-600 mb-12">
ฉบับที่ {{version}} | อัปเดตล่าสุด: {{lastUpdated}}
</p>
<div class="max-w-4xl mx-auto prose prose-lg">
<h2 class="text-2xl font-bold mb-4">1. ผู้ควบคุมข้อมูล</h2>
<p class="mb-6">
<strong>{args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}</strong> เป็นผู้ควบคุมข้อมูลส่วนบุคคลของคุณ
<br />
ติดต่อ: [ข้อมูลการติดต่อของคุณ]
</p>
<h2 class="text-2xl font-bold mb-4">2. ข้อมูลที่เก็บรวบรวม</h2>
<p class="mb-6">เราเก็บรวบรวมข้อมูลส่วนบุคคลประเภทต่อไปนี้:</p>
<ul class="mb-6">
<li>ข้อมูลการติดต่อ (ชื่อ, อีเมล, เบอร์โทรศัพท์) เมื่อคุณติดต่อเรา</li>
<li>ข้อมูลการใช้งาน (จำนวนการดูหน้า, ระยะเวลาเซสชัน) ผ่านการวิเคราะห์</li>
<li>การตั้งค่าคุกกี้และบันทึกการยินยอม</li>
<li>ข้อมูลทางเทคนิค (ที่อยู่ IP, ประเภทเบราว์เซอร์, ข้อมูลอุปกรณ์)</li>
</ul>
<h2 class="text-2xl font-bold mb-4">3. วัตถุประสงค์ในการประมวลผล</h2>
<p class="mb-6">เราประมวลผลข้อมูลของคุณเพื่อวัตถุประสงค์ดังนี้:</p>
<ul class="mb-6">
<li>เพื่อให้บริการและบำรุงรักษาบริการของเรา</li>
<li>เพื่อสื่อสารกับคุณเกี่ยวกับบริการของเรา</li>
<li>เพื่อปรับปรุงเว็บไซต์ผ่านการวิเคราะห์ (ด้วยความยินยอมของคุณ)</li>
<li>เพื่อปฏิบัติตามข้อผูกพันทางกฎหมาย</li>
</ul>
<p class="mb-6">
<strong>ฐานทางกฎหมาย:</strong> ความยินยอม ผลประโยชน์โดยชอบด้วยกฎหมาย ความจำเป็นตามสัญญา
</p>
<h2 class="text-2xl font-bold mb-4">4. ระยะเวลาเก็บรักษาข้อมูล</h2>
<p class="mb-6">
เราเก็บรักษาข้อมูลส่วนบุคคลตราบเท่าที่จำเป็นเพื่อวัตถุประสงค์ที่ระบุในนโยบายนี้
หรือตามที่กฎหมายกำหนด บันทึกการยินยอมถูกเก็บรักษาไว้ 10 ปีเพื่อปฏิบัติตามข้อกำหนด PDPA
</p>
<h2 class="text-2xl font-bold mb-4">5. การเปิดเผยข้อมูล</h2>
<p class="mb-6">เราไม่ขายหรือให้เช่าข้อมูลส่วนบุคคลของคุณ เราอาจแบ่งปันข้อมูลกับ:</p>
<ul class="mb-6">
<li>ผู้ให้บริการที่ประมวลผลข้อมูลในนามของเรา</li>
<li>หน่วยงานทางกฎหมายเมื่อ-required by law</li>
<li>ผู้ให้บริการการวิเคราะห์ (เฉพาะเมื่อคุณยินยอม)</li>
</ul>
<h2 class="text-2xl font-bold mb-4">6. คุกกี้และการติดตาม</h2>
<p class="mb-6">
เราใช้คุกกี้และเทคโนโลยีที่คล้ายคลึงกัน คุกกี้ที่จำเป็นจะทำงานเสมอ
คุกกี้การวิเคราะห์และการตลาดต้องการความยินยอมที่ชัดเจนจากคุณ
คุณสามารถจัดการการตั้งค่าได้ผ่านแบนเนอร์การยินยอมคุกกี้ของเรา
</p>
<h2 class="text-2xl font-bold mb-4">7. สิทธิ์ของคุณ (PDPA)</h2>
<p class="mb-6">คุณมีสิทธิ์ดังต่อไปนี้ภายใต้ PDPA:</p>
<ul class="mb-6">
<li>สิทธิ์ในการเข้าถึงข้อมูลส่วนบุคคลของคุณ</li>
<li>สิทธิ์ในการแก้ไขข้อมูลที่ไม่ถูกต้อง</li>
<li>สิทธิ์ในการลบข้อมูล (สิทธิ์ที่จะถูกลืม)</li>
<li>สิทธิ์ในการระงับการประมวลผล</li>
<li>สิทธิ์ในการพกพาข้อมูล</li>
<li>สิทธิ์ในการคัดค้านการประมวลผล</li>
<li>สิทธิ์ในการถอนความยินยอมเมื่อใดก็ได้</li>
</ul>
<h2 class="text-2xl font-bold mb-4">8. ความปลอดภัยของข้อมูล</h2>
<p class="mb-6">
เราใช้มาตรการรักษาความปลอดภัยที่เหมาะสมทั้งทางเทคนิคและองค์กร
เพื่อปกป้องข้อมูลส่วนบุคคลของคุณจากการเข้าถึง การแก้ไข
การเปิดเผย หรือการทำลายโดยไม่ได้รับอนุญาต
</p>
<h2 class="text-2xl font-bold mb-4">9. การโอนข้อมูลข้ามประเทศ</h2>
<p class="mb-6">
[ถ้ามี] ข้อมูลของคุณอาจถูกโอนไปยังและประมวลผลในประเทศอื่นๆ นอกเหนือจากประเทศไทย
เรารับรองว่ามีมาตรการคุ้มครองที่เหมาะสมสำหรับการโอนดังกล่าว
</p>
<h2 class="text-2xl font-bold mb-4">10. การติดต่อและข้อร้องเรียน</h2>
<p class="mb-6">
สำหรับคำถามใดๆ หรือเพื่อใช้สิทธิ์ของคุณ ติดต่อเราได้ที่: [อีเมลติดต่อของคุณ]
<br /><br />
คุณยังมีสิทธิ์ในการยื่นข้อร้องเรียนต่อคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล (PDPC)
</p>
<h2 class="text-2xl font-bold mb-4">11. การอัปเดตนโยบาย</h2>
<p class="mb-6">
เราอาจอัปเดตนโยบายนี้เป็นครั้งคราว เราจะแจ้งให้คุณทราบถึงการเปลี่ยนแปลงใดๆ
โดยการลงประกาศนโยบายใหม่บนหน้านี้และอัปเดตเลขฉบับที่
</p>
<div class="mt-12 pt-8 border-t">
<p class="text-gray-500">
For the English version of this policy, please see:
<a href="/privacy-policy" class="text-primary hover:underline">Privacy Policy</a>
</p>
</div>
</div>
</div>
</section>
</BaseLayout>
"""
else:
return f"""---
import BaseLayout from '../../layouts/BaseLayout.astro';
const lastUpdated = new Date().toLocaleDateString('en-US', {{
year: 'numeric',
month: 'long',
day: 'numeric'
}});
const version = '1.0.0';
---
<BaseLayout title="Privacy Policy | {args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}">
<section class="py-20 bg-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
Privacy Policy
</h1>
<p class="text-center text-gray-600 mb-12">
Version {{version}} | Last updated: {{lastUpdated}}
</p>
<div class="max-w-4xl mx-auto prose prose-lg">
<h2 class="text-2xl font-bold mb-4">1. Data Controller</h2>
<p class="mb-6">
<strong>{args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}</strong> is the data controller responsible for your personal data.
<br />
Contact: [Your contact information]
</p>
<h2 class="text-2xl font-bold mb-4">2. Data We Collect</h2>
<p class="mb-6">We collect the following types of personal data:</p>
<ul class="mb-6">
<li>Contact information (name, email, phone number) when you contact us</li>
<li>Usage data (page views, session duration) via analytics</li>
<li>Cookie preferences and consent records</li>
<li>Technical data (IP address, browser type, device information)</li>
</ul>
<h2 class="text-2xl font-bold mb-4">3. Purpose of Processing</h2>
<p class="mb-6">We process your data for the following purposes:</p>
<ul class="mb-6">
<li>To provide and maintain our services</li>
<li>To communicate with you about our services</li>
<li>To improve our website through analytics (with your consent)</li>
<li>To comply with legal obligations</li>
</ul>
<p class="mb-6">
<strong>Legal Basis:</strong> Consent, legitimate interest, contractual necessity
</p>
<h2 class="text-2xl font-bold mb-4">4. Data Retention</h2>
<p class="mb-6">
We retain personal data for as long as necessary to fulfill the purposes outlined in this policy,
or as required by law. Consent records are retained for 10 years to comply with PDPA requirements.
</p>
<h2 class="text-2xl font-bold mb-4">5. Data Sharing & Disclosure</h2>
<p class="mb-6">We do not sell or rent your personal data. We may share data with:</p>
<ul class="mb-6">
<li>Service providers who process data on our behalf</li>
<li>Legal authorities when required by law</li>
<li>Analytics providers (only with your consent)</li>
</ul>
<h2 class="text-2xl font-bold mb-4">6. Cookies & Tracking</h2>
<p class="mb-6">
We use cookies and similar technologies. Essential cookies are always active.
Analytics and marketing cookies require your explicit consent. You can manage
your preferences through our cookie consent banner.
</p>
<h2 class="text-2xl font-bold mb-4">7. Your Rights (PDPA)</h2>
<p class="mb-6">You have the following rights under PDPA:</p>
<ul class="mb-6">
<li>Right to access your personal data</li>
<li>Right to rectification (correction) of inaccurate data</li>
<li>Right to erasure (right to be forgotten)</li>
<li>Right to restrict processing</li>
<li>Right to data portability</li>
<li>Right to object to processing</li>
<li>Right to withdraw consent at any time</li>
</ul>
<h2 class="text-2xl font-bold mb-4">8. Data Security</h2>
<p class="mb-6">
We implement appropriate technical and organizational measures to protect your personal data
against unauthorized access, alteration, disclosure, or destruction.
</p>
<h2 class="text-2xl font-bold mb-4">9. Cross-Border Transfers</h2>
<p class="mb-6">
[If applicable] Your data may be transferred to and processed in countries other than Thailand.
We ensure appropriate safeguards are in place for such transfers.
</p>
<h2 class="text-2xl font-bold mb-4">10. Contact & Complaints</h2>
<p class="mb-6">
For any questions or to exercise your rights, contact us at: [Your contact email]
<br /><br />
You also have the right to lodge a complaint with the Personal Data Protection Committee (PDPC).
</p>
<h2 class="text-2xl font-bold mb-4">11. Policy Updates</h2>
<p class="mb-6">
We may update this policy from time to time. We will notify you of any changes by posting
the new policy on this page and updating the version number.
</p>
<div class="mt-12 pt-8 border-t">
<p class="text-gray-500">
สำหรับเวอร์ชันภาษาไทย โปรดดูที่:
<a href="/th/privacy-policy" class="text-primary hover:underline">นโยบายความเป็นส่วนตัว</a>
</p>
</div>
</div>
</div>
</section>
</BaseLayout>
"""
def get_terms_template(lang):
"""Generate Terms & Conditions template."""
if lang == 'th':
return """---
import BaseLayout from '../../layouts/BaseLayout.astro';
const lastUpdated = new Date().toLocaleDateString('th-TH', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
---
<BaseLayout title="ข้อกำหนดและเงื่อนไข | Website">
<section class="py-20 bg-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
ข้อกำหนดและเงื่อนไข
</h1>
<p class="text-center text-gray-600 mb-12">
อัปเดตล่าสุด: {lastUpdated}
</p>
<div class="max-w-4xl mx-auto prose prose-lg">
<h2 class="text-2xl font-bold mb-4">1. การยอมรับเงื่อนไข</h2>
<p class="mb-6">
ด้วยการเข้าถึงและใช้เว็บไซต์นี้ คุณยอมรับและตกลงที่จะถูกผูกพันด้วยข้อกำหนด
และบทบัญญัติของข้อตกลงนี้
</p>
<h2 class="text-2xl font-bold mb-4">2. บริการ</h2>
<p class="mb-6">
เว็บไซต์ให้บริการ [ระบุบริการของคุณ] รายละเอียดบริการที่สมบูรณ์จะแจ้งให้ทราบแยกต่างหากเมื่อมีการใช้บริการ
</p>
<h2 class="text-2xl font-bold mb-4">3. ทรัพย์สินทางปัญญา</h2>
<p class="mb-6">
เนื้อหาทั้งหมดบนเว็บไซต์นี้ รวมถึงข้อความ กราฟิก โลโก้ และซอฟต์แวร์
เป็นทรัพย์สินของเว็บไซต์และอยู่ภายใต้การคุ้มครองกฎหมายลิขสิทธิ์ของไทยและสากล
</p>
<h2 class="text-2xl font-bold mb-4">4. หน้าที่ของผู้ใช้</h2>
<p class="mb-6">
คุณตกลงที่จะใช้เว็บไซต์นี้เพื่อวัตถุประสงค์ที่ถูกต้องตามกฎหมายเท่านั้น
และในวิธีที่ไม่ละเมิดสิทธิ์ จำกัด หรือยับยั้งการใช้งานของผู้อื่น
</p>
<h2 class="text-2xl font-bold mb-4">5. การจำกัดความรับผิด</h2>
<p class="mb-6">
เว็บไซต์จะไม่รับผิดต่อความเสียหายทางอ้อม โดยบังเอิญ เฉพาะเรื่อง หรือเชิงลงโทษ
อันเกิดจากการใช้หรือไม่สามารถใช้เว็บไซต์นี้
</p>
<h2 class="text-2xl font-bold mb-4">6. กฎหมายที่ใช้บังคับ</h2>
<p class="mb-6">
ข้อกำหนดเหล่านี้จะอยู่ภายใต้การตีความและบังคับตามกฎหมายของประเทศไทย
โดยไม่คำนึงถึงหลักการขัดกันแห่งกฎหมาย
</p>
<h2 class="text-2xl font-bold mb-4">7. การระงับข้อพิพาท</h2>
<p class="mb-6">
ข้อพิพาทใดๆ ที่เกิดจากข้อกำหนดนี้จะได้รับการแก้ไขผ่านการเจรจาต่อรองด้วยดี
หากไม่สำเร็จ ข้อพิพาทจะถูกยื่นต่อศาลที่มีเขตอำนาจในประเทศไทย
</p>
<h2 class="text-2xl font-bold mb-4">8. การแก้ไข</h2>
<p class="mb-6">
เราขอสงวนสิทธิ์ในการแก้ไขข้อกำหนดเหล่านี้ได้ตลอดเวลา
การใช้เว็บไซต์ต่อไปหลังจากมีการเปลี่ยนแปลงถือเป็นการยอมรับการเปลี่ยนแปลง tersebut
</p>
<h2 class="text-2xl font-bold mb-4">9. ข้อมูลการติดต่อ</h2>
<p class="mb-6">
สำหรับคำถามเกี่ยวกับข้อกำหนดเหล่านี้ โปรดติดต่อเราที่: [อีเมลติดต่อของคุณ]
</p>
<div class="mt-12 pt-8 border-t">
<p class="text-gray-500">
For the English version of these terms, please see:
<a href="/terms-and-conditions" class="text-primary hover:underline">Terms and Conditions</a>
</p>
</div>
</div>
</div>
</section>
</BaseLayout>
"""
else:
return """---
import BaseLayout from '../../layouts/BaseLayout.astro';
const lastUpdated = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
---
<BaseLayout title="Terms & Conditions | Website">
<section class="py-20 bg-white">
<div class="container mx-auto px-4">
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
Terms and Conditions
</h1>
<p class="text-center text-gray-600 mb-12">
Last updated: {lastUpdated}
</p>
<div class="max-w-4xl mx-auto prose prose-lg">
<h2 class="text-2xl font-bold mb-4">1. Acceptance of Terms</h2>
<p class="mb-6">
By accessing and using this website, you accept and agree to be bound by the terms
and provision of this agreement.
</p>
<h2 class="text-2xl font-bold mb-4">2. Services</h2>
<p class="mb-6">
Website provides [describe your services]. Detailed service terms are provided
separately upon engagement.
</p>
<h2 class="text-2xl font-bold mb-4">3. Intellectual Property</h2>
<p class="mb-6">
All content on this website, including text, graphics, logos, and software, is the
property of Website and is protected by Thai and international copyright laws.
</p>
<h2 class="text-2xl font-bold mb-4">4. User Obligations</h2>
<p class="mb-6">
You agree to use this website only for lawful purposes and in a way that does not
infringe the rights of, restrict or inhibit anyone else's use of the website.
</p>
<h2 class="text-2xl font-bold mb-4">5. Limitation of Liability</h2>
<p class="mb-6">
Website shall not be liable for any indirect, incidental, special, consequential
or punitive damages resulting from your use of or inability to use this website.
</p>
<h2 class="text-2xl font-bold mb-4">6. Governing Law</h2>
<p class="mb-6">
These terms shall be governed by and construed in accordance with the laws of
Thailand, without regard to its conflict of law provisions.
</p>
<h2 class="text-2xl font-bold mb-4">7. Dispute Resolution</h2>
<p class="mb-6">
Any disputes arising from these terms shall be resolved through good faith negotiations.
If unsuccessful, disputes shall be submitted to the competent courts of Thailand.
</p>
<h2 class="text-2xl font-bold mb-4">8. Modifications</h2>
<p class="mb-6">
We reserve the right to modify these terms at any time. Continued use of the website
following any changes constitutes acceptance of those changes.
</p>
<h2 class="text-2xl font-bold mb-4">9. Contact Information</h2>
<p class="mb-6">
For questions about these terms, please contact us at: [Your contact email]
</p>
<div class="mt-12 pt-8 border-t">
<p class="text-gray-500">
สำหรับเวอร์ชันภาษาไทย โปรดดูที่:
<a href="/th/terms-and-conditions" class="text-primary hover:underline">ข้อกำหนดและเงื่อนไข</a>
</p>
</div>
</div>
</div>
</section>
</BaseLayout>
"""
def get_cookie_banner_template():
"""Get cookie banner component template."""
return """---
const siteName = "Website";
---
<div
id="cookie-consent-banner"
class="fixed bottom-0 left-0 right-0 bg-white shadow-lg p-6 z-50 hidden"
data-component="cookie-banner"
>
<div class="container mx-auto max-w-4xl">
<h2 class="text-xl font-bold mb-4">🍪 Cookie Consent</h2>
<p class="mb-6 text-base">
We use cookies to improve your experience. By clicking "Accept All",
you consent to our use of cookies.
<a href="/privacy-policy" class="text-blue-600 underline">Learn more</a>
</p>
<div class="flex gap-4 flex-wrap">
<button
id="consent-reject"
class="px-6 py-3 bg-gray-200 hover:bg-gray-300 rounded text-base"
>
Reject Non-Essential
</button>
<button
id="consent-accept"
class="px-6 py-3 bg-blue-600 text-white hover:bg-blue-700 rounded text-base"
>
Accept All
</button>
<button
id="consent-customize"
class="px-6 py-3 border border-blue-600 text-blue-600 hover:bg-blue-50 rounded text-base"
>
Customize
</button>
</div>
</div>
</div>
<script>
function initCookieBanner() {
const banner = document.getElementById('cookie-consent-banner');
window.addEventListener('show-consent-banner', () => {
const existingConsent = localStorage.getItem('consent-preferences');
if (!existingConsent) {
banner?.classList.remove('hidden');
}
});
document.getElementById('consent-accept')?.addEventListener('click', () => {
handleConsent({ essential: true, analytics: true, marketing: true });
banner?.classList.add('hidden');
});
document.getElementById('consent-reject')?.addEventListener('click', () => {
handleConsent({ essential: true, analytics: false, marketing: false });
banner?.classList.add('hidden');
});
document.getElementById('consent-customize')?.addEventListener('click', () => {
const event = new CustomEvent('open-consent-preferences');
window.dispatchEvent(event);
});
async function handleConsent(consent) {
localStorage.setItem('consent-preferences', JSON.stringify({
timestamp: new Date().toISOString(),
...consent
}));
const sessionId = crypto.randomUUID();
await fetch('/api/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
locale: document.documentElement.lang,
...consent,
policyVersion: '1.0.0',
}),
});
if (consent.analytics) {
location.reload();
}
}
}
initCookieBanner();
</script>
"""
def get_admin_dashboard_template():
"""Get admin dashboard template."""
return """---
import { db, ConsentLog, desc } from 'astro:db';
const ADMIN_PASSWORD = Astro.env.ADMIN_PASSWORD || 'changeme';
let logs = [];
let isAuthenticated = false;
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const password = formData.get('password');
if (password === ADMIN_PASSWORD) {
isAuthenticated = true;
logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
}
}
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Consent Logs Admin</title>
<style>
body { font-family: system-ui; background: #f3f4f6; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1.5rem; }
.login-form { max-width: 400px; background: white; padding: 2rem; border-radius: 0.5rem; }
input[type="password"] { width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid #d1d5db; border-radius: 0.375rem; }
button { width: 100%; padding: 0.75rem; background: #2563eb; color: white; border: none; border-radius: 0.375rem; cursor: pointer; }
table { width: 100%; background: white; border-radius: 0.5rem; overflow: hidden; }
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
th { background: #f9fafb; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; }
</style>
</head>
<body>
<div class="container">
<h1>🔐 Consent Logs Admin</h1>
{!isAuthenticated ? (
<div class="login-form">
<h2>Admin Login</h2>
<form method="POST">
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
</div>
) : (
<div>
<a href="/admin/consent-logs" style="display: inline-block; margin-bottom: 1rem; padding: 0.5rem 1rem; background: #2563eb; color: white; text-decoration: none; border-radius: 0.375rem;">Refresh</a>
<table>
<thead>
<tr>
<th>Date</th><th>Locale</th><th>Session ID</th><th>Essential</th><th>Analytics</th><th>Marketing</th><th>Policy Ver</th>
</tr>
</thead>
<tbody>
{logs.map(log => (
<tr>
<td>{new Date(log.timestamp).toLocaleString()}</td>
<td>{log.locale}</td>
<td style="font-family: monospace;">{log.sessionId}</td>
<td>{log.essential ? '' : ''}</td>
<td>{log.analytics ? '' : ''}</td>
<td>{log.marketing ? '' : ''}</td>
<td>{log.policyVersion}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</body>
</html>
"""
def get_db_config_template():
"""Get database configuration template."""
return """import { defineDb, defineTable, column } from 'astro:db';
const ConsentLog = defineTable({
columns: {
id: column.number({ primaryKey: true }),
sessionId: column.text({ unique: true }),
timestamp: column.date(),
locale: column.text(),
essential: column.boolean(),
analytics: column.boolean(),
marketing: column.boolean(),
policyVersion: column.text(),
ipHash: column.text(),
userAgent: column.text(),
},
});
export default defineDb({
tables: { ConsentLog },
});
"""
def get_db_seed_template():
"""Get database seed template."""
return """import { db, ConsentLog } from 'astro:db';
export default async function seed() {
await db.insert(ConsentLog).values([
{
sessionId: 'dev-session-001',
timestamp: new Date(),
locale: 'en',
essential: true,
analytics: true,
marketing: false,
policyVersion: '1.0.0',
ipHash: 'dev1234567890abcd',
userAgent: 'Mozilla/5.0 (development)',
},
]);
}
"""
def get_i18n_lib_template(languages):
"""Get i18n utilities template."""
lang_entries = []
for lang in languages:
if lang == 'th':
lang_entries.append(" th: { name: 'ไทย', locale: 'th' },")
else:
lang_entries.append(" en: { name: 'English', locale: 'en' },")
return f"""export const languages = {{
{chr(10).join(lang_entries)}
}};
export const defaultLocale = 'en';
export function getLanguageFromLocale(locale: string) {{
return languages[locale as keyof typeof languages] || languages.en;
}}
"""
def create_api_endpoints(output_dir):
"""Create API endpoint files."""
api_dir = output_dir / 'src' / 'pages' / 'api' / 'consent'
api_dir.mkdir(parents=True, exist_ok=True)
# POST endpoint
post_endpoint = """import type { APIRoute } from 'astro';
import { db, ConsentLog } from 'astro:db';
import { createHash } from 'crypto';
export const POST: APIRoute = async ({ request }) => {
try {
const data = await request.json();
const { sessionId, locale, essential, analytics, marketing, policyVersion } = data;
if (!sessionId || !locale) {
return new Response(
JSON.stringify({ error: 'Missing required fields' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const ipHash = createHash('sha256').update(ip).digest('hex').substring(0, 16);
await db.insert(ConsentLog).values({
sessionId,
timestamp: new Date(),
locale,
essential: essential || false,
analytics: analytics || false,
marketing: marketing || false,
policyVersion,
ipHash,
userAgent: request.headers.get('user-agent') || '',
});
return new Response(
JSON.stringify({ success: true, sessionId }),
{ status: 201, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Consent logging error:', error);
return new Response(
JSON.stringify({ error: 'Failed to log consent' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
"""
(api_dir / 'POST.ts').write_text(post_endpoint)
# GET endpoint
get_endpoint = """import type { APIRoute } from 'astro';
import { db, ConsentLog, desc } from 'astro:db';
export const GET: APIRoute = async () => {
try {
const logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
return new Response(
JSON.stringify({ success: true, logs }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Consent fetch error:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch consent logs' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
"""
(api_dir / 'GET.ts').write_text(get_endpoint)
# DELETE endpoint
delete_dir = api_dir / '[sessionId]'
delete_dir.mkdir(parents=True, exist_ok=True)
delete_endpoint = """import type { APIRoute } from 'astro';
import { db, ConsentLog, eq } from 'astro:db';
export const DELETE: APIRoute = async ({ params }) => {
try {
const { sessionId } = params;
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'Session ID required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const result = await db.delete(ConsentLog).where(
eq(ConsentLog.sessionId, sessionId)
);
return new Response(
JSON.stringify({ success: true, deleted: result.changes > 0 }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Consent deletion error:', error);
return new Response(
JSON.stringify({ error: 'Failed to delete consent' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
"""
(delete_dir / 'DELETE.ts').write_text(delete_endpoint)
if __name__ == "__main__":
main()