# mifi Ventures Landing Site

A minimal, production-ready static website for mifi Ventures, LLC — a software engineering consulting business.

## 🏗️ Technology Stack

- **Frontend**: SvelteKit (Svelte 5) with adapter-static — prerendered HTML/CSS, zero app JS (no hydration)
- **Build**: Vite, PostCSS (autoprefixer), Critters (critical CSS inlining)
- **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
- ✅ **Minimal JS** — only a tiny copyright-year script; no Svelte runtime or app bundle

## 🚀 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`      | SvelteKit dev server at http://localhost:5173 with **live reload**             |
| `pnpm run build`    | SvelteKit build → `dist/`, then Critters inlines critical CSS                  |
| `pnpm run preview`  | Serve `dist/` (Critters-processed) at http://localhost:4173 — same as deployed |
| `pnpm test`         | Run unit tests (Vitest)                                                        |
| `pnpm run lint`     | ESLint (JS/TS/Svelte)                                                          |
| `pnpm run lint:css` | Stylelint (global CSS + Svelte styles)                                         |
| `pnpm run format`   | Prettier (JS/TS/Svelte/CSS/JSON)                                               |

### Option 1: pnpm dev (recommended for editing)

From the project root:

```bash
pnpm run dev
```

Opens http://localhost:5173 with live reload when you change files in `src/` or `static/`.

### Option 2: Preview production build

After building, serve the static output:

```bash
pnpm run build
pnpm run preview
```

Preview uses `serve dist` so you see the same HTML/CSS as in production (including critical CSS).

### 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:5173** (or the port shown) 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

Content and links are driven by data and components:

- **Meta/SEO**: `src/lib/seo.ts`, `src/lib/data/home-meta.ts`, and per-route `+page.ts` load functions
- **Section copy**: `src/lib/data/content.ts`, `src/lib/data/experience.ts`, `src/lib/data/engagements.ts`
- **JSON-LD**: `src/lib/data/json-ld.ts`
- **Layout and sections**: `src/routes/+layout.svelte`, `src/routes/+page.svelte`, and components in `src/lib/components/`

To change company info, calendar link, social links, or resume path, edit the data modules and `src/lib/data/home-meta.ts` (or the relevant route’s meta).

## 🗂️ Project Structure

```
mifi-ventures-landing/
├── .devcontainer/          # Dev container for local development
│   ├── devcontainer.json   # Dev container config (extensions)
│   └── Dockerfile          # Dev container image (Node)
├── .woodpecker/            # CI/CD pipelines (see below)
│   ├── lint-and-build.yaml # PR: lint, test, build (no deploy)
│   └── deploy.yaml          # main: lint, test, build, Docker, push, webhook
├── Dockerfile              # Production container (nginx:alpine)
├── nginx.conf              # nginx web server configuration
├── svelte.config.js        # SvelteKit config (adapter-static)
├── vite.config.ts          # Vite config
├── postcss.config.js       # PostCSS (autoprefixer)
├── scripts/critters.mjs    # Post-build critical CSS inlining
├── static/                 # Static assets (copied to dist as-is)
│   ├── favicon.svg, favicon.ico, robots.txt
│   ├── copyright-year.js   # Minimal client script (footer year)
│   └── assets/             # Fonts, images, logos, resume.pdf, og-image.png
├── src/
│   ├── app.css             # Global tokens + base styles
│   ├── app.html            # HTML shell for SvelteKit
│   ├── app.d.ts            # SvelteKit types
│   ├── routes/
│   │   ├── +layout.ts      # Prerender, csr: false
│   │   ├── +layout.svelte  # Shell, head, skip link, slot
│   │   ├── +page.ts        # Home page meta (load)
│   │   └── +page.svelte    # Home page content (components)
│   └── lib/
│       ├── seo.ts          # Meta defaults, mergeMeta, PageMeta type
│       ├── copyright-year.ts
│       ├── data/           # home-meta, json-ld, content, experience, engagements
│       └── components/     # Hero, sections, Footer, Logo, etc.
├── tests/                  # Playwright visual regression
└── README.md               # This file
```

