- Build static blog từ Markdown bằng bash + curl + Claude API, không cần Hugo/Jekyll.
- Claude generate meta description, summary, related posts tự động.
- Output HTML deploy thẳng lên nginx VPS, không cần Node.js runtime.
- Cost: 1 USD Claude API đủ build 500 bài post.
- Phù hợp dev solo muốn full control, không bị lock framework.
Bạn viết blog Markdown, muốn deploy lên VPS riêng nhưng ngại Hugo (Go), Jekyll (Ruby), Astro (Node). Mỗi cái có quirk riêng, dependency hell. Bash + Claude API là combo minimalist: 200 dòng script làm SSG nhỏ, AI tự generate metadata, không cần framework.
Cách này phù hợp dev solo muốn personal blog plain HTML, hoặc startup muốn landing page tĩnh nhanh. Output là HTML thuần, deploy lên nginx VPS 2GB serve được hàng triệu request/ngày không vấn đề.
Bài này hướng dẫn viết full script: parse Markdown, gọi Claude generate meta/excerpt, render HTML từ template, build sitemap, deploy nginx. Test trên Cloud VPS TND 2GB Debian 12.
1. Cấu trúc thư mục project
my-blog/
├── posts/
│ ├── 2026-05-23-hello-world.md
│ └── 2026-05-30-tips-vps.md
├── templates/
│ ├── post.html
│ ├── index.html
│ └── style.css
├── build.sh
├── deploy.sh
└── dist/ (output)
├── index.html
├── posts/
│ ├── hello-world/index.html
│ └── tips-vps/index.html
├── sitemap.xml
└── style.css2. Cài dependency
# Markdown parser
sudo apt install -y pandoc jq curl
# Claude API key
export ANTHROPIC_API_KEY=sk-ant-xxx
# Verify
pandoc --version
jq --version3. Viết script build.sh - parse Markdown
#!/bin/bash
# build.sh
set -e
SRC="posts"
DIST="dist"
TEMPLATE="templates"
rm -rf $DIST
mkdir -p $DIST/posts
# Copy static assets
cp $TEMPLATE/style.css $DIST/
# Loop posts
for md in $SRC/*.md; do
filename=$(basename "$md" .md)
# Parse frontmatter: 2026-05-23-hello-world.md -> date + slug
date=$(echo $filename | cut -d'-' -f1-3)
slug=$(echo $filename | cut -d'-' -f4-)
echo "Processing $slug..."
# Convert MD to HTML body
body=$(pandoc "$md" -f markdown -t html)
# Extract title (first H1)
title=$(head -1 "$md" | sed 's/^# //')
# Call Claude to generate meta description
excerpt=$(./scripts/claude-meta.sh "$md")
# Render từ template
mkdir -p "$DIST/posts/$slug"
sed -e "s/{{TITLE}}/$title/g"
-e "s/{{DATE}}/$date/g"
-e "s/{{EXCERPT}}/$excerpt/g"
$TEMPLATE/post.html |
sed "s|{{BODY}}|$body|g"
> "$DIST/posts/$slug/index.html"
done4. Script claude-meta.sh - gọi Claude API
#!/bin/bash
# scripts/claude-meta.sh
# Input: file markdown
# Output: meta description 155 char
MD_FILE=$1
CONTENT=$(cat "$MD_FILE")
PROMPT="Đọc bài blog markdown sau và viết 1 meta description SEO tiếng Việt,
chính xác 150-155 ký tự, mời gọi click, không tag, trả về plain text:
---
$CONTENT
---"
curl -s https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHROPIC_API_KEY"
-H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d "$(jq -n --arg prompt "$PROMPT" '{
model: "claude-3-5-haiku-20241022",
max_tokens: 200,
messages: [{role: "user", content: $prompt}]
}')"
| jq -r '.content[0].text'
| tr -d 'n'
| head -c 155Dùng Claude Haiku rẻ nhất (~0.25 USD/1M token). Mỗi bài 500 từ -> ~700 token -> ~0.0002 USD. 500 bài chỉ tốn 0.1 USD.
5. Template post.html
{{TITLE}} - My Blog
{{TITLE}}
{{BODY}}
← Back to home
6. Build index.html với danh sách post
# Thêm vào build.sh sau loop
echo "Building index..."
cat > $DIST/index.html <<'HTML'
My Blog
Blog của tôi
HTML
# Sort by date desc
for md in $(ls -r $SRC/*.md); do
filename=$(basename "$md" .md)
date=$(echo $filename | cut -d'-' -f1-3)
slug=$(echo $filename | cut -d'-' -f4-)
title=$(head -1 "$md" | sed 's/^# //')
echo "- $title -
" >> $DIST/index.html
done
cat >> $DIST/index.html <<'HTML'
HTML7. Tạo sitemap.xml tự động
# Thêm vào build.sh
echo "Generating sitemap..."
cat > $DIST/sitemap.xml <<'XML'
XML
for md in $SRC/*.md; do
filename=$(basename "$md" .md)
date=$(echo $filename | cut -d'-' -f1-3)
slug=$(echo $filename | cut -d'-' -f4-)
echo " https://blog.com/posts/$slug/ $date " >> $DIST/sitemap.xml
done
echo " " >> $DIST/sitemap.xml8. Claude generate related posts (advanced)
# scripts/claude-related.sh
# Tìm 3 post liên quan dựa trên semantic similarity
MD_FILE=$1
ALL_TITLES=$(ls posts/*.md | xargs -I{} basename {} .md | cut -d'-' -f4- | tr 'n' ',')
PROMPT="Bài blog hiện tại: $(head -50 $MD_FILE)
Danh sách tất cả slug: $ALL_TITLES
Chọn 3 slug phù hợp nhất làm related posts, trả về JSON array: ["slug1", "slug2", "slug3"]"
curl -s https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHROPIC_API_KEY"
-H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d "$(jq -n --arg p "$PROMPT" '{
model: "claude-3-5-haiku-20241022",
max_tokens: 200,
messages: [{role: "user", content: $p}]
}')"
| jq -r '.content[0].text'9. Deploy script lên nginx VPS
# deploy.sh
#!/bin/bash
set -e
VPS="user@vps-ip"
REMOTE_DIR="/var/www/blog"
./build.sh
echo "Deploying to $VPS:$REMOTE_DIR..."
rsync -avz --delete dist/ $VPS:$REMOTE_DIR/
ssh $VPS "sudo systemctl reload nginx"
echo "Done!"10. Nginx config cho static blog
# /etc/nginx/sites-available/blog
server {
listen 80;
server_name blog.com www.blog.com;
root /var/www/blog;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Cache static asset 1 năm
location ~* .(css|js|jpg|png|webp|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip
gzip on;
gzip_types text/css text/html application/json;
}
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# SSL với certbot
sudo certbot --nginx -d blog.com -d www.blog.com11. Auto rebuild khi commit git
# Trên VPS, tạo webhook receive Git push
sudo apt install -y webhook
# /etc/webhook.conf
[
{
"id": "rebuild-blog",
"execute-command": "/var/www/build-trigger.sh",
"command-working-directory": "/var/www/blog-src"
}
]
# /var/www/build-trigger.sh
#!/bin/bash
cd /var/www/blog-src
git pull origin main
./build.sh
rsync -avz --delete dist/ /var/www/blog/
sudo systemctl reload nginx
# Start webhook
sudo systemctl enable --now webhook
# GitHub repo Settings -> Webhooks
# URL: https://blog.com:9000/hooks/rebuild-blog
# Trigger: push event12. So sánh với SSG framework
| Tiêu chí | Bash + Claude | Hugo | Astro |
|---|---|---|---|
| Setup time | 30 phút | 1 giờ | 2 giờ |
| Dependency | Bash, pandoc, jq | Go binary | Node 100MB |
| Build speed (100 post) | 2 phút | 1 giây | 30 giây |
| Customization | Toàn quyền sửa script | Theme + shortcode | Component MDX |
| SEO auto | Claude generate | Manual | Manual |
| Learning curve | Bash basic | HCL template | JSX |
Static blog VPS 2GB serve triệu request/ngày
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Nginx serve HTML tĩnh không lo down, chi phí 199k/tháng.
Xem 8 cấu hình Cloud VPS →FAQ
Bash script có ổn định cho blog 1000+ bài không?
Có. Bash xử lý 1000 file/30 giây không vấn đề. Bottleneck là Claude API call. Nên cache excerpt vào .json file, chỉ gọi lại khi MD thay đổi.
Cost Claude API thực tế khi build 100 bài là bao nhiêu?
Dùng Haiku model: ~0.02 USD cho 100 bài (mỗi bài ~500 từ). Sonnet đắt hơn 10 lần. Haiku đủ tốt cho generate meta description ngắn.
Có hỗ trợ syntax highlight code block không?
Có. Pandoc render code block với class language-xxx, dùng Prism.js hoặc Highlight.js client-side. Hoặc dùng pandoc --highlight-style=github để render server-side.
RSS feed có generate tự động không?
Thêm 1 vòng loop trong build.sh: cho mỗi post export
Có thể dùng Gemini hoặc GPT thay Claude không?
Có. Đổi endpoint và format request trong claude-meta.sh. Gemini Flash rẻ hơn Claude Haiku 5x cho task ngắn. Cost ~0.005 USD cho 100 bài.
Bash script khó maintain không?
Phù hợp dưới 300 dòng. Vượt thì nên switch sang Python hoặc Go cho dễ debug. Bash mạnh cho prototype, không lý tưởng cho long-term complex logic.



