Remove Astro templates, fix sitemap template for Next.js

- Delete: CookieConsent.astro (old Astro component)
- Delete: consent.ts, right-to-be-forgotten.ts (Astro API routes)
- Update: route.ts is now proper Next.js route handler
- Update: sitemap-template.md - replace Astro pages structure with Next.js app/ structure
- Update: payload-nextjs-notes.md - fix MongoDB port reference
- Note: seo-multi-channel auto_publish.py is for Astro sites (kept as-is)
This commit is contained in:
2026-04-17 11:03:10 +07:00
parent 47e0258694
commit ce8483e546
7 changed files with 344 additions and 733 deletions

View File

@@ -3,7 +3,7 @@
## PostgreSQL Connection Issues
### Wrong port
- Docker container `astro-starter-db-1` exposes PostgreSQL on port **5555** (not 5432)
- Docker container `payload-db-1` exposes MongoDB on port **27017** (default)
- Fix: Use `localhost:5555` in DATABASE_URL for local development
### Wrong database name

View File

@@ -160,39 +160,59 @@ src/
│ └── team/
│ ├── member-1.md
│ └── ...
├── layouts/
│ ├── BaseLayout.astro
│ ├── PageLayout.astro
│ ├── BlogLayout.astro
└── AuthLayout.astro
├── app/
│ ├── (frontend)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ │ │ └── page.tsx
│ │ ├── services/
│ │ │ ├── page.tsx
│ │ │ └── [slug]/
│ │ │ └── page.tsx
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ └── [slug]/
│ │ │ └── page.tsx
│ │ ├── contact/
│ │ │ └── page.tsx
│ │ ├── privacy-policy/
│ │ │ └── page.tsx
│ │ ├── terms-of-service/
│ │ │ └── page.tsx
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── register/
│ │ │ └── page.tsx
│ │ └── account/
│ │ ├── page.tsx
│ │ ├── profile/
│ │ │ └── page.tsx
│ │ ├── orders/
│ │ │ └── page.tsx
│ │ └── settings/
│ │ └── page.tsx
│ └── (payload)/
│ ├── admin/
│ │ └── [[...segments]]/
│ │ └── page.tsx
│ └── api/
│ └── ...
├── components/
│ ├── Navigation.astro
│ ├── Footer.astro
│ ├── Hero.astro
│ ├── ServiceCard.astro
│ ├── BlogCard.astro
│ ├── ContactForm.astro
│ ├── CookieConsent.astro
│ ├── Navigation.tsx
│ ├── Footer.tsx
│ ├── Hero.tsx
│ ├── ServiceCard.tsx
│ ├── BlogCard.tsx
│ ├── ContactForm.tsx
│ ├── CookieConsent.tsx
│ └── ...
└── pages/
├── index.astro
├── about.astro
├── services/
│ ├── index.astro
│ └── [slug].astro
├── blog/
│ ├── index.astro
│ └── [slug].astro
├── contact.astro
├── privacy-policy.astro
├── terms-of-service.astro
├── login.astro
├── register.astro
└── account/
├── index.astro
├── profile.astro
├── orders.astro
└── settings.astro
└── collections/
├── Posts.ts
├── Pages.ts
├── Media.ts
├── Users.ts
└── ConsentLogs.ts
```
---
@@ -230,7 +250,7 @@ src/
- ติดต่อเรา - ติดต่อ - นโยบายความเป็นส่วนตัว - {EMAIL}
- สมัครสมาชิก - เงื่อนไขการให้บริการ
Copyright (c) {YEAR} {SITE_NAME} | Built with Astro
Copyright (c) {YEAR} {SITE_NAME} | Built with Next.js + Payload CMS
```
---

View File

