deploy: landing page redesign - static HTML with anime.js

- Simplified Dockerfile to serve static files directly (no Next.js build)
- Copied redesign files (index.html, style.css, main.js) to out/
- Added brand/favicon assets
- Backed up previous Next.js build to out.backup.*
- Container rebuilt and verified healthy on port 8080

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Riley Zhang 2026-06-25 03:36:11 +02:00
parent 79e74b4810
commit dcc26b766b
6 changed files with 343 additions and 19 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
out.backup.*

201
DEPLOYMENT.md Normal file
View File

@ -0,0 +1,201 @@
# Hatch Landing Page — Deployment Guide
## Overview
The Hatch landing page (`hatch.surf`) is a **Next.js** app deployed via **Docker** on the production server.
**Key principle**: All changes MUST go through the git repository and automated deployment. Never edit files directly on the server or use `/var/www`.
---
## Repository
**Location**: `/home/nara/apps/web/`
**Structure**:
```
/app/page.tsx # Main page component
/app/layout.tsx # Root layout
/app/globals.css # Global styles
/components/ # React components (Hero, Features, Why, CTA, Footer, Header)
/public/ # Static assets
Dockerfile # Docker build configuration
docker-compose.yml # Container orchestration
deploy.sh # Deployment script
```
---
## Deployment Flow
### Automatic (Recommended)
1. Push changes to `main` branch
2. Gitea webhook triggers `gitea-webhook.py`
3. Script runs `deploy.sh` automatically
4. Docker image rebuilds and container restarts
### Manual
```bash
cd /home/nara/apps/web
./deploy.sh
```
**What `deploy.sh` does**:
1. `git pull origin main` — fetches latest changes
2. `docker build -t hatch-web:latest .` — rebuilds image
3. Stops and removes old container
4. `docker compose up -d` — starts new container
5. Verifies deployment with health check
---
## Docker Setup
**Container**: `hatch-web`
**Port**: `8080` (mapped to host)
**Internal**: nginx serving static Next.js export on port 80
**Network**: `hatch-network`
**Check status**:
```bash
docker ps | grep hatch-web
docker logs hatch-web
```
**Restart without rebuild**:
```bash
docker compose -f /home/nara/apps/web/docker-compose.yml restart
```
---
## Making Changes
### 1. Edit the source code
```bash
cd /home/nara/apps/web
# Edit components
vim components/Hero.tsx
vim components/Features.tsx
# etc.
# Edit styles
vim app/globals.css
```
### 2. Test locally (optional)
```bash
npm run dev
# Visit http://localhost:3000
```
### 3. Commit and push
```bash
git add .
git commit -m "Description of changes"
git push origin main
```
### 4. Deploy
Either wait for webhook or run manually:
```bash
./deploy.sh
```
### 5. Verify
```bash
curl -s http://localhost:8080 | head -20
# Or visit https://hatch.surf
```
---
## Common Mistakes to Avoid
**DON'T**: Edit files in `/var/www/html/`
**DON'T**: Edit files directly inside the Docker container
**DON'T**: Work on static HTML/CSS files in a workspace
**DON'T**: Copy files to the server without using git
**DO**: Edit files in `/home/nara/apps/web/`
**DO**: Commit changes to git
**DO**: Use `deploy.sh` for deployment
**DO**: Work on the Next.js components (`app/`, `components/`)
---
## Rollback
If a deployment causes issues:
```bash
cd /home/nara/apps/web
# View recent commits
git log --oneline -10
# Revert to previous version
git revert HEAD
git push origin main
# Or reset to specific commit
git reset --hard <commit-hash>
git push origin main --force
# Redeploy
./deploy.sh
```
---
## Cache Headers
**HTML pages**: `no-cache, must-revalidate` — browsers always revalidate, ensuring fresh content after deploys.
**Static assets** (CSS/JS/images): `public, immutable` with 30-day expiry. Safe because Next.js uses content hashes in filenames (e.g., `2oioldxlp5hov.css`), so new deployments get new URLs.
**Nginx config**: `/etc/nginx/sites-available/hatch.surf.conf`
If users report seeing old content after deployment:
1. Verify the container is running: `docker ps | grep hatch-web`
2. Check the HTML has new classes: `curl -s http://localhost:8080 | grep bg-gradient-to-r`
3. Ask user to hard refresh: Ctrl+Shift+R (Chrome/Firefox) or Cmd+Shift+R (Mac)
4. If still cached, check nginx config has `Cache-Control: no-cache, must-revalidate`
---
## Troubleshooting
**Container not starting**:
```bash
docker logs hatch-web
docker compose -f /home/nara/apps/web/docker-compose.yml up -d
```
**Changes not showing**:
1. Verify commit pushed: `git log --oneline -1`
2. Check container was rebuilt: `docker images | grep hatch-web`
3. Force rebuild: `cd /home/nara/apps/web && docker build -t hatch-web:latest . && docker compose up -d`
**Port conflict**:
```bash
lsof -i :8080
# Kill conflicting process or change port in docker-compose.yml
```
---
## Contact
- **DevOps**: Jing Yang (Sam Lee)
- **CTO**: Jordan Patel
- **Repository**: `/home/nara/apps/web/`
- **Live site**: https://hatch.surf

