feat: update website-creator to static mode with ConsentOS + tracking scripts

- Remove Astro DB (no longer needed for consent logging)
- Change from SSR to static output mode
- Add TrackingScripts.astro with GA4, GTM, Umami, Clarity, FB Pixel, Google Ads, TikTok, LINE
- Use ConsentOS consent-loader.js for auto-blocking tracking scripts
- Update Dockerfile to nginx static hosting
- Remove old consent template (custom consent no longer needed)
- Update SKILL.md, AGENTS.md, README.md documentation
- Add nginx.conf for static hosting
This commit is contained in:
2026-04-21 21:19:32 +07:00
parent d1edc9cd6c
commit c38cc4ae26
19 changed files with 378 additions and 1099 deletions

View File

@@ -7,11 +7,36 @@ PUBLIC_SITE_URL=https://your-domain.com
TINA_TOKEN=your-tina-token-from-tina-cloud
TINA_CLIENT_ID=your-client-id
# Astro DB - Persistent Storage
# IMPORTANT: Set OUT_DIR to /data for Docker/Easypanel persistent volume
# This ensures database persists across redeployments
OUT_DIR=/data
# ConsentOS - Consent Management (moreminimore.com)
PUBLIC_CONSENT_SITE_ID=your-consent-site-id
PUBLIC_CONSENT_API_BASE=https://consent.moreminimore.com
# Optional: External Turso database (if using external DB instead of local SQLite)
# TURSO_DATABASE_URL=libsql://your-db.turso.io
# TURSO_AUTH_TOKEN=your-auth-token
# ===== TRACKING SCRIPTS =====
# All tracking scripts are blocked by ConsentOS until user gives consent
# --- Analytics ---
# Google Analytics 4
PUBLIC_GA4_ID=G-XXXXXXXXXX
# Google Tag Manager
PUBLIC_GTM_ID=GTM-XXXXXXX
# Umami (self-hosted analytics)
PUBLIC_UMAMI_URL=https://umami.example.com
PUBLIC_UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Microsoft Clarity (heatmaps)
PUBLIC_CLARITY_ID=xxxxxxxxxx
# --- Marketing / Advertising ---
# Facebook Pixel
PUBLIC_FB_PIXEL_ID=123456789
# Google Ads Conversion
PUBLIC_GOOGLE_ADS_ID=AW-123456789
# TikTok Pixel
PUBLIC_TIKTOK_PIXEL_ID=XXXXXXXX
# LINE Channel Tag (Thailand)
PUBLIC_LINE_CHANNEL_ID=1234567890

View File

