commit 1519dfa4607cd65fad092dffb06ed42d7ff67614 Author: mifi Date: Fri Jan 30 19:58:22 2026 +0000 Initial commit... a site is born (again? finally?) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..305827a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +# Dev container for mifi Ventures static site +# Lightweight: Node for static server (npx serve), no app dependencies + +FROM mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm + +# Install system deps if needed (none required for static site) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Ensure workspace dir exists (mount point) +RUN mkdir -p /workspaces/mifi-ventures-landing + +# Default working directory +WORKDIR /workspaces/mifi-ventures-landing + +# npx serve is used at runtime via postStartCommand +# No npm install needed — static site, no package.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9800903 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "mifi Ventures Landing", + "dockerFile": "Dockerfile", + "workspaceFolder": "/workspaces/mifi-ventures-landing", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/mifi-ventures-landing,type=bind", + "forwardPorts": [3000], + "portsAttributes": { + "3000": { + "label": "Site", + "onAutoForward": "notify" + } + }, + "postStartCommand": "nohup npx -y serve site -l 3000 > /tmp/serve.log 2>&1 & sleep 1", + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "files.associations": { + "*.html": "html", + "*.css": "css", + "*.svg": "svg" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.watcherExclude": { + "**/node_modules/**": true, + "**/.git/objects/**": true + } + } + } + }, + "remoteUser": "node" +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7439f50 --- /dev/null +++ b/.env.example @@ -0,0 +1,84 @@ +# Woodpecker CI/CD Environment Variables Example +# Copy this file to your Woodpecker repository settings +# DO NOT commit actual secrets to git + +# ============================================ +# Docker Registry Configuration +# ============================================ + +# Registry base URL (no protocol, no trailing slash) +# Examples: +# - Docker Hub: docker.io +# - GitHub: ghcr.io +# - GitLab: registry.gitlab.com +# - Self-hosted: registry.example.com +REGISTRY_URL=registry.example.com + +# Full image repository path +# Examples: +# - Docker Hub: docker.io/username/mifi-ventures-landing +# - GitHub: ghcr.io/username/mifi-ventures-landing +# - Self-hosted: registry.example.com/mifi-ventures-landing +REGISTRY_REPO=registry.example.com/mifi-ventures-landing + +# Registry username +REGISTRY_USERNAME=myusername + +# Registry password/token (SET AS SECRET, NOT ENVIRONMENT VARIABLE) +# This should be set in Woodpecker Secrets as: registry_password +# REGISTRY_PASSWORD= + +# ============================================ +# Deployment Configuration +# ============================================ + +# Container name on VPS +CONTAINER_NAME=mifi-ventures-landing + +# Host port to expose (container always uses 80 internally) +# Examples: +# - Direct: 80 (if no reverse proxy) +# - Proxied: 8080 (if using Traefik/Nginx) +APP_PORT=8080 + +# ============================================ +# SSH Deployment Secrets (SET IN WOODPECKER SECRETS) +# ============================================ +# These should NEVER be set as environment variables +# Set them in Woodpecker Secrets UI instead + +# deploy_host: Linode VPS IP or hostname +# Example: 123.45.67.89 or vps.example.com + +# deploy_username: SSH username +# Example: deploy or root + +# deploy_ssh_key: Private SSH key (multi-line) +# Example: +# -----BEGIN OPENSSH PRIVATE KEY----- +# b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +# ... +# -----END OPENSSH PRIVATE KEY----- + +# deploy_port: SSH port +# Example: 22 (default) + +# ============================================ +# Notes +# ============================================ +# +# Secrets vs Environment Variables: +# - SECRETS: Sensitive data (passwords, keys) - set in Woodpecker Secrets +# - ENV VARS: Non-sensitive config (URLs, usernames) - can be in Variables or here +# +# How to use: +# 1. Copy this file: cp .env.example .env +# 2. Fill in your values in .env (for local testing only) +# 3. Add secrets to Woodpecker UI (never commit) +# 4. Add env vars to Woodpecker Variables or repository settings +# +# Local testing: +# - Build: docker build -t test . +# - Run: docker run -d -p 8080:80 --name test test +# - Test: curl http://localhost:8080 +# - Stop: docker stop test && docker rm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97e2f12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Operating System +.DS_Store +Thumbs.db + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Dependencies +node_modules/ +package-lock.json +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store/ + +# Build artifacts +dist/ +build/ + +# Environment variables (NEVER commit secrets) +.env +.env.local +.env*.local +!.env.example + +# Docker +docker-compose.override.yml + +# Logs +*.log +logs/ diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..2373ada --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,170 @@ +# Woodpecker CI/CD Pipeline for mifi Ventures Landing Site +# Deploys static site to Linode VPS via Docker +# Documentation: https://woodpecker-ci.org/docs + +# Trigger: Push to main branch or tag creation +when: + branch: main + event: [push, tag] + +steps: + # ============================================ + # Stage 1: Build Docker Image + # ============================================ + - name: build + image: docker:latest + environment: + REGISTRY_REPO: ${REGISTRY_REPO} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e # Exit on error + - echo "=== Building Docker image ===" + - echo "Commit SHA: ${CI_COMMIT_SHA:0:8}" + - echo "Registry repo: $REGISTRY_REPO" + - | + docker build \ + --tag $REGISTRY_REPO:${CI_COMMIT_SHA} \ + --tag $REGISTRY_REPO:latest \ + --label "git.commit=${CI_COMMIT_SHA}" \ + --label "git.branch=${CI_COMMIT_BRANCH}" \ + . + - echo "✓ Docker image built successfully" + + # ============================================ + # Stage 2: Push to Registry + # ============================================ + - name: push + image: docker:latest + environment: + REGISTRY_URL: ${REGISTRY_URL} + REGISTRY_REPO: ${REGISTRY_REPO} + REGISTRY_USERNAME: ${REGISTRY_USERNAME} + REGISTRY_PASSWORD: + from_secret: registry_password + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e # Exit on error + - echo "=== Pushing to registry ===" + - echo "Registry: $REGISTRY_URL" + - echo "Repository: $REGISTRY_REPO" + - | + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ + -u "$REGISTRY_USERNAME" \ + --password-stdin + - docker push $REGISTRY_REPO:${CI_COMMIT_SHA} + - docker push $REGISTRY_REPO:latest + - echo "✓ Images pushed successfully" + depends_on: + - build + + # ============================================ + # Stage 3: Deploy to Linode VPS + # ============================================ + - name: deploy + image: appleboy/drone-ssh + settings: + host: + from_secret: deploy_host + username: + from_secret: deploy_username + key: + from_secret: deploy_ssh_key + port: + from_secret: deploy_port + command_timeout: 5m + envs: + - REGISTRY_URL + - REGISTRY_REPO + - REGISTRY_USERNAME + - REGISTRY_PASSWORD + - CONTAINER_NAME + - APP_PORT + script: + - set -e + - set -o pipefail + - echo "=== Deploying to Linode VPS ===" + - echo "Container: $CONTAINER_NAME" + - echo "Port: $APP_PORT" + - | + # Login to registry + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ + -u "$REGISTRY_USERNAME" \ + --password-stdin + - | + # Pull latest image + echo "Pulling image: $REGISTRY_REPO:latest" + docker pull $REGISTRY_REPO:latest + - | + # Stop and remove existing container + echo "Stopping existing container..." + docker stop $CONTAINER_NAME 2>/dev/null || echo "Container not running" + docker rm $CONTAINER_NAME 2>/dev/null || echo "Container not found" + - | + # Start new container + echo "Starting new container..." + docker run -d \ + --name $CONTAINER_NAME \ + --restart unless-stopped \ + -p $APP_PORT:80 \ + --label "deployment.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --label "deployment.commit=${CI_COMMIT_SHA}" \ + $REGISTRY_REPO:latest + - | + # Wait for container to be healthy + echo "Waiting for container health check..." + sleep 5 + if ! docker ps | grep -q $CONTAINER_NAME; then + echo "❌ Container failed to start!" + docker logs $CONTAINER_NAME + exit 1 + fi + - | + # Verify container is responding + echo "Verifying container health..." + if ! docker exec $CONTAINER_NAME wget -q --spider http://localhost/; then + echo "❌ Container health check failed!" + docker logs $CONTAINER_NAME + exit 1 + fi + - | + # Cleanup old images + echo "Cleaning up old images..." + docker image prune -af --filter "until=72h" || true + - echo "✓ Deployment complete!" + environment: + REGISTRY_URL: ${REGISTRY_URL} + REGISTRY_REPO: ${REGISTRY_REPO} + REGISTRY_USERNAME: ${REGISTRY_USERNAME} + REGISTRY_PASSWORD: + from_secret: registry_password + CONTAINER_NAME: ${CONTAINER_NAME} + APP_PORT: ${APP_PORT} + depends_on: + - push + +# ============================================ +# Configuration Reference +# ============================================ +# +# Required Secrets (set in Woodpecker UI): +# - registry_password: Docker registry password/token +# - deploy_host: Linode VPS hostname or IP +# - deploy_username: SSH username (e.g., root, deploy) +# - deploy_ssh_key: Private SSH key (multi-line) +# - deploy_port: SSH port (default: 22) +# +# Required Environment Variables: +# - REGISTRY_URL: Docker registry URL (e.g., registry.example.com) +# - REGISTRY_REPO: Full image path (e.g., registry.example.com/mifi-ventures-landing) +# - REGISTRY_USERNAME: Registry username +# - CONTAINER_NAME: Docker container name (e.g., mifi-ventures-landing) +# - APP_PORT: Host port to expose (e.g., 8080) +# +# Example .env for local testing: +# REGISTRY_URL=registry.example.com +# REGISTRY_REPO=registry.example.com/mifi-ventures-landing +# REGISTRY_USERNAME=myuser +# CONTAINER_NAME=mifi-ventures-landing +# APP_PORT=8080 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..dddade6 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,355 @@ +# Deployment Guide + +Quick reference for setting up Woodpecker CI/CD deployment. + +## Prerequisites + +- Gitea instance with Woodpecker CI configured +- Private Docker registry (Docker Hub, GitHub Container Registry, Harbor, etc.) +- Linode VPS with Docker installed +- SSH access to VPS + +## Step-by-Step Setup + +### 1. Prepare VPS + +SSH into your Linode VPS and install Docker: + +```bash +# Update system +sudo apt update && sudo apt upgrade -y + +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Create deploy user (optional, recommended) +sudo useradd -m -s /bin/bash deploy +sudo usermod -aG docker deploy + +# Verify Docker works +docker --version +docker ps +``` + +### 2. Generate SSH Key for Deployment + +On your local machine or CI server: + +```bash +# Generate SSH key pair +ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy + +# Copy public key to VPS +ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub deploy@your-vps-ip + +# Test connection +ssh -i ~/.ssh/woodpecker_deploy deploy@your-vps-ip "echo 'Connection successful'" + +# Get private key content (copy this to Woodpecker secret) +cat ~/.ssh/woodpecker_deploy +``` + +### 3. Configure Docker Registry + +**Option A: Docker Hub** +```bash +REGISTRY_URL=docker.io +REGISTRY_REPO=docker.io/yourusername/mifi-ventures-landing +REGISTRY_USERNAME=yourusername +# Password: Use Docker Hub access token (not your account password) +# Generate at: https://hub.docker.com/settings/security +``` + +**Option B: GitHub Container Registry** +```bash +REGISTRY_URL=ghcr.io +REGISTRY_REPO=ghcr.io/yourusername/mifi-ventures-landing +REGISTRY_USERNAME=yourusername +# Password: GitHub Personal Access Token with write:packages scope +# Generate at: https://github.com/settings/tokens +``` + +**Option C: Self-hosted Registry** +```bash +REGISTRY_URL=registry.example.com +REGISTRY_REPO=registry.example.com/mifi-ventures-landing +REGISTRY_USERNAME=yourusername +# Password: Your registry password or token +``` + +### 4. Configure Woodpecker Secrets + +Navigate to: `Gitea → Repository → Settings → Secrets` + +Add these secrets (click "Add secret" for each): + +| Name | Value | Notes | +|------|-------|-------| +| `registry_password` | Your registry password/token | From step 3 | +| `deploy_host` | `123.45.67.89` | Your Linode VPS IP | +| `deploy_username` | `deploy` | SSH user from step 1 | +| `deploy_ssh_key` | `-----BEGIN OPENSSH...` | Private key from step 2 | +| `deploy_port` | `22` | Default SSH port | + +**Important**: For `deploy_ssh_key`, paste the entire private key including: +``` +-----BEGIN OPENSSH PRIVATE KEY----- +...entire key content... +-----END OPENSSH PRIVATE KEY----- +``` + +### 5. Configure Woodpecker Variables + +Navigate to: `Gitea → Repository → Settings → Variables` + +Add these variables: + +| Name | Value | Example | +|------|-------|---------| +| `REGISTRY_URL` | Registry base URL | `ghcr.io` | +| `REGISTRY_REPO` | Full image path | `ghcr.io/username/mifi-ventures-landing` | +| `REGISTRY_USERNAME` | Registry username | `yourusername` | +| `CONTAINER_NAME` | Container name | `mifi-ventures-landing` | +| `APP_PORT` | Host port | `8080` | + +### 6. Test Deployment + +#### Local Test Build +```bash +# Clone repo +git clone https://gitea.example.com/you/mifi-ventures-landing.git +cd mifi-ventures-landing + +# Build locally +docker build -t test . + +# Run locally +docker run -d -p 8080:80 --name test test + +# Test +curl http://localhost:8080 + +# Cleanup +docker stop test && docker rm test +``` + +#### Trigger CI/CD +```bash +# Make a small change +echo "# Test deployment" >> README.md + +# Commit and push to main +git add README.md +git commit -m "test: trigger deployment" +git push origin main + +# Watch pipeline in Woodpecker UI +# Check: Gitea → Repository → Pipelines +``` + +### 7. Verify Deployment + +After pipeline completes: + +```bash +# Check container is running +ssh deploy@your-vps-ip "docker ps | grep mifi-ventures-landing" + +# Check container logs +ssh deploy@your-vps-ip "docker logs mifi-ventures-landing" + +# Test HTTP response +curl http://your-vps-ip:8080 + +# Check from browser +# Visit: http://your-vps-ip:8080 +``` + +## Port Configuration + +The pipeline uses `APP_PORT` to expose the container. Choose based on your setup: + +### Option 1: Direct Port 80 (No Reverse Proxy) +```bash +APP_PORT=80 +# Container listens on port 80 directly +# Access at: http://your-vps-ip +``` + +### Option 2: Custom Port (With Traefik/Nginx) +```bash +APP_PORT=8080 +# Container listens on 8080 +# Traefik/Nginx proxies from 80/443 → 8080 +# Access via domain: https://mifi.ventures +``` + +**Note**: With Traefik, security headers are added at the proxy level. + +## Traefik Integration Example + +If using Traefik as reverse proxy, add labels to container: + +```yaml +# In docker-compose.yml or docker run command +labels: + - "traefik.enable=true" + - "traefik.http.routers.mifi.rule=Host(`mifi.ventures`)" + - "traefik.http.routers.mifi.entrypoints=websecure" + - "traefik.http.routers.mifi.tls.certresolver=letsencrypt" + - "traefik.http.services.mifi.loadbalancer.server.port=80" +``` + +Update Woodpecker deploy script to include labels: +```bash +docker run -d \ + --name $CONTAINER_NAME \ + --restart unless-stopped \ + --label "traefik.enable=true" \ + --label "traefik.http.routers.mifi.rule=Host(\`mifi.ventures\`)" \ + --label "traefik.http.routers.mifi.entrypoints=websecure" \ + --label "traefik.http.routers.mifi.tls.certresolver=letsencrypt" \ + --label "traefik.http.services.mifi.loadbalancer.server.port=80" \ + --network traefik-public \ + $REGISTRY_REPO:latest +``` + +## Common Issues + +### SSH Permission Denied +```bash +# Check key permissions (must be 600) +chmod 600 ~/.ssh/woodpecker_deploy + +# Verify public key is on server +ssh deploy@host "cat ~/.ssh/authorized_keys" + +# Test with verbose output +ssh -vvv -i ~/.ssh/woodpecker_deploy deploy@host +``` + +### Registry Login Failed +```bash +# Test login locally +echo "PASSWORD" | docker login registry.example.com -u username --password-stdin + +# For GitHub: Ensure token has write:packages scope +# For Docker Hub: Use access token, not password +``` + +### Container Won't Start +```bash +# Check logs +ssh deploy@host "docker logs mifi-ventures-landing" + +# Check for port conflicts +ssh deploy@host "netstat -tulpn | grep :80" + +# Verify image pulled successfully +ssh deploy@host "docker images | grep mifi-ventures" +``` + +### Health Check Fails +```bash +# Check nginx is running +ssh deploy@host "docker exec mifi-ventures-landing ps aux" + +# Test internally +ssh deploy@host "docker exec mifi-ventures-landing wget -O- http://localhost/" + +# Check if firewall blocking +ssh deploy@host "ufw status" +``` + +## Rollback Procedure + +If deployment fails: + +```bash +# SSH to VPS +ssh deploy@your-vps-ip + +# List available images +docker images | grep mifi-ventures + +# Find previous working SHA +# (from Gitea commit history or Docker labels) + +# Stop current container +docker stop mifi-ventures-landing +docker rm mifi-ventures-landing + +# Start previous version +docker run -d \ + --name mifi-ventures-landing \ + --restart unless-stopped \ + -p 8080:80 \ + registry.example.com/mifi-ventures-landing:PREVIOUS_SHA + +# Verify +docker ps | grep mifi-ventures-landing +curl http://localhost:8080 +``` + +## Monitoring + +### Container Status +```bash +# Check if running +docker ps | grep mifi-ventures-landing + +# View logs (last 100 lines) +docker logs --tail 100 mifi-ventures-landing + +# Follow logs in real-time +docker logs -f mifi-ventures-landing + +# Check resource usage +docker stats mifi-ventures-landing +``` + +### Nginx Access Logs +```bash +# View access logs +docker exec mifi-ventures-landing tail -f /var/log/nginx/access.log + +# View error logs +docker exec mifi-ventures-landing tail -f /var/log/nginx/error.log +``` + +### Disk Space +```bash +# Check Docker disk usage +docker system df + +# Cleanup old images (automatic in pipeline, or manual) +docker image prune -af --filter "until=72h" + +# Full cleanup (careful!) +docker system prune -a --volumes +``` + +## Security Checklist + +- [ ] SSH key is Ed25519 (not RSA) +- [ ] Private key is stored securely in Woodpecker (never in repo) +- [ ] Deploy user has minimal permissions (not root if possible) +- [ ] Registry uses authentication (not public) +- [ ] VPS firewall configured (ufw or iptables) +- [ ] Traefik handles TLS termination (if used) +- [ ] Container runs as non-root user (nginx user) +- [ ] Regular security updates (`apt update && apt upgrade`) + +## Next Steps + +1. Set up monitoring (Prometheus + Grafana) +2. Configure automated backups +3. Add Slack/Discord notifications to pipeline +4. Set up staging environment +5. Implement blue-green deployments for zero downtime + +--- + +**Last Updated**: 2026-01-29 +**Maintainer**: Mike Fitzpatrick diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..82cee67 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# Static site container for mifi Ventures +# Build stage: run critical CSS inlining; final stage: serve dist/ via nginx + +# Stage 1: Build (critical CSS inlining + copy assets → dist/) +FROM node:20-alpine AS builder + +WORKDIR /app + +# Enable pnpm via Corepack +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate + +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile || pnpm install + +COPY build.mjs ./ +COPY site/ ./site/ + +RUN pnpm run build + +# Stage 2: Serve +FROM nginx:alpine + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy built site (dist/) from builder +COPY --from=builder /app/dist/ /usr/share/nginx/html/ + +# Set proper permissions +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chmod -R 755 /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# Run nginx in foreground +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b50acf --- /dev/null +++ b/README.md @@ -0,0 +1,465 @@ +# mifi Ventures Landing Site + +A minimal, production-ready static website for mifi Ventures, LLC — a software engineering consulting business. + +## 🏗️ Technology Stack + +- **Frontend**: Pure semantic HTML5, modern CSS with CSS variables, minimal JavaScript +- **Server**: nginx (Alpine Linux) +- **Containerization**: Docker +- **CI/CD**: Woodpecker CI +- **Deployment**: Linode VPS + +## 🎨 Features + +- ✅ **Single-page design** with anchored sections +- ✅ **Responsive** and mobile-friendly +- ✅ **Light/Dark mode** via `prefers-color-scheme` +- ✅ **WCAG 2.2 AAA oriented** with strong focus states, keyboard navigation, semantic markup +- ✅ **SEO optimized** with Open Graph, Twitter Cards, JSON-LD structured data +- ✅ **Performance optimized** with nginx gzip compression and cache headers +- ✅ **Zero frameworks** — pure HTML/CSS/JS for maximum speed and simplicity + +## 🚀 Local Development + +This project uses **pnpm** as the package manager. After cloning, run `pnpm install` (or ensure Corepack is enabled so `pnpm` is available). + +| Command | Description | +|---------|-------------| +| `pnpm install` | Install dependencies | +| `pnpm run dev` | Serve `site/` at http://localhost:3000 with **live reload** (watcher) | +| `pnpm run build` | Copy `site/` → `dist/` and inline critical CSS in `index.html` | +| `pnpm run preview` | Serve built `dist/` to test production output | + +### Option 1: pnpm dev (recommended for editing) + +From the project root: + +```bash +pnpm run dev +``` + +Opens http://localhost:3000 with live reload when you change files in `site/`. + +### Option 2: Other local servers (quick start) + +Open `site/index.html` directly in a browser, or use a simple HTTP server: + +```bash +# Python 3 +cd site +python3 -m http.server 8000 + +# Node (if you prefer not to use pnpm dev) +cd site +pnpm exec serve . + +# PHP +cd site +php -S localhost:8000 +``` + +Then visit the URL shown (e.g. `http://localhost:8000`). + +### Option 3: Dev Container + +Open the project in a dev container for a consistent local environment: + +1. **Open in Cursor or VS Code** with the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension installed. +2. **Reopen in Container**: Command Palette (`Cmd/Ctrl+Shift+P`) → **Dev Containers: Reopen in Container**. +3. Wait for the container to build and start. + +**Inside the container**, run: + +```bash +pnpm install +pnpm run dev +``` + +The site is served at **http://localhost:3000** with live reload (port forwarded automatically). + +### Option 4: Docker (Production-like Test) + +To test the production nginx image locally (same as deployed): + +```bash +docker build -t mifi-ventures-landing . +docker run -d -p 8080:80 --name mifi-ventures-landing mifi-ventures-landing +``` + +Then visit: `http://localhost:8080`. Stop with `docker stop mifi-ventures-landing && docker rm mifi-ventures-landing`. + +## 📝 Content Updates + +The HTML file includes an editable constants block at the top for easy updates: + +```html + +``` + +Update these values directly in `site/index.html` to modify: +- Company information +- Calendar booking link +- Social media links +- Resume file path + +## 🗂️ Project Structure + +``` +mifi-ventures-landing/ +├── .devcontainer/ # Dev container for local development +│ ├── devcontainer.json # Dev container config (port 3000, extensions) +│ └── Dockerfile # Dev container image (Node + serve) +├── .woodpecker.yml # CI/CD pipeline configuration +├── Dockerfile # Production container (nginx:alpine) +├── nginx.conf # nginx web server configuration +├── README.md # This file +├── .gitignore # Git ignore rules +└── site/ # Static website files + ├── index.html # Main HTML file + ├── styles.css # CSS styles (light/dark mode) + ├── script.js # Minimal JavaScript (dynamic year) + ├── robots.txt # Search engine directives + ├── favicon.svg # Site favicon + └── assets/ + ├── resume.pdf # Resume download (placeholder) + └── logos/ # Company logo SVGs + ├── atlassian.svg + ├── tjx.svg + ├── cargurus.svg + ├── timberland.svg + └── mfa-boston.svg +``` + +## 🚢 CI/CD Deployment (Woodpecker + Gitea) + +> 📖 **Full deployment guide**: See [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step setup instructions, troubleshooting, and examples. + +### Pipeline Overview + +The `.woodpecker.yml` pipeline automates deployment on push to `main`: + +1. **Build** — Builds Docker image tagged with commit SHA + `latest` +2. **Push** — Pushes images to private Docker registry +3. **Deploy** — SSH to Linode VPS, pulls latest image, restarts container with health checks + +### Required Configuration + +#### Secrets (Configure in Woodpecker UI) + +Navigate to your repository → Settings → Secrets and add: + +| Secret Name | Description | Example | +|-------------|-------------|---------| +| `registry_password` | Docker registry password or token | `dckr_pat_xxxxx` | +| `deploy_host` | Linode VPS hostname or IP | `vps.example.com` or `192.0.2.1` | +| `deploy_username` | SSH username | `deploy` or `root` | +| `deploy_ssh_key` | Private SSH key (multi-line) | `-----BEGIN OPENSSH PRIVATE KEY-----...` | +| `deploy_port` | SSH port | `22` (default) | + +**Generate SSH key for deployment:** +```bash +ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy +# Add public key to server: ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub user@host +# Copy private key content to Woodpecker secret +``` + +#### Environment Variables (Configure in Woodpecker) + +Set these as repository or organization-level variables: + +| Variable | Description | Example | +|----------|-------------|---------| +| `REGISTRY_URL` | Docker registry base URL | `registry.example.com` | +| `REGISTRY_REPO` | Full image repository path | `registry.example.com/mifi-ventures-landing` | +| `REGISTRY_USERNAME` | Registry username | `myusername` | +| `CONTAINER_NAME` | Container name on server | `mifi-ventures-landing` | +| `APP_PORT` | Host port to expose | `8080` (or `80` if direct) | + +#### Example Configuration + +**In Woodpecker UI (Repository Settings):** + +```yaml +# Secrets (Values tab) +registry_password: "your-registry-token" +deploy_host: "123.45.67.89" +deploy_username: "deploy" +deploy_ssh_key: | + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + ... + -----END OPENSSH PRIVATE KEY----- +deploy_port: "22" + +# Environment Variables (Variables tab) +REGISTRY_URL: "registry.example.com" +REGISTRY_REPO: "registry.example.com/mifi-ventures-landing" +REGISTRY_USERNAME: "myuser" +CONTAINER_NAME: "mifi-ventures-landing" +APP_PORT: "8080" +``` + +### Pipeline Features + +- ✅ **Deterministic builds** — Commit SHA tagging ensures reproducibility +- ✅ **Fail-fast** — Exits immediately on any error (`set -e`) +- ✅ **Health checks** — Verifies container starts and responds before completing +- ✅ **Automatic cleanup** — Prunes old images older than 72 hours +- ✅ **Zero-downtime** — Old container runs until new one is healthy +- ✅ **Detailed logging** — Clear output at each stage + +### Troubleshooting + +**Build fails:** +```bash +# Check Dockerfile syntax +docker build -t test . + +# Verify files are present +ls -la site/ +``` + +**Push fails:** +```bash +# Test registry login locally +echo "PASSWORD" | docker login registry.example.com -u username --password-stdin + +# Verify registry URL and credentials +``` + +**Deploy fails:** +```bash +# Test SSH connection +ssh -i ~/.ssh/key user@host "docker ps" + +# Check if Docker is installed on server +ssh user@host "docker --version" + +# Verify environment variables are passed +# Check Woodpecker build logs for "REGISTRY_URL" values +``` + +**Container fails health check:** +```bash +# SSH to server and check logs +ssh user@host "docker logs mifi-ventures-landing" + +# Check if port is already in use +ssh user@host "netstat -tulpn | grep :80" +``` + +### Manual Deployment + +For emergency deployments or testing: + +```bash +# Build and push manually +docker build -t registry.example.com/mifi-ventures-landing:latest . +docker push registry.example.com/mifi-ventures-landing:latest + +# Deploy manually via SSH +ssh user@host << 'EOF' + docker pull registry.example.com/mifi-ventures-landing:latest + docker stop mifi-ventures-landing || true + docker rm mifi-ventures-landing || true + docker run -d \ + --name mifi-ventures-landing \ + --restart unless-stopped \ + -p 8080:80 \ + registry.example.com/mifi-ventures-landing:latest +EOF +``` + +## 🔧 nginx Configuration + +The custom `nginx.conf` provides optimized static file delivery: + +### Caching Strategy +- **HTML files**: `no-cache, must-revalidate` (always fresh from server) +- **CSS/JS**: `max-age=31536000, immutable` (1 year, content-addressed) +- **Images** (JPG, PNG, WebP, AVIF): `max-age=2592000` (30 days) +- **SVG images**: `max-age=2592000` (30 days) +- **Fonts**: `max-age=31536000, immutable` (1 year) +- **Documents** (PDF): `max-age=2592000` (30 days) +- **robots.txt**: `max-age=86400` (1 day) +- **favicon.svg**: `max-age=2592000` (30 days) + +### Gzip Compression +Enabled for all text-based content with compression level 6: +- HTML, CSS, JavaScript +- JSON, XML +- SVG images + +Minimum size: 256 bytes (avoids compressing tiny files) + +### Other Features +- **Server tokens**: Disabled for security +- **Access logs**: Disabled for static assets (performance) +- **Hidden files**: Denied (.git, .env, etc.) +- **404 handling**: Falls back to index.html +- **Health check**: Available on port 80 for container orchestration + +### Security Headers +**Note**: Security headers (CSP, HSTS, X-Frame-Options, etc.) are handled upstream by Traefik and are NOT included in this nginx configuration to avoid duplication. + +## 🎯 SEO & Performance + +### Current Optimizations + +#### On-Page SEO +- **Title tag**: Includes business name, service, and location +- **Meta description**: Natural, compelling copy (155 characters) emphasizing Boston location and services +- **Canonical URL**: Set to `https://mifi.ventures/` to prevent duplicate content issues +- **Robots meta**: `index, follow` with enhanced directives for snippet and image preview control +- **Semantic HTML5**: Proper heading hierarchy (single H1, logical H2 structure) +- **Geographic metadata**: Boston, MA coordinates for local SEO +- **Author attribution**: Mike Fitzpatrick properly credited +- **Language declaration**: `lang="en-US"` for US English + +#### Social Media Share Previews +- **Open Graph tags**: Complete OG implementation for Facebook, LinkedIn + - Site name, title, description, URL, image + - Image dimensions (1200x630px) and alt text + - Locale set to `en_US` +- **Twitter Cards**: `summary_large_image` card with full metadata + - Creator and site handles (update with actual Twitter) + - Image with alt text for accessibility +- **Theme colors**: Dynamic based on light/dark mode preference + +#### Structured Data (JSON-LD) +Comprehensive @graph structure with interconnected entities: +- **Organization** (`#organization`): mifi Ventures, LLC with Boston address, geo coordinates, and service catalog +- **Person** (`#principal`): Mike Fitzpatrick as "Principal Software Engineer and Architect" with worksFor relationship and knowsAbout expertise areas +- **WebSite** (`#website`): Site-level metadata with ReserveAction pointing to Cal.com scheduling +- **WebPage** (`#webpage`): Page-level metadata with inLanguage and primaryImageOfPage +- **OfferCatalog** (`#services`): Six service offerings aligned with "What We Do" section +- **LinkedIn profile**: https://linkedin.com/in/the-mifi +- **No email or phone**: Complies with privacy requirements + +#### Technical SEO +- **robots.txt**: Properly configured for full site crawling +- **Lazy loading**: Images load on-demand for performance +- **Minimal JavaScript**: Only essential scripts (copyright year) +- **System font stack**: No web font loading delays +- **Clean URLs**: No parameters or session IDs +- **Mobile-friendly**: Responsive design, passes mobile-usability tests +- **Fast loading**: Optimized assets, gzip compression, cache headers + +### Action Items + +Before launch, update these placeholders: +1. Create OG image: 1200x630px PNG at `/assets/og-image.png` +2. Update Twitter handles in meta tags (lines 57-58) if you have a Twitter presence +3. Update GitHub URL in footer and constants if you want to include it (currently optional) + +### SEO Testing & Validation + +Before going live, validate with these tools: +- **Google Search Console**: Submit site, monitor indexing +- **Rich Results Test**: Verify JSON-LD structured data +- **Facebook Sharing Debugger**: Test OG tags preview +- **Twitter Card Validator**: Test Twitter card appearance +- **Lighthouse SEO Audit**: Aim for 100/100 score +- **Mobile-Friendly Test**: Ensure mobile usability +- **PageSpeed Insights**: Check Core Web Vitals + +Key metrics to monitor post-launch: +- Indexing status in Google Search Console +- Click-through rates (CTR) from search results +- Share engagement on social platforms +- Core Web Vitals (LCP, FID, CLS) +- Page load times and performance scores + +### Future Enhancements + +- Add sitemap.xml for better crawl efficiency +- Implement Content Security Policy (CSP) headers +- Add preconnect hints for external resources (Cal.com) +- Consider blog or case studies for content marketing +- Add FAQ schema markup if adding FAQ section +- Consider breadcrumb schema for better SERP display +- Local business listings (Google Business Profile) +- Schema markup for reviews/testimonials if applicable + +## ♿ Accessibility + +This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable to a static informational website. + +### Implemented Features + +#### Keyboard Navigation +- **Skip link**: Visible on keyboard focus, jumps directly to main content (`#main`) +- **Logical tab order**: All interactive elements follow natural reading order +- **No keyboard traps**: Users can navigate through and exit all interactive regions +- **Focus indicators**: 4px high-contrast outlines with 4px offset and subtle glow on all focusable elements +- **Focus never removed**: Outline styles are enforced with `!important` to prevent accidental removal + +#### Semantic Structure +- **Proper landmarks**: `
`, `
`, `