# Website Creator Skill - Technical Specification **Version:** 2.0 **Last Updated:** 2026-03-08 **Framework:** Astro 5.x **Compliance:** Thailand PDPA --- ## ๐ŸŽฏ Overview This specification defines the complete structure and implementation for the `website-creator` skill, which generates PDPA-compliant Astro websites with: - Bilingual support (Thai/English) - Umami Analytics integration - Cookie consent management - Consent logging database - Easypanel deployment --- ## ๐Ÿ“ Standard Folder Structure ``` {website-name}/ โ”œโ”€โ”€ public/ โ”‚ โ”œโ”€โ”€ favicon.ico โ”‚ โ”œโ”€โ”€ favicon.svg โ”‚ โ”œโ”€โ”€ images/ โ”‚ โ”‚ โ””โ”€โ”€ logo.svg โ”‚ โ””โ”€โ”€ robots.txt โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ components/ โ”‚ โ”‚ โ”œโ”€โ”€ common/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Header.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Footer.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ LanguageSwitcher.astro โ”‚ โ”‚ โ”œโ”€โ”€ consent/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ CookieBanner.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ConsentPreferences.astro โ”‚ โ”‚ โ””โ”€โ”€ ui/ โ”‚ โ”‚ โ”œโ”€โ”€ Button.astro โ”‚ โ”‚ โ”œโ”€โ”€ Card.astro โ”‚ โ”‚ โ””โ”€โ”€ Section.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ layouts/ โ”‚ โ”‚ โ””โ”€โ”€ BaseLayout.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ pages/ โ”‚ โ”‚ โ”œโ”€โ”€ index.astro # Home (redirects to default locale) โ”‚ โ”‚ โ”œโ”€โ”€ th/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ about.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ contact.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ privacy-policy.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terms-and-conditions.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ blog/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ [slug].astro โ”‚ โ”‚ โ”œโ”€โ”€ en/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ about.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ contact.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ privacy-policy.astro โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terms-and-conditions.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ blog/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.astro โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ [slug].astro โ”‚ โ”‚ โ””โ”€โ”€ admin/ โ”‚ โ”‚ โ””โ”€โ”€ consent-logs.astro # Password-protected admin โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ pages/api/ โ”‚ โ”‚ โ””โ”€โ”€ consent/ โ”‚ โ”‚ โ”œโ”€โ”€ POST.ts # Log consent โ”‚ โ”‚ โ”œโ”€โ”€ GET.ts # Get consent logs (admin) โ”‚ โ”‚ โ””โ”€โ”€ [sessionId]/DELETE.ts # Delete consent (right to be forgotten) โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ styles/ โ”‚ โ”‚ โ””โ”€โ”€ global.css โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content/ โ”‚ โ”‚ โ”œโ”€โ”€ blog/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ (th)/ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ *.md โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ (en)/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ *.md โ”‚ โ”‚ โ””โ”€โ”€ config.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ lib/ โ”‚ โ”‚ โ”œโ”€โ”€ i18n.ts # i18n utilities โ”‚ โ”‚ โ”œโ”€โ”€ consent.ts # Consent utilities โ”‚ โ”‚ โ””โ”€โ”€ utils.ts โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ middleware.ts # i18n middleware โ”‚ โ”œโ”€โ”€ db/ โ”‚ โ”œโ”€โ”€ config.ts # Astro DB schema โ”‚ โ””โ”€โ”€ seed.ts # Development seed data โ”‚ โ”œโ”€โ”€ Dockerfile โ”œโ”€โ”€ docker-compose.yml โ”œโ”€โ”€ package.json โ”œโ”€โ”€ astro.config.mjs โ”œโ”€โ”€ tailwind.config.mjs โ”œโ”€โ”€ tsconfig.json โ”œโ”€โ”€ .env.example โ”œโ”€โ”€ .gitignore โ”œโ”€โ”€ README.md โ”œโ”€โ”€ DEPLOYMENT.md โ”œโ”€โ”€ CONTENT-GUIDE.md โ””โ”€โ”€ CHECKLIST.md ``` --- ## ๐Ÿ”ง Configuration Files ### astro.config.mjs ```javascript 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', // Static + server endpoints for API i18n: { locales: ['en', 'th'], defaultLocale: 'en', routing: { prefixDefaultLocale: false, // /about for EN, /th/about for TH fallbackType: 'rewrite', }, fallback: { th: 'en', // Fallback Thai โ†’ English }, }, integrations: [ tailwindcss(), db(), sitemap({ i18n: { defaultLocale: 'en', }, }), ], }); ``` ### db/config.ts (Consent Logging Schema) ```typescript 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(), // 'th' | 'en' essential: column.boolean(), analytics: column.boolean(), marketing: column.boolean(), policyVersion: column.text(), ipHash: column.text(), userAgent: column.text(), }, }); export default defineDb({ tables: { ConsentLog }, }); ``` ### package.json (Dependencies) ```json { "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" }, "scripts": { "dev": "astro dev", "build": "astro build --remote", "preview": "astro preview", "db:push": "astro db push --remote", "db:seed": "astro db seed" } } ``` --- ## ๐ŸŒ i18n Implementation ### src/middleware.ts ```typescript import { defineMiddleware, sequence } from "astro:middleware"; import { middleware } from "astro:i18n"; // Custom middleware (optional - for additional logic) export const customMiddleware = defineMiddleware(async (ctx, next) => { const response = await next(); return response; }); export const onRequest = sequence( customMiddleware, middleware({ redirectToDefaultLocale: true, prefixDefaultLocale: false, }) ); ``` ### src/lib/i18n.ts ```typescript export const languages = { en: { name: 'English', locale: 'en', }, th: { name: 'เน„เธ—เธข', locale: 'th', }, }; export const defaultLocale = 'en'; export function getLanguageFromLocale(locale: string) { return languages[locale as keyof typeof languages] || languages.en; } ``` ### src/components/common/LanguageSwitcher.astro ```astro --- import { getRelativeLocaleUrl } from 'astro:i18n'; import { languages } from '../../lib/i18n'; interface Props { currentLocale: string; } const { currentLocale } = Astro.props; const currentPath = Astro.url.pathname; ---
{Object.values(languages).map((lang) => ( {lang.name} ))}
``` --- ## ๐Ÿช Cookie Consent Implementation ### src/components/consent/CookieBanner.astro ```astro --- const siteName = "Website Name"; const policyUrl = "/privacy-policy"; --- ``` ### src/pages/api/consent/POST.ts ```typescript 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(); // Validate required fields 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' } } ); } // Hash IP address for privacy const ip = request.headers.get('x-forwarded-for') || 'unknown'; const ipHash = createHash('sha256').update(ip).digest('hex').substring(0, 16); // Insert consent record 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' } } ); } }; ``` ### src/pages/api/consent/[sessionId]/DELETE.ts ```typescript 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' } } ); } // Delete consent record (right to be forgotten) 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' } } ); } }; ``` ### src/pages/admin/consent-logs.astro ```astro --- // Password-protected admin page for viewing consent logs import { db, ConsentLog, desc } from 'astro:db'; // Simple password protection (in production, use proper auth) 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); } } --- Consent Logs Admin