@@ -1,7 +1,7 @@
# Astro Tina Starter - Agent Knowledge Base
**Generated:** 2026-04-17
**Version:** 1.0.0
**Generated:** 2026-04-17
**Version:** 2.0.0
**Type:** Astro 6 + Tina CMS Starter Template
---
@@ -17,16 +17,14 @@ Starter template for building websites with Astro 6, Tina CMS, and Tailwind CSS
| Framework | Astro | 6.1.7 |
| CMS | Tina CMS | 2.x |
| Styling | Tailwind CSS | 4.x |
| Database | Astro DB | 0.14.x |
| State | Nano Stores | 0.11.x |
### Key Features
- Self-hosted Tina CMS with schema-based content
- Tailwind CSS 4.x using `@tailwindcss/vite` plugin
- Astro DB for consent logging (PDPA compliant)
- External consent system integration
- Thai language support with Noto Sans Thai
- Docker-ready deployment
- Docker-ready with nginx
---
@@ -37,9 +35,6 @@ astro-tina-starter/
├── .tina/
│ ├── config.ts # Tina CMS configuration
│ └── schema.ts # Content schema definitions
├── db/
│ ├── config.ts # Astro DB schema
│ └── seed.ts # Database seed script
├── src/
│ ├── styles/
│ │ └── global.css # Tailwind v4 styles + @theme
@@ -48,7 +43,8 @@ astro-tina-starter/
│ ├── pages/
│ │ └── index.astro
│ ├── components/
│ │ ── Header.astro
│ │ ── Header.astro
│ │ └── TrackingScripts.astro # Tracking scripts (GA4, FB Pixel, etc.)
│ └── content/
│ ├── config.ts # Astro content collections
│ ├── posts/ # Blog posts (MDX)
@@ -57,6 +53,7 @@ astro-tina-starter/
├── public/
│ └── favicon.svg
├── Dockerfile
├── nginx.conf
├── astro.config.mjs
├── tsconfig.json
└── package.json
@@ -100,9 +97,33 @@ Tina CMS manages content in `src/content/`:
Schema defined in `.tina/schema.ts`.
### Astro DB Schema
### ConsentOS + Tracking System
Consent log table for PDPA compliance in `db/config.ts`.
ConsentOS (`consent.moreminimore.com`) manages consent and blocking:
```bash
# ConsentOS
PUBLIC_CONSENT_SITE_ID=your-site-id
PUBLIC_CONSENT_API_BASE=https://consent.moreminimore.com
# Analytics
PUBLIC_GA4_ID=G-XXXXXXXXXX
PUBLIC_GTM_ID=GTM-XXXXXXX
PUBLIC_UMAMI_URL=https://umami.example.com
PUBLIC_UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PUBLIC_CLARITY_ID=xxxxxxxxxx
# Marketing
PUBLIC_FB_PIXEL_ID=123456789
PUBLIC_GOOGLE_ADS_ID=AW-123456789
PUBLIC_TIKTOK_PIXEL_ID=XXXXXXXX
PUBLIC_LINE_CHANNEL_ID=1234567890
```
**Tracking Scripts:**
- `TrackingScripts.astro` - contains all tracking scripts
- Scripts are auto-blocked by ConsentOS until user consent
- Categories: `analytics`, `marketing`
---
@@ -116,7 +137,17 @@ No external API credentials required for this template.
|----------|-------------|
| `TINA_TOKEN` | Tina CMS production authentication |
| `TINA_CLIENT_ID` | Tina CMS client ID |
| `DATABASE_URL` | Custom database connection (optional) |
| `PUBLIC_CONSENT_SITE_ID` | ConsentOS site ID |
| `PUBLIC_CONSENT_API_BASE` | ConsentOS API base URL |
| `PUBLIC_GA4_ID` | Google Analytics 4 |
| `PUBLIC_GTM_ID` | Google Tag Manager |
| `PUBLIC_UMAMI_URL` | Umami analytics URL |
| `PUBLIC_UMAMI_WEBSITE_ID` | Umami website ID |
| `PUBLIC_CLARITY_ID` | Microsoft Clarity |
| `PUBLIC_FB_PIXEL_ID` | Facebook Pixel |
| `PUBLIC_GOOGLE_ADS_ID` | Google Ads conversion ID |
| `PUBLIC_TIKTOK_PIXEL_ID` | TikTok Pixel |
| `PUBLIC_LINE_CHANNEL_ID` | LINE Channel Tag |
---
@@ -127,39 +158,13 @@ No external API credentials required for this template.
npm install
# Development
npm run dev # Full dev (Tina + Astro)
npm run dev:astro # Astro only
npm run dev:tina # Tina CMS only
npm run dev
# Build
npm run build # Production build
npm run preview # Preview production build
npm run build
# Database
npm run db:push # Push schema to database
npm run db:seed # Seed database
```
---
## PDPA COMPLIANCE
Template includes consent logging via Astro DB:
```typescript
// db/config.ts
export const ConsentLog = defineTable({
columns: {
action: text(),
purpose: text(),
analytics: boolean(),
marketing: boolean(),
functional: boolean(),
userAgent: text(),
ip: text(),
timestamp: text(),
},
})
# Preview
npm run preview
```
---
@@ -181,6 +186,10 @@ docker build -t astro-tina-starter .
docker run -p 8080:80 astro-tina-starter
```
### Easypanel
Static hosting - no persistent volume needed.
### Manual
```bash

View File

@@ -1,3 +1,4 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
@@ -7,19 +8,17 @@ RUN npm install
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
# Static files stage
FROM nginx:alpine AS runner
WORKDIR /app
RUN mkdir -p /data
# Copy static files from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN npm install --omit=dev
ENV HOST=0.0.0.0
ENV PORT=4321
ENV NODE_ENV=production
# Copy nginx config
COPY nginx.conf /etc/nginx/http.d/default.conf
EXPOSE 4321
EXPOSE 80
CMD ["node", "dist/server/entry.mjs"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -4,21 +4,17 @@ Astro 6.1.7 + Tina CMS starter template with Tailwind CSS 4.x
## Tech Stack
- **Framework:** Astro 6.1.7 (SSR mode)
- **Framework:** Astro 6.1.7 (static mode)
- **CMS:** Tina CMS (self-hosted)
- **Styling:** Tailwind CSS 4.x with `@tailwindcss/vite`
- **Database:** Astro DB (LibSQL/SQLite)
- **Adapter:** @astrojs/node (SSR)
- **State:** Nano Stores + React
- **Language:** TypeScript
## Features
- Self-hosted Tina CMS with schema-based content
- Tailwind CSS 4.x using `@tailwindcss/vite` plugin
- Astro DB for consent logging (PDPA compliant)
- SSR mode for API routes
- Docker-ready with persistent storage
- ConsentOS + Tracking Scripts (GA4, Facebook Pixel, etc.)
- Docker-ready with nginx
- Thai language support foundation
## Quick Start
@@ -41,40 +37,36 @@ During development, access Tina CMS at:
For production, you'll need a TINA_TOKEN environment variable.
## Easypanel Deployment
## ConsentOS + Tracking
This template is designed for **Easypanel** with persistent volume support.
### Important: Persistent Storage
**Astro DB stores SQLite at `/data/`** - this directory is mounted as a persistent volume in Easypanel.
When deploying:
1. Set environment variable `OUT_DIR=/data`
2. Mount persistent volume to `/data` in Easypanel
3. Database will persist across redeployments
This template includes ConsentOS (`consent.moreminimore.com`) for PDPA-compliant consent management and auto-blocking of tracking scripts.
### Environment Variables
```bash
# Required for persistent storage
OUT_DIR=/data
# ConsentOS
PUBLIC_CONSENT_SITE_ID=your-site-id
PUBLIC_CONSENT_API_BASE=https://consent.moreminimore.com
# Optional - Tina CMS
TINA_TOKEN=your-tina-token
# Analytics
PUBLIC_GA4_ID=G-XXXXXXXXXX
PUBLIC_GTM_ID=GTM-XXXXXXX
PUBLIC_UMAMI_URL=https://umami.example.com
PUBLIC_UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PUBLIC_CLARITY_ID=xxxxxxxxxx
# Optional - External database (instead of local SQLite)
# TURSO_DATABASE_URL=libsql://your-db.turso.io
# TURSO_AUTH_TOKEN=your-auth-token
# Marketing
PUBLIC_FB_PIXEL_ID=123456789
PUBLIC_GOOGLE_ADS_ID=AW-123456789
PUBLIC_TIKTOK_PIXEL_ID=XXXXXXXX
PUBLIC_LINE_CHANNEL_ID=1234567890
```
### Easypanel Setup
### How It Works
1. Create new service from Git repo
2. Set build command: `npm run build`
3. Set start command: `node dist/server/entry.mjs`
4. Add environment variable: `OUT_DIR=/data`
5. Mount persistent volume to `/data`
1. `TrackingScripts.astro` contains all tracking codes with `data-consent-category` attributes
2. ConsentOS `consent-loader.js` scans and auto-blocks scripts until user consent
3. Categories: `analytics`, `marketing`
## Project Structure
@@ -83,22 +75,20 @@ astro-tina-starter/
├── .tina/
│ ├── config.ts # Tina CMS configuration
│ └── schema.ts # Content schema definitions
├── db/
│ ├── config.ts # Astro DB schema (consent logs)
│ └── seed.ts # Database seed script
├── src/
│ ├── styles/
│ │ └── global.css # Tailwind v4 styles
│ ├── layouts/
│ │ └── Layout.astro
│ ├── pages/
│ │ ── index.astro
│ │ └── api/ # API routes (consent, etc.)
│ │ ── index.astro
│ ├── components/
│ │ ── Header.astro
│ │ ── Header.astro
│ │ └── TrackingScripts.astro # Tracking scripts
│ └── content/
│ └── config.ts # Tina content collections
├── Dockerfile # Multi-stage Node.js (not nginx)
├── Dockerfile
├── nginx.conf
└── package.json
```
@@ -116,34 +106,16 @@ The configuration is done via CSS `@theme` block in `src/styles/global.css`.
}
```
## Astro DB
## Docker
The template includes a consent-log table for PDPA compliance:
```bash
# Build
docker build -t astro-tina .
```ts
// db/config.ts
import { defineDb, defineTable, column } from 'astro:db';
const ConsentLog = defineTable({
columns: {
id: column.number({ primaryKey: true }),
action: column.text(),
purpose: column.text(),
analytics: column.boolean({ default: false }),
marketing: column.boolean({ default: false }),
functional: column.boolean({ default: false }),
userAgent: column.text({ optional: true }),
ip: column.text({ optional: true }),
timestamp: column.date(),
sessionId: column.text({ optional: true }),
},
});
export default defineDb({ tables: { ConsentLog } });
# Run
docker run -p 8080:80 astro-tina
```
Database file location: `/data/astro.db` (persistent across redeployments)
## License
MIT
MIT

