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

@@ -1,337 +0,0 @@
#!/usr/bin/env bash
#===============================================================================
# migrate-to-payload.sh - Migrate Astro content to Payload CMS with Lexical
#
# Usage: ./migrate-to-payload.sh [source-path] [target-path]
#
# This script migrates content from Astro MDX/Markdown to Payload CMS Lexical.
# - Converts .md/.mdx files to Payload CMS Lexical JSON format
# - Creates Payload collection entries
# - Preserves frontmatter as collection fields
#
# Requirements:
# - node.js 20+
# - npm
#
#===============================================================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
SOURCE_PATH="${1:-}"
TARGET_PATH="${2:-.}"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_usage() {
cat << EOF
Usage: $(basename "$0") [source-path] [target-path]
Migrate Astro content to Payload CMS with Lexical
Arguments:
source-path Path to Astro project with content
target-path Path to Next.js + Payload CMS project
Examples:
$(basename "$0") /path/to/astro-site /path/to/payload-site
EOF
}
detect_content_type() {
log_info "Detecting content structure..."
cd "$SOURCE_PATH"
if [ -d "src/content" ]; then
CONTENT_DIR="src/content"
elif [ -d "content" ]; then
CONTENT_DIR="content"
elif [ -d "src/pages" ]; then
CONTENT_DIR="src/pages"
else
log_error "No content directory found"
exit 1
fi
log_success "Content directory: $CONTENT_DIR"
}
backup_content() {
log_info "Backing up content..."
BACKUP_DIR="/tmp/migration-backup-$(date +%s)"
mkdir -p "$BACKUP_DIR"
if [ -d "$SOURCE_PATH/$CONTENT_DIR" ]; then
cp -r "$SOURCE_PATH/$CONTENT_DIR" "$BACKUP_DIR/"
fi
log_success "Backup at: $BACKUP_DIR"
}
analyze_content() {
log_info "Analyzing content..."
cd "$SOURCE_PATH"
local md_count=$(find "$CONTENT_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | wc -l)
local astro_count=$(find . -type f -name "*.astro" 2>/dev/null | grep -v node_modules | wc -l)
echo ""
echo " Content files: $md_count"
echo " Astro components: $astro_count"
echo ""
find "$CONTENT_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | head -20
}
create_lexical_content() {
log_info "Converting MDX to Payload CMS Lexical format..."
cd "$SOURCE_PATH"
local output_dir="$TARGET_PATH/src/content-migration"
mkdir -p "$output_dir"
find "$CONTENT_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | while read -r file; do
local relative_path="${file#$SOURCE_PATH/$CONTENT_DIR/}"
local filename=$(basename "$file" .mdx .md | sed 's/\.mdx$//' | sed 's/\.md$//')
local slug=$(echo "$filename" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
local frontmatter=""
local content=""
if grep -q "^---" "$file" 2>/dev/null; then
frontmatter=$(sed -n '/^---/,/^---/p' "$file" | head -n -1 | tail -n +2)
content=$(awk '/^---/{found=1; next} found' "$file")
else
content=$(cat "$file")
fi
local title=$(echo "$frontmatter" | grep -i "^title:" | cut -d':' -f2- | tr -d ' "' | head -1)
local date=$(echo "$frontmatter" | grep -i "^date:" | cut -d':' -f2- | tr -d ' "' | head -1)
local description=$(echo "$frontmatter" | grep -i "^description:" | cut -d':' -f2- | tr -d ' "' | head -1)
local author=$(echo "$frontmatter" | grep -i "^author:" | cut -d':' -f2- | tr -d ' "' | head -1)
local image=$(echo "$frontmatter" | grep -i "^image:" | cut -d':' -f2- | tr -d ' "' | head -1)
local tags=$(echo "$frontmatter" | grep -i "^tags:" | cut -d':' -f2- | tr -d '[]"' | head -1)
title=${title:-$filename}
date=${date:-$(date +%Y-%m-%d)}
cat > "$output_dir/${slug}.json" << JSONEOF
{
"title": "$title",
"slug": "$slug",
"createdAt": "$date",
"updatedAt": "$(date +%Y-%m-%d)",
"meta": {
"title": "$title",
"description": "$description"
},
"author": "$author",
"heroImage": "$image",
"tags": ["$tags"],
"content": {
"root": {
"type": "root",
"format": "",
"indent": 0,
"version": 1,
"children": [
{
"type": "paragraph",
"version": 1,
"children": [
{
"type": "text",
"version": 1,
"text": "$content",
"mode": "tokenized",
"style": ""
}
]
}
]
}
}
}
JSONEOF
echo " Converted: $filename$slug.json"
done
log_success "Conversion complete: $output_dir/"
}
create_payload_import_script() {
log_info "Creating Payload import script..."
local output_dir="$TARGET_PATH/scripts"
mkdir -p "$output_dir"
cat > "$output_dir/import-content.ts" << 'TSEOF'
import { payload } from '../src/lib/payload'
import { promises as fs } from 'fs'
import path from 'path'
async function importContent() {
const contentDir = path.join(process.cwd(), 'src/content-migration')
try {
const files = await fs.readdir(contentDir)
const jsonFiles = files.filter(f => f.endsWith('.json'))
for (const file of jsonFiles) {
const filePath = path.join(contentDir, file)
const content = JSON.parse(await fs.readFile(filePath, 'utf-8'))
await payload.create({
collection: 'posts',
data: {
title: content.title,
slug: content.slug,
createdAt: content.createdAt,
updatedAt: content.updatedAt,
meta: content.meta,
author: content.author,
heroImage: content.heroImage,
tags: content.tags,
content: content.content,
_status: 'published',
},
})
console.log(`Imported: ${content.title}`)
}
console.log(`\nSuccessfully imported ${jsonFiles.length} posts`)
} catch (error) {
console.error('Import failed:', error)
process.exit(1)
}
}
importContent()
TSEOF
log_success "Created: $output_dir/import-content.ts"
}
create_migration_report() {
log_info "Creating migration report..."
cd "$SOURCE_PATH"
local page_count=$(find "$CONTENT_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | wc -l)
cat > "$TARGET_PATH/MIGRATION_REPORT.md" << EOF
# Migration Report: Astro → Payload CMS
## Source
- **Type:** Astro
- **Path:** $SOURCE_PATH
- **Backup:** $BACKUP_DIR
- **Date:** $(date)
## Statistics
- **Total Posts:** $page_count
## Content Migration
Content has been converted to Payload CMS Lexical JSON format in:
\`\`\`
src/content-migration/
\`\`\`
## Next Steps
1. **Review converted content:**
\`\`\`bash
ls src/content-migration/
\`\`\`
2. **Configure Payload collection:**
Make sure you have a 'posts' collection in \`src/collections/Posts.ts\`
3. **Import content to Payload:**
\`\`\`bash
npx tsx scripts/import-content.ts
\`\`\`
4. **Verify in admin:**
- Go to http://localhost:3002/admin
- Navigate to Posts collection
- Verify content and rich text editor (Lexical)
## Notes
- MDX/Markdown content is converted to Lexical JSON format
- Frontmatter fields (title, date, description) are mapped to collection fields
- Complex MDX components need manual conversion in Payload admin
- Images need to be re-uploaded to Payload Media
EOF
log_success "Migration report: $TARGET_PATH/MIGRATION_REPORT.md"
}
main() {
echo "=============================================="
echo " Astro → Payload CMS Migration Tool"
echo " Convert MDX/MD to Payload CMS with Lexical"
echo "=============================================="
echo ""
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
print_usage
exit 0
fi
if [ -z "$SOURCE_PATH" ]; then
print_usage
echo ""
log_error "Please specify source path"
exit 1
fi
if [ ! -d "$SOURCE_PATH" ]; then
log_error "Source path not found: $SOURCE_PATH"
exit 1
fi
if [ ! -d "$TARGET_PATH" ]; then
log_error "Target path not found: $TARGET_PATH"
exit 1
fi
detect_content_type
backup_content
analyze_content
create_lexical_content
create_payload_import_script
create_migration_report
echo ""
echo "=============================================="
log_success "Migration preparation complete!"
echo "=============================================="
echo ""
echo "Next steps:"
echo " 1. cd $TARGET_PATH"
echo " 2. Review converted content in src/content-migration/"
echo " 3. Run: npm run dev"
echo " 4. Import: npx tsx scripts/import-content.ts"
echo " 5. Verify in Payload admin (http://localhost:3002/admin)"
echo ""
}
main "$@"

View File

@@ -0,0 +1,327 @@
#!/usr/bin/env bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
BACKEND_PATH="${1:-./tina-backend}"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_usage() {
cat << EOF
Usage: $(basename "$0") [target-path]
Install Tina CMS Backend (self-hosted)
Arguments:
target-path Path where Tina backend will be installed (default: ./tina-backend)
Examples:
$(basename "$0") /opt/tina-backend
This script installs a self-hosted Tina CMS backend with:
- Auth.js authentication
- SQLite database adapter
- Git provider for content
- Next.js API routes
Requirements:
- Node.js 18+
- npm or yarn
- git
EOF
}
main() {
echo "=============================================="
echo " Tina CMS Backend Installer (Self-Hosted)"
echo "=============================================="
echo ""
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
print_usage
exit 0
fi
if [ -d "$BACKEND_PATH" ]; then
log_error "Directory already exists: $BACKEND_PATH"
exit 1
fi
log_info "Creating Tina backend at: $BACKEND_PATH"
mkdir -p "$BACKEND_PATH"
cd "$BACKEND_PATH"
log_info "Creating package.json..."
cat > package.json << 'PKGEOF'
{
"name": "tina-backend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "tinacms dev",
"build": "tinacms build",
"start": "tinacms start"
},
"dependencies": {
"@auth/core": "^0.34.0",
"@auth/drizzle-adapter": "^1.4.0",
"@libsql/client": "^0.14.0",
"@tinacms/auth": "^2.0.0",
"@tinacms/database": "^2.0.0",
"@tinacms/git-provider": "^2.0.0",
"@tinacms/graphql": "^2.0.0",
"@tinacms/mssql": "^2.0.0",
"@tinacms/server": "^2.0.0",
"drizzle-orm": "^0.38.0",
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tinacms": "^2.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
PKGEOF
log_info "Creating TypeScript config..."
cat > tsconfig.json << 'TSEOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx"
},
"include": ["src/**/*", "tina/**/*"],
"exclude": ["node_modules"]
}
TSEOF
mkdir -p src/app/api/auth/\[...nextauth\]
mkdir -p src/app/api/tina/\[\[...tina\]\]
mkdir -p tina
log_info "Creating Auth.js configuration..."
cat > src/auth.config.ts << 'AUTHEOF'
import { AuthConfig } from "@auth/core/types";
const authConfig: AuthConfig = {
secret: process.env.NEXTAUTH_SECRET || "your-secret-change-in-production",
providers: [
{
id: "github",
name: "GitHub",
type: "oauth",
clientId: process.env.GITHUB_ID || "",
clientSecret: process.env.GITHUB_SECRET || "",
},
],
callbacks: {
async session({ session, token }) {
if (session.user && token.sub) {
session.user.email = token.email as string;
}
return session;
},
},
pages: {
signIn: "/auth/signin",
},
};
export default authConfig;
AUTHEOF
log_info "Creating NextAuth API route..."
cat > 'src/app/api/auth/[...nextauth]/route.ts' << 'NEXTAUTHEOF'
import { NextRequest, NextResponse } from "next/server";
import { AuthHandler } from "@auth/core";
import authConfig from "../../../auth.config";
const authHandler = (req: NextRequest) =>
AuthHandler({
...authConfig,
req: req as any,
resolve(): Promise<any> {
throw new Error("Function not implemented.");
},
secret: authConfig.secret!,
trustHost: true,
});
export { authHandler as GET, authHandler as POST };
NEXTAUTHEOF
log_info "Creating Tina API route..."
cat > 'src/app/api/tina/[[...tina]]/route.ts' << 'TINAEOF'
import { TinaNodeBackend } from "@tinacms/server";
import authConfig from "../../../../auth.config";
import { branchName } from "./branch";
const tinaBackend = TinaNodeBackend({
authConfig: authConfig as any,
branch: branchName,
});
export { tinaBackend as GET, tinaBackend as POST };
TINAEOF
log_info "Creating Tina branch configuration..."
cat > src/app/api/tina/branch.ts << 'BRANCHEMAP'
export const branchName = process.env.TINA_BRANCH || "main";
BRANCHEMAP
log_info "Creating database schema..."
mkdir -p src/lib
cat > src/lib/schema.ts << 'DBEOF'
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
name: text("name"),
email: text("email").unique(),
emailVerified: integer("email_verified", { mode: "boolean" }),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
});
export const accounts = sqliteTable("accounts", {
id: text("id").primaryKey(),
userId: text("user_id")
.references(() => users.id)
.notNull(),
type: text("type").notNull(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
});
export const sessions = sqliteTable("sessions", {
id: text("id").primaryKey(),
sessionToken: text("session_token").unique(),
userId: text("user_id")
.references(() => users.id)
.notNull(),
expires: integer("expires", { mode: "timestamp" }).notNull(),
});
export const verificationTokens = sqliteTable("verification_tokens", {
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: integer("expires", { mode: "timestamp" }).notNull(),
});
DBEOF
log_info "Creating database client..."
cat > src/lib/db.ts << 'DBCEOF'
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";
const client = createClient({
url: process.env.DATABASE_URL || "file:local.db",
});
export const db = drizzle(client, { schema });
DBCEOF
log_info "Creating environment template..."
cat > .env.example << 'ENVEOF'
NEXTAUTH_SECRET=generate-a-random-secret-here
NEXTAUTH_URL=http://localhost:3000
GITHUB_ID=your-github-oauth-app-client-id
GITHUB_SECRET=your-github-oauth-app-client-secret
DATABASE_URL=file:local.db
TINA_BRANCH=main
ENVEOF
log_info "Creating README..."
cat > README.md << 'READMEEOF'
# Tina CMS Backend (Self-Hosted)
Self-hosted Tina CMS backend with Auth.js authentication and SQLite database.
## Setup
1. Install dependencies:
```bash
npm install
```
2. Configure environment:
```bash
cp .env.example .env
# Edit .env with your settings
```
3. Set up GitHub OAuth App:
- Go to https://github.com/settings/developers
- Create a new OAuth App
- Set callback URL to: `http://your-domain.com/api/auth/callback/github`
4. Start development:
```bash
npm run dev
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| NEXTAUTH_SECRET | Random secret for NextAuth |
| NEXTAUTH_URL | Your site URL |
| GITHUB_ID | GitHub OAuth Client ID |
| GITHUB_SECRET | GitHub OAuth Client Secret |
| DATABASE_URL | SQLite database path |
| TINA_BRANCH | Git branch for content |
## Connecting Frontend
In your Astro frontend's `tina/config.ts`:
```ts
import { defineConfig } from "tinacms";
export default defineConfig({
apiUrl: "https://your-tina-backend.com",
contentApiUrl: "https://your-tina-backend.com",
});
```
READMEEOF
log_success "Tina backend created at: $BACKEND_PATH"
echo ""
echo "Next steps:"
echo " 1. cd $BACKEND_PATH"
echo " 2. npm install"
echo " 3. cp .env.example .env"
echo " 4. Configure GitHub OAuth App"
echo " 5. npm run dev"
echo ""
}
main "$@"

View File

@@ -0,0 +1,443 @@
#!/usr/bin/env bash
#===============================================================================
# migrate-tina.sh - Migrate existing websites to Astro + Tina CMS
#
# Usage: ./migrate-tina.sh [source-path] [target-path]
#
# This script migrates websites to Astro + Tina CMS:
# - Converts content to Tina CMS format
# - Sets up Astro DB for consent logging
# - Adds PDPA-compliant consent system
# - Preserves content and structure
#
# Requirements:
# - node.js 20+
# - npm
# - git
#
#===============================================================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
SOURCE_PATH="${1:-}"
TARGET_PATH="${2:-.}"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_usage() {
cat << EOF
Usage: $(basename "$0") [source-path] [target-path]
Migrate existing website to Astro + Tina CMS
Arguments:
source-path Path to existing website project
target-path Path for the migrated Astro + Tina project
Examples:
$(basename "$0") /path/to/existing-site /path/to/migrated-site
Features:
- Detects source website technology (Astro, Next.js, etc.)
- Converts content to Tina CMS format
- Sets up Astro DB for consent logging (PDPA compliant)
- Adds cookie consent banner with Thai law compliance
- Preserves SEO metadata and content structure
EOF
}
detect_source_type() {
log_info "Detecting source website type..."
cd "$SOURCE_PATH"
if [ -f "astro.config.mjs" ] || [ -f "astro.config.ts" ]; then
SOURCE_TYPE="astro"
log_success "Detected: Astro"
elif [ -f "next.config.js" ] || [ -f "next.config.mjs" ] || [ -f "package.json" ] && grep -q "next" package.json 2>/dev/null; then
SOURCE_TYPE="nextjs"
log_success "Detected: Next.js"
elif [ -f "package.json" ] && grep -q "remix" package.json 2>/dev/null; then
SOURCE_TYPE="remix"
log_success "Detected: Remix"
elif [ -d "src/content" ] || [ -d "content/posts" ]; then
SOURCE_TYPE="generic"
log_success "Detected: Generic static site"
else
log_warning "Could not detect source type, assuming generic"
SOURCE_TYPE="generic"
fi
}
analyze_source_content() {
log_info "Analyzing source content..."
cd "$SOURCE_PATH"
local md_count=$(find . -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | grep -v node_modules | wc -l)
local astro_count=$(find . -type f -name "*.astro" 2>/dev/null | grep -v node_modules | wc -l)
local pages_count=$(find . -type f \( -name "*.tsx" -o -name "*.jsx" \) 2>/dev/null | grep -v node_modules | grep -E "pages/|app/" | wc -l)
echo ""
echo " Analysis Results:"
echo " ─────────────────"
echo " Markdown/MDX files: $md_count"
echo " Astro components: $astro_count"
echo " Pages (tsx/jsx): $pages_count"
echo ""
# List sample content files
if [ $md_count -gt 0 ]; then
echo " Sample content files:"
find . -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | grep -v node_modules | head -5 | while read -r f; do
echo " - $f"
done
echo ""
fi
}
copy_template() {
log_info "Copying Astro+Tina template..."
local template_dir="$(dirname "$(dirname "$(readlink -f "$0")")")/templates/astro-tina-starter"
if [ ! -d "$template_dir" ]; then
log_error "Template not found: $template_dir"
exit 1
fi
cp -r "$template_dir"/* "$TARGET_PATH/"
cp -r "$template_dir"/.* "$TARGET_PATH/" 2>/dev/null || true
log_success "Template copied to: $TARGET_PATH"
}
migrate_content() {
log_info "Migrating content to Tina format..."
cd "$SOURCE_PATH"
# Detect content directory
local content_dir=""
if [ -d "src/content" ]; then
content_dir="src/content"
elif [ -d "content" ]; then
content_dir="content"
elif [ -d "content/posts" ]; then
content_dir="content/posts"
fi
if [ -z "$content_dir" ]; then
log_warning "No content directory found, creating default structure"
mkdir -p "$TARGET_PATH/src/content"
return
fi
# Create Tina content directory
mkdir -p "$TARGET_PATH/src/content"
# Copy markdown/mdx files
find "$content_dir" -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | while read -r file; do
local relative_path="${file#$SOURCE_PATH/$content_dir/}"
local target_file="$TARGET_PATH/src/content/$relative_path"
mkdir -p "$(dirname "$target_file")"
cp "$file" "$target_file"
echo " Migrated: $relative_path"
done
log_success "Content migration complete"
}
add_consent_system() {
log_info "Adding PDPA-compliant consent system..."
local consent_template="$(dirname "$(dirname "$(readlink -f "$0")")")/templates/consent"
if [ ! -d "$consent_template" ]; then
log_warning "Consent template not found, skipping"
return
fi
# Copy consent files
cp -r "$consent_template"/* "$TARGET_PATH/src/components/consent/" 2>/dev/null || true
log_success "Consent system added"
}
create_tina_schema() {
log_info "Creating Tina CMS schema..."
cd "$TARGET_PATH"
# Ensure .tina directory exists
mkdir -p .tina
# Create or update schema
cat > .tina/schema.ts << 'EOF'
import { defineSchema, config } from 'tinacms'
// Your content collections
const schema = defineSchema({
collections: [
{
name: 'post',
label: 'Posts',
path: 'src/content/posts',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
required: true,
},
{
type: 'string',
name: 'slug',
label: 'Slug',
required: true,
},
{
type: 'datetime',
name: 'date',
label: 'Date',
},
{
type: 'string',
name: 'author',
label: 'Author',
},
{
type: 'string',
name: 'image',
label: 'Featured Image',
},
{
type: 'string',
name: 'description',
label: 'Description',
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
},
],
},
{
name: 'page',
label: 'Pages',
path: 'src/content/pages',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
required: true,
},
{
type: 'string',
name: 'slug',
label: 'Slug',
required: true,
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
},
],
},
],
})
export default config({
schema,
// Other config options
})
EOF
log_success "Tina schema created"
}
create_migration_report() {
log_info "Creating migration report..."
cd "$SOURCE_PATH"
local md_count=$(find . -type f \( -name "*.md" -o -name "*.mdx" \) 2>/dev/null | grep -v node_modules | wc -l)
cat > "$TARGET_PATH/MIGRATION_REPORT.md" << EOF
# Migration Report: → Astro + Tina CMS
## Source
- **Original Type:** $SOURCE_TYPE
- **Path:** $SOURCE_PATH
- **Date:** $(date)
## Statistics
- **Content Files Migrated:** $md_count
## What's Included
### ✅ Astro 6.1.7
Modern static site framework with excellent performance.
### ✅ Tina CMS
Self-hosted Git-based CMS for visual content editing.
### ✅ Tailwind CSS 4.x
Latest Tailwind with @tailwindcss/vite plugin.
### ✅ Astro DB
Built-in database for consent logging and dynamic content.
### ✅ PDPA Consent System
Thai Personal Data Protection Act compliant cookie consent:
- Cookie banner with Accept/Reject/Preferences
- Consent logging in Astro DB
- API endpoint for consent management
### ✅ Nano Stores
Lightweight client-side state management.
## Project Structure
\`\`\`
$TARGET_PATH/
├── src/
│ ├── components/
│ │ └── consent/ # PDPA consent system
│ ├── content/
│ │ ├── posts/ # Blog posts (Tina managed)
│ │ └── pages/ # Static pages (Tina managed)
│ ├── layouts/
│ │ └── Layout.astro
│ ├── pages/
│ │ └── index.astro
│ └── styles/
│ └── global.css
├── .tina/
│ └── schema.ts # Tina content schema
├── db/
│ └── config.ts # Astro DB config
├── Dockerfile
└── AGENTS.md # AI agent instructions
\`\`\`
## Next Steps
1. **Install dependencies:**
\`\`\`bash
cd $TARGET_PATH
npm install
\`\`\`
2. **Set up environment:**
\`\`\`bash
cp .env.example .env
# Edit .env with your settings
\`\`\`
3. **Start development:**
\`\`\`bash
npm run dev
\`\`\`
4. **Access Tina Admin:**
- Visit \`http://localhost:4321/admin\` (when in dev mode)
- Or \`http://localhost:4321/___tina\` for direct access
5. **Configure Tina Backend** (for production):
\`\`\`bash
./scripts/install-tina-backend.sh
\`\`\`
## Tina CMS Setup
For production, you'll need to set up the Tina backend:
\`\`\`bash
./scripts/install-tina-backend.sh
\`\`\`
This will install:
- Auth.js for authentication
- Database adapter for content storage
- Git provider for content management
## PDPA Compliance
The consent system logs:
- User consent choices (accept/reject)
- Cookie categories (analytics, marketing, functional)
- Timestamp and user agent
- IP address (for compliance auditing)
Logs are stored in Astro DB and can be exported for compliance reporting.
EOF
log_success "Migration report: $TARGET_PATH/MIGRATION_REPORT.md"
}
main() {
echo "=============================================="
echo " Website → Astro + Tina CMS Migration Tool"
echo "=============================================="
echo ""
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
print_usage
exit 0
fi
if [ -z "$SOURCE_PATH" ]; then
print_usage
echo ""
log_error "Please specify source path"
exit 1
fi
if [ ! -d "$SOURCE_PATH" ]; then
log_error "Source path not found: $SOURCE_PATH"
exit 1
fi
if [ ! -d "$TARGET_PATH" ]; then
mkdir -p "$TARGET_PATH"
fi
detect_source_type
analyze_source_content
copy_template
migrate_content
add_consent_system
create_tina_schema
create_migration_report
echo ""
echo "=============================================="
log_success "Migration complete!"
echo "=============================================="
echo ""
echo "Next steps:"
echo " 1. cd $TARGET_PATH"
echo " 2. npm install"
echo " 3. npm run dev"
echo " 4. See MIGRATION_REPORT.md for details"
echo ""
}
main "$@"

View File

@@ -1,119 +1,79 @@
#!/usr/bin/env bash
#===============================================================================
# new-project.sh - สร้าง Next.js + Payload CMS project ใหม่จาก Template
#
# Usage: ./new-project.sh [project-name] [project-path]
#
# สร้าง Next.js + Payload CMS project ใหม่โดย:
# 1. คัดลอก nextjs-payload-starter template
# 2. ติดตั้ง dependencies
# 3. ตั้งค่า environment
#
# Requirements:
# - git
# - node.js 20+
# - npm
#
#===============================================================================
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
NC='\033[0m'
# Default values
PROJECT_NAME="${1:-}"
PROJECT_PATH="${2:-.}"
# Get skill directory
SKILL_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
TEMPLATE_DIR="$SKILL_DIR/templates/nextjs-payload-starter"
TEMPLATE_DIR="$SKILL_DIR/templates/astro-tina-starter"
#-------------------------------------------------------------------------------
# Helper functions
#-------------------------------------------------------------------------------
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_usage() {
cat << EOF
Usage: $(basename "$0") [project-name] [project-path]
สร้าง Next.js + Payload CMS project ใหม่จาก Template
Create new Astro + Tina CMS project from template
Arguments:
project-name ชื่อ project (optional)
project-path ที่อยู่ project (default: current directory)
project-name Project name (optional)
project-path Project location (default: current directory)
Examples:
$(basename "$0") my-website
$(basename "$0") my-website /path/to/projects/
Creates:
- Astro 6.1.7 framework
- Tailwind CSS 4.x
- Tina CMS (self-hosted)
- Astro DB for consent logging
- PDPA-compliant consent system
EOF
}
#-------------------------------------------------------------------------------
# Pre-flight checks
#-------------------------------------------------------------------------------
check_requirements() {
log_info "ตรวจสอบความต้องการของระบบ..."
log_info "Checking requirements..."
# Check git
if ! command -v git &> /dev/null; then
log_error "git ไม่พบ กรุณาติดตั้ง git ก่อน"
log_error "git not found"
exit 1
fi
# Check node
if ! command -v node &> /dev/null; then
log_error "node.js ไม่พบ กรุณาติดตั้ง node.js ก่อน"
log_error "node.js not found"
exit 1
fi
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 20 ]; then
log_error "node.js version ต้อง >= 20 (ตอนนี้: $(node -v))"
log_error "node.js >= 20 required (current: $(node -v))"
exit 1
fi
# Check npm
if ! command -v npm &> /dev/null; then
log_error "npm ไม่พบ กรุณาติดตั้ง npm ก่อน"
log_error "npm not found"
exit 1
fi
# Check template exists
if [ ! -d "$TEMPLATE_DIR" ]; then
log_error "ไม่พบ Next.js Payload Starter Template: $TEMPLATE_DIR"
log_error "Template not found: $TEMPLATE_DIR"
exit 1
fi
log_success "ความต้องการของระบบผ่าน (git, node $(node -v), npm)"
log_success "Requirements OK (git, node $(node -v), npm)"
}
#-------------------------------------------------------------------------------
# Create project directory
#-------------------------------------------------------------------------------
setup_directory() {
local actual_project_path="$PROJECT_PATH"
@@ -121,13 +81,11 @@ setup_directory() {
actual_project_path="$PROJECT_PATH/$PROJECT_NAME"
fi
# Create directory
mkdir -p "$actual_project_path"
# Check if directory is empty
if [ "$(ls -A "$actual_project_path" | wc -l)" -gt 0 ]; then
log_warning "Directory ไม่ว่าง: $actual_project_path"
read -p "ดำเนินต่อ? (y/n): " -n 1 -r
log_warning "Directory not empty: $actual_project_path"
read -p "Continue? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
@@ -138,32 +96,37 @@ setup_directory() {
log_info "Project path: $PROJECT_PATH"
}
#-------------------------------------------------------------------------------
# Copy template
#-------------------------------------------------------------------------------
copy_template() {
log_info "คัดลอก Next.js Payload Starter Template..."
log_info "Copying Astro+Tina template..."
# Copy all template files
cp -r "$TEMPLATE_DIR/"* "$PROJECT_PATH/"
cp -r "$TEMPLATE_DIR/src/collections/access" "$PROJECT_PATH/src/collections/" 2>/dev/null || true
cp -r "$TEMPLATE_DIR"/.* "$PROJECT_PATH/" 2>/dev/null || true
# Copy consent API if exists
if [ -d "$SKILL_DIR/templates/consent/api" ]; then
mkdir -p "$PROJECT_PATH/src/pages/api"
cp "$SKILL_DIR/templates/consent/api/"* "$PROJECT_PATH/src/pages/api/" 2>/dev/null || true
fi
log_success "คัดลอก template เสร็จสมบูรณ์"
log_success "Template copied"
}
#-------------------------------------------------------------------------------
# Copy legal templates
#-------------------------------------------------------------------------------
copy_consent_system() {
log_info "Adding PDPA consent system..."
local consent_template="$SKILL_DIR/templates/consent"
if [ -d "$consent_template" ]; then
mkdir -p "$PROJECT_PATH/src/components/consent"
cp "$consent_template/ConsentBanner.astro" "$PROJECT_PATH/src/components/consent/" 2>/dev/null || true
cp "$consent_template/stores/"* "$PROJECT_PATH/src/stores/" 2>/dev/null || true
mkdir -p "$PROJECT_PATH/src/pages/api"
cp "$consent_template/api/consent.ts" "$PROJECT_PATH/src/pages/api/" 2>/dev/null || true
mkdir -p "$PROJECT_PATH/db"
cp "$consent_template/db/config.ts" "$PROJECT_PATH/db/" 2>/dev/null || true
fi
log_success "Consent system added"
}
copy_legal_templates() {
log_info "คัดลอก PDPA templates..."
log_info "Copying PDPA legal templates..."
mkdir -p "$PROJECT_PATH/src/content/pages"
@@ -175,168 +138,92 @@ copy_legal_templates() {
cp "$SKILL_DIR/templates/terms-of-service.md" "$PROJECT_PATH/src/content/pages/"
fi
log_success "คัดลอก PDPA templates เสร็จสมบูรณ์"
log_success "Legal templates copied"
}
#-------------------------------------------------------------------------------
# Install dependencies
#-------------------------------------------------------------------------------
install_dependencies() {
log_info "ติดตั้ง dependencies..."
log_info "Installing dependencies..."
cd "$PROJECT_PATH"
npm install
log_success "ติดตั้ง dependencies เสร็จสมบูรณ์"
log_success "Dependencies installed"
}
#-------------------------------------------------------------------------------
# Setup environment
#-------------------------------------------------------------------------------
setup_environment() {
log_info "ตั้งค่า environment..."
log_info "Setting up environment..."
cd "$PROJECT_PATH"
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
log_success "สร้าง .env จาก .env.example"
log_warning "กรุณาแก้ไข .env และใส่ DATABASE_URL ที่ถูกต้อง"
log_success "Created .env from .env.example"
else
cat > .env << 'EOF'
# Payload CMS
PAYLOAD_SECRET=change-this-secret-key-at-least-32-characters
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
# Server
SERVER_URL=http://localhost:4321
NODE_ENV=development
PUBLIC_SITE_URL=http://localhost:4321
TINA_TOKEN=your-tina-token
EOF
log_success "สร้าง .env เริ่มต้น"
log_warning "กรุณาแก้ไข .env และใส่ DATABASE_URL ที่ถูกต้อง"
log_success "Created default .env"
fi
else
log_info ".env มีอยู่แล้ว"
fi
}
#-------------------------------------------------------------------------------
# Create AI_RULES.md
#-------------------------------------------------------------------------------
create_ai_rules() {
log_info "สร้าง AI_RULES.md..."
cd "$PROJECT_PATH"
cat > AI_RULES.md << 'EOF'
# AI Rules
## Tech Stack Overview
- **Frontend:** Next.js App Router + TypeScript
- **Backend/CMS:** Payload CMS 3.0
- **Database:** MongoDB (via mongooseAdapter)
- **Styling:** Tailwind CSS v4
- **Authentication:** Payload built-in auth with role-based access
- **Image Handling:** Payload Media collection
## File Organization
- **Collections:** Define Payload collections in `src/collections/`
- **Pages:** Next.js App Router in `src/app/`
- **Components:** Reusable components in `src/components/`
- **Styles:** Global styles in `src/app/globals.css`
## Never Modify These Files
- `src/payload-types.ts` - Auto-generated by Payload
- `src/migrations/` - Database migration files
## Thai-First
- ใช้ Kanit หรือ Noto Sans Thai fonts
- Thai typography CSS
- Thai structured data (LocalBusiness, Organization)
- ภาษาไทยเป็นหลักใน content
EOF
log_success "สร้าง AI_RULES.md เสร็จสมบูรณ์"
}
#-------------------------------------------------------------------------------
# Initialize git
#-------------------------------------------------------------------------------
init_git() {
log_info "เริ่มต้น git..."
log_info "Initializing git..."
cd "$PROJECT_PATH"
if [ ! -d ".git" ]; then
git init
git add .
git commit -m "Initial commit: Next.js + Payload CMS starter"
log_success "เริ่มต้น git เสร็จสมบูรณ์"
else
log_info "git repo มีอยู่แล้ว"
git commit -m "Initial commit: Astro + Tina CMS starter"
log_success "Git initialized"
fi
}
#-------------------------------------------------------------------------------
# Show project structure
#-------------------------------------------------------------------------------
show_structure() {
log_info "โครงสร้าง project:"
log_info "Project structure:"
cd "$PROJECT_PATH"
echo ""
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.astro" -o -name "*.mjs" -o -name "*.css" -o -name "*.md" -o -name "package.json" \) 2>/dev/null | grep -v node_modules | sort | head -30
}
#-------------------------------------------------------------------------------
# Main
#-------------------------------------------------------------------------------
main() {
echo "=============================================="
echo " Next.js + Payload CMS Project Creator"
echo " Using Next.js Payload Starter"
echo " Astro + Tina CMS Project Creator"
echo "=============================================="
echo ""
# Parse arguments
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
print_usage
exit 0
fi
# Run steps
check_requirements
setup_directory
copy_template
copy_consent_system
copy_legal_templates
install_dependencies
setup_environment
create_ai_rules
init_git
show_structure
echo ""
echo "=============================================="
log_success "สร้าง Next.js + Payload CMS project เสร็จสมบูรณ์!"
log_success "Project created successfully!"
echo "=============================================="
echo ""
echo "ขั้นตอนถัดไป:"
echo "Next steps:"
echo " 1. cd $PROJECT_PATH"
echo " 2. แก้ไข .env (MONGODB_URL, PAYLOAD_SECRET)"
echo " 3. npm install"
echo " 4. npm run dev"
echo " 5. เปิด http://localhost:3002/admin สำหรับ Payload admin"
echo " 2. npm run dev"
echo " 3. Open http://localhost:4321"
echo ""
echo "For Tina CMS admin:"
echo " - npm run dev"
echo " - Visit http://localhost:4321/admin"
echo ""
}
main "$@"
main "$@"