935 lines
23 KiB
Markdown
935 lines
23 KiB
Markdown
# 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;
|
|
---
|
|
|
|
<div class="language-switcher">
|
|
{Object.values(languages).map((lang) => (
|
|
<a
|
|
href={getRelativeLocaleUrl(lang.locale, currentPath)}
|
|
class:list={['lang-link', lang.locale === currentLocale && 'active']}
|
|
lang={lang.locale}
|
|
>
|
|
{lang.name}
|
|
</a>
|
|
))}
|
|
</div>
|
|
|
|
<style>
|
|
.language-switcher {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
.lang-link {
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s;
|
|
}
|
|
.lang-link.active {
|
|
opacity: 1;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
---
|
|
|
|
## 🍪 Cookie Consent Implementation
|
|
|
|
### src/components/consent/CookieBanner.astro
|
|
|
|
```astro
|
|
---
|
|
const siteName = "Website Name";
|
|
const policyUrl = "/privacy-policy";
|
|
---
|
|
|
|
<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">
|
|
We use cookies to improve your experience. By clicking "Accept All",
|
|
you consent to our use of cookies.
|
|
<a href={policyUrl} 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"
|
|
>
|
|
Reject Non-Essential
|
|
</button>
|
|
<button
|
|
id="consent-accept"
|
|
class="px-6 py-3 bg-blue-600 text-white hover:bg-blue-700 rounded"
|
|
>
|
|
Accept All
|
|
</button>
|
|
<button
|
|
id="consent-customize"
|
|
class="px-6 py-3 border border-blue-600 text-blue-600 hover:bg-blue-50 rounded"
|
|
>
|
|
Customize
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Cookie consent logic with astro-consent integration
|
|
function initCookieBanner() {
|
|
const banner = document.getElementById('cookie-consent-banner');
|
|
const acceptBtn = document.getElementById('consent-accept');
|
|
const rejectBtn = document.getElementById('consent-reject');
|
|
const customizeBtn = document.getElementById('consent-customize');
|
|
|
|
// Check if consent already given
|
|
const existingConsent = localStorage.getItem('consent-preferences');
|
|
if (!existingConsent) {
|
|
banner?.classList.remove('hidden');
|
|
}
|
|
|
|
acceptBtn?.addEventListener('click', () => {
|
|
handleConsent({ essential: true, analytics: true, marketing: true });
|
|
banner?.classList.add('hidden');
|
|
});
|
|
|
|
rejectBtn?.addEventListener('click', () => {
|
|
handleConsent({ essential: true, analytics: false, marketing: false });
|
|
banner?.classList.add('hidden');
|
|
});
|
|
|
|
customizeBtn?.addEventListener('click', () => {
|
|
// Open preferences modal
|
|
const event = new CustomEvent('open-consent-preferences');
|
|
window.dispatchEvent(event);
|
|
});
|
|
|
|
async function handleConsent(consent: any) {
|
|
// Store in localStorage
|
|
localStorage.setItem('consent-preferences', JSON.stringify({
|
|
timestamp: new Date().toISOString(),
|
|
...consent
|
|
}));
|
|
|
|
// Log to database
|
|
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',
|
|
}),
|
|
});
|
|
|
|
// Initialize analytics if consented
|
|
if (consent.analytics) {
|
|
initializeAnalytics();
|
|
}
|
|
}
|
|
|
|
function initializeAnalytics() {
|
|
// Load Umami tracking script
|
|
const script = document.createElement('script');
|
|
script.defer = true;
|
|
script.src = 'https://analytics.example.com/script.js';
|
|
script.setAttribute('data-website-id', import.meta.env.UMAMI_WEBSITE_ID);
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
|
|
initCookieBanner();
|
|
</script>
|
|
```
|
|
|
|
### 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);
|
|
}
|
|
}
|
|
---
|
|
|
|
<html>
|
|
<head>
|
|
<title>Consent Logs Admin</title>
|
|
</head>
|
|
<body>
|
|
<div class="container mx-auto p-8">
|
|
<h1 class="text-3xl font-bold mb-8">Consent Logs</h1>
|
|
|
|
{!isAuthenticated ? (
|
|
<form method="POST" class="max-w-md">
|
|
<label class="block mb-4">
|
|
<span class="block text-sm font-medium mb-2">Admin Password</span>
|
|
<input
|
|
type="password"
|
|
name="password"
|
|
class="w-full px-4 py-2 border rounded"
|
|
required
|
|
/>
|
|
</label>
|
|
<button
|
|
type="submit"
|
|
class="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
>
|
|
Login
|
|
</button>
|
|
</form>
|
|
) : (
|
|
<div>
|
|
<div class="mb-4">
|
|
<a href="/admin/consent-logs" class="text-blue-600 underline">Refresh</a>
|
|
</div>
|
|
<table class="w-full border">
|
|
<thead>
|
|
<tr class="bg-gray-100">
|
|
<th class="p-3 text-left">Date</th>
|
|
<th class="p-3 text-left">Locale</th>
|
|
<th class="p-3 text-left">Session ID</th>
|
|
<th class="p-3 text-left">Essential</th>
|
|
<th class="p-3 text-left">Analytics</th>
|
|
<th class="p-3 text-left">Marketing</th>
|
|
<th class="p-3 text-left">Policy Ver</th>
|
|
<th class="p-3 text-left">IP Hash</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map((log) => (
|
|
<tr class="border-t">
|
|
<td class="p-3">{new Date(log.timestamp).toLocaleString()}</td>
|
|
<td class="p-3">{log.locale}</td>
|
|
<td class="p-3 font-mono text-sm">{log.sessionId}</td>
|
|
<td class="p-3">{log.essential ? '✅' : '❌'}</td>
|
|
<td class="p-3">{log.analytics ? '✅' : '❌'}</td>
|
|
<td class="p-3">{log.marketing ? '✅' : '❌'}</td>
|
|
<td class="p-3">{log.policyVersion}</td>
|
|
<td class="p-3 font-mono text-sm">{log.ipHash}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 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';
|
|
---
|
|
|
|
<head>
|
|
<!-- Other head content -->
|
|
|
|
<!-- Umami Analytics - Loaded conditionally -->
|
|
<script is:inline>
|
|
// Check consent before loading
|
|
const consent = JSON.parse(localStorage.getItem('consent-preferences') || '{}');
|
|
if (consent.analytics) {
|
|
const script = document.createElement('script');
|
|
script.defer = true;
|
|
script.src = 'https://{umamiDomain}/script.js';
|
|
script.setAttribute('data-website-id', '{umamiWebsiteId}');
|
|
document.head.appendChild(script);
|
|
}
|
|
</script>
|
|
</head>
|
|
```
|
|
|
|
---
|
|
|
|
## 📄 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**
|