## 🚢 CI/CD Deployment (Woodpecker + Gitea)

> 📖 **Full deployment guide**: See [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step setup instructions, troubleshooting, and examples.

### Pipeline Overview

Woodpecker uses two workflows (`.woodpecker/lint-and-build.yaml` and `.woodpecker/deploy.yaml`):

- **Pull requests**: Opening or updating a PR runs **lint** (ESLint + Stylelint), **tests** (Vitest), and a **test build** (SvelteKit + Critters) on the branch. No Docker image or deploy.
- **Push to main** (or tag / manual run): Runs the same lint, test, and build, then:
    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
# Build locally first (must succeed before Docker)
pnpm install
pnpm run build

# Check Dockerfile syntax
docker build -t test .

# Verify source is present
ls -la src/ static/
```

**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**: `<header>`, `<main>`, `<footer>`, and `<nav>` for clear page regions
- **Single H1**: One `<h1>` element ("mifi Ventures") with logical H2 nesting for all sections
- **ARIA labelledby**: All sections connected to their headings via `aria-labelledby` attributes
- **Language declaration**: `lang="en-US"` attribute on `<html>` element

#### Visual & Color

- **AAA contrast ratios**: All text meets AAA standards (7:1 for normal text, 4.5:1 for large text)
    - Light mode: `#1a1a1a` text on `#ffffff` background (16.1:1 ratio)
    - Dark mode: `#f5f5f5` text on `#0a0a0a` background (18.4:1 ratio)
- **Color independence**: No information conveyed by color alone
- **High contrast mode**: Enhanced borders, outlines, and contrast for users with `prefers-contrast: high`

#### Interactive Elements

- **Adequate touch targets**: All buttons and links meet minimum 44x44px size (AAA requirement)
- **Descriptive link text**: All links have meaningful text or enhanced ARIA labels
- **External link warnings**: Links opening in new tabs clearly labeled "(opens in new tab)"
- **Button spacing**: Generous gaps between CTAs prevent accidental activation

#### Motion & Animation

- **Respects `prefers-reduced-motion`**: All animations and transforms disabled when users prefer reduced motion
- **Safe default animations**: Subtle hover effects that don't cause vestibular issues
- **No auto-playing content**: No carousels, videos, or content that moves automatically

#### Images & Media

- **Descriptive alt text**: All images have clear, concise alternative text
- **Text fallbacks**: Logo strip includes visually-hidden text that appears if images fail
- **Mobile text list**: On small screens, logo images replaced with accessible text list
- **Decorative images marked**: Images that don't convey content use appropriate ARIA attributes

#### Screen Reader Support

- **Clear labels**: All form controls, buttons, and navigation have proper labels
- **ARIA landmarks**: Supplementary ARIA roles for enhanced screen reader navigation
- **Visually-hidden content**: Important text available to screen readers but hidden visually where appropriate
- **Logical reading order**: Content structure follows visual hierarchy

### Testing Recommendations

For best results, test with:

- **Keyboard only**: Tab through entire page without mouse
- **Screen readers**: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
- **Browser extensions**: axe DevTools, WAVE, Lighthouse accessibility audit
- **High contrast mode**: Windows High Contrast, macOS Increase Contrast
- **Zoom**: Test at 200% and 400% zoom levels
- **Reduced motion**: Enable in OS settings and verify animations stop

### Known Limitations

- Logo images use CSS filters for dark mode adaptation (works well but may not be perfect for all logos)
- External link icons not implemented (relies on ARIA labels and "opens in new tab" text)
- No live regions or dynamic content requiring ARIA live announcements

## 📄 License

© 2026 mifi Ventures, LLC. All rights reserved.

## 📞 Contact

For inquiries, visit [mifi.ventures](https://mifi.ventures/) or [schedule a call](https://cal.mifi.ventures/the-mifi).
