| name | deployment |
| description | Deploy generated tutorials to free hosting services. Covers Vercel, Netlify, Surge, GitHub Pages, and Cloudflare Pages with auth flows, SPA routing, troubleshooting, and QR code generation. |
Deployment Skill
Provider Comparison
| Provider | Free Tier | Auth | Deploy Time | Custom Domain | SPA Routing | Bandwidth |
|---|
| Vercel | 100 deploys/day | OAuth or Token | ~10s | Yes | Auto (with config) | 100GB/mo |
| Netlify | 500 deploys/mo | OAuth or Token | ~15s | Yes | _redirects file | 100GB/mo |
| Surge | Unlimited | Email only | ~5s | Built-in | 200.html convention | Unlimited |
| GitHub Pages | Unlimited | gh CLI | ~30s | CNAME file | 404.html fallback | 100GB/mo |
| Cloudflare Pages | 500 deploys/mo | OAuth or Token | ~20s | Yes | _redirects file | Unlimited |
Vercel (Recommended)
Authentication Flow
export VERCEL_TOKEN="your_token_here"
npx vercel login
Deployment Commands
Always set a project name. A bare npx vercel --prod --yes names the project after the
working directory (syllabus-output), so every tutorial lands on the same generic URL and
overwrites the previous one. Name it after the tutorial title instead. (--name is deprecated
and ignored by current Vercel CLI — use vercel link --project.)
cd syllabus-output
TITLE=$(node -p "require('./src/data/syllabus.json').title || 'tutorial'")
SLUG="learn-$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' | cut -c1-40 | sed -E 's/-+$//')"
rm -rf .vercel
npx vercel project add "$SLUG" 2>/dev/null || true
npx vercel link --yes --project "$SLUG"
npx vercel --prod --yes
Different topics get distinct URLs automatically; re-deploying the same topic updates the
same URL. For a unique URL on every build, append a suffix: SLUG="$SLUG-$(date +%s | tail -c 5)".
SPA Routing Config
Create syllabus-output/vercel.json:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Troubleshooting
| Issue | Fix |
|---|
| "Not authenticated" | Run npx vercel login or set VERCEL_TOKEN |
Generic URL (syllabus-output.vercel.app) / overwrote previous tutorial | Name the project after the topic before deploy: rm -rf .vercel && npx vercel link --yes --project learn-<topic> |
| "Project not found" | Use --yes flag to auto-create |
| 404 on refresh | Add vercel.json with rewrites |
| "Too many requests" | Wait a minute, free tier has rate limits |
| Build fails on Vercel | We deploy pre-built dist/, so set Output Directory to dist |
Netlify
Authentication Flow
export NETLIFY_AUTH_TOKEN="your_token_here"
npx netlify login
Deployment Commands
cd syllabus-output
npx netlify deploy --prod --dir=dist
npx netlify deploy --prod --dir=dist --site=learn-python-basics
SPA Routing Config
Create syllabus-output/dist/_redirects (AFTER build):
/* /index.html 200
Or use public/_redirects (BEFORE build — Vite copies public/ to dist/):
/* /index.html 200
Surge (Zero-Auth Fallback)
Authentication
Surge only requires an email — entered inline the first time. No OAuth, no browser pop-ups, no dashboard required.
npx surge dist learn-python-basics.surge.sh
npx surge dist learn-python-basics.surge.sh
export SURGE_LOGIN=user@example.com
export SURGE_TOKEN=your_token
npx surge dist learn-python-basics.surge.sh
SPA Routing
cp dist/index.html dist/200.html
Domain Naming Convention
Generate a deterministic subdomain from the tutorial title:
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 40);
const domain = `learn-${slug}.surge.sh`;
GitHub Pages
Prerequisites
- Project must be a git repo
- Should be pushed to GitHub
gh CLI must be authenticated
Deployment Commands
cd syllabus-output
npm run build
cp dist/index.html dist/404.html
npx gh-pages -d dist
gh repo create learn-python-basics --public --source=. --push
Vite Base Path Config
When deploying to GitHub Pages, update vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
base: '/repo-name/',
plugins: [react()]
});
Cloudflare Pages
Authentication
export CLOUDFLARE_API_TOKEN="your_token"
npx wrangler login
Deployment Commands
cd syllabus-output
npx wrangler pages deploy dist --project-name=learn-python-basics
SPA Routing
Create dist/_redirects:
/* /index.html 200
Pre-Deployment Checklist
Run these checks before deploying:
ls -la dist/
test -f dist/index.html || echo "ERROR: No index.html in dist/"
grep -r "localhost" dist/ && echo "WARNING: localhost URLs found" || echo "OK: No localhost URLs"
du -sh dist/
ls dist/audio/*.mp3 2>/dev/null && echo "Audio files present" || echo "No audio files (OK if using Web Speech)"
QR Code Generation
Generate a terminal-printable QR code for the deployed URL so the user can quickly open it on their phone:
npx qrcode-terminal "https://learn-python-basics.vercel.app"
echo ""
echo "📱 Open on your phone:"
echo " https://learn-python-basics.vercel.app"
echo ""
Post-Deploy Verification
URL="https://learn-python-basics.vercel.app"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
if [ "$STATUS" = "200" ]; then
echo "✅ Site is live: $URL"
else
echo "⚠️ Site returned HTTP $STATUS — may need a few seconds to propagate"
fi
STATUS_DEEP=$(curl -s -o /dev/null -w "%{http_code}" "$URL/lesson/les-01-01")
if [ "$STATUS_DEEP" = "200" ]; then
echo "✅ SPA routing works"
else
echo "⚠️ SPA routing may not be configured correctly"
fi
Error Recovery
| Error | Recovery |
|---|
| Auth failed on Vercel | Try VERCEL_TOKEN env var → fall back to Surge |
| Auth failed on Netlify | Try NETLIFY_AUTH_TOKEN env var → fall back to Surge |
| Surge email prompt hangs | Use SURGE_LOGIN + SURGE_TOKEN env vars |
| GitHub Pages: not a git repo | Fall back to Surge or Vercel |
| Cloudflare: rate limit | Fall back to Vercel or Surge |
| All providers fail | Print manual instructions with dist/ path |
Manual Fallback Instructions
If all automated deploys fail, print:
Your tutorial is built and ready at: syllabus-output/dist/
To deploy manually:
Option 1: Drag & drop dist/ to https://app.netlify.com/drop
Option 2: npx surge syllabus-output/dist your-name.surge.sh
Option 3: Upload dist/ to any static hosting service