# 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 test:e2e` | Run Playwright visual regression (e2e)                                         |
| `pnpm run test:e2e:update-snapshots` | Regenerate e2e snapshots (in devcontainer = CI; see Visual regression) |
| `pnpm run test:ci` | Run same tests as CI: lint, lint:css, unit tests, e2e (used by pre-commit hook) |
| `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)                                               |

A **pre-commit hook** (Husky) runs `pnpm run test:ci` so the same tests as CI run before each commit. Skip with `git commit --no-verify` if needed.

### 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. The devcontainer uses the **same image as CI** (`mcr.microsoft.com/playwright:v1.58.0-noble`), so e2e snapshots generated inside it match CI.

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. (If you already had a devcontainer open, use **Rebuild Container** once to pick up the Playwright Noble image.)

**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). Run `pnpm run test:e2e:update-snapshots` here to regenerate the Linux snapshot when the layout changes.

### Visual regression (e2e)

E2e tests use Playwright and compare full-page screenshots to committed snapshots. CI and the **devcontainer** both use `mcr.microsoft.com/playwright:v1.58.0-noble`, so snapshots generated in the devcontainer match CI.

**To update the Linux snapshot locally (devcontainer):**

1. Rebuild the devcontainer once (so it uses the Playwright Noble image; see Option 3 below).
2. Run `pnpm run test:e2e:update-snapshots` (no Docker needed — same environment as CI).
3. Commit the updated file(s) under `tests/visual.spec.ts-snapshots/`.

**If you’re not using the devcontainer:** run the **update-e2e-snapshots** workflow manually in Woodpecker (requires a `git_push_token` secret), or run `pnpm run test:e2e:update-snapshots` on a host with Docker.

**Running e2e locally:** `pnpm run test:e2e` mirrors CI when Docker is available (runs tests in the same Playwright image). Without Docker (e.g. inside the devcontainer), it runs in the current environment. The config uses Linux snapshot paths so baselines stay consistent; run in Docker or devcontainer for matching rendering.

### 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)
│   ├── ci.yaml             # one clone/workspace: install → lint → build → test
│   └── deploy.yaml          # Docker → push → webhook (main only, after ci)
├── 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/beasties.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/ci.yaml`, `deploy.yaml`):

- **Pull requests** (and **push/tag/manual on main**): **ci** runs install → lint → build → test in one workspace (one clone, one install). No Docker or deploy on PRs.
- **Push to main** (or tag / manual on main): After ci succeeds, **deploy** runs:
    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)                           |
| `git_push_token`    | *(Optional)* Repo push token for manual **update-e2e-snapshots** workflow (fallback when not using devcontainer) | Gitea/Forgejo personal access token |

**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).