View File

@@ -1,13 +1,13 @@
import { defineConfig } from 'astro/config'
import tailwindcss from '@tailwindcss/vite'
import tina from 'tinacms'
import node from '@astrojs/node'
import { fileURLToPath } from 'url'
import path from 'path'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
site: 'https://example.com',
integrations: [
tina({
enabled: !!process.env.TINA_TOKEN,
@@ -28,10 +28,7 @@ export default defineConfig({
},
},
},
output: 'server',
adapter: node({
mode: 'standalone'
}),
output: 'static',
build: {
assets: '_assets',
},

View File

@@ -1,22 +0,0 @@
import { defineDb, defineTable, column } from 'astro:db';
const ConsentLog = defineTable({
columns: {
id: column.number({ primaryKey: true }),
action: column.text(),
purpose: column.text(),
analytics: column.boolean({ default: false }),
marketing: column.boolean({ default: false }),
functional: column.boolean({ default: false }),
userAgent: column.text({ optional: true }),
ip: column.text({ optional: true }),
timestamp: column.date(),
sessionId: column.text({ optional: true }),
},
});
export default defineDb({
tables: {
ConsentLog,
},
});

View File

@@ -1,7 +0,0 @@
import { db } from 'astro:db'
import { sql } from 'astro/db'
export default async function seed() {
// Seed default settings if needed
console.log('Database seeded successfully')
}

View File

@@ -0,0 +1,14 @@
server {
listen 80;
root /app/dist;
index index.html;
location / {
try_files $uri $uri/ $uri.html =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "astro-tina-starter",
"type": "module",
"version": "1.0.0",
"version": "2.0.0",
"description": "Astro 6 + Tina CMS starter template with Tailwind CSS 4.x",
"scripts": {
"dev": "tinacms dev --port 3001 & astro dev",
@@ -9,19 +9,14 @@
"dev:tina": "tinacms dev --port 3001",
"build": "tinacms build && astro build",
"preview": "astro preview",
"astro": "astro",
"db:push": "astro db push",
"db:seed": "astro db seed"
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/db": "^0.14.3",
"@astrojs/node": "^9.0.0",
"@nanostores/react": "^0.7.3",
"@astrojs/mdx": "^4.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"astro": "^6.1.7",
"nanostores": "^0.11.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.0.0",

View File

@@ -0,0 +1,167 @@
---
const ga4Id = import.meta.env.PUBLIC_GA4_ID
const gtmId = import.meta.env.PUBLIC_GTM_ID
const umamiUrl = import.meta.env.PUBLIC_UMAMI_URL
const umamiWebsiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID
const clarityId = import.meta.env.PUBLIC_CLARITY_ID
const fbPixelId = import.meta.env.PUBLIC_FB_PIXEL_ID
const googleAdsId = import.meta.env.PUBLIC_GOOGLE_ADS_ID
const tiktokPixelId = import.meta.env.PUBLIC_TIKTOK_PIXEL_ID
const lineChannelId = import.meta.env.PUBLIC_LINE_CHANNEL_ID
---
<!-- Google Analytics 4 -->
{ga4Id && (
<script
data-consent-category="analytics"
async
src={`https://www.googletagmanager.com/gtag/js?id=${ga4Id}`}
></script>
)}
{ga4Id && (
<script
data-consent-category="analytics"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${ga4Id}');
`
}}
></script>
)}
<!-- Google Tag Manager -->
{gtmId && (
<script
data-consent-category="analytics"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmId}');
`
}}
></script>
)}
<!-- Umami Analytics -->
{umamiUrl && umamiWebsiteId && (
<script
data-consent-category="analytics"
async
src={`${umamiUrl}/script.js`}
data-website-id={umamiWebsiteId}
></script>
)}
<!-- Microsoft Clarity -->
{clarityId && (
<script
data-consent-category="analytics"
dangerouslySetInnerHTML={{
__html: `
(function(c,l,a,r,i,t,y){
a[q]=a[q]||function(){(a[q].q=a[q].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${clarityId}");
`
}}
></script>
)}
<!-- Facebook Pixel -->
{fbPixelId && (
<script
data-consent-category="marketing"
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${fbPixelId}');
fbq('track', 'PageView');
`
}}
></script>
)}
{fbPixelId && (
<noscript data-consent-category="marketing">
<img
height="1"
width="1"
style="display:none"
src={`https://www.facebook.com/tr?id=${fbPixelId}&ev=PageView&noscript=1`}
alt=""
/>
</noscript>
)}
<!-- Google Ads Conversion -->
{googleAdsId && (
<script
data-consent-category="marketing"
async
src={`https://www.googletagmanager.com/gtag/js?id=${googleAdsId}`}
></script>
)}
<!-- TikTok Pixel -->
{tiktokPixelId && (
<script
data-consent-category="marketing"
dangerouslySetInnerHTML={{
__html: `
!function (w, d, t) {
w.TiktokAnalyticsObject = t;
var ttq = w[t] = w[t] || [];
ttq.methods = ["page", "track", "identify", "instances", "debug", "on", "off", "once", "ready", "alias", "group", "enableCookie", "disableCookie"];
ttq.setAndDefer = function (t, e) { t[e] = function () { t.push([e].concat(Array.prototype.slice.call(arguments, 0))) } };
for (var i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]);
ttq.instance = function (t) {
var e = t.slice(0);
return ttq.push([e]), ttq
};
for (var i = 0; i < ttq.methods.length; i++) {
var e = ttq.methods[i];
ttq[e] = ttq.instance.bind(ttq, e)
}
ttq.load = function (t, e) {
var n = "https://analytics.tiktok.com/i18n/pixel/events.js";
n = n + "?sdkid=" + t + "&lib=" + e;
var i = d.createElement("script");
i.type = "text/javascript";
i.src = n;
d.getElementsByTagName("head")[0].appendChild(i)
};
ttq.load("${tiktokPixelId}", "exc");
ttq.page()
}(window, document, 'ttq');
`
}}
></script>
)}
<!-- LINE Channel Tag -->
{lineChannelId && (
<script
data-consent-category="marketing"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${lineChannelId}');
`
}}
></script>
)}

View File

@@ -1,5 +1,6 @@
---
import "@/styles/global.css"
import TrackingScripts from "@/components/TrackingScripts.astro"
interface Props {
title?: string
@@ -10,6 +11,9 @@ const {
title = "Astro Tina Starter",
description = "Astro 6 + Tina CMS starter template",
} = Astro.props
const consentSiteId = import.meta.env.PUBLIC_CONSENT_SITE_ID || 'demo'
const consentApiBase = import.meta.env.PUBLIC_CONSENT_API_BASE || 'https://consent.moreminimore.com'
---
<!doctype html>
@@ -23,5 +27,15 @@ const {
</head>
<body class="bg-primary-50 text-primary-900 min-h-screen">
<slot />
<!-- Tracking Scripts (ConsentOS will auto-block until consent) -->
<TrackingScripts />
<!-- ConsentOS - Consent Management -->
<script
src={`${consentApiBase}/consent-loader.js`}
data-site-id={consentSiteId}
data-api-base={consentApiBase}
></script>
</body>
</html>