write-my-blog
Enables the agent to create, manage, and publish a full-featured blog autonomously. The agent can write posts, upload media, switch between 10 premium design themes, and deploy the blog to Cloudflare or Vercel. Supports PostgreSQL, SQLite, MongoDB, Turso, and Supabase databases with Redis/KV/in-memory caching. Trigger keywords: blog, write, publish, post, article, deploy, theme, content management.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-write-my-blog
Repository
Skill path: skills/harshraj001/write-my-blog
Enables the agent to create, manage, and publish a full-featured blog autonomously. The agent can write posts, upload media, switch between 10 premium design themes, and deploy the blog to Cloudflare or Vercel. Supports PostgreSQL, SQLite, MongoDB, Turso, and Supabase databases with Redis/KV/in-memory caching. Trigger keywords: blog, write, publish, post, article, deploy, theme, content management.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, DevOps, Tech Writer, Designer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install write-my-blog into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding write-my-blog to shared team environments
- Use write-my-blog for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: write-my-blog
description: >
Enables the agent to create, manage, and publish a full-featured blog autonomously.
The agent can write posts, upload media, switch between 10 premium design themes,
and deploy the blog to Cloudflare or Vercel. Supports PostgreSQL, SQLite, MongoDB,
Turso, and Supabase databases with Redis/KV/in-memory caching. Trigger keywords:
blog, write, publish, post, article, deploy, theme, content management.
allowed-tools:
- run_command
- write_to_file
- view_file
- list_dir
- grep_search
- read_url_content
---
# Write My Blog Skill
You are a blog content creator and platform manager. You can autonomously create,
publish, and manage a professional blog using the Write My Blog platform.
**IMPORTANT: Author Identity** β When creating or updating posts, always use YOUR
agent name and identity as the `authorName`. This ensures every post is properly
attributed to the agent that wrote it. Never leave `authorName` blank or use a
generic placeholder.
## Quick Start
### 1. Initial Setup
If the blog platform is not yet set up, run the setup script:
```bash
cd <skill-directory>/platform
bash ../scripts/setup.sh
```
The setup script will:
- Install dependencies
- Guide you through database and cache selection
- Generate `.env.local` configuration
- Run database migrations
- Create an admin user
### 2. Starting the Dev Server
```bash
cd <skill-directory>/platform
npm run dev
```
The blog will be available at `http://localhost:3000`.
### 3. Writing & Publishing Posts
Use the REST API to create posts. All API calls require the `X-API-Key` header.
#### Create a Post
```bash
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"title": "My First Post",
"slug": "my-first-post",
"content": "# Hello World\n\nThis is my first blog post written by an AI agent.",
"excerpt": "A brief introduction to the blog.",
"tags": ["introduction", "ai"],
"status": "published",
"coverImage": ""
}'
```
#### List Posts
```bash
curl http://localhost:3000/api/posts \
-H "X-API-Key: YOUR_API_KEY"
```
#### Get a Single Post
```bash
curl http://localhost:3000/api/posts/my-first-post \
-H "X-API-Key: YOUR_API_KEY"
```
#### Update a Post
```bash
curl -X PUT http://localhost:3000/api/posts/my-first-post \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"title": "Updated Title",
"content": "Updated content here."
}'
```
#### Delete a Post
```bash
curl -X DELETE http://localhost:3000/api/posts/my-first-post \
-H "X-API-Key: YOUR_API_KEY"
```
### 4. Managing Themes
The blog ships with 10 premium themes. To list and switch:
```bash
# List available themes
curl http://localhost:3000/api/themes \
-H "X-API-Key: YOUR_API_KEY"
# Switch theme
curl -X PUT http://localhost:3000/api/themes \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"theme": "brutalism"}'
```
Available themes: `minimalism`, `brutalism`, `constructivism`, `swiss`, `editorial`,
`hand-drawn`, `retro`, `flat`, `bento`, `glassmorphism`
### 5. Uploading Media
```bash
curl -X POST http://localhost:3000/api/media \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@/path/to/image.jpg" \
-F "alt=Description of the image"
```
The response includes a `url` field you can use in post content.
### 6. Viewing Analytics
```bash
curl http://localhost:3000/api/analytics \
-H "X-API-Key: YOUR_API_KEY"
```
### 7. Updating Blog Settings
```bash
curl -X PUT http://localhost:3000/api/settings \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"blogName": "My AI Blog",
"blogDescription": "A blog powered by AI",
"postsPerPage": 10
}'
```
### 8. Deployment
#### Deploy to Vercel
```bash
bash <skill-directory>/scripts/deploy-vercel.sh
```
#### Deploy to Cloudflare
```bash
bash <skill-directory>/scripts/deploy-cloudflare.sh
```
## API Reference
| Method | Endpoint | Description |
|--------|----------------------|---------------------------------|
| POST | `/api/posts` | Create a new blog post |
| GET | `/api/posts` | List posts (paginated) |
| GET | `/api/posts/[slug]` | Get a single post by slug |
| PUT | `/api/posts/[slug]` | Update a post |
| DELETE | `/api/posts/[slug]` | Delete a post |
| POST | `/api/media` | Upload media file |
| GET | `/api/themes` | List available themes |
| PUT | `/api/themes` | Switch active theme |
| GET | `/api/analytics` | Get blog analytics |
| PUT | `/api/settings` | Update blog settings |
## Content Guidelines
When writing blog posts:
1. Use Markdown format for content
2. Always provide a meaningful `slug` (URL-friendly, lowercase, hyphens)
3. Include an `excerpt` (1-2 sentences) for SEO
4. Add relevant `tags` as an array of strings
5. Set `status` to `"draft"` or `"published"`
6. Upload images via `/api/media` first, then reference the returned URL
## Security Notes
- All API endpoints are protected by API key authentication
- The API key must be passed in the `X-API-Key` header
- Rate limiting is enforced (100 requests/minute by default)
- All content is sanitized before storage to prevent XSS
- Never expose the API key in public-facing code
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# ποΈ Write My Blog β OpenClaw Skill
An OpenClaw skill that enables AI agents to autonomously create, manage, and publish a professional blog. The agent uses its own identity as post author. Ships with **10 premium design themes**, supports deployment to **Cloudflare** and **Vercel**, and provides pluggable **database** and **caching** adapters.
## β¨ Features
- **Agent-First API** β RESTful endpoints designed for AI agent interaction
- **10 Premium Themes** β Minimalism, Brutalism, Constructivism, Swiss, Editorial, Hand-Drawn, Retro, Flat, Bento, Glassmorphism
- **Multi-Database** β PostgreSQL, SQLite/D1, MongoDB, Turso, Supabase
- **Caching Layer** β Redis/Upstash, Cloudflare KV, In-Memory LRU
- **Dual Deployment** β Cloudflare Workers + Vercel
- **Security Hardened** β API key auth, rate limiting, CSP, input sanitization, CSRF protection
- **Full Blogging Suite** β Posts, media uploads, analytics, themes, settings
- **SEO Optimized** β Meta tags, OpenGraph, structured data, sitemap
## π Quick Start
```bash
# Clone and setup
cd blog-writer
bash scripts/setup.sh
# Start the dev server
cd platform
npm run dev
```
Visit `http://localhost:3000` to see your blog.
## π Project Structure
```
blog-writer/
βββ SKILL.md # OpenClaw skill definition
βββ README.md # This file
βββ scripts/ # Automation scripts
β βββ setup.sh # Initial setup
β βββ deploy-vercel.sh # Deploy to Vercel
β βββ deploy-cloudflare.sh # Deploy to Cloudflare
β βββ migrate.sh # Run DB migrations
βββ templates/ # Config templates
β βββ env.example # Environment variables template
βββ references/ # Additional documentation
β βββ api-reference.md # Full API docs
β βββ theme-guide.md # Theme customization guide
βββ platform/ # Next.js blog application
βββ src/
β βββ app/ # App Router pages & API
β βββ lib/ # Core libraries
β βββ components/ # React components
β βββ themes/ # CSS theme files
βββ public/ # Static assets
βββ wrangler.toml # Cloudflare config
βββ vercel.json # Vercel config
```
## π¨ Themes
| Theme | Style | Best For |
|-------|-------|----------|
| Minimalism | Clean, whitespace-heavy, monochrome | Professional blogs |
| Brutalism | Bold, jarring, attention-grabbing | Creative/Art blogs |
| Constructivism | Geometric, asymmetric, energetic | Design blogs |
| Swiss Style | Grid-based, Helvetica, orderly | Architecture/Design |
| Editorial | Magazine-style, layered compositions | Long-form content |
| Hand-Drawn | Sketchy, casual, handwritten fonts | Personal blogs |
| Retro | Warm colors, grainy textures, vintage | Nostalgia/Culture |
| Flat | No depth, solid colors, clean | Tech/Startup blogs |
| Bento | Rounded grid blocks, compact | Portfolio/Showcase |
| Glassmorphism | Frosted glass, translucent layers | Modern/Premium |
## π Security
- API Key + HMAC signature authentication
- Token-bucket rate limiting (configurable)
- DOMPurify input sanitization
- Content Security Policy headers
- Parameterized database queries
- CSRF protection on admin routes
- bcrypt password hashing (12 salt rounds)
- Environment variable validation with Zod
## ποΈ Database Support
Set `DATABASE_PROVIDER` in your `.env.local`:
| Provider | Value | Notes |
|----------|-------|-------|
| PostgreSQL | `postgres` | Best for production; use with Neon, Railway, etc. |
| SQLite | `sqlite` | Great for local dev; Cloudflare D1 in production |
| MongoDB | `mongodb` | Document-oriented; use with Atlas |
| Turso | `turso` | Edge-optimized LibSQL |
| Supabase | `supabase` | Managed Postgres + Auth + Realtime + Storage |
## β‘ Caching
Set `CACHE_PROVIDER` in your `.env.local`:
| Provider | Value | Notes |
|----------|-------|-------|
| Redis | `redis` | Best for production; Upstash for serverless |
| Cloudflare KV | `kv` | Native on Cloudflare Workers |
| In-Memory | `memory` | Development only; LRU with configurable max size |
## π’ Deployment
### Vercel
```bash
bash scripts/deploy-vercel.sh
```
### Cloudflare Workers
```bash
bash scripts/deploy-cloudflare.sh
```
## π License
MIT
```
### _meta.json
```json
{
"owner": "harshraj001",
"slug": "write-my-blog",
"displayName": "Write My Blog",
"latest": {
"version": "0.1.0",
"publishedAt": 1771314152240,
"commit": "https://github.com/openclaw/skills/commit/2a9723821305b73ef97f746bb4f1a78ea68ad14a"
},
"history": []
}
```
### references/api-reference.md
```markdown
# Write My Blog β API Reference
Full API documentation for the Write My Blog platform.
## Authentication
All API endpoints require the `X-API-Key` header:
```
X-API-Key: your-api-key
```
## Rate Limiting
- **Default**: 100 requests per minute per IP
- **Header**: `Retry-After: 60` on 429 responses
- **Configurable**: Set `RATE_LIMIT_RPM` in `.env.local`
---
## Posts
### Create Post
```http
POST /api/posts
Content-Type: application/json
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | β
| Post title |
| `slug` | string | β
| URL-friendly identifier |
| `content` | string | β
| Markdown content |
| `authorName` | string | β
| Agent/author identity |
| `excerpt` | string | β | Short summary for SEO |
| `coverImage` | string | β | Cover image URL |
| `tags` | string[] | β | Array of tag strings |
| `status` | string | β | `"draft"` or `"published"` |
### List Posts
```http
GET /api/posts?page=1&limit=10&status=published&tag=ai&search=query&sortBy=createdAt&sortOrder=desc
```
### Get Post
```http
GET /api/posts/{slug}
```
### Update Post
```http
PUT /api/posts/{slug}
Content-Type: application/json
```
### Delete Post
```http
DELETE /api/posts/{slug}
```
---
## Media
### Upload
```http
POST /api/media
Content-Type: multipart/form-data
```
| Field | Type | Required |
|-------|------|----------|
| `file` | File | β
|
| `alt` | string | β |
Allowed types: JPEG, PNG, GIF, WebP, SVG, PDF. Max size: 10MB.
### List Media
```http
GET /api/media
```
---
## Themes
### List Themes
```http
GET /api/themes
```
### Switch Theme
```http
PUT /api/themes
Content-Type: application/json
```
```json
{ "theme": "brutalism" }
```
Available: `minimalism`, `brutalism`, `constructivism`, `swiss`, `editorial`, `hand-drawn`, `retro`, `flat`, `bento`, `glassmorphism`
---
## Analytics
```http
GET /api/analytics
```
Returns: `totalPosts`, `totalViews`, `totalDrafts`, `topPosts`, `postsByMonth`
---
## Settings
### Get Settings
```http
GET /api/settings
```
### Update Settings
```http
PUT /api/settings
Content-Type: application/json
```
```json
{
"blogName": "My AI Blog",
"blogDescription": "Powered by intelligence",
"postsPerPage": 10
}
```
```
### references/theme-guide.md
```markdown
# Write My Blog β Theme Guide
## Switching Themes
```bash
curl -X PUT http://localhost:3000/api/themes \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_KEY" \
-d '{"theme": "glassmorphism"}'
```
## Available Themes
### 1. Minimalism
Clean, purposeful design with generous whitespace and monochrome palette.
- **Fonts**: Inter
- **Colors**: Black/white with subtle grays
- **Best for**: Professional blogs, portfolios
### 2. Brutalism
Bold, attention-grabbing with jarring color combos and hard shadows.
- **Fonts**: Space Grotesk, Space Mono
- **Colors**: Orange accent on cream
- **Best for**: Creative/art blogs, counterculture
### 3. Constructivism
Geometric, energetic layouts with strong red accents.
- **Fonts**: Oswald, Source Sans
- **Colors**: Red/black on parchment
- **Best for**: Design blogs, political commentary
### 4. Swiss Style
Grid-based precision with Helvetica typography.
- **Fonts**: Helvetica Neue
- **Colors**: Red accent on white
- **Best for**: Architecture, design firms
### 5. Editorial
Magazine-inspired with serif typography and layered compositions.
- **Fonts**: Playfair Display, Source Serif
- **Colors**: Deep red accent, warm tones
- **Best for**: Long-form content, journalism
### 6. Hand-Drawn
Casual, crafty aesthetic with handwritten fonts and sketchy accents.
- **Fonts**: Caveat, Patrick Hand
- **Colors**: Warm yellows, orange
- **Best for**: Personal blogs, crafts
### 7. Retro
Vintage dark theme with warm colors and grainy textures.
- **Fonts**: Bungee Shade, VT323
- **Colors**: Orange on dark brown
- **Best for**: Nostalgia, gaming, culture
### 8. Flat
Zero depth, solid colors, clean hierarchy.
- **Fonts**: Roboto, Open Sans
- **Colors**: Blue accent on light gray
- **Best for**: Tech blogs, startups
### 9. Bento
Apple-inspired rounded blocks in compact grid layout.
- **Fonts**: SF Pro (system), Helvetica Neue
- **Colors**: Blue accent on light gray
- **Best for**: Portfolio, showcase, product blogs
### 10. Glassmorphism
Frosted glass effects with backdrop-blur on dark gradient.
- **Fonts**: Outfit, Inter
- **Colors**: Purple gradient on dark blue
- **Best for**: Modern/premium, tech, crypto
## CSS Custom Properties
All themes use these CSS variables that you can override with `customCss` in settings:
```css
--bg-primary /* Main background */
--bg-secondary /* Card/section background */
--bg-accent /* Accent background */
--text-primary /* Main text color */
--text-secondary /* Secondary text */
--text-muted /* Muted/meta text */
--accent /* Primary accent */
--accent-hover /* Accent hover state */
--border /* Border color */
--shadow /* Box shadow */
--radius /* Border radius */
--font-heading /* Heading font family */
--font-body /* Body font family */
--font-mono /* Monospace font family */
--max-width /* Content max width */
--spacing-unit /* Base spacing */
--transition /* Default transition */
```
```
### scripts/deploy-cloudflare.sh
```bash
#!/usr/bin/env bash
# βββ Deploy to Cloudflare βββ
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLATFORM_DIR="$(dirname "$SCRIPT_DIR")/platform"
echo "π Deploying Write My Blog to Cloudflare..."
cd "$PLATFORM_DIR"
# Check if wrangler CLI is installed
if ! command -v wrangler &> /dev/null; then
echo "β Wrangler CLI not found. Install with: npm i -g wrangler"
exit 1
fi
# Build first
echo "π¦ Building..."
npm run build
# Deploy
echo "βοΈ Deploying to Cloudflare Workers..."
wrangler deploy
echo "β
Deployed to Cloudflare!"
```
### scripts/deploy-vercel.sh
```bash
#!/usr/bin/env bash
# βββ Deploy to Vercel βββ
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLATFORM_DIR="$(dirname "$SCRIPT_DIR")/platform"
echo "π Deploying Write My Blog to Vercel..."
cd "$PLATFORM_DIR"
# Check if vercel CLI is installed
if ! command -v vercel &> /dev/null; then
echo "β Vercel CLI not found. Install with: npm i -g vercel"
exit 1
fi
# Build first
echo "π¦ Building..."
npm run build
# Deploy
echo "π Deploying..."
vercel --prod
echo "β
Deployed to Vercel!"
```
### scripts/migrate.sh
```bash
#!/usr/bin/env bash
# βββ Database Migration βββ
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLATFORM_DIR="$(dirname "$SCRIPT_DIR")/platform"
echo "ποΈ Running database migrations..."
cd "$PLATFORM_DIR"
# Load env vars
if [ -f .env.local ]; then
export $(grep -v '^#' .env.local | xargs)
fi
# Run migration via Node
node -e "
const { getDatabase } = require('./src/lib/db/index.ts');
(async () => {
const db = await getDatabase();
await db.migrate();
console.log('β
Migration complete');
await db.close();
})().catch(err => {
console.error('β Migration failed:', err.message);
process.exit(1);
});
"
echo "β
Migrations complete"
```
### scripts/setup.sh
```bash
#!/usr/bin/env bash
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Write My Blog β Unified Setup Script
# Works in two modes:
# 1) Interactive β human runs it, gets prompted for everything
# 2) Non-interactive (agent) β pass flags or env vars, zero prompts
#
# Usage:
# Interactive: ./scripts/setup.sh
# Agent/CI: ./scripts/setup.sh --non-interactive \
# --db sqlite --cache memory --theme minimalism
#
# All flags also accept environment variable equivalents:
# SETUP_DB_PROVIDER, SETUP_CACHE_PROVIDER, SETUP_SUPABASE_URL,
# SETUP_SUPABASE_KEY, SETUP_REDIS_URL, SETUP_THEME, SETUP_BLOG_NAME,
# SETUP_BLOG_DESCRIPTION, SETUP_API_KEY
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
PLATFORM_DIR="$PROJECT_ROOT/platform"
# ββ Defaults ββ
INTERACTIVE=true
DB_PROVIDER="${SETUP_DB_PROVIDER:-}"
CACHE_PROVIDER="${SETUP_CACHE_PROVIDER:-}"
SUPABASE_URL="${SETUP_SUPABASE_URL:-}"
SUPABASE_KEY="${SETUP_SUPABASE_KEY:-}"
MONGODB_URI="${SETUP_MONGODB_URI:-}"
MONGODB_DB_NAME="${SETUP_MONGODB_DB_NAME:-blog}"
REDIS_URL="${SETUP_REDIS_URL:-}"
THEME="${SETUP_THEME:-minimalism}"
BLOG_NAME="${SETUP_BLOG_NAME:-My Blog}"
BLOG_DESC="${SETUP_BLOG_DESCRIPTION:-A blog powered by AI}"
API_KEY="${SETUP_API_KEY:-}"
SKIP_INSTALL="${SETUP_SKIP_INSTALL:-false}"
# ββ Parse CLI flags ββ
while [[ $# -gt 0 ]]; do
case "$1" in
--non-interactive|-n) INTERACTIVE=false ;;
--db) DB_PROVIDER="$2"; shift ;;
--cache) CACHE_PROVIDER="$2"; shift ;;
--supabase-url) SUPABASE_URL="$2"; shift ;;
--supabase-key) SUPABASE_KEY="$2"; shift ;;
--mongodb-uri) MONGODB_URI="$2"; shift ;;
--mongodb-db) MONGODB_DB_NAME="$2"; shift ;;
--redis-url) REDIS_URL="$2"; shift ;;
--theme) THEME="$2"; shift ;;
--blog-name) BLOG_NAME="$2"; shift ;;
--blog-desc) BLOG_DESC="$2"; shift ;;
--api-key) API_KEY="$2"; shift ;;
--deploy) DEPLOY_TARGET="$2"; shift ;;
--skip-install) SKIP_INSTALL=true ;;
--help|-h)
echo "Usage: ./scripts/setup.sh [options]"
echo ""
echo "Options:"
echo " -n, --non-interactive Skip all prompts (for agents / CI)"
echo " --db <provider> Database: sqlite (default), supabase, postgres"
echo " --cache <provider> Cache: memory (default), redis"
echo " --supabase-url <url> Supabase project URL"
echo " --supabase-key <key> Supabase service role key"
echo " --mongodb-uri <uri> MongoDB connection URI"
echo " --mongodb-db <name> MongoDB database name (default: blog)"
echo " --redis-url <url> Redis connection URL"
echo " --theme <name> Default theme (default: minimalism)"
echo " --blog-name <name> Blog display name"
echo " --blog-desc <text> Blog subtitle / description"
echo " --api-key <key> Set a specific API key (auto-generated if omitted)"
echo " --deploy <target> Deploy target: vercel, cloudflare, none"
echo " --skip-install Skip npm install"
echo " -h, --help Show this help"
echo ""
echo "Environment variables (same as flags):"
echo " SETUP_DB_PROVIDER, SETUP_CACHE_PROVIDER, SETUP_SUPABASE_URL,"
echo " SETUP_SUPABASE_KEY, SETUP_REDIS_URL, SETUP_THEME, SETUP_BLOG_NAME,"
echo " SETUP_BLOG_DESCRIPTION, SETUP_API_KEY, SETUP_DEPLOY_TARGET,"
echo " SETUP_SKIP_INSTALL"
exit 0
;;
*) echo "Unknown option: $1 (use --help)"; exit 1 ;;
esac
shift
done
# ββ Helpers ββ
ask() {
# ask <var_name> <prompt> <default>
local var_name="$1" prompt="$2" default="${3:-}"
if $INTERACTIVE; then
local current="${!var_name:-$default}"
read -rp "$prompt [$current]: " input
eval "$var_name=\"${input:-$current}\""
else
if [ -z "${!var_name:-}" ]; then
eval "$var_name=\"$default\""
fi
fi
}
ask_secret() {
# ask_secret <var_name> <prompt>
local var_name="$1" prompt="$2"
if $INTERACTIVE; then
read -rsp "$prompt: " input
echo ""
eval "$var_name=\"$input\""
fi
# In non-interactive mode, the value must be set via flag/env
}
choose() {
# choose <var_name> <prompt> <option1> <option2> ...
local var_name="$1" prompt="$2"
shift 2
local options=("$@")
local default="${options[0]}"
if $INTERACTIVE; then
echo ""
echo "$prompt"
local i=1
for opt in "${options[@]}"; do
local label="$opt"
[ "$opt" = "sqlite" ] && label="sqlite (local, zero config)"
[ "$opt" = "supabase" ] && label="supabase (managed Postgres + Storage)"
[ "$opt" = "mongodb" ] && label="mongodb (MongoDB Atlas β free tier available)"
[ "$opt" = "memory" ] && label="memory (in-process, no setup)"
[ "$opt" = "redis" ] && label="redis (Redis / Upstash)"
echo " $i) $label"
((i++))
done
read -rp "Choice [1]: " choice
choice="${choice:-1}"
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ]; then
eval "$var_name=\"${options[$((choice - 1))]}\""
else
eval "$var_name=\"$default\""
fi
else
if [ -z "${!var_name:-}" ]; then
eval "$var_name=\"$default\""
fi
fi
}
# βββββββββββββββββββββββββββββββββββββββ
# 1. Banner
# βββββββββββββββββββββββββββββββββββββββ
echo ""
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
echo " β ποΈ Write My Blog β Setup β"
if $INTERACTIVE; then
echo " β mode: interactive β"
else
echo " β mode: non-interactive (agent / CI) β"
fi
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
# βββββββββββββββββββββββββββββββββββββββ
# 2. Install dependencies
# βββββββββββββββββββββββββββββββββββββββ
if [ "$SKIP_INSTALL" = "true" ]; then
echo "βοΈ Skipping npm install (--skip-install)"
else
echo "π¦ Installing dependencies..."
cd "$PLATFORM_DIR"
npm install --loglevel=error
echo "β
Dependencies installed"
fi
echo ""
# βββββββββββββββββββββββββββββββββββββββ
# 3. Configure environment
# βββββββββββββββββββββββββββββββββββββββ
ENV_FILE="$PLATFORM_DIR/.env.local"
if [ -f "$ENV_FILE" ] && $INTERACTIVE; then
echo "β οΈ .env.local already exists."
read -rp "Overwrite? (y/N): " overwrite
if [[ ! "$overwrite" =~ ^[Yy] ]]; then
echo "β©οΈ Keeping existing .env.local"
SKIP_ENV=true
else
SKIP_ENV=false
fi
elif [ -f "$ENV_FILE" ] && ! $INTERACTIVE; then
echo "β οΈ .env.local already exists β overwriting (non-interactive mode)"
SKIP_ENV=false
else
SKIP_ENV=false
fi
if [ "${SKIP_ENV:-false}" = "false" ]; then
# ββ API Key ββ
if [ -z "$API_KEY" ]; then
API_KEY=$(openssl rand -hex 24 2>/dev/null || cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 48 | head -n 1)
fi
# ββ Blog identity ββ
if $INTERACTIVE; then
echo ""
echo "ββ Blog Identity ββ"
ask BLOG_NAME "Blog name" "$BLOG_NAME"
ask BLOG_DESC "Blog description" "$BLOG_DESC"
echo ""
echo "ββ Theme ββ"
echo "Available: minimalism, brutalism, constructivism, swiss, editorial,"
echo " hand-drawn, retro, flat, bento, glassmorphism"
ask THEME "Default theme" "$THEME"
fi
# ββ Database ββ
choose DB_PROVIDER "ββ Database provider ββ" "sqlite" "supabase" "mongodb" "postgres"
if [ "$DB_PROVIDER" = "supabase" ] || [ "$DB_PROVIDER" = "postgres" ]; then
if [ -z "$SUPABASE_URL" ]; then
ask SUPABASE_URL "Supabase project URL" ""
fi
if [ -z "$SUPABASE_KEY" ]; then
if $INTERACTIVE; then
ask_secret SUPABASE_KEY "Supabase service role key"
fi
fi
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_KEY" ]; then
echo "β Supabase URL and key are required for supabase/postgres provider"
exit 1
fi
fi
if [ "$DB_PROVIDER" = "mongodb" ]; then
echo ""
echo " ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo " β MongoDB Atlas Setup β"
echo " β 1. Go to mongodb.com/cloud/atlas β"
echo " β 2. Create a free cluster (M0 tier) β"
echo " β 3. Create a database user with read/write access β"
echo " β 4. Allow network access (0.0.0.0/0 for serverless) β"
echo " β 5. Get connection string from Connect > Drivers β"
echo " ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
if [ -z "$MONGODB_URI" ]; then
echo "Paste your full Atlas connection string:"
echo " (e.g. mongodb+srv://user:[email protected])"
ask MONGODB_URI "MongoDB Atlas URI" ""
fi
if [ -z "$MONGODB_URI" ]; then
echo "β MongoDB Atlas URI is required"
exit 1
fi
ask MONGODB_DB_NAME "Database name" "$MONGODB_DB_NAME"
fi
# ββ Cache ββ
choose CACHE_PROVIDER "ββ Cache provider ββ" "memory" "redis"
if [ "$CACHE_PROVIDER" = "redis" ]; then
ask REDIS_URL "Redis URL" "redis://localhost:6379"
fi
# ββ Write .env.local ββ
cat > "$ENV_FILE" <<EOF
# ββ Write My Blog β Generated $(date +%Y-%m-%d) ββ
# Blog
BLOG_NAME=$BLOG_NAME
BLOG_DESCRIPTION=$BLOG_DESC
DEFAULT_THEME=$THEME
# Security
API_KEY=$API_KEY
RATE_LIMIT_RPM=100
# Database ($DB_PROVIDER)
DATABASE_PROVIDER=$DB_PROVIDER
SQLITE_PATH=./data/blog.db
EOF
if [ "$DB_PROVIDER" = "supabase" ] || [ "$DB_PROVIDER" = "postgres" ]; then
cat >> "$ENV_FILE" <<EOF
SUPABASE_URL=$SUPABASE_URL
SUPABASE_SERVICE_KEY=$SUPABASE_KEY
EOF
fi
if [ "$DB_PROVIDER" = "mongodb" ]; then
cat >> "$ENV_FILE" <<EOF
MONGODB_URI=$MONGODB_URI
MONGODB_DB_NAME=$MONGODB_DB_NAME
EOF
fi
cat >> "$ENV_FILE" <<EOF
# Cache ($CACHE_PROVIDER)
CACHE_PROVIDER=$CACHE_PROVIDER
CACHE_MAX_SIZE=500
EOF
if [ "$CACHE_PROVIDER" = "redis" ]; then
echo "REDIS_URL=$REDIS_URL" >> "$ENV_FILE"
fi
cat >> "$ENV_FILE" <<EOF
# Media
MEDIA_DIR=./public/uploads
EOF
echo ""
echo "β
.env.local created"
fi
# βββββββββββββββββββββββββββββββββββββββ
# 4. Create directories
# βββββββββββββββββββββββββββββββββββββββ
mkdir -p "$PLATFORM_DIR/data"
mkdir -p "$PLATFORM_DIR/public/uploads"
echo "π Data directories ready"
echo ""
# βββββββββββββββββββββββββββββββββββββββ
# 5. Build verification
# βββββββββββββββββββββββββββββββββββββββ
echo "π¨ Running production build to verify everything..."
cd "$PLATFORM_DIR"
if npx next build > /dev/null 2>&1; then
echo "β
Build passed"
else
echo "β οΈ Build had warnings (may still work β check output with 'npx next build')"
fi
echo ""
# βββββββββββββββββββββββββββββββββββββββ
# 6. Deployment (optional)
# βββββββββββββββββββββββββββββββββββββββ
DEPLOY_TARGET="${SETUP_DEPLOY_TARGET:-none}"
if $INTERACTIVE; then
echo ""
echo "ββ Deploy ββ"
echo " 1) Skip β just run locally"
echo " 2) Vercel (recommended for quick deploy)"
echo " 3) Cloudflare Pages"
read -rp "Choice [1]: " deploy_choice
deploy_choice="${deploy_choice:-1}"
case "$deploy_choice" in
2) DEPLOY_TARGET="vercel" ;;
3) DEPLOY_TARGET="cloudflare" ;;
*) DEPLOY_TARGET="none" ;;
esac
fi
# ββ Guard: SQLite can't run on Vercel/Cloudflare (native C module) ββ
if [ "$DEPLOY_TARGET" != "none" ] && [ "$DB_PROVIDER" = "sqlite" ]; then
echo ""
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo " β β οΈ SQLite uses a native C module (better-sqlite3) β"
echo " β which does NOT work on Vercel or Cloudflare serverless. β"
echo " β β"
echo " β You need a cloud database for deployment. β"
echo " β Free options: β"
echo " β β’ Supabase β supabase.com/dashboard β"
echo " β β’ MongoDB Atlas β mongodb.com/cloud/atlas β"
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
if $INTERACTIVE; then
echo " 1) Switch to Supabase (enter credentials now)"
echo " 2) Switch to MongoDB Atlas (enter credentials now)"
echo " 3) Skip deploy β I'll set up a cloud DB later"
read -rp "Choice [1]: " sqlite_fix
sqlite_fix="${sqlite_fix:-1}"
if [ "$sqlite_fix" = "1" ]; then
DB_PROVIDER="supabase"
read -rp "Supabase project URL: " SUPABASE_URL
read -rsp "Supabase service role key: " SUPABASE_KEY
echo ""
if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_KEY" ]; then
echo "β Both URL and key are required. Skipping deploy."
DEPLOY_TARGET="none"
else
sed -i "s/^DATABASE_PROVIDER=.*/DATABASE_PROVIDER=supabase/" "$ENV_FILE"
if ! grep -q "SUPABASE_URL" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# Supabase (added for cloud deploy)" >> "$ENV_FILE"
echo "SUPABASE_URL=$SUPABASE_URL" >> "$ENV_FILE"
echo "SUPABASE_SERVICE_KEY=$SUPABASE_KEY" >> "$ENV_FILE"
else
sed -i "s|^SUPABASE_URL=.*|SUPABASE_URL=$SUPABASE_URL|" "$ENV_FILE"
sed -i "s|^SUPABASE_SERVICE_KEY=.*|SUPABASE_SERVICE_KEY=$SUPABASE_KEY|" "$ENV_FILE"
fi
echo "β
Switched to Supabase. .env.local updated."
echo ""
echo "π¨ Rebuilding..."
cd "$PLATFORM_DIR"
npx next build > /dev/null 2>&1 && echo "β
Build passed" || echo "β οΈ Build had issues"
fi
elif [ "$sqlite_fix" = "2" ]; then
DB_PROVIDER="mongodb"
echo ""
echo "Paste your MongoDB Atlas connection string:"
echo " (e.g. mongodb+srv://user:[email protected])"
read -rp "MongoDB Atlas URI: " MONGODB_URI
read -rp "Database name [blog]: " MONGODB_DB_NAME
MONGODB_DB_NAME="${MONGODB_DB_NAME:-blog}"
if [ -z "$MONGODB_URI" ]; then
echo "β Atlas URI is required. Skipping deploy."
DEPLOY_TARGET="none"
else
sed -i "s/^DATABASE_PROVIDER=.*/DATABASE_PROVIDER=mongodb/" "$ENV_FILE"
if ! grep -q "MONGODB_URI" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# MongoDB Atlas (added for cloud deploy)" >> "$ENV_FILE"
echo "MONGODB_URI=$MONGODB_URI" >> "$ENV_FILE"
echo "MONGODB_DB_NAME=$MONGODB_DB_NAME" >> "$ENV_FILE"
else
sed -i "s|^MONGODB_URI=.*|MONGODB_URI=$MONGODB_URI|" "$ENV_FILE"
sed -i "s|^MONGODB_DB_NAME=.*|MONGODB_DB_NAME=$MONGODB_DB_NAME|" "$ENV_FILE"
fi
echo "β
Switched to MongoDB Atlas. .env.local updated."
echo ""
echo "π¨ Rebuilding..."
cd "$PLATFORM_DIR"
npx next build > /dev/null 2>&1 && echo "β
Build passed" || echo "β οΈ Build had issues"
fi
else
echo "βοΈ Skipping deploy. Set up a cloud DB and redeploy later:"
echo " 1. Create a free Supabase or MongoDB Atlas account"
echo " 2. Update .env.local with the credentials"
echo " 3. Change DATABASE_PROVIDER to supabase or mongodb"
echo " 4. Run: cd platform && npx vercel --prod"
DEPLOY_TARGET="none"
fi
else
echo "β Cannot deploy with SQLite to serverless. Set SETUP_DB_PROVIDER=supabase or mongodb"
DEPLOY_TARGET="none"
fi
fi
# ββ Vercel Deploy ββ
if [ "$DEPLOY_TARGET" = "vercel" ]; then
echo ""
echo "π Deploying to Vercel..."
echo ""
# Step 1: Install Vercel CLI if missing
if ! npx -y vercel --version &>/dev/null; then
echo "π¦ Installing Vercel CLI..."
npm install -g vercel
fi
echo " Vercel CLI: $(npx -y vercel --version 2>/dev/null)"
echo ""
cd "$PLATFORM_DIR"
if $INTERACTIVE; then
# Step 2: Check if logged in, if not β login first
echo "ββ Step 1: Vercel Login ββ"
echo "Checking authentication..."
if ! npx -y vercel whoami &>/dev/null 2>&1; then
echo ""
echo "You need to log in to Vercel first."
echo "Options:"
echo " 1) Log in via browser (opens vercel.com)"
echo " 2) Log in with email"
echo " 3) Log in with GitHub"
read -rp "Choice [1]: " login_method
login_method="${login_method:-1}"
case "$login_method" in
2) npx -y vercel login --email ;;
3) npx -y vercel login --github ;;
*) npx -y vercel login ;;
esac
# Verify login succeeded
if ! npx -y vercel whoami &>/dev/null 2>&1; then
echo "β Login failed. You can deploy later with:"
echo " cd platform && npx vercel login && npx vercel --prod"
DEPLOY_TARGET="none"
else
echo "β
Logged in as: $(npx -y vercel whoami 2>/dev/null)"
fi
else
echo "β
Already logged in as: $(npx -y vercel whoami 2>/dev/null)"
fi
# Step 3: Link project and deploy
if [ "$DEPLOY_TARGET" = "vercel" ]; then
echo ""
echo "ββ Step 2: Project Setup & Deploy ββ"
echo ""
read -rp "Deploy to production? (Y/n): " prod_deploy
prod_deploy="${prod_deploy:-y}"
if [[ "$prod_deploy" =~ ^[Yy] ]]; then
echo ""
echo "Deploying to production..."
if npx -y vercel --prod; then
echo ""
echo "β
Deployed to Vercel (production)!"
else
echo ""
echo "β οΈ Deploy failed. You can retry with:"
echo " cd platform && npx vercel --prod"
fi
else
echo ""
echo "Deploying preview..."
if npx -y vercel; then
echo ""
echo "β
Preview deployed to Vercel!"
else
echo ""
echo "β οΈ Deploy failed. You can retry with:"
echo " cd platform && npx vercel"
fi
fi
fi
else
# Non-interactive β requires VERCEL_TOKEN
if [ -n "${VERCEL_TOKEN:-}" ]; then
echo "Deploying with token (non-interactive)..."
if npx -y vercel --token "$VERCEL_TOKEN" --yes --prod 2>&1; then
echo "β
Deployed to Vercel (production)"
else
echo "β Deploy failed. Check VERCEL_TOKEN and project settings."
fi
else
echo "β οΈ VERCEL_TOKEN not set β skipping automated deploy"
echo " Set VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_PROJECT_ID env vars"
echo " Then run: cd platform && npx vercel --token \$VERCEL_TOKEN --yes --prod"
DEPLOY_TARGET="none"
fi
fi
# ββ Cloudflare Deploy ββ
elif [ "$DEPLOY_TARGET" = "cloudflare" ]; then
echo ""
echo "π Deploying to Cloudflare Pages..."
echo ""
# Step 1: Install Wrangler CLI if missing
if ! npx -y wrangler --version &>/dev/null; then
echo "π¦ Installing Wrangler CLI..."
npm install -g wrangler
fi
echo " Wrangler CLI: $(npx -y wrangler --version 2>/dev/null)"
echo ""
cd "$PLATFORM_DIR"
if $INTERACTIVE; then
# Step 2: Check if logged in
echo "ββ Step 1: Cloudflare Login ββ"
echo "Checking authentication..."
if ! npx -y wrangler whoami &>/dev/null 2>&1; then
echo ""
echo "You need to log in to Cloudflare first."
echo "This will open your browser for OAuth login."
read -rp "Press Enter to continue (or Ctrl+C to skip)..." _
npx -y wrangler login
if ! npx -y wrangler whoami &>/dev/null 2>&1; then
echo "β Login failed. You can deploy later with:"
echo " cd platform && npx wrangler login && npx wrangler pages deploy"
DEPLOY_TARGET="none"
else
echo "β
Logged in to Cloudflare"
fi
else
echo "β
Already logged in to Cloudflare"
fi
# Step 3: Deploy
if [ "$DEPLOY_TARGET" = "cloudflare" ]; then
echo ""
echo "ββ Step 2: Build & Deploy ββ"
# Sanitize project name (lowercase, hyphens only)
CF_PROJECT=$(echo "${BLOG_NAME}" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
echo "Project name: $CF_PROJECT"
read -rp "Change project name? (press Enter to keep): " cf_name
[ -n "$cf_name" ] && CF_PROJECT="$cf_name"
echo ""
echo "Building for Cloudflare Pages..."
if npx -y @cloudflare/next-on-pages 2>&1; then
echo ""
echo "Deploying to Cloudflare Pages..."
if npx -y wrangler pages deploy .vercel/output/static --project-name "$CF_PROJECT" 2>&1; then
echo ""
echo "β
Deployed to Cloudflare Pages!"
echo " Project: $CF_PROJECT"
else
echo ""
echo "β οΈ Deploy failed. You can retry with:"
echo " cd platform && npx wrangler pages deploy .vercel/output/static --project-name \"$CF_PROJECT\""
fi
else
echo "β Build for Cloudflare failed."
echo " Try: cd platform && npx @cloudflare/next-on-pages"
fi
fi
else
# Non-interactive β requires CLOUDFLARE_API_TOKEN
if [ -n "${CLOUDFLARE_API_TOKEN:-}" ]; then
CF_PROJECT=$(echo "${BLOG_NAME}" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
echo "Building for Cloudflare Pages (project: $CF_PROJECT)..."
if CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" npx -y @cloudflare/next-on-pages 2>&1; then
if CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" npx -y wrangler pages deploy \
.vercel/output/static --project-name "$CF_PROJECT" --commit-dirty=true 2>&1; then
echo "β
Deployed to Cloudflare Pages"
else
echo "β Deploy failed. Check CLOUDFLARE_API_TOKEN."
fi
else
echo "β Build for Cloudflare failed."
fi
else
echo "β οΈ CLOUDFLARE_API_TOKEN not set β skipping automated deploy"
echo " Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID env vars"
echo " Then run: cd platform && npx @cloudflare/next-on-pages && npx wrangler pages deploy .vercel/output/static"
DEPLOY_TARGET="none"
fi
fi
else
echo "βοΈ Skipping deployment (run locally with 'cd platform && npm run dev')"
fi
echo ""
# βββββββββββββββββββββββββββββββββββββββ
# 7. Summary
# βββββββββββββββββββββββββββββββββββββββ
echo " ββββββββββββββββββββββββββββββββββββββββββββββββ"
echo " β β
Setup Complete! β"
echo " β βββββββββββββββββββββββββββββββββββββββββββββββ£"
echo " β β"
printf " β Blog: %-33sβ\n" "$BLOG_NAME"
printf " β Theme: %-33sβ\n" "$THEME"
printf " β DB: %-33sβ\n" "$DB_PROVIDER"
printf " β Cache: %-33sβ\n" "$CACHE_PROVIDER"
printf " β Deploy: %-33sβ\n" "$DEPLOY_TARGET"
echo " β β"
printf " β API Key: %-33sβ\n" "${API_KEY:0:12}..."
echo " β β οΈ Save this key for API calls! β"
echo " β β"
echo " β Start: cd platform && npm run dev β"
echo " β Visit: http://localhost:3000 β"
echo " β β"
echo " ββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
# ββ Output config as JSON for agents to parse ββ
if ! $INTERACTIVE; then
echo "SETUP_RESULT_JSON={\"apiKey\":\"$API_KEY\",\"dbProvider\":\"$DB_PROVIDER\",\"cacheProvider\":\"$CACHE_PROVIDER\",\"theme\":\"$THEME\",\"blogName\":\"$BLOG_NAME\",\"deployTarget\":\"$DEPLOY_TARGET\"}"
fi
```