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
```