Consent Logs

{!isAuthenticated ? (
) : (
{logs.map((log) => ( ))}
Date Locale Session ID Essential Analytics Marketing Policy Ver IP Hash
{new Date(log.timestamp).toLocaleString()} {log.locale} {log.sessionId} {log.essential ? 'โœ…' : 'โŒ'} {log.analytics ? 'โœ…' : 'โŒ'} {log.marketing ? 'โœ…' : 'โŒ'} {log.policyVersion} {log.ipHash}
)}
``` --- ## ๐Ÿ“Š Umami Analytics Integration ### Conditional Loading (Based on Consent) ```astro --- // In BaseLayout.astro const umamiWebsiteId = Astro.env.UMAMI_WEBSITE_ID; const umamiDomain = Astro.env.UMAMI_DOMAIN || 'analytics.example.com'; --- ``` --- ## ๐Ÿ“„ PDPA-Compliant Privacy Policy ### Structure (Both TH/EN) ```markdown # Privacy Policy ## 1. Data Controller Information - Company name, address, contact - DPO contact (if applicable) ## 2. Types of Data Collected - Personal data categories - Collection methods ## 3. Purpose of Data Processing - Legal basis (consent, legitimate interest, etc.) - Specific purposes ## 4. Data Retention Period - How long we keep data - Deletion criteria ## 5. Data Sharing & Disclosure - Third parties - Cross-border transfers ## 6. Cookies & Tracking - Types of cookies used - Consent mechanism ## 7. Your Rights (PDPA) - Right to access - Right to rectification - Right to erasure (deletion) - Right to restrict processing - Right to data portability - Right to object - Right to withdraw consent ## 8. Data Security - Security measures - Breach notification ## 9. Contact & Complaints - How to contact us - PDPC complaint process ## 10. Policy Updates - Last updated date - Version number ``` **Note:** Full template text will be in Thai and English with all PDPA-mandated disclosures. --- ## ๐Ÿณ Docker Configuration ### Dockerfile ```dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY --from=builder /app/dist ./dist COPY --from=builder /app/db ./db # Install SQLite runtime dependencies RUN apk add --no-cache sqlite-libs EXPOSE 80 # Set environment variables ENV NODE_ENV=production ENV ASTRO_DB_REMOTE_URL=file:/app/data/consent.db ENV ASTRO_DB_APP_TOKEN= CMD ["sh", "-c", "mkdir -p /app/data && npx astro preview --host 0.0.0.0 --port 80"] ``` ### docker-compose.yml ```yaml version: '3.8' services: website: build: . ports: - "80:80" environment: - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} - UMAMI_DOMAIN=${UMAMI_DOMAIN} - ADMIN_PASSWORD=${ADMIN_PASSWORD} - ASTRO_DB_REMOTE_URL=file:/app/data/consent.db volumes: - consent-data:/app/data restart: unless-stopped volumes: consent-data: ``` --- ## ๐ŸŽจ Design System ### Typography (from existing SKILL.md) ```css /* Global styles */ html { font-size: 18px; /* Base size */ } @media (min-width: 1280px) { html { font-size: 20px; } } @media (min-width: 1536px) { html { font-size: 22px; } } @media (min-width: 1920px) { html { font-size: 24px; } } ``` ### Color Scheme ```css :root { /* Default colors - customizable per website */ --color-primary: #2563eb; --color-secondary: #1e40af; --color-accent: #f59e0b; /* Neutral */ --color-gray-50: #f9fafb; --color-gray-100: #f3f4f6; --color-gray-200: #e5e7eb; --color-gray-300: #d1d5db; --color-gray-400: #9ca3af; --color-gray-500: #6b7280; --color-gray-600: #4b5563; --color-gray-700: #374151; --color-gray-800: #1f2937; --color-gray-900: #111827; } ``` --- ## ๐Ÿ“ Content Collections ### src/content/config.ts ```typescript import { defineCollection, z } from 'astro:content'; const blogCollection = defineCollection({ type: 'content', schema: ({ image }) => z.object({ title: z.string(), description: z.string(), pubDate: z.date(), updatedDate: z.date().optional(), heroImage: image().optional(), locale: z.enum(['en', 'th']), tags: z.array(z.string()).optional(), author: z.string().optional(), }), }); export const collections = { blog: blogCollection, }; ``` --- ## ๐Ÿ—‚๏ธ Environment Variables ### .env.example ```bash # Umami Analytics UMAMI_WEBSITE_ID=your-website-id-here UMAMI_DOMAIN=analytics.example.com # Admin ADMIN_PASSWORD=change-this-secure-password # Database (for production) 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="Example Website" ``` --- ## ๐Ÿš€ Generation Workflow ### Python Script CLI ```bash python3 create_astro_website.py \ --name "Deal Plus Tech" \ --type "corporate" \ --languages "th,en" \ --primary-color "#2563eb" \ --secondary-color "#1e40af" \ --features "blog,products,contact" \ --umami-id "xxx-xxx-xxx" \ --output "./dealplustech-website" ``` ### Script Responsibilities 1. **Validate input** (name, languages, features) 2. **Create folder structure** (copy templates) 3. **Generate configs** (astro.config.mjs, package.json) 4. **Create i18n pages** (TH/EN versions) 5. **Generate legal pages** (Privacy Policy, Terms) 6. **Setup database** (db/config.ts, seed.ts) 7. **Create components** (Header, Footer, Consent) 8. **Add Docker files** (Dockerfile, docker-compose.yml) 9. **Generate documentation** (README, DEPLOYMENT, etc.) 10. **Initialize Git repo** (optional) --- ## โœ… Quality Assurance ### Pre-deployment Checklist - [ ] All pages render without errors - [ ] i18n routing works (TH/EN switch) - [ ] Cookie banner appears on first visit - [ ] Consent is logged to database - [ ] Umami loads only with consent - [ ] Admin page accessible with password - [ ] Data deletion works (right to be forgotten) - [ ] Docker build succeeds - [ ] All TypeScript types correct - [ ] Lighthouse score > 90 ### PDPA Compliance Checklist - [ ] Privacy Policy contains all 12+ disclosures - [ ] Cookie consent is opt-in (not pre-ticked) - [ ] Granular consent choices (essential/analytics/marketing) - [ ] Consent withdrawal as easy as acceptance - [ ] Consent logs stored with timestamp - [ ] Data deletion mechanism exists - [ ] Policy version tracking implemented - [ ] Thai language available (or bilingual) --- ## ๐Ÿ”„ Refactoring Existing Websites ### Migration Script ```bash python3 refactor_existing_website.py \ --input "./dealplustech-astro" \ --output "./dealplustech-astro-refactored" \ --add-features "i18n,consent,umami" \ --languages "th,en" ``` ### Migration Steps 1. **Backup existing content** (blog posts, products) 2. **Create new structure** (standardized folders) 3. **Migrate content** (copy to new locations) 4. **Add i18n routing** (split TH/EN) 5. **Integrate consent** (add components, API) 6. **Add Umami** (conditional loading) 7. **Update Dockerfile** (for Astro DB) 8. **Test thoroughly** (all features) --- ## ๐Ÿ“Š Success Metrics - **Consistency:** Every website has identical structure - **Compliance:** 100% PDPA compliant - **Maintainability:** Easy to update all websites simultaneously - **Performance:** Lighthouse score > 90 - **Developer Experience:** Generate new website in < 5 minutes --- **END OF SPECIFICATION**