feat: migrate website-creator from Next.js+Payload to Astro+Tina CMS

Major changes:
- Replace Payload CMS with Tina CMS (self-hosted)
- Add Astro DB for consent logging (PDPA compliant)
- Update Tailwind v3 to v4 (@tailwindcss/vite plugin)
- Add astro-tina-starter template
- Rewrite consent template for Astro (ConsentBanner.astro, Astro DB, Nano Stores)
- Add install-tina-backend.sh for self-hosted Tina per customer
- Rename convert-astro.sh to migrate-tina.sh
- Add AGENTS.md template for generated websites
- Delete all Payload/Next.js files

Technical updates:
- Astro DB using defineDb with eq operators for queries
- Tailwind v4 with @theme block
- Tina CMS local development mode
- Proper Astro API routes for consent

Research-verified with official documentation (April 2026)
This commit is contained in:
2026-04-17 14:52:59 +07:00
parent ce8483e546
commit 628298183a
74 changed files with 3536 additions and 11431 deletions

View File

@@ -0,0 +1,120 @@
import type { APIRoute } from 'astro';
import { db, eq } from 'astro:db';
import { ConsentLog } from '../db/config';
export const POST: APIRoute = async ({ request, clientAddress }) => {
try {
const body = await request.json();
const {
action = 'accept',
purpose = 'all',
analytics = false,
marketing = false,
functional = false,
} = body;
const ip = clientAddress || 'unknown';
const userAgent = request.headers.get('user-agent') || 'unknown';
const doc = await db.insert(ConsentLog).values({
action,
purpose,
analytics,
marketing,
functional,
ip,
userAgent,
timestamp: new Date(),
});
return new Response(JSON.stringify({
success: true,
doc,
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Consent API error:', error);
return new Response(JSON.stringify({
success: false,
error: 'Failed to log consent',
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const sessionId = url.searchParams.get('sessionId');
let docs;
if (sessionId) {
docs = await db.select().from(ConsentLog).where(
eq(ConsentLog.sessionId, sessionId)
);
} else {
docs = await db.select().from(ConsentLog);
}
return new Response(JSON.stringify({
success: true,
docs,
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Consent GET error:', error);
return new Response(JSON.stringify({
success: false,
error: 'Failed to retrieve consent logs',
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
export const DELETE: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const sessionId = url.searchParams.get('sessionId');
if (!sessionId) {
return new Response(JSON.stringify({
success: false,
error: 'sessionId is required',
}), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const deleted = await db.delete(ConsentLog).where(
eq(ConsentLog.sessionId, sessionId)
);
return new Response(JSON.stringify({
success: true,
deleted,
message: 'All consent records for this session have been deleted',
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Right to be forgotten error:', error);
return new Response(JSON.stringify({
success: false,
error: 'Failed to delete consent records',
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};

View File

@@ -1,39 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@/payload.config'
/**
* DELETE /api/consent - Right to be forgotten (GDPR/PDPA)
*
* Deletes all consent records for a given session or user
*/
export async function DELETE(request: NextRequest) {
try {
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 })
}
// Find and delete all consent logs for this session
const result = await payload.delete({
collection: 'consent-logs',
where: {
sessionId: { equals: sessionId },
},
})
return NextResponse.json({
success: true,
deleted: result.deletedDocs?.length || 0,
message: 'All consent records for this session have been deleted'
})
} catch (error) {
console.error('Right to be forgotten error:', error)
return NextResponse.json({ error: 'Failed to delete consent records' }, { status: 500 })
}
}