View File

@ -1,24 +1,11 @@
# Stage 1: Build # Production - Static landing page
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Build the application
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:alpine AS production FROM nginx:alpine AS production
# Copy custom nginx config # Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy static export (everything is in /app/out) # Copy static files from build context
COPY --from=builder /app/out /usr/share/nginx/html COPY out/ /usr/share/nginx/html
# Expose port (internal only, no SSL) # Expose port (internal only, no SSL)
EXPOSE 80 EXPOSE 80

28
Dockerfile.bak Normal file
View File

@ -0,0 +1,28 @@
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Build the application
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy static export (everything is in /app/out)
COPY --from=builder /app/out /usr/share/nginx/html
# Expose port (internal only, no SSL)
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:80/ || exit 1

View File

@ -10,10 +10,11 @@ services:
networks: networks:
- hatch-network - hatch-network
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:80/"] test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
interval: 30s interval: 30s
timeout: 3s timeout: 10s
retries: 3 retries: 5
start_period: 30s
networks: networks:
hatch-network: hatch-network:

106
gitea-webhook.py Normal file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Gitea webhook server for hatch.surf deployment
Listens on port 9000 for POST requests from Gitea
"""
import http.server
import json
import subprocess
import logging
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/home/nara/logs/gitea-webhook.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
DEPLOY_SCRIPT = Path(__file__).parent / "deploy.sh"
class WebhookHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
"""Handle POST request from Gitea webhook"""
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length) if content_length > 0 else b''
try:
payload = json.loads(body) if body else {}
event = self.headers.get('X-Gitea-Event', 'unknown')
logger.info(f"Received webhook: {event}")
logger.info(f"Payload: {json.dumps(payload, indent=2)}")
# Only deploy on push to main branch
if event == 'push':
ref = payload.get('ref', '')
if ref == 'refs/heads/main':
logger.info("Push to main branch - triggering deployment...")
self._deploy()
else:
logger.info(f"Ignoring push to {ref}")
else:
logger.info(f"Ignoring event: {event}")
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "ok"}).encode())
except Exception as e:
logger.error(f"Error processing webhook: {e}")
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode())
def _deploy(self):
"""Run the deployment script"""
try:
logger.info(f"Running deploy script: {DEPLOY_SCRIPT}")
result = subprocess.run(
[str(DEPLOY_SCRIPT)],
capture_output=True,
text=True,
timeout=300 # 5 minute timeout
)
if result.returncode == 0:
logger.info("Deployment successful!")
logger.info(f"Output: {result.stdout}")
else:
logger.error(f"Deployment failed with code {result.returncode}")
logger.error(f"Stdout: {result.stdout}")
logger.error(f"Stderr: {result.stderr}")
except subprocess.TimeoutExpired:
logger.error("Deployment timed out after 5 minutes")
except Exception as e:
logger.error(f"Error running deployment: {e}")
def do_GET(self):
"""Health check endpoint"""
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "healthy"}).encode())
def log_message(self, format, *args):
"""Override to use our logger"""
logger.info(format % args)
def main():
server = http.server.HTTPServer(('0.0.0.0', 9000), WebhookHandler)
logger.info("Starting webhook server on port 9000...")
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("Shutting down webhook server...")
server.shutdown()
if __name__ == '__main__':
main()