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
This commit is contained in:
934
skills/thai-frontend-dev/SPECIFICATION.md
Normal file
934
skills/thai-frontend-dev/SPECIFICATION.md
Normal file
@@ -0,0 +1,934 @@
|
||||
# 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**
|
||||
Reference in New Issue
Block a user