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

@@ -142,7 +142,7 @@ Output:
- meta_description: 150-160 chars
- slug: Auto-generated (Thai-friendly)
- images: Saved to website repo
- astro_ready: true (content collections format)
- payload_cms_ready: true (REST API format)
```
**X/Twitter Thread:**
@@ -415,55 +415,55 @@ def save_image_for_channel(image_data: bytes, topic: str, channel: str) -> str:
### **Website-Creator Integration:**
```python
def publish_blog_to_astro(article_md: str, website_repo: str) -> Dict:
def publish_blog_to_payload(
article_md: str,
website_url: str,
payload_token: str
) -> Dict:
"""
Publish blog post to Astro content collections
Returns deployment status
Publish blog post to Payload CMS via REST API
Returns publication status
"""
# Parse frontmatter
frontmatter = parse_frontmatter(article_md)
# Detect language
lang = detect_content_language(article_md)
# Generate slug
slug = generate_slug(frontmatter['title'], lang)
# Determine output path
output_path = os.path.join(
website_repo,
'src/content/blog',
f'({lang})',
f'{slug}.md'
if lang == 'th':
slug = f'th/{slug}'
# Convert markdown to Lexical JSON
content = markdown_to_lexical(article_md)
# Prepare Payload document
payload_doc = {
'title': frontmatter['title'],
'slug': slug,
'content': content,
'status': 'draft',
'description': frontmatter.get('description', ''),
}
# Send to Payload CMS API
response = requests.post(
f'{website_url}/api/posts',
headers={'Authorization': f'Bearer {payload_token}'},
json=payload_doc
)
# Ensure directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Write article
with open(output_path, 'w', encoding='utf-8') as f:
f.write(article_md)
# Copy images if any
if 'images' in frontmatter:
for img in frontmatter['images']:
# Copy from temp location to website repo
dest_path = os.path.join(website_repo, 'public', img['src'].lstrip('/'))
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.copy(img['local_path'], dest_path)
# Git commit and push
subprocess.run(['git', 'add', '.'], cwd=website_repo, check=True)
subprocess.run(['git', 'commit', '-m', f'Add blog post: {slug}'], cwd=website_repo, check=True)
subprocess.run(['git', 'push', 'origin', 'main'], cwd=website_repo, check=True)
# Return deployment info
result = response.json()
# Return publication info
return {
'published': True,
'id': result.get('id'),
'slug': slug,
'language': lang,
'path': output_path,
'deployment_url': f"https://your-domain.com/blog/{slug}" if lang == 'en' else f"https://your-domain.com/th/{slug}"
'admin_url': f'{website_url}/admin/collections/posts/{result.get("id")}',
'api_url': f'{website_url}/api/posts',
}
```