@@ -1,462 +0,0 @@
---
// CookieConsent.astro - PDPA Cookie Consent Banner
// ทำงานจริง: ถ้า reject จะไม่ load tracking scripts
interface Props {
position?: 'bottom' | 'top';
theme?: 'light' | 'dark';
}
const { position = 'bottom', theme = 'light' } = Astro.props;
// Consent states
const CONSENT_TYPES = {
ESSENTIAL: 'essential',
ANALYTICS: 'analytics',
MARKETING: 'marketing',
FUNCTIONAL: 'functional',
} as const;
---
<div id="cookie-consent-banner" class={`cookie-consent cookie-consent--${position} cookie-consent--${theme}`} hidden>
<div class="cookie-consent__content">
<div class="cookie-consent__text">
<h3 class="cookie-consent__title">นโยบายคุกกี้</h3>
<p class="cookie-consent__description">
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งานเว็บไซต์ของคุณ
คุณสามารถเลือกได้ว่าจะอนุญาตคุกกี้ประเภทใด
<a href="/privacy-policy" target="_blank">อ่านนโยบายความเป็นส่วนตัว</a>
</p>
</div>
<div class="cookie-consent__categories">
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้ที่จำเป็น</span>
<span class="cookie-consent__badge cookie-consent__badge--required">จำเป็นเสมอ</span>
</div>
<p class="cookie-consent__category-desc">ใช้สำหรับการทำงานพื้นฐานของเว็บไซต์ ไม่สามารถปิดได้</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้วิเคราะห์</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-analytics" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ช่วยให้เราเข้าใจพฤติกรรมการใช้งานเว็บไซต์</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้การตลาด</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-marketing" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ใช้สำหรับแสดงโฆษณาที่ตรงกับความสนใจของคุณ</p>
</div>
<div class="cookie-consent__category">
<div class="cookie-consent__category-header">
<span class="cookie-consent__category-name">คุกกี้ฟังก์ชัน</span>
<label class="cookie-consent__toggle">
<input type="checkbox" id="consent-functional" checked />
<span class="cookie-consent__toggle-slider"></span>
</label>
</div>
<p class="cookie-consent__category-desc">ช่วยจดจำการตั้งค่าของคุณ</p>
</div>
</div>
<div class="cookie-consent__actions">
<button id="cookie-consent-accept-all" class="cookie-consent__btn cookie-consent__btn--primary">
ยอมรับทั้งหมด
</button>
<button id="cookie-consent-reject-all" class="cookie-consent__btn cookie-consent__btn--secondary">
ปฏิเสธทั้งหมด
</button>
<button id="cookie-consent-save" class="cookie-consent__btn cookie-consent__btn--outline">
บันทึกการตั้งค่า
</button>
</div>
</div>
</div>
<style>
.cookie-consent {
position: fixed;
left: 0;
right: 0;
z-index: 9999;
background: var(--color-bg, #ffffff);
border-top: 1px solid var(--color-border, #e5e7eb);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
font-family: 'Kanit', 'Noto Sans Thai', system-ui, sans-serif;
}
.cookie-consent--bottom {
bottom: 0;
}
.cookie-consent--top {
top: 0;
}
.cookie-consent[hidden] {
display: none;
}
.cookie-consent__content {
max-width: 800px;
margin: 0 auto;
}
.cookie-consent__title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: var(--color-text, #111827);
}
.cookie-consent__description {
font-size: 0.875rem;
color: var(--color-text-secondary, #6b7280);
margin: 0 0 1rem 0;
line-height: 1.6;
}
.cookie-consent__description a {
color: var(--color-primary, #3b82f6);
text-decoration: underline;
}
.cookie-consent__categories {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.cookie-consent__category {
background: var(--color-bg-secondary, #f9fafb);
border-radius: 0.5rem;
padding: 1rem;
}
.cookie-consent__category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.cookie-consent__category-name {
font-weight: 500;
color: var(--color-text, #111827);
}
.cookie-consent__badge {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-weight: 500;
}
.cookie-consent__badge--required {
background: var(--color-primary, #3b82f6);
color: white;
}
.cookie-consent__category-desc {
font-size: 0.8125rem;
color: var(--color-text-secondary, #6b7280);
margin: 0;
line-height: 1.5;
}
.cookie-consent__toggle {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.cookie-consent__toggle input {
opacity: 0;
width: 0;
height: 0;
}
.cookie-consent__toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-border, #d1d5db);
border-radius: 24px;
transition: 0.3s;
}
.cookie-consent__toggle-slider::before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.cookie-consent__toggle input:checked + .cookie-consent__toggle-slider {
background: var(--color-primary, #3b82f6);
}
.cookie-consent__toggle input:checked + .cookie-consent__toggle-slider::before {
transform: translateX(20px);
}
.cookie-consent__toggle input:disabled + .cookie-consent__toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
.cookie-consent__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.cookie-consent__btn {
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
border: none;
}
.cookie-consent__btn--primary {
background: var(--color-primary, #3b82f6);
color: white;
}
.cookie-consent__btn--primary:hover {
background: var(--color-primary-dark, #2563eb);
}
.cookie-consent__btn--secondary {
background: var(--color-text, #111827);
color: white;
}
.cookie-consent__btn--secondary:hover {
background: var(--color-text-dark, #000000);
}
.cookie-consent__btn--outline {
background: transparent;
color: var(--color-text, #111827);
border: 1px solid var(--color-border, #d1d5db);
}
.cookie-consent__btn--outline:hover {
background: var(--color-bg-secondary, #f9fafb);
}
/* Dark theme */
.cookie-consent--dark {
--color-bg: #1f2937;
--color-bg-secondary: #374151;
--color-border: #4b5563;
--color-text: #f9fafb;
--color-text-secondary: #d1d5db;
--color-primary: #60a5fa;
--color-primary-dark: #3b82f6;
}
@media (max-width: 640px) {
.cookie-consent {
padding: 1rem;
}
.cookie-consent__actions {
flex-direction: column;
}
.cookie-consent__btn {
width: 100%;
}
}
</style>
<script>
// Consent Manager
class ConsentManager {
private readonly CONSENT_KEY = 'cookie_consent';
private readonly API_URL = '/api/consent';
async init() {
// Check if consent already given
const existing = this.getStoredConsent();
if (!existing) {
this.showBanner();
} else {
this.applyConsent(existing);
}
// Bind event listeners
this.bindEvents();
}
private bindEvents() {
const acceptAll = document.getElementById('cookie-consent-accept-all');
const rejectAll = document.getElementById('cookie-consent-reject-all');
const save = document.getElementById('cookie-consent-save');
acceptAll?.addEventListener('click', () => this.acceptAll());
rejectAll?.addEventListener('click', () => this.rejectAll());
save?.addEventListener('click', () => this.saveCustom());
}
private showBanner() {
const banner = document.getElementById('cookie-consent-banner');
banner?.removeAttribute('hidden');
}
private hideBanner() {
const banner = document.getElementById('cookie-consent-banner');
banner?.setAttribute('hidden', '');
}
private getStoredConsent(): Record<string, boolean> | null {
const stored = localStorage.getItem(this.CONSENT_KEY);
return stored ? JSON.parse(stored) : null;
}
private async saveConsent(consent: Record<string, boolean>) {
// Save to localStorage
localStorage.setItem(this.CONSENT_KEY, JSON.stringify(consent));
// Send to server
try {
await fetch(this.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...consent,
session_id: this.getSessionId(),
}),
});
} catch (error) {
console.error('Failed to save consent:', error);
}
// Apply consent
this.applyConsent(consent);
this.hideBanner();
}
private async acceptAll() {
const consent = {
essential: true,
analytics: true,
marketing: true,
functional: true,
};
await this.saveConsent(consent);
}
private async rejectAll() {
const consent = {
essential: true, // Always required
analytics: false,
marketing: false,
functional: false,
};
await this.saveConsent(consent);
}
private async saveCustom() {
const consent = {
essential: true, // Always required
analytics: (document.getElementById('consent-analytics') as HTMLInputElement)?.checked ?? false,
marketing: (document.getElementById('consent-marketing') as HTMLInputElement)?.checked ?? false,
functional: (document.getElementById('consent-functional') as HTMLInputElement)?.checked ?? false,
};
await this.saveConsent(consent);
}
private applyConsent(consent: Record<string, boolean>) {
// Essential cookies - always on (handled by server)
// Analytics
if (consent.analytics) {
this.enableAnalytics();
} else {
this.disableAnalytics();
}
// Marketing
if (consent.marketing) {
this.enableMarketing();
} else {
this.disableMarketing();
}
// Functional
if (consent.functional) {
this.enableFunctional();
} else {
this.disableFunctional();
}
}
private enableAnalytics() {
// Enable GA4 etc.
window.dispatchEvent(new CustomEvent('consent:analytics:Granted'));
}
private disableAnalytics() {
// Disable GA4, clear existing cookies
window.dispatchEvent(new CustomEvent('consent:analytics:Denied'));
}
private enableMarketing() {
window.dispatchEvent(new CustomEvent('consent:marketing:Granted'));
}
private disableMarketing() {
window.dispatchEvent(new CustomEvent('consent:marketing:Denied'));
}
private enableFunctional() {
window.dispatchEvent(new CustomEvent('consent:functional:Granted'));
}
private disableFunctional() {
window.dispatchEvent(new CustomEvent('consent:functional:Denied'));
}
private getSessionId(): string {
let sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem('session_id', sessionId);
}
return sessionId;
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new ConsentManager().init();
});
</script>

View File

@@ -1,81 +0,0 @@
import type { APIRoute } from 'astro'
// POST /api/consent - บันทึก consent
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json()
const { session_id, essential, analytics, marketing, functional } = body
if (!session_id) {
return new Response(JSON.stringify({ error: 'session_id is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// Get client info
const ipAddress = request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
// Build consent record
const consentTypes = []
if (essential) consentTypes.push('essential')
if (analytics) consentTypes.push('analytics')
if (marketing) consentTypes.push('marketing')
if (functional) consentTypes.push('functional')
// In Payload CMS, you would save this to the consent-logs collection
// For now, return success (Payload integration happens at build time)
const record = {
sessionId: session_id,
consentType: consentTypes.length === 4 ? 'accept_all' : consentTypes.join(','),
granted: analytics || marketing || functional,
ipAddress,
userAgent,
metadata: { essential, analytics, marketing, functional },
createdAt: new Date().toISOString(),
}
// Log for debugging (remove in production)
console.log('[Consent API] New consent record:', JSON.stringify(record))
return new Response(JSON.stringify({ success: true, record }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
console.error('[Consent API] Error:', error)
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
}
// GET /api/consent - ตรวจสอบ consent ของ session
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(JSON.stringify({ error: 'session_id is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// In Payload CMS, query the consent-logs collection
// For now, return not found (Payload integration happens at build time)
return new Response(JSON.stringify({ error: 'Not implemented in template' }), {
status: 501,
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
console.error('[Consent API] Error:', error)
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
}

View File

@@ -1,86 +0,0 @@
import type { APIRoute } from 'astro'
// Right to be Forgotten API - PDPA Article 17
// DELETE /api/consent?session_id=xxx - ลบข้อมูลของ session นี้
export const DELETE: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'session_id is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// In Payload CMS, you would:
// 1. Find all consent-logs with this sessionId
// 2. Delete them
// 3. Also delete any user data associated with this session
// Example Payload query (for reference):
// await payload.delete({
// collection: 'consent-logs',
// where: { sessionId: { equals: sessionId } },
// })
console.log(`[Right to be Forgotten] Deleting data for session: ${sessionId}`)
return new Response(
JSON.stringify({
success: true,
message: 'ข้อมูลของคุณถูกลบแล้ว',
deletedAt: new Date().toISOString(),
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('[Right to be Forgotten] Error:', error)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
}
// GET /api/consent/export - ขอ export ข้อมูลของตัวเอง (PDPA Article 31)
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url)
const sessionId = url.searchParams.get('session_id')
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'session_id is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
// In Payload CMS, query consent-logs for this session
// Return the data as JSON for the user to review
// Example Payload query (for reference):
// const logs = await payload.find({
// collection: 'consent-logs',
// where: { sessionId: { equals: sessionId } },
// })
return new Response(
JSON.stringify({
success: true,
message: 'ข้อมูลของคุณ',
data: [], // Replace with actual Payload query result
requestedAt: new Date().toISOString(),
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('[Consent Export] Error:', error)
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
}

View File

@@ -3,78 +3,37 @@ import { getPayload } from 'payload'
import config from '@/payload.config'
/**
* POST /api/consent - Record consent action
*
* Request body:
* {
* action: 'accept' | 'reject' | 'update',
* purpose: 'analytics' | 'marketing' | 'functional' | 'all',
* analytics: boolean,
* marketing: boolean,
* functional: boolean,
* previousConsent?: { analytics: boolean, marketing: boolean, functional: boolean }
* }
* DELETE /api/consent - Right to be forgotten (GDPR/PDPA)
*
* Deletes all consent records for a given session or user
*/
export async function POST(request: NextRequest) {
export async function DELETE(request: NextRequest) {
try {
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const body = await request.json()
const { action, purpose, analytics, marketing, functional, previousConsent } = body
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
// Validate required fields
if (!action || !['accept', 'reject', 'update'].includes(action)) {
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
}
if (!purpose || !['analytics', 'marketing', 'functional', 'all'].includes(purpose)) {
return NextResponse.json({ error: 'Invalid purpose' }, { status: 400 })
if (!sessionId) {
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 })
}
// Get IP and User Agent
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]
|| request.headers.get('x-real-ip')
|| 'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
// Create consent log
const consentLog = await payload.create({
// Find and delete all consent logs for this session
const result = await payload.delete({
collection: 'consent-logs',
data: {
action,
purpose,
analytics: analytics ?? false,
marketing: marketing ?? false,
functional: functional ?? false,
userAgent,
ip,
timestamp: new Date().toISOString(),
previousConsent: previousConsent || null,
newConsent: {
analytics: analytics ?? false,
marketing: marketing ?? false,
functional: functional ?? false,
},
where: {
sessionId: { equals: sessionId },
},
})
return NextResponse.json({ success: true, doc: consentLog })
return NextResponse.json({
success: true,
deleted: result.deletedDocs?.length || 0,
message: 'All consent records for this session have been deleted'
})
} catch (error) {
console.error('Consent logging error:', error)
return NextResponse.json({ error: 'Failed to log consent' }, { status: 500 })
console.error('Right to be forgotten error:', error)
return NextResponse.json({ error: 'Failed to delete consent records' }, { status: 500 })
}
}
/**
* GET /api/consent - Get current consent status (from cookie or localStorage)
* This endpoint is mainly for verification, actual consent is stored client-side
*/
export async function GET(request: NextRequest) {
// Consent is stored client-side in localStorage
// This endpoint is for compliance verification
return NextResponse.json({
message: 'Consent is stored client-side',
purposes: ['analytics', 'marketing', 'functional'],
note: 'Use POST to update consent preferences'
})
}
}