The Svelte 5 SSG Migration (#1)
- Migrates the site to Svelte 5 - Still generates a static site with inlined critical path CSS for the ultimate in performance - Opens up future possibilities for site growth Reviewed-on: #1 Co-authored-by: mifi <badmf@mifi.dev> Co-committed-by: mifi <badmf@mifi.dev>
@@ -3,14 +3,17 @@
|
|||||||
"dockerFile": "Dockerfile",
|
"dockerFile": "Dockerfile",
|
||||||
"workspaceFolder": "/workspaces/mifi-ventures-landing",
|
"workspaceFolder": "/workspaces/mifi-ventures-landing",
|
||||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/mifi-ventures-landing,type=bind",
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/mifi-ventures-landing,type=bind",
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [5173, 4173],
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"3000": {
|
"5173": {
|
||||||
"label": "Site",
|
"label": "Dev (Vite)",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"4173": {
|
||||||
|
"label": "Preview (Vite)",
|
||||||
"onAutoForward": "notify"
|
"onAutoForward": "notify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postStartCommand": "nohup npx -y serve site -l 3000 > /tmp/serve.log 2>&1 & sleep 1",
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
10
.gitignore
vendored
@@ -22,6 +22,16 @@ pnpm-debug.log*
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# SvelteKit / Vite
|
||||||
|
.svelte-kit/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Test outputs
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
coverage/
|
||||||
|
.playwright/
|
||||||
|
|
||||||
# Environment variables (NEVER commit secrets)
|
# Environment variables (NEVER commit secrets)
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.svelte-kit/
|
||||||
|
node_modules/
|
||||||
|
pnpm-lock.yaml
|
||||||
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 90,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
||||||
106
.woodpecker.yml
@@ -1,106 +0,0 @@
|
|||||||
# 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, tag creation, or manual run from Woodpecker UI
|
|
||||||
when:
|
|
||||||
branch: main
|
|
||||||
event: [push, tag, manual]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# ============================================
|
|
||||||
# Stage 1: Build Docker Image
|
|
||||||
# ============================================
|
|
||||||
- name: build
|
|
||||||
image: docker:latest
|
|
||||||
environment:
|
|
||||||
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
|
|
||||||
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: git.mifi.dev
|
|
||||||
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
|
|
||||||
REGISTRY_USERNAME:
|
|
||||||
from_secret: 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: Trigger Portainer stack redeploy (webhook)
|
|
||||||
# ============================================
|
|
||||||
- name: deploy
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
environment:
|
|
||||||
PORTAINER_WEBHOOK_URL:
|
|
||||||
from_secret: portainer_webhook_url
|
|
||||||
commands:
|
|
||||||
- set -e
|
|
||||||
- echo "=== Triggering Portainer stack redeploy ==="
|
|
||||||
- |
|
|
||||||
resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL")
|
|
||||||
body=$(echo "$resp" | head -n -1)
|
|
||||||
code=$(echo "$resp" | tail -n 1)
|
|
||||||
if [ "$code" != "200" ] && [ "$code" != "204" ]; then
|
|
||||||
echo "Webhook failed (HTTP $code): $body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✓ Portainer redeploy triggered (HTTP $code)"
|
|
||||||
depends_on:
|
|
||||||
- push
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Configuration Reference
|
|
||||||
# ============================================
|
|
||||||
#
|
|
||||||
# Woodpecker has no separate "Variables" UI — use Secrets for everything.
|
|
||||||
#
|
|
||||||
# Required Secrets (Repo → Settings → Secrets):
|
|
||||||
# - registry_username: Your Gitea username (used for docker login)
|
|
||||||
# - registry_password: Gitea container registry password or token
|
|
||||||
# - portainer_webhook_url: Portainer stack webhook URL (Redeploy trigger)
|
|
||||||
#
|
|
||||||
# REGISTRY_URL and REGISTRY_REPO are set in this file (above).
|
|
||||||
#
|
|
||||||
# Portainer: Add stack from "Git repository" with this repo, compose path
|
|
||||||
# docker-compose.yml. Enable GitOps → Webhook and "Re-pull image".
|
|
||||||
# Add Gitea registry in Portainer (Settings → Registries) so the host can pull.
|
|
||||||
#
|
|
||||||
# If pipeline doesn't run on push: ensure the repo is activated in Woodpecker,
|
|
||||||
# Gitea has a webhook to Woodpecker for this repo, and your default branch is main.
|
|
||||||
# If Gitea and Woodpecker run on the same host, Gitea may need [webhook]
|
|
||||||
# ALLOWED_HOST_LIST=external,loopback in app.ini so webhooks can reach Woodpecker.
|
|
||||||
35
.woodpecker/ci.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# CI workflow: one clone, one workspace — install → lint → build → test.
|
||||||
|
# Runs on pull requests, push/tag/manual on main, or manual from any branch.
|
||||||
|
# Deploy workflow depends on this (ci) and runs only on main.
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
- event: tag
|
||||||
|
- event: manual
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:20-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile || pnpm install
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
image: node:20-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
- pnpm run lint
|
||||||
|
- pnpm run lint:css
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
image: node:20-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
- pnpm run build
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
image: node:20-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
- pnpm test
|
||||||
76
.woodpecker/deploy.yaml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Deploy workflow: Docker image → push to registry → Portainer webhook.
|
||||||
|
# Runs on push to main, tag, or manual (only when on main).
|
||||||
|
# Waits for ci workflow (install → lint → build → test) to succeed first.
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 'Docker image build'
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
- name: 'Push to registry'
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY_URL: git.mifi.dev
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
|
||||||
|
REGISTRY_USERNAME:
|
||||||
|
from_secret: registry_username
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: registry_password
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- 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:
|
||||||
|
- 'Docker image build'
|
||||||
|
|
||||||
|
- name: 'Trigger Portainer stack redeploy'
|
||||||
|
image: curlimages/curl:latest
|
||||||
|
environment:
|
||||||
|
PORTAINER_WEBHOOK_URL:
|
||||||
|
from_secret: portainer_webhook_url
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Triggering Portainer stack redeploy ==="
|
||||||
|
- |
|
||||||
|
resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL")
|
||||||
|
body=$(echo "$resp" | head -n -1)
|
||||||
|
code=$(echo "$resp" | tail -n 1)
|
||||||
|
if [ "$code" != "200" ] && [ "$code" != "204" ]; then
|
||||||
|
echo "Webhook failed (HTTP $code): $body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Portainer redeploy triggered (HTTP $code)"
|
||||||
|
depends_on:
|
||||||
|
- 'Push to registry'
|
||||||
10
Dockerfile
@@ -1,7 +1,7 @@
|
|||||||
# Static site container for mifi Ventures
|
# Static site container for mifi Ventures
|
||||||
# Build stage: run critical CSS inlining; final stage: serve dist/ via nginx
|
# Build stage: SvelteKit build + Critters; final stage: serve dist/ via nginx
|
||||||
|
|
||||||
# Stage 1: Build (critical CSS inlining + copy assets → dist/)
|
# Stage 1: Build (SvelteKit + critical CSS inlining → dist/)
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -12,8 +12,10 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|||||||
COPY package.json pnpm-lock.yaml* ./
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
RUN pnpm install --frozen-lockfile || pnpm install
|
RUN pnpm install --frozen-lockfile || pnpm install
|
||||||
|
|
||||||
COPY build.mjs ./
|
COPY svelte.config.js vite.config.ts tsconfig.json postcss.config.js ./
|
||||||
COPY site/ ./site/
|
COPY src/ ./src/
|
||||||
|
COPY static/ ./static/
|
||||||
|
COPY scripts/ ./scripts/
|
||||||
|
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|||||||
174
README.md
@@ -4,7 +4,8 @@ A minimal, production-ready static website for mifi Ventures, LLC — a software
|
|||||||
|
|
||||||
## 🏗️ Technology Stack
|
## 🏗️ Technology Stack
|
||||||
|
|
||||||
- **Frontend**: Pure semantic HTML5, modern CSS with CSS variables, minimal JavaScript
|
- **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)
|
- **Server**: nginx (Alpine Linux)
|
||||||
- **Containerization**: Docker
|
- **Containerization**: Docker
|
||||||
- **CI/CD**: Woodpecker CI
|
- **CI/CD**: Woodpecker CI
|
||||||
@@ -18,18 +19,22 @@ A minimal, production-ready static website for mifi Ventures, LLC — a software
|
|||||||
- ✅ **WCAG 2.2 AAA oriented** with strong focus states, keyboard navigation, semantic markup
|
- ✅ **WCAG 2.2 AAA oriented** with strong focus states, keyboard navigation, semantic markup
|
||||||
- ✅ **SEO optimized** with Open Graph, Twitter Cards, JSON-LD structured data
|
- ✅ **SEO optimized** with Open Graph, Twitter Cards, JSON-LD structured data
|
||||||
- ✅ **Performance optimized** with nginx gzip compression and cache headers
|
- ✅ **Performance optimized** with nginx gzip compression and cache headers
|
||||||
- ✅ **Zero frameworks** — pure HTML/CSS/JS for maximum speed and simplicity
|
- ✅ **Minimal JS** — only a tiny copyright-year script; no Svelte runtime or app bundle
|
||||||
|
|
||||||
## 🚀 Local Development
|
## 🚀 Local Development
|
||||||
|
|
||||||
This project uses **pnpm** as the package manager. After cloning, run `pnpm install` (or ensure Corepack is enabled so `pnpm` is available).
|
This project uses **pnpm** as the package manager. After cloning, run `pnpm install` (or ensure Corepack is enabled so `pnpm` is available).
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
| ------------------- | ------------------------------------------------------------------------------ |
|
||||||
| `pnpm install` | Install dependencies |
|
| `pnpm install` | Install dependencies |
|
||||||
| `pnpm run dev` | Serve `site/` at http://localhost:3000 with **live reload** (watcher) |
|
| `pnpm run dev` | SvelteKit dev server at http://localhost:5173 with **live reload** |
|
||||||
| `pnpm run build` | Copy `site/` → `dist/` and inline critical CSS in `index.html` |
|
| `pnpm run build` | SvelteKit build → `dist/`, then Critters inlines critical CSS |
|
||||||
| `pnpm run preview` | Serve built `dist/` to test production output |
|
| `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)
|
### Option 1: pnpm dev (recommended for editing)
|
||||||
|
|
||||||
@@ -39,27 +44,18 @@ From the project root:
|
|||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Opens http://localhost:3000 with live reload when you change files in `site/`.
|
Opens http://localhost:5173 with live reload when you change files in `src/` or `static/`.
|
||||||
|
|
||||||
### Option 2: Other local servers (quick start)
|
### Option 2: Preview production build
|
||||||
|
|
||||||
Open `site/index.html` directly in a browser, or use a simple HTTP server:
|
After building, serve the static output:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Python 3
|
pnpm run build
|
||||||
cd site
|
pnpm run preview
|
||||||
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`).
|
Preview uses `serve dist` so you see the same HTML/CSS as in production (including critical CSS).
|
||||||
|
|
||||||
### Option 3: Dev Container
|
### Option 3: Dev Container
|
||||||
|
|
||||||
@@ -76,7 +72,7 @@ pnpm install
|
|||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The site is served at **http://localhost:3000** with live reload (port forwarded automatically).
|
The site is served at **http://localhost:5173** (or the port shown) with live reload (port forwarded automatically).
|
||||||
|
|
||||||
### Option 4: Docker (Production-like Test)
|
### Option 4: Docker (Production-like Test)
|
||||||
|
|
||||||
@@ -91,53 +87,51 @@ Then visit: `http://localhost:8080`. Stop with `docker stop mifi-ventures-landin
|
|||||||
|
|
||||||
## 📝 Content Updates
|
## 📝 Content Updates
|
||||||
|
|
||||||
The HTML file includes an editable constants block at the top for easy updates:
|
Content and links are driven by data and components:
|
||||||
|
|
||||||
```html
|
- **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`
|
||||||
EDITABLE CONSTANTS:
|
- **JSON-LD**: `src/lib/data/json-ld.ts`
|
||||||
- DOMAIN
|
- **Layout and sections**: `src/routes/+layout.svelte`, `src/routes/+page.svelte`, and components in `src/lib/components/`
|
||||||
- ORG_NAME
|
|
||||||
- PRINCIPAL_NAME
|
|
||||||
- CAL_LINK
|
|
||||||
- RESUME_PATH
|
|
||||||
- LINKEDIN_URL
|
|
||||||
- GITHUB_URL
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
Update these values directly in `site/index.html` to modify:
|
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).
|
||||||
- Company information
|
|
||||||
- Calendar booking link
|
|
||||||
- Social media links
|
|
||||||
- Resume file path
|
|
||||||
|
|
||||||
## 🗂️ Project Structure
|
## 🗂️ Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
mifi-ventures-landing/
|
mifi-ventures-landing/
|
||||||
├── .devcontainer/ # Dev container for local development
|
├── .devcontainer/ # Dev container for local development
|
||||||
│ ├── devcontainer.json # Dev container config (port 3000, extensions)
|
│ ├── devcontainer.json # Dev container config (extensions)
|
||||||
│ └── Dockerfile # Dev container image (Node + serve)
|
│ └── Dockerfile # Dev container image (Node)
|
||||||
├── .woodpecker.yml # CI/CD pipeline configuration
|
├── .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)
|
├── Dockerfile # Production container (nginx:alpine)
|
||||||
├── nginx.conf # nginx web server configuration
|
├── nginx.conf # nginx web server configuration
|
||||||
├── README.md # This file
|
├── svelte.config.js # SvelteKit config (adapter-static)
|
||||||
├── .gitignore # Git ignore rules
|
├── vite.config.ts # Vite config
|
||||||
└── site/ # Static website files
|
├── postcss.config.js # PostCSS (autoprefixer)
|
||||||
├── index.html # Main HTML file
|
├── scripts/critters.mjs # Post-build critical CSS inlining
|
||||||
├── styles.css # CSS styles (light/dark mode)
|
├── static/ # Static assets (copied to dist as-is)
|
||||||
├── script.js # Minimal JavaScript (dynamic year)
|
│ ├── favicon.svg, favicon.ico, robots.txt
|
||||||
├── robots.txt # Search engine directives
|
│ ├── copyright-year.js # Minimal client script (footer year)
|
||||||
├── favicon.svg # Site favicon
|
│ └── assets/ # Fonts, images, logos, resume.pdf, og-image.png
|
||||||
└── assets/
|
├── src/
|
||||||
├── resume.pdf # Resume download (placeholder)
|
│ ├── app.css # Global tokens + base styles
|
||||||
└── logos/ # Company logo SVGs
|
│ ├── app.html # HTML shell for SvelteKit
|
||||||
├── atlassian.svg
|
│ ├── app.d.ts # SvelteKit types
|
||||||
├── tjx.svg
|
│ ├── routes/
|
||||||
├── cargurus.svg
|
│ │ ├── +layout.ts # Prerender, csr: false
|
||||||
├── timberland.svg
|
│ │ ├── +layout.svelte # Shell, head, skip link, slot
|
||||||
└── mfa-boston.svg
|
│ │ ├── +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)
|
## 🚢 CI/CD Deployment (Woodpecker + Gitea)
|
||||||
@@ -146,9 +140,12 @@ mifi-ventures-landing/
|
|||||||
|
|
||||||
### Pipeline Overview
|
### Pipeline Overview
|
||||||
|
|
||||||
The `.woodpecker.yml` pipeline automates deployment on push to `main`:
|
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`
|
1. **Build** — Builds Docker image tagged with commit SHA + `latest`
|
||||||
|
|
||||||
2. **Push** — Pushes images to private Docker registry
|
2. **Push** — Pushes images to private Docker registry
|
||||||
3. **Deploy** — SSH to Linode VPS, pulls latest image, restarts container with health checks
|
3. **Deploy** — SSH to Linode VPS, pulls latest image, restarts container with health checks
|
||||||
|
|
||||||
@@ -159,7 +156,7 @@ The `.woodpecker.yml` pipeline automates deployment on push to `main`:
|
|||||||
Navigate to your repository → Settings → Secrets and add:
|
Navigate to your repository → Settings → Secrets and add:
|
||||||
|
|
||||||
| Secret Name | Description | Example |
|
| Secret Name | Description | Example |
|
||||||
|-------------|-------------|---------|
|
| ------------------- | --------------------------------- | ---------------------------------------- |
|
||||||
| `registry_password` | Docker registry password or token | `dckr_pat_xxxxx` |
|
| `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_host` | Linode VPS hostname or IP | `vps.example.com` or `192.0.2.1` |
|
||||||
| `deploy_username` | SSH username | `deploy` or `root` |
|
| `deploy_username` | SSH username | `deploy` or `root` |
|
||||||
@@ -167,6 +164,7 @@ Navigate to your repository → Settings → Secrets and add:
|
|||||||
| `deploy_port` | SSH port | `22` (default) |
|
| `deploy_port` | SSH port | `22` (default) |
|
||||||
|
|
||||||
**Generate SSH key for deployment:**
|
**Generate SSH key for deployment:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
|
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
|
# Add public key to server: ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub user@host
|
||||||
@@ -178,7 +176,7 @@ ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
|
|||||||
Set these as repository or organization-level variables:
|
Set these as repository or organization-level variables:
|
||||||
|
|
||||||
| Variable | Description | Example |
|
| Variable | Description | Example |
|
||||||
|----------|-------------|---------|
|
| ------------------- | -------------------------- | -------------------------------------------- |
|
||||||
| `REGISTRY_URL` | Docker registry base URL | `registry.example.com` |
|
| `REGISTRY_URL` | Docker registry base URL | `registry.example.com` |
|
||||||
| `REGISTRY_REPO` | Full image repository path | `registry.example.com/mifi-ventures-landing` |
|
| `REGISTRY_REPO` | Full image repository path | `registry.example.com/mifi-ventures-landing` |
|
||||||
| `REGISTRY_USERNAME` | Registry username | `myusername` |
|
| `REGISTRY_USERNAME` | Registry username | `myusername` |
|
||||||
@@ -191,22 +189,21 @@ Set these as repository or organization-level variables:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Secrets (Values tab)
|
# Secrets (Values tab)
|
||||||
registry_password: "your-registry-token"
|
registry_password: 'your-registry-token'
|
||||||
deploy_host: "123.45.67.89"
|
deploy_host: '123.45.67.89'
|
||||||
deploy_username: "deploy"
|
deploy_username: 'deploy'
|
||||||
deploy_ssh_key: |
|
deploy_ssh_key: |
|
||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
...
|
...
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
deploy_port: "22"
|
deploy_port: '22'
|
||||||
|
|
||||||
# Environment Variables (Variables tab)
|
# Environment Variables (Variables tab)
|
||||||
REGISTRY_URL: "registry.example.com"
|
REGISTRY_URL: 'registry.example.com'
|
||||||
REGISTRY_REPO: "registry.example.com/mifi-ventures-landing"
|
REGISTRY_REPO: 'registry.example.com/mifi-ventures-landing'
|
||||||
REGISTRY_USERNAME: "myuser"
|
REGISTRY_USERNAME: 'myuser'
|
||||||
CONTAINER_NAME: "mifi-ventures-landing"
|
CONTAINER_NAME: 'mifi-ventures-landing'
|
||||||
APP_PORT: "8080"
|
APP_PORT: '8080'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pipeline Features
|
### Pipeline Features
|
||||||
@@ -221,15 +218,21 @@ APP_PORT: "8080"
|
|||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
**Build fails:**
|
**Build fails:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Build locally first (must succeed before Docker)
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
# Check Dockerfile syntax
|
# Check Dockerfile syntax
|
||||||
docker build -t test .
|
docker build -t test .
|
||||||
|
|
||||||
# Verify files are present
|
# Verify source is present
|
||||||
ls -la site/
|
ls -la src/ static/
|
||||||
```
|
```
|
||||||
|
|
||||||
**Push fails:**
|
**Push fails:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test registry login locally
|
# Test registry login locally
|
||||||
echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
||||||
@@ -238,6 +241,7 @@ echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deploy fails:**
|
**Deploy fails:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test SSH connection
|
# Test SSH connection
|
||||||
ssh -i ~/.ssh/key user@host "docker ps"
|
ssh -i ~/.ssh/key user@host "docker ps"
|
||||||
@@ -250,6 +254,7 @@ ssh user@host "docker --version"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Container fails health check:**
|
**Container fails health check:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SSH to server and check logs
|
# SSH to server and check logs
|
||||||
ssh user@host "docker logs mifi-ventures-landing"
|
ssh user@host "docker logs mifi-ventures-landing"
|
||||||
@@ -285,6 +290,7 @@ EOF
|
|||||||
The custom `nginx.conf` provides optimized static file delivery:
|
The custom `nginx.conf` provides optimized static file delivery:
|
||||||
|
|
||||||
### Caching Strategy
|
### Caching Strategy
|
||||||
|
|
||||||
- **HTML files**: `no-cache, must-revalidate` (always fresh from server)
|
- **HTML files**: `no-cache, must-revalidate` (always fresh from server)
|
||||||
- **CSS/JS**: `max-age=31536000, immutable` (1 year, content-addressed)
|
- **CSS/JS**: `max-age=31536000, immutable` (1 year, content-addressed)
|
||||||
- **Images** (JPG, PNG, WebP, AVIF): `max-age=2592000` (30 days)
|
- **Images** (JPG, PNG, WebP, AVIF): `max-age=2592000` (30 days)
|
||||||
@@ -295,7 +301,9 @@ The custom `nginx.conf` provides optimized static file delivery:
|
|||||||
- **favicon.svg**: `max-age=2592000` (30 days)
|
- **favicon.svg**: `max-age=2592000` (30 days)
|
||||||
|
|
||||||
### Gzip Compression
|
### Gzip Compression
|
||||||
|
|
||||||
Enabled for all text-based content with compression level 6:
|
Enabled for all text-based content with compression level 6:
|
||||||
|
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- JSON, XML
|
- JSON, XML
|
||||||
- SVG images
|
- SVG images
|
||||||
@@ -303,6 +311,7 @@ Enabled for all text-based content with compression level 6:
|
|||||||
Minimum size: 256 bytes (avoids compressing tiny files)
|
Minimum size: 256 bytes (avoids compressing tiny files)
|
||||||
|
|
||||||
### Other Features
|
### Other Features
|
||||||
|
|
||||||
- **Server tokens**: Disabled for security
|
- **Server tokens**: Disabled for security
|
||||||
- **Access logs**: Disabled for static assets (performance)
|
- **Access logs**: Disabled for static assets (performance)
|
||||||
- **Hidden files**: Denied (.git, .env, etc.)
|
- **Hidden files**: Denied (.git, .env, etc.)
|
||||||
@@ -310,6 +319,7 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
|||||||
- **Health check**: Available on port 80 for container orchestration
|
- **Health check**: Available on port 80 for container orchestration
|
||||||
|
|
||||||
### Security Headers
|
### 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.
|
**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
|
## 🎯 SEO & Performance
|
||||||
@@ -317,6 +327,7 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
|||||||
### Current Optimizations
|
### Current Optimizations
|
||||||
|
|
||||||
#### On-Page SEO
|
#### On-Page SEO
|
||||||
|
|
||||||
- **Title tag**: Includes business name, service, and location
|
- **Title tag**: Includes business name, service, and location
|
||||||
- **Meta description**: Natural, compelling copy (155 characters) emphasizing Boston location and services
|
- **Meta description**: Natural, compelling copy (155 characters) emphasizing Boston location and services
|
||||||
- **Canonical URL**: Set to `https://mifi.ventures/` to prevent duplicate content issues
|
- **Canonical URL**: Set to `https://mifi.ventures/` to prevent duplicate content issues
|
||||||
@@ -327,6 +338,7 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
|||||||
- **Language declaration**: `lang="en-US"` for US English
|
- **Language declaration**: `lang="en-US"` for US English
|
||||||
|
|
||||||
#### Social Media Share Previews
|
#### Social Media Share Previews
|
||||||
|
|
||||||
- **Open Graph tags**: Complete OG implementation for Facebook, LinkedIn
|
- **Open Graph tags**: Complete OG implementation for Facebook, LinkedIn
|
||||||
- Site name, title, description, URL, image
|
- Site name, title, description, URL, image
|
||||||
- Image dimensions (1200x630px) and alt text
|
- Image dimensions (1200x630px) and alt text
|
||||||
@@ -337,7 +349,9 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
|||||||
- **Theme colors**: Dynamic based on light/dark mode preference
|
- **Theme colors**: Dynamic based on light/dark mode preference
|
||||||
|
|
||||||
#### Structured Data (JSON-LD)
|
#### Structured Data (JSON-LD)
|
||||||
|
|
||||||
Comprehensive @graph structure with interconnected entities:
|
Comprehensive @graph structure with interconnected entities:
|
||||||
|
|
||||||
- **Organization** (`#organization`): mifi Ventures, LLC with Boston address, geo coordinates, and service catalog
|
- **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
|
- **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
|
- **WebSite** (`#website`): Site-level metadata with ReserveAction pointing to Cal.com scheduling
|
||||||
@@ -347,6 +361,7 @@ Comprehensive @graph structure with interconnected entities:
|
|||||||
- **No email or phone**: Complies with privacy requirements
|
- **No email or phone**: Complies with privacy requirements
|
||||||
|
|
||||||
#### Technical SEO
|
#### Technical SEO
|
||||||
|
|
||||||
- **robots.txt**: Properly configured for full site crawling
|
- **robots.txt**: Properly configured for full site crawling
|
||||||
- **Lazy loading**: Images load on-demand for performance
|
- **Lazy loading**: Images load on-demand for performance
|
||||||
- **Minimal JavaScript**: Only essential scripts (copyright year)
|
- **Minimal JavaScript**: Only essential scripts (copyright year)
|
||||||
@@ -358,6 +373,7 @@ Comprehensive @graph structure with interconnected entities:
|
|||||||
### Action Items
|
### Action Items
|
||||||
|
|
||||||
Before launch, update these placeholders:
|
Before launch, update these placeholders:
|
||||||
|
|
||||||
1. Create OG image: 1200x630px PNG at `/assets/og-image.png`
|
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
|
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)
|
3. Update GitHub URL in footer and constants if you want to include it (currently optional)
|
||||||
@@ -365,6 +381,7 @@ Before launch, update these placeholders:
|
|||||||
### SEO Testing & Validation
|
### SEO Testing & Validation
|
||||||
|
|
||||||
Before going live, validate with these tools:
|
Before going live, validate with these tools:
|
||||||
|
|
||||||
- **Google Search Console**: Submit site, monitor indexing
|
- **Google Search Console**: Submit site, monitor indexing
|
||||||
- **Rich Results Test**: Verify JSON-LD structured data
|
- **Rich Results Test**: Verify JSON-LD structured data
|
||||||
- **Facebook Sharing Debugger**: Test OG tags preview
|
- **Facebook Sharing Debugger**: Test OG tags preview
|
||||||
@@ -374,6 +391,7 @@ Before going live, validate with these tools:
|
|||||||
- **PageSpeed Insights**: Check Core Web Vitals
|
- **PageSpeed Insights**: Check Core Web Vitals
|
||||||
|
|
||||||
Key metrics to monitor post-launch:
|
Key metrics to monitor post-launch:
|
||||||
|
|
||||||
- Indexing status in Google Search Console
|
- Indexing status in Google Search Console
|
||||||
- Click-through rates (CTR) from search results
|
- Click-through rates (CTR) from search results
|
||||||
- Share engagement on social platforms
|
- Share engagement on social platforms
|
||||||
@@ -398,6 +416,7 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
|||||||
### Implemented Features
|
### Implemented Features
|
||||||
|
|
||||||
#### Keyboard Navigation
|
#### Keyboard Navigation
|
||||||
|
|
||||||
- **Skip link**: Visible on keyboard focus, jumps directly to main content (`#main`)
|
- **Skip link**: Visible on keyboard focus, jumps directly to main content (`#main`)
|
||||||
- **Logical tab order**: All interactive elements follow natural reading order
|
- **Logical tab order**: All interactive elements follow natural reading order
|
||||||
- **No keyboard traps**: Users can navigate through and exit all interactive regions
|
- **No keyboard traps**: Users can navigate through and exit all interactive regions
|
||||||
@@ -405,12 +424,14 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
|||||||
- **Focus never removed**: Outline styles are enforced with `!important` to prevent accidental removal
|
- **Focus never removed**: Outline styles are enforced with `!important` to prevent accidental removal
|
||||||
|
|
||||||
#### Semantic Structure
|
#### Semantic Structure
|
||||||
|
|
||||||
- **Proper landmarks**: `<header>`, `<main>`, `<footer>`, and `<nav>` for clear page regions
|
- **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
|
- **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
|
- **ARIA labelledby**: All sections connected to their headings via `aria-labelledby` attributes
|
||||||
- **Language declaration**: `lang="en-US"` attribute on `<html>` element
|
- **Language declaration**: `lang="en-US"` attribute on `<html>` element
|
||||||
|
|
||||||
#### Visual & Color
|
#### Visual & Color
|
||||||
|
|
||||||
- **AAA contrast ratios**: All text meets AAA standards (7:1 for normal text, 4.5:1 for large text)
|
- **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)
|
- Light mode: `#1a1a1a` text on `#ffffff` background (16.1:1 ratio)
|
||||||
- Dark mode: `#f5f5f5` text on `#0a0a0a` background (18.4:1 ratio)
|
- Dark mode: `#f5f5f5` text on `#0a0a0a` background (18.4:1 ratio)
|
||||||
@@ -418,23 +439,27 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
|||||||
- **High contrast mode**: Enhanced borders, outlines, and contrast for users with `prefers-contrast: high`
|
- **High contrast mode**: Enhanced borders, outlines, and contrast for users with `prefers-contrast: high`
|
||||||
|
|
||||||
#### Interactive Elements
|
#### Interactive Elements
|
||||||
|
|
||||||
- **Adequate touch targets**: All buttons and links meet minimum 44x44px size (AAA requirement)
|
- **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
|
- **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)"
|
- **External link warnings**: Links opening in new tabs clearly labeled "(opens in new tab)"
|
||||||
- **Button spacing**: Generous gaps between CTAs prevent accidental activation
|
- **Button spacing**: Generous gaps between CTAs prevent accidental activation
|
||||||
|
|
||||||
#### Motion & Animation
|
#### Motion & Animation
|
||||||
|
|
||||||
- **Respects `prefers-reduced-motion`**: All animations and transforms disabled when users prefer reduced motion
|
- **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
|
- **Safe default animations**: Subtle hover effects that don't cause vestibular issues
|
||||||
- **No auto-playing content**: No carousels, videos, or content that moves automatically
|
- **No auto-playing content**: No carousels, videos, or content that moves automatically
|
||||||
|
|
||||||
#### Images & Media
|
#### Images & Media
|
||||||
|
|
||||||
- **Descriptive alt text**: All images have clear, concise alternative text
|
- **Descriptive alt text**: All images have clear, concise alternative text
|
||||||
- **Text fallbacks**: Logo strip includes visually-hidden text that appears if images fail
|
- **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
|
- **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
|
- **Decorative images marked**: Images that don't convey content use appropriate ARIA attributes
|
||||||
|
|
||||||
#### Screen Reader Support
|
#### Screen Reader Support
|
||||||
|
|
||||||
- **Clear labels**: All form controls, buttons, and navigation have proper labels
|
- **Clear labels**: All form controls, buttons, and navigation have proper labels
|
||||||
- **ARIA landmarks**: Supplementary ARIA roles for enhanced screen reader navigation
|
- **ARIA landmarks**: Supplementary ARIA roles for enhanced screen reader navigation
|
||||||
- **Visually-hidden content**: Important text available to screen readers but hidden visually where appropriate
|
- **Visually-hidden content**: Important text available to screen readers but hidden visually where appropriate
|
||||||
@@ -443,6 +468,7 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
|||||||
### Testing Recommendations
|
### Testing Recommendations
|
||||||
|
|
||||||
For best results, test with:
|
For best results, test with:
|
||||||
|
|
||||||
- **Keyboard only**: Tab through entire page without mouse
|
- **Keyboard only**: Tab through entire page without mouse
|
||||||
- **Screen readers**: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
|
- **Screen readers**: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
|
||||||
- **Browser extensions**: axe DevTools, WAVE, Lighthouse accessibility audit
|
- **Browser extensions**: axe DevTools, WAVE, Lighthouse accessibility audit
|
||||||
|
|||||||
164
SEO-CHECKLIST.md
@@ -1,164 +0,0 @@
|
|||||||
# SEO Pre-Launch Checklist
|
|
||||||
|
|
||||||
Use this checklist before deploying to production.
|
|
||||||
|
|
||||||
## ✅ Meta Tags
|
|
||||||
|
|
||||||
- [ ] Title tag is descriptive and includes location (under 60 characters)
|
|
||||||
- [ ] Meta description is compelling and natural (150-160 characters)
|
|
||||||
- [ ] Canonical URL is set to `https://mifi.ventures/`
|
|
||||||
- [ ] Robots meta allows indexing (`index, follow`)
|
|
||||||
- [ ] Language is declared (`lang="en-US"`)
|
|
||||||
- [ ] Author meta tag is present
|
|
||||||
- [ ] Geographic meta tags include Boston coordinates
|
|
||||||
|
|
||||||
## ✅ Open Graph Tags
|
|
||||||
|
|
||||||
- [ ] `og:type` is set to "website"
|
|
||||||
- [ ] `og:url` matches canonical URL
|
|
||||||
- [ ] `og:site_name` is "mifi Ventures"
|
|
||||||
- [ ] `og:title` is descriptive and compelling
|
|
||||||
- [ ] `og:description` matches meta description
|
|
||||||
- [ ] `og:image` points to actual 1200x630px image
|
|
||||||
- [ ] `og:image:width` and `og:image:height` are specified
|
|
||||||
- [ ] `og:image:alt` provides context
|
|
||||||
- [ ] `og:locale` is set to "en_US"
|
|
||||||
|
|
||||||
## ✅ Twitter Cards
|
|
||||||
|
|
||||||
- [ ] Card type is `summary_large_image`
|
|
||||||
- [ ] `twitter:title` matches OG title
|
|
||||||
- [ ] `twitter:description` matches OG description
|
|
||||||
- [ ] `twitter:image` matches OG image
|
|
||||||
- [ ] `twitter:image:alt` is descriptive
|
|
||||||
- [ ] `twitter:creator` handle is updated (if applicable)
|
|
||||||
- [ ] `twitter:site` handle is updated (if applicable)
|
|
||||||
|
|
||||||
## ✅ Structured Data (JSON-LD)
|
|
||||||
|
|
||||||
- [ ] Uses @graph structure with stable @id anchors
|
|
||||||
- [ ] **Organization** entity (`#organization`) is complete
|
|
||||||
- [ ] Legal name matches LLC
|
|
||||||
- [ ] Boston address is accurate
|
|
||||||
- [ ] Geographic coordinates are correct
|
|
||||||
- [ ] hasOfferCatalog links to services
|
|
||||||
- [ ] **Person** entity (`#principal`) is complete
|
|
||||||
- [ ] Name and title are accurate
|
|
||||||
- [ ] worksFor links to organization
|
|
||||||
- [ ] knowsAbout lists relevant expertise
|
|
||||||
- [ ] LinkedIn URL is correct (https://linkedin.com/in/the-mifi)
|
|
||||||
- [ ] **WebSite** entity (`#website`) is complete
|
|
||||||
- [ ] potentialAction/ReserveAction points to Cal.com
|
|
||||||
- [ ] Action name is descriptive
|
|
||||||
- [ ] **WebPage** entity (`#webpage`) is complete
|
|
||||||
- [ ] isPartOf links to website
|
|
||||||
- [ ] primaryImageOfPage is set
|
|
||||||
- [ ] inLanguage is "en-US"
|
|
||||||
- [ ] **OfferCatalog** entity (`#services`) is complete
|
|
||||||
- [ ] All 6 services from "What We Do" are listed
|
|
||||||
- [ ] Descriptions match page copy
|
|
||||||
- [ ] No email or phone anywhere in JSON-LD
|
|
||||||
- [ ] All @id values use mifi.ventures domain
|
|
||||||
|
|
||||||
## ✅ Content & Copy
|
|
||||||
|
|
||||||
- [ ] No keyword stuffing in any content
|
|
||||||
- [ ] Copy sounds natural and professional
|
|
||||||
- [ ] Boston location is mentioned naturally
|
|
||||||
- [ ] Services are clearly described
|
|
||||||
- [ ] No email or phone numbers in meta tags
|
|
||||||
- [ ] All text is grammatically correct
|
|
||||||
- [ ] Tone matches brand (professional, technical, credible)
|
|
||||||
|
|
||||||
## ✅ Technical SEO
|
|
||||||
|
|
||||||
- [ ] `robots.txt` exists and allows crawling
|
|
||||||
- [ ] All images have descriptive alt text
|
|
||||||
- [ ] Heading hierarchy is correct (one H1, logical H2s)
|
|
||||||
- [ ] Links have descriptive anchor text
|
|
||||||
- [ ] No broken links (404s)
|
|
||||||
- [ ] HTTPS is enforced (if applicable)
|
|
||||||
- [ ] Mobile-responsive design
|
|
||||||
- [ ] Fast page load (< 3 seconds)
|
|
||||||
- [ ] favicon.svg is present and loads
|
|
||||||
|
|
||||||
## ✅ Assets
|
|
||||||
|
|
||||||
- [ ] OG image created (1200x630px, under 1MB)
|
|
||||||
- [ ] OG image uploaded to `/assets/og-image.png`
|
|
||||||
- [ ] OG image looks good when scaled down
|
|
||||||
- [ ] OG image includes readable text
|
|
||||||
- [ ] Resume PDF is uploaded (if publishing)
|
|
||||||
- [ ] Company logos are actual logos (not placeholders)
|
|
||||||
|
|
||||||
## ✅ External Links
|
|
||||||
|
|
||||||
- [ ] LinkedIn URL updated in constants and JSON-LD
|
|
||||||
- [ ] GitHub URL updated in constants and JSON-LD
|
|
||||||
- [ ] Cal.com link is correct and working
|
|
||||||
- [ ] All external links open in new tab with `rel="noopener noreferrer"`
|
|
||||||
|
|
||||||
## ✅ Pre-Launch Testing
|
|
||||||
|
|
||||||
Run these tests and fix any issues:
|
|
||||||
|
|
||||||
### Google Tools
|
|
||||||
- [ ] Google Rich Results Test: [https://search.google.com/test/rich-results](https://search.google.com/test/rich-results)
|
|
||||||
- Should show valid ProfessionalService schema
|
|
||||||
- [ ] Google Mobile-Friendly Test: [https://search.google.com/test/mobile-friendly](https://search.google.com/test/mobile-friendly)
|
|
||||||
- Should pass without issues
|
|
||||||
- [ ] Google Lighthouse (in Chrome DevTools)
|
|
||||||
- SEO score: 100/100
|
|
||||||
- Performance: 90+/100
|
|
||||||
- Accessibility: 100/100
|
|
||||||
- Best Practices: 100/100
|
|
||||||
|
|
||||||
### Social Media Preview Tools
|
|
||||||
- [ ] Facebook Sharing Debugger: [https://developers.facebook.com/tools/debug/](https://developers.facebook.com/tools/debug/)
|
|
||||||
- Preview looks correct
|
|
||||||
- Image loads properly
|
|
||||||
- [ ] Twitter Card Validator: [https://cards-dev.twitter.com/validator](https://cards-dev.twitter.com/validator)
|
|
||||||
- Card preview looks good
|
|
||||||
- Image displays correctly
|
|
||||||
- [ ] LinkedIn Post Inspector: [https://www.linkedin.com/post-inspector/](https://www.linkedin.com/post-inspector/)
|
|
||||||
- Preview is accurate
|
|
||||||
|
|
||||||
### SEO Validators
|
|
||||||
- [ ] Schema.org Validator: [https://validator.schema.org/](https://validator.schema.org/)
|
|
||||||
- No errors in JSON-LD
|
|
||||||
- [ ] W3C HTML Validator: [https://validator.w3.org/](https://validator.w3.org/)
|
|
||||||
- No critical HTML errors
|
|
||||||
|
|
||||||
### Manual Checks
|
|
||||||
- [ ] View source and verify all meta tags are present
|
|
||||||
- [ ] Test share preview by sharing URL on social media
|
|
||||||
- [ ] Check that `robots.txt` is accessible at `https://mifi.ventures/robots.txt`
|
|
||||||
- [ ] Verify canonical URL in browser DevTools
|
|
||||||
- [ ] Test page on mobile device
|
|
||||||
- [ ] Verify skip link appears on Tab key press
|
|
||||||
|
|
||||||
## ✅ Post-Launch
|
|
||||||
|
|
||||||
After going live:
|
|
||||||
|
|
||||||
- [ ] Submit site to Google Search Console
|
|
||||||
- [ ] Submit site to Bing Webmaster Tools
|
|
||||||
- [ ] Create and submit sitemap.xml (future enhancement)
|
|
||||||
- [ ] Share on social media to test previews
|
|
||||||
- [ ] Monitor Google Search Console for indexing issues
|
|
||||||
- [ ] Set up Google Analytics (if desired)
|
|
||||||
- [ ] Monitor Core Web Vitals
|
|
||||||
- [ ] Check search appearance after 1-2 weeks
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
Record any issues or observations during testing:
|
|
||||||
|
|
||||||
```
|
|
||||||
[Add your notes here]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: [Date]
|
|
||||||
**Reviewed By**: [Name]
|
|
||||||
48
build.mjs
@@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Build script: copies entire site/ to dist/, then inlines critical CSS in dist/index.html.
|
|
||||||
* Uses Critters (no headless browser) so the build runs in any environment.
|
|
||||||
* Dockerfile copies only dist/ — single source of truth for the built site.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Critters from "critters";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
const ROOT = path.join(__dirname);
|
|
||||||
const SITE = path.join(ROOT, "site");
|
|
||||||
const DIST = path.join(ROOT, "dist");
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Copy entire site structure to dist
|
|
||||||
if (fs.existsSync(DIST)) {
|
|
||||||
fs.rmSync(DIST, { recursive: true });
|
|
||||||
}
|
|
||||||
fs.cpSync(SITE, DIST, { recursive: true });
|
|
||||||
console.log("✓ Copied site/ → dist/");
|
|
||||||
|
|
||||||
// Inline critical CSS in dist/index.html (Critters reads/writes relative to path)
|
|
||||||
const indexPath = path.join(DIST, "index.html");
|
|
||||||
let html = fs.readFileSync(indexPath, "utf8");
|
|
||||||
|
|
||||||
const critters = new Critters({
|
|
||||||
path: DIST,
|
|
||||||
preload: "default",
|
|
||||||
noscriptFallback: true,
|
|
||||||
pruneSource: false,
|
|
||||||
logLevel: "warn",
|
|
||||||
});
|
|
||||||
|
|
||||||
html = await critters.process(html);
|
|
||||||
fs.writeFileSync(indexPath, html, "utf8");
|
|
||||||
console.log("✓ Critical CSS inlined → dist/index.html");
|
|
||||||
|
|
||||||
console.log("Build complete. Output: dist/");
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Deployment Guide
|
# Deployment Guide
|
||||||
|
|
||||||
Woodpecker builds the site, pushes the image to **Gitea’s container registry**, then triggers a **Portainer stack redeploy** via webhook. The stack on your Linode VPS pulls the new image and recreates the container.
|
Woodpecker runs the SvelteKit build (`pnpm run build` → `dist/`), builds the Docker image from that output, pushes the image to **Gitea’s container registry**, then triggers a **Portainer stack redeploy** via webhook. The stack on your Linode VPS pulls the new image and recreates the container. Opening a pull request runs a separate workflow (lint, tests, and a test build) on the branch without building or pushing the Docker image.
|
||||||
|
|
||||||
## Portainer stack options
|
## Portainer stack options
|
||||||
|
|
||||||
@@ -34,9 +34,11 @@ Portainer pulls `git.mifi.dev/mifi-ventures/landing:latest`. If that image has n
|
|||||||
|
|
||||||
**Option B – Push from your machine:**
|
**Option B – Push from your machine:**
|
||||||
|
|
||||||
Use Docker (or OrbStack, Colima, Rancher Desktop) from the repo root. **If you’re on Apple Silicon (M1/M2/M3) or another ARM Mac**, the VPS is x86_64, so build for that platform to avoid “exec format error”:
|
From the repo root, run `pnpm install` and `pnpm run build` to produce `dist/`, then use Docker (or OrbStack, Colima, Rancher Desktop). **If you’re on Apple Silicon (M1/M2/M3) or another ARM Mac**, the VPS is x86_64, so build for that platform to avoid “exec format error”:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build # SvelteKit → dist/; Critters inlines critical CSS
|
||||||
docker login git.mifi.dev # use your Gitea username and token/password
|
docker login git.mifi.dev # use your Gitea username and token/password
|
||||||
docker buildx build --platform linux/amd64 -t git.mifi.dev/mifi-ventures/landing:latest --push .
|
docker buildx build --platform linux/amd64 -t git.mifi.dev/mifi-ventures/landing:latest --push .
|
||||||
```
|
```
|
||||||
@@ -86,17 +88,25 @@ Woodpecker has no separate “Variables” UI — add everything under **Repo
|
|||||||
| `registry_password` | Gitea token or password (package write to the repo’s registry) |
|
| `registry_password` | Gitea token or password (package write to the repo’s registry) |
|
||||||
| `portainer_webhook_url` | Portainer stack webhook URL (from step 2) |
|
| `portainer_webhook_url` | Portainer stack webhook URL (from step 2) |
|
||||||
|
|
||||||
`REGISTRY_URL` and `REGISTRY_REPO` are set in `.woodpecker.yml`; you don’t need to add them anywhere.
|
`REGISTRY_URL` and `REGISTRY_REPO` are set in `.woodpecker/deploy.yaml`; you don’t need to add them anywhere.
|
||||||
|
|
||||||
### 5. Test Deployment
|
### 5. Test Deployment
|
||||||
|
|
||||||
#### Local Test Build
|
#### Local Test Build
|
||||||
|
|
||||||
|
The Docker image is built from the contents of `dist/`. That directory is produced by the SvelteKit build, so you must run `pnpm run build` before `docker build` (or let the Dockerfile run it inside the image).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone repo
|
# Clone repo
|
||||||
git clone https://git.mifi.dev/mifi-ventures/landing.git
|
git clone https://git.mifi.dev/mifi-ventures/landing.git
|
||||||
cd landing
|
cd landing
|
||||||
|
|
||||||
# Build locally
|
# Install deps and build static site (SvelteKit + Critters)
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# Build Docker image (uses dist/ from the previous step if built on host,
|
||||||
|
# or the Dockerfile runs the build inside the container)
|
||||||
docker build -t test .
|
docker build -t test .
|
||||||
|
|
||||||
# Run locally
|
# Run locally
|
||||||
@@ -109,6 +119,8 @@ curl http://localhost:8080
|
|||||||
docker stop test && docker rm test
|
docker stop test && docker rm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To test the static site without Docker, run `pnpm run preview` after `pnpm run build` and open the URL shown (e.g. http://localhost:4173).
|
||||||
|
|
||||||
#### Trigger CI/CD
|
#### Trigger CI/CD
|
||||||
```bash
|
```bash
|
||||||
# Make a small change
|
# Make a small change
|
||||||
@@ -247,5 +259,5 @@ If a bad image was deployed:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: 2026-01-30
|
**Last Updated**: 2026-01-31
|
||||||
**Maintainer**: Mike Fitzpatrick
|
**Maintainer**: Mike Fitzpatrick
|
||||||
45
eslint.config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import svelteConfig from './svelte.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'.svelte-kit/**',
|
||||||
|
'dist/**',
|
||||||
|
'build/**',
|
||||||
|
'node_modules/**',
|
||||||
|
'site/**',
|
||||||
|
'static/**',
|
||||||
|
'build.mjs'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
projectService: true,
|
||||||
|
extraFileExtensions: ['.svelte'],
|
||||||
|
svelteConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.mjs', 'build.mjs'],
|
||||||
|
languageOptions: { globals: { console: 'readonly', process: 'readonly' } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'svelte/no-at-html-tags': 'warn',
|
||||||
|
'svelte/require-each-key': 'off',
|
||||||
|
'svelte/no-navigation-without-resolve': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
51
package.json
@@ -1,17 +1,54 @@
|
|||||||
{
|
{
|
||||||
"name": "mifi-ventures-landing",
|
"name": "mifi-ventures-landing",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "https://git.mifi.dev/mifi-ventures/landing.git",
|
"repository": "https://git.mifi.dev/mifi-ventures/landing.git",
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@10.28.2",
|
||||||
"description": "mifi Ventures landing site — static build with critical CSS inlining",
|
"description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.mjs",
|
"build": "vite build && node scripts/critters.mjs",
|
||||||
"preview": "npx serve dist",
|
"dev": "vite dev",
|
||||||
"dev": "live-server site --port=3000 --open=/"
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write \"src/**/*.{ts,js,svelte,css,json}\"",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"lint:css": "stylelint \"src/**/*.css\" \"src/**/*.svelte\"",
|
||||||
|
"lint:css:fix": "stylelint \"src/**/*.css\" \"src/**/*.svelte\" --fix",
|
||||||
|
"preview": "serve dist -p 4173",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:unit": "vitest run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:all": "vitest run && playwright test",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
|
"@playwright/test": "^1.58.1",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.15.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"critters": "^0.0.24",
|
"critters": "^0.0.24",
|
||||||
"live-server": "^1.2.2"
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-svelte": "^3.14.0",
|
||||||
|
"postcss-html": "^1.8.1",
|
||||||
|
"postcss-preset-env": "^11.1.2",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
|
"serve": "^14.2.5",
|
||||||
|
"stylelint": "^17.1.0",
|
||||||
|
"stylelint-config-standard": "^40.0.0",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-eslint-parser": "^1.4.1",
|
||||||
|
"tslib": "^2.8.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"typescript-eslint": "^8.54.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
playwright.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: 'tests',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4173',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||||
|
webServer: process.env.CI
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
command: 'pnpm run preview',
|
||||||
|
url: 'http://localhost:4173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
5118
pnpm-lock.yaml
generated
17
postcss.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* PostCSS: nesting, future CSS (stage 2), and autoprefixing.
|
||||||
|
* postcss-preset-env compiles nesting (&), custom properties, and other
|
||||||
|
* stage-2 features, and runs autoprefixer from browserslist.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-env': {
|
||||||
|
stage: 2,
|
||||||
|
features: {
|
||||||
|
'nesting-rules': true,
|
||||||
|
'custom-properties': true,
|
||||||
|
},
|
||||||
|
autoprefixer: { grid: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
71
scripts/critters.mjs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Post-build: inline critical CSS in dist/*.html (SvelteKit adapter-static output).
|
||||||
|
* Runs after vite build; Critters reads/writes relative to dist/.
|
||||||
|
*
|
||||||
|
* Critters with preload:'swap' adds onload but does not set rel="preload" as="style",
|
||||||
|
* so the link stays render-blocking. We fix that in postProcessSwapLinks().
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Critters from 'critters';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
const DIST = path.join(ROOT, 'dist');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Critters leaves rel="stylesheet" on swap links; change to rel="preload" as="style"
|
||||||
|
* so the full CSS loads async and only applies on load (non-blocking).
|
||||||
|
*/
|
||||||
|
// function postProcessSwapLinks(html) {
|
||||||
|
// return html.replace(/<link\s+([^>]*)>/gi, (full, attrs) => {
|
||||||
|
// if (
|
||||||
|
// !/rel="stylesheet"/i.test(attrs) ||
|
||||||
|
// !/onload="this\.rel='stylesheet'"/i.test(attrs)
|
||||||
|
// ) {
|
||||||
|
// return full;
|
||||||
|
// }
|
||||||
|
// const fixed = attrs
|
||||||
|
// .replace(/\brel="stylesheet"\s*/i, 'rel="preload" as="style" ')
|
||||||
|
// .replace(
|
||||||
|
// /\bonload="this\.rel='stylesheet'"/i,
|
||||||
|
// 'onload="this.onload=null;this.rel=\'stylesheet\'"',
|
||||||
|
// );
|
||||||
|
// return `<link ${fixed}>`;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!fs.existsSync(DIST)) {
|
||||||
|
console.error('dist/ not found. Run vite build first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const critters = new Critters({
|
||||||
|
path: DIST,
|
||||||
|
preload: 'default',
|
||||||
|
noscriptFallback: true,
|
||||||
|
pruneSource: false,
|
||||||
|
logLevel: 'warn',
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = fs.readdirSync(DIST).filter((f) => f.endsWith('.html'));
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(DIST, file);
|
||||||
|
let html = fs.readFileSync(filePath, 'utf8');
|
||||||
|
html = await critters.process(html);
|
||||||
|
// html = postProcessSwapLinks(html);
|
||||||
|
fs.writeFileSync(filePath, html, 'utf8');
|
||||||
|
console.log('✓ Critical CSS inlined → dist/' + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Critical CSS step complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
834
site/index.html
@@ -1,834 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en-US">
|
|
||||||
<head>
|
|
||||||
<!-- Google tag (gtag.js) -->
|
|
||||||
<script
|
|
||||||
async
|
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"
|
|
||||||
></script>
|
|
||||||
<script defer src="/assets/js/ga-init.js"></script>
|
|
||||||
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<title>mifi Ventures — Software Engineering Consulting | Boston, MA</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications. Specializing in frontend architecture, performance optimization, and modern web development."
|
|
||||||
/>
|
|
||||||
<link rel="canonical" href="https://mifi.ventures/" />
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/fraunces-v38-latin-600.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/fraunces-v38-latin-700.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/inter-v20-latin-regular.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/inter-v20-latin-italic.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/inter-v20-latin-500.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/inter-v20-latin-600.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/assets/fonts/inter-v20-latin-700.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="robots"
|
|
||||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
|
||||||
/>
|
|
||||||
<meta name="author" content="Mike Fitzpatrick" />
|
|
||||||
<meta name="geo.region" content="US-MA" />
|
|
||||||
<meta name="geo.placename" content="Boston" />
|
|
||||||
<meta name="geo.position" content="42.360082;-71.058880" />
|
|
||||||
<meta name="ICBM" content="42.360082, -71.058880" />
|
|
||||||
|
|
||||||
<!-- Theme Color -->
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#0052cc"
|
|
||||||
media="(prefers-color-scheme: light)"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#4da6ff"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://mifi.ventures/" />
|
|
||||||
<meta property="og:site_name" content="mifi Ventures" />
|
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content="mifi Ventures — Software Engineering Consulting | Boston, MA"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications. Specializing in frontend architecture, performance optimization, and modern web development."
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:image"
|
|
||||||
content="https://mifi.ventures/assets/og-image.png"
|
|
||||||
/>
|
|
||||||
<meta property="og:image:width" content="1200" />
|
|
||||||
<meta property="og:image:height" content="630" />
|
|
||||||
<meta
|
|
||||||
property="og:image:alt"
|
|
||||||
content="mifi Ventures — Software Engineering Consulting"
|
|
||||||
/>
|
|
||||||
<meta property="og:locale" content="en_US" />
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:url" content="https://mifi.ventures/" />
|
|
||||||
<meta
|
|
||||||
name="twitter:title"
|
|
||||||
content="mifi Ventures — Software Engineering Consulting | Boston, MA"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
|
||||||
content="Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications. Specializing in frontend architecture, performance optimization, and modern web development."
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="twitter:image"
|
|
||||||
content="https://mifi.ventures/assets/og-image.png"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="twitter:image:alt"
|
|
||||||
content="mifi Ventures — Software Engineering Consulting"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
||||||
|
|
||||||
<!-- Styles -->
|
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
|
||||||
|
|
||||||
<!-- Structured Data (JSON-LD) -->
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"@type": "Organization",
|
|
||||||
"@id": "https://mifi.ventures/#organization",
|
|
||||||
"name": "mifi Ventures, LLC",
|
|
||||||
"legalName": "mifi Ventures, LLC",
|
|
||||||
"url": "https://mifi.ventures/",
|
|
||||||
"logo": {
|
|
||||||
"@type": "ImageObject",
|
|
||||||
"url": "https://mifi.ventures/favicon.svg"
|
|
||||||
},
|
|
||||||
"description": "Software engineering consulting specializing in product-focused frontend architecture, performance optimization, and accessibility-first engineering.",
|
|
||||||
"founder": {
|
|
||||||
"@id": "https://mifi.ventures/#principal"
|
|
||||||
},
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"addressLocality": "Boston",
|
|
||||||
"addressRegion": "MA",
|
|
||||||
"addressCountry": "US"
|
|
||||||
},
|
|
||||||
"geo": {
|
|
||||||
"@type": "GeoCoordinates",
|
|
||||||
"latitude": 42.360082,
|
|
||||||
"longitude": -71.05888
|
|
||||||
},
|
|
||||||
"areaServed": {
|
|
||||||
"@type": "Country",
|
|
||||||
"name": "United States"
|
|
||||||
},
|
|
||||||
"hasOfferCatalog": {
|
|
||||||
"@id": "https://mifi.ventures/#services"
|
|
||||||
},
|
|
||||||
"sameAs": [
|
|
||||||
"https://www.linkedin.com/in/the-mifi",
|
|
||||||
"https://github.com/the-mifi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Person",
|
|
||||||
"@id": "https://mifi.ventures/#principal",
|
|
||||||
"name": "Mike Fitzpatrick",
|
|
||||||
"jobTitle": "Principal Software Engineer and Architect",
|
|
||||||
"description": "Senior full-stack engineer and architect helping teams ship reliable, accessible, high-performance web products.",
|
|
||||||
"url": "https://mifi.ventures/",
|
|
||||||
"worksFor": {
|
|
||||||
"@id": "https://mifi.ventures/#organization"
|
|
||||||
},
|
|
||||||
"knowsAbout": [
|
|
||||||
"Frontend Architecture",
|
|
||||||
"UI Architecture",
|
|
||||||
"React Development",
|
|
||||||
"Web Performance Optimization",
|
|
||||||
"Core Web Vitals",
|
|
||||||
"Technical SEO",
|
|
||||||
"Web Accessibility (WCAG)",
|
|
||||||
"Component Libraries",
|
|
||||||
"Design Systems",
|
|
||||||
"JavaScript",
|
|
||||||
"TypeScript",
|
|
||||||
"Modern Web Development",
|
|
||||||
"Greenfield Product Development",
|
|
||||||
"Legacy System Modernization",
|
|
||||||
"Code Refactoring"
|
|
||||||
],
|
|
||||||
"sameAs": [
|
|
||||||
"https://www.linkedin.com/in/the-mifi",
|
|
||||||
"https://github.com/the-mifi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "WebSite",
|
|
||||||
"@id": "https://mifi.ventures/#website",
|
|
||||||
"url": "https://mifi.ventures/",
|
|
||||||
"name": "mifi Ventures",
|
|
||||||
"description": "Software Engineering Consulting — Boston, MA",
|
|
||||||
"publisher": {
|
|
||||||
"@id": "https://mifi.ventures/#organization"
|
|
||||||
},
|
|
||||||
"potentialAction": {
|
|
||||||
"@type": "ReserveAction",
|
|
||||||
"target": {
|
|
||||||
"@type": "EntryPoint",
|
|
||||||
"urlTemplate": "https://cal.mifi.ventures/the-mifi"
|
|
||||||
},
|
|
||||||
"name": "Schedule a 30-minute intro call"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "WebPage",
|
|
||||||
"@id": "https://mifi.ventures/#webpage",
|
|
||||||
"url": "https://mifi.ventures/",
|
|
||||||
"name": "mifi Ventures — Software Engineering Consulting | Boston, MA",
|
|
||||||
"description": "Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications.",
|
|
||||||
"isPartOf": {
|
|
||||||
"@id": "https://mifi.ventures/#website"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"@id": "https://mifi.ventures/#organization"
|
|
||||||
},
|
|
||||||
"mainEntity": {
|
|
||||||
"@id": "https://mifi.ventures/#organization"
|
|
||||||
},
|
|
||||||
"primaryImageOfPage": {
|
|
||||||
"@type": "ImageObject",
|
|
||||||
"url": "https://mifi.ventures/favicon.svg"
|
|
||||||
},
|
|
||||||
"inLanguage": "en-US"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "OfferCatalog",
|
|
||||||
"@id": "https://mifi.ventures/#services",
|
|
||||||
"name": "Software Engineering Consulting Services",
|
|
||||||
"description": "Consulting services offered by mifi Ventures",
|
|
||||||
"numberOfItems": 6,
|
|
||||||
"itemListElement": [
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Frontend and UI Architecture",
|
|
||||||
"description": "Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Greenfield Product Development",
|
|
||||||
"description": "Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Performance Optimization",
|
|
||||||
"description": "Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Accessibility Engineering",
|
|
||||||
"description": "Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "System Modernization",
|
|
||||||
"description": "Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "End-to-End Feature Delivery",
|
|
||||||
"description": "End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Skip to main content link for keyboard users -->
|
|
||||||
<a href="#main" class="skip-link">Skip to main content</a>
|
|
||||||
|
|
||||||
<!-- Nav Section -->
|
|
||||||
<nav id="nav" class="nav" aria-label="Main navigation">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="nav-toggle"
|
|
||||||
class="nav-toggle-input"
|
|
||||||
aria-hidden="true"
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<div class="mobile-nav-header">
|
|
||||||
<span class="mobile nav-header-logo">
|
|
||||||
<img
|
|
||||||
src="/assets/wordmark.svg"
|
|
||||||
alt="mifi Ventures"
|
|
||||||
width="250"
|
|
||||||
height="33"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<label
|
|
||||||
for="nav-toggle"
|
|
||||||
class="nav-toggle"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
|
||||||
<span class="nav-toggle-inner">
|
|
||||||
<span class="nav-toggle-line"></span>
|
|
||||||
<span class="nav-toggle-line"></span>
|
|
||||||
<span class="nav-toggle-line"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="nav-menu container">
|
|
||||||
<span class="nav-header-logo desktop">
|
|
||||||
<img
|
|
||||||
src="/assets/wordmark.svg"
|
|
||||||
alt="mifi Ventures"
|
|
||||||
width="250"
|
|
||||||
height="33"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="#what-we-do" class="nav-link">Services</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="#impact" class="nav-link">Impact</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="#how-we-work" class="nav-link">Process</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="#schedule" class="nav-link">Contact</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="nav-item nav-back-to-top">
|
|
||||||
<a href="#header" class="nav-link">Back to top</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Header + Hero Section -->
|
|
||||||
<header id="header" class="hero">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="logo">
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
viewBox="0 0 3934 513"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:serif="http://www.serif.com/"
|
|
||||||
style="
|
|
||||||
fill-rule: evenodd;
|
|
||||||
clip-rule: evenodd;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-miterlimit: 2;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M0,504.667l0,-362.333l82.5,0l0,83.5l-9.5,-13.5c6.444,-26.222 19.75,-45.778 39.917,-58.667c20.167,-12.889 43.806,-19.333 70.917,-19.333c29.556,0 55.694,7.694 78.417,23.083c22.722,15.389 37.417,35.861 44.083,61.417l-25,2.167c11.222,-29.222 27.917,-50.972 50.083,-65.25c22.167,-14.278 47.75,-21.417 76.75,-21.417c25.667,0 48.611,5.778 68.833,17.333c20.222,11.556 36.194,27.611 47.917,48.167c11.722,20.556 17.583,44.333 17.583,71.333l0,233.5l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-13.889,0 -26.139,3.194 -36.75,9.583c-10.611,6.389 -18.833,15.333 -24.667,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-14,0 -26.278,3.194 -36.833,9.583c-10.556,6.389 -18.75,15.333 -24.583,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M614.5,504.667l0,-362.333l87.5,0l0,362.333l-87.5,0Zm0,-403.333l0,-93.333l87.5,0l0,93.333l-87.5,0Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M823.5,504.667l0,-284.833l-63.833,0l0,-77.5l63.833,0l0,-12c0,-27.889 5.639,-51.472 16.917,-70.75c11.278,-19.278 27.194,-34.028 47.75,-44.25c20.556,-10.222 44.778,-15.333 72.667,-15.333c5.444,0 11.389,0.333 17.833,1c6.444,0.667 11.778,1.444 16,2.333l0,75.333c-4.111,-0.889 -8.083,-1.444 -11.917,-1.667c-3.833,-0.222 -7.361,-0.333 -10.583,-0.333c-19.333,0 -34.361,4.361 -45.083,13.083c-10.722,8.722 -16.083,22.25 -16.083,40.583l0,12l158.667,0l0,77.5l-158.667,0l0,284.833l-87.5,0Zm213.667,0l0,-362.333l87.5,0l0,362.333l-87.5,0Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M1474.74,50.297c0,-3.892 1.365,-6.915 4.096,-9.068c2.731,-2.153 6.86,-3.229 12.388,-3.229l106.333,0c5.83,0 10.083,1.052 12.76,3.156c2.677,2.104 4.016,5.056 4.016,8.854c0,2.844 -0.913,5.219 -2.74,7.125c-1.826,1.906 -5.043,3.74 -9.651,5.5l-13.286,4.479c-6.694,2.681 -12.347,6.961 -16.958,12.841c-4.611,5.88 -8.964,15.216 -13.057,28.008l-114.453,337.807c-2.128,6.608 -3.694,12.082 -4.695,16.424c-1.002,4.342 -1.503,8.846 -1.503,13.513l0,15.141c0,4.351 -1.242,7.741 -3.727,10.172c-2.484,2.431 -5.817,3.646 -9.997,3.646l-71.854,0c-4.354,0 -7.762,-1.215 -10.224,-3.646c-2.462,-2.431 -3.693,-5.965 -3.693,-10.604l0,-14.969c0,-3.542 -0.508,-7.257 -1.523,-11.146c-1.016,-3.889 -2.428,-8.453 -4.237,-13.693l-125.854,-363.062c-2.097,-6.191 -4.4,-10.66 -6.909,-13.406c-2.509,-2.747 -5.987,-4.977 -10.435,-6.693l-14.141,-4.193c-7.497,-2.809 -11.245,-7.128 -11.245,-12.958c0,-3.892 1.394,-6.915 4.182,-9.068c2.788,-2.153 7.002,-3.229 12.641,-3.229l151.687,0c5.75,0 9.968,1.076 12.654,3.229c2.686,2.153 4.029,5.175 4.029,9.068c0,3.128 -1.044,5.646 -3.133,7.552c-2.089,1.906 -5.221,3.55 -9.398,4.932l-25.328,4.427c-5.542,1.573 -8.939,4.237 -10.193,7.992c-1.253,3.755 -0.444,9.841 2.427,18.258l124.146,361.656l-26.328,20.599l125.198,-369.797c3.542,-10.618 4.107,-18.966 1.695,-25.044c-2.411,-6.078 -9.02,-10.76 -19.826,-14.044l-21.573,-4.193c-3.972,-1.382 -7.014,-2.978 -9.125,-4.789c-2.111,-1.811 -3.167,-4.327 -3.167,-7.549Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M1905.721,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.062,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M2076.711,205.234l0,253.599c0,6.017 0.997,10.479 2.99,13.385c1.993,2.906 4.998,4.97 9.016,6.193l14.234,3.453c6.413,2.337 9.62,6.03 9.62,11.078c0,7.816 -5.082,11.724 -15.245,11.724l-124.286,0c-5.035,0 -8.755,-1.004 -11.161,-3.013c-2.406,-2.009 -3.609,-4.721 -3.609,-8.138c0,-2.733 0.865,-5.056 2.596,-6.969c1.731,-1.913 4.438,-3.458 8.122,-4.635l15.234,-3.5c4.031,-1.222 7.04,-3.262 9.026,-6.12c1.986,-2.858 2.979,-7.281 2.979,-13.271l0,-204.677c0,-4.844 -0.782,-8.342 -2.346,-10.495c-1.564,-2.153 -4.126,-3.467 -7.685,-3.943l-20.542,-1c-3.559,-0.667 -6.109,-1.819 -7.651,-3.456c-1.542,-1.637 -2.312,-3.734 -2.312,-6.289c0,-2.972 0.918,-5.387 2.753,-7.245c1.835,-1.858 5.192,-3.66 10.07,-5.406l62.24,-21.5c6.941,-2.556 12.56,-4.39 16.857,-5.503c4.297,-1.113 8.263,-1.669 11.898,-1.669c5.688,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.375 4.336,12.667Zm-9.385,71.365l-12.724,-13.036l13.526,-11.927c26.417,-23.639 49.119,-40.515 68.107,-50.628c18.988,-10.113 37.15,-15.169 54.487,-15.169c26.26,0 46.657,8.724 61.19,26.172c14.533,17.448 23.444,41.141 26.732,71.078l20.208,174.552c0.729,6.417 2.038,11.217 3.927,14.401c1.889,3.184 4.993,5.387 9.313,6.609l13.427,3.26c3.684,1.16 6.391,2.697 8.122,4.612c1.731,1.915 2.596,4.246 2.596,6.992c0,3.417 -1.175,6.129 -3.526,8.138c-2.351,2.009 -6.115,3.013 -11.292,3.013l-125.594,0c-10.198,0 -15.297,-3.908 -15.297,-11.724c-0,-5.017 3.177,-8.71 9.531,-11.078l14.896,-3.453c4.448,-1.222 7.823,-3.425 10.125,-6.609c2.302,-3.184 3.087,-7.905 2.354,-14.161l-18.995,-162.974c-2.476,-20.635 -7.75,-36.081 -15.823,-46.336c-8.073,-10.255 -19.927,-15.383 -35.562,-15.383c-9.844,0 -20.199,2.655 -31.065,7.966c-10.866,5.311 -22.546,13.293 -35.039,23.945l-13.625,11.74Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M2387.845,220.714l-18.271,-4.667c-4.927,-1.479 -8.352,-3.2 -10.273,-5.161c-1.922,-1.962 -2.883,-4.276 -2.883,-6.943c0,-3.51 1.223,-6.199 3.669,-8.065c2.446,-1.866 5.704,-2.799 9.773,-2.799l21.99,0c5.101,-0 9.294,-0.87 12.581,-2.609c3.286,-1.74 6.416,-4.936 9.388,-9.589l34.552,-51.276c3.59,-4.91 7.104,-8.504 10.542,-10.784c3.438,-2.28 6.943,-3.419 10.516,-3.419c3.878,0 6.89,1.22 9.034,3.659c2.144,2.439 3.216,5.905 3.216,10.398l0,288.479c0,15.747 3.147,27.707 9.44,35.88c6.293,8.174 15.034,12.26 26.221,12.26c7.67,0 13.736,-1.373 18.198,-4.12c4.462,-2.747 8.049,-6.002 10.763,-9.766c2.714,-3.764 5.304,-7.236 7.771,-10.417c2.467,-3.181 5.447,-5.205 8.94,-6.073c2.733,-0.177 4.901,0.624 6.505,2.404c1.604,1.78 2.375,4.773 2.312,8.982c-0.507,11.427 -4.431,21.975 -11.773,31.643c-7.342,9.668 -17.384,17.448 -30.125,23.339c-12.741,5.891 -27.367,8.836 -43.878,8.836c-26.191,0 -46.827,-6.628 -61.909,-19.883c-15.082,-13.255 -22.622,-33.345 -22.622,-60.268l0,-192.13c0,-5.128 -1.021,-9.007 -3.063,-11.635c-2.042,-2.628 -5.58,-4.72 -10.615,-6.276Zm61.109,-0.854l0.281,-26.781l106.25,0c4.444,-0 7.866,0.858 10.266,2.573c2.399,1.715 3.599,4.257 3.599,7.625c0,4.733 -2.383,8.68 -7.148,11.841c-4.766,3.161 -12.326,4.742 -22.68,4.742l-90.568,0Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M2855.8,465.307l0,-21.885l-2.146,-1.526l0,-187.313c0,-4.847 -0.782,-8.346 -2.346,-10.497c-1.564,-2.151 -4.126,-3.464 -7.685,-3.94l-20.542,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.917,-5.382 2.75,-7.24c1.833,-1.858 5.189,-3.661 10.068,-5.411l62.24,-21.495c6.924,-2.559 12.538,-4.394 16.844,-5.505c4.306,-1.111 8.276,-1.667 11.911,-1.667c5.687,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.373 4.336,12.661l0,253.365c0,6.017 0.997,10.503 2.99,13.456c1.993,2.953 4.998,4.994 9.016,6.122l14.516,3.312c3.812,1.16 6.609,2.701 8.388,4.622c1.78,1.922 2.669,4.312 2.669,7.169c0,3.417 -1.223,6.129 -3.669,8.138c-2.446,2.009 -6.258,3.013 -11.435,3.013l-65.932,0c-10.229,0 -18.6,-3.578 -25.112,-10.734c-6.512,-7.156 -9.768,-16.698 -9.768,-28.625Zm-208.76,-50.839l0,-159.885c0,-4.847 -0.786,-8.346 -2.357,-10.497c-1.571,-2.151 -4.143,-3.464 -7.716,-3.94l-20.547,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.918,-5.382 2.753,-7.24c1.835,-1.858 5.19,-3.661 10.065,-5.411l62.286,-21.495c7.226,-2.653 12.964,-4.511 17.214,-5.576c4.25,-1.064 7.908,-1.596 10.974,-1.596c5.972,0 10.428,1.576 13.367,4.729c2.939,3.153 4.409,7.373 4.409,12.661l0,197.422c0,20.92 5.023,36.531 15.07,46.833c10.047,10.302 23.452,15.453 40.216,15.453c10.382,0 21.431,-2.572 33.146,-7.716c11.715,-5.144 23.986,-13.207 36.813,-24.19l13.62,-11.74l12.724,13.031l-13.526,11.927c-26.653,24.323 -49.98,41.37 -69.982,51.141c-20.002,9.771 -38.973,14.656 -56.914,14.656c-27.354,0 -49.469,-8.744 -66.344,-26.232c-16.875,-17.488 -25.313,-41.35 -25.313,-71.586Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M3130.82,330.943c0,-31.67 4.576,-58.287 13.729,-79.852c9.153,-21.564 21.071,-37.831 35.755,-48.799c14.684,-10.969 30.319,-16.453 46.906,-16.453c20.118,0 35.681,5.683 46.69,17.049c11.009,11.366 16.513,27.369 16.513,48.008c0,17.24 -3.655,30.185 -10.964,38.836c-7.309,8.651 -16.743,12.977 -28.302,12.977c-11.556,0 -20.418,-3.171 -26.586,-9.513c-6.168,-6.342 -9.284,-15.235 -9.346,-26.68l-0.047,-11.573c-0.111,-7.236 -1.802,-12.64 -5.073,-16.211c-3.271,-3.571 -8.644,-5.357 -16.12,-5.357c-8.635,0 -16.97,3.531 -25.003,10.594c-8.033,7.062 -14.597,17.733 -19.693,32.01c-5.095,14.278 -7.643,32.392 -7.643,54.344l-10.818,0.62Zm6.812,-125.427l4.005,81.208l0,171.87c0,5.497 1.199,9.661 3.596,12.495c2.398,2.833 6.598,4.719 12.602,5.656l29.714,4.427c4.462,0.701 7.769,2.029 9.922,3.982c2.153,1.953 3.229,4.694 3.229,8.221c0,3.51 -1.299,6.27 -3.896,8.279c-2.597,2.009 -6.398,3.013 -11.401,3.013l-147.318,0c-5.083,0 -8.836,-1.013 -11.258,-3.039c-2.422,-2.026 -3.633,-4.739 -3.633,-8.138c0,-2.747 0.885,-5.085 2.656,-7.016c1.771,-1.931 4.514,-3.483 8.229,-4.656l15.068,-3.406c4.035,-1.128 7.044,-3.137 9.029,-6.026c1.984,-2.889 2.977,-7.295 2.977,-13.219l0,-204.391c0,-4.861 -0.779,-8.379 -2.336,-10.555c-1.557,-2.175 -4.114,-3.503 -7.669,-3.982l-20.667,-1c-3.51,-0.667 -6.04,-1.818 -7.589,-3.453c-1.549,-1.635 -2.323,-3.724 -2.323,-6.266c0,-2.955 0.942,-5.393 2.826,-7.315c1.884,-1.922 5.232,-3.709 10.044,-5.362l61.26,-20.76c8.799,-3.306 15.307,-5.466 19.526,-6.482c4.219,-1.016 7.575,-1.523 10.068,-1.523c4.08,0 7.156,1.345 9.229,4.036c2.073,2.691 3.443,7.158 4.109,13.401Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M3622.771,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.063,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M3813.044,487.698c15.937,0 28.422,-4.111 37.453,-12.333c9.031,-8.222 13.547,-18.745 13.547,-31.568c0,-8.125 -1.861,-15.469 -5.583,-22.031c-3.722,-6.562 -10.543,-12.562 -20.464,-17.997c-9.92,-5.436 -24.125,-10.471 -42.615,-15.107c-31.625,-7.188 -56.268,-15.988 -73.93,-26.401c-17.661,-10.413 -30.004,-22.363 -37.029,-35.849c-7.024,-13.486 -10.536,-28.356 -10.536,-44.609c0,-29.288 10.358,-52.604 31.073,-69.948c20.715,-17.344 50.299,-26.016 88.75,-26.016c14.302,0 26.049,1.234 35.24,3.703c9.191,2.469 16.712,4.961 22.562,7.477c5.851,2.516 10.873,3.773 15.068,3.773c4.382,0 7.961,-1.258 10.737,-3.773c2.776,-2.516 5.492,-5.031 8.148,-7.547c2.656,-2.516 5.993,-3.773 10.01,-3.773c2.781,0 5.272,0.905 7.471,2.716c2.2,1.811 4.03,5.162 5.492,10.055l22.667,71.646c2.066,6.226 2.702,11.364 1.909,15.414c-0.793,4.05 -3.287,6.918 -7.482,8.602c-4.097,1.556 -7.667,1.551 -10.708,-0.013c-3.042,-1.564 -5.937,-4.451 -8.687,-8.659c-10.062,-18.646 -20.847,-33.42 -32.354,-44.323c-11.507,-10.903 -23.586,-18.714 -36.237,-23.435c-12.651,-4.72 -25.85,-7.081 -39.596,-7.081c-19.764,0 -34.641,4.182 -44.633,12.547c-9.991,8.365 -14.987,19.563 -14.987,33.594c0,8.41 2.122,16.051 6.367,22.924c4.245,6.873 12.054,13.202 23.427,18.987c11.373,5.785 27.584,11.288 48.633,16.51c27.465,6.378 49.29,14.37 65.474,23.974c16.184,9.604 27.82,21.081 34.909,34.43c7.089,13.349 10.633,28.883 10.633,46.602c0,18.205 -4.623,34.268 -13.87,48.19c-9.247,13.922 -22.141,24.768 -38.682,32.539c-16.542,7.771 -35.79,11.656 -57.745,11.656c-13.83,0 -25.069,-1.468 -33.719,-4.404c-8.649,-2.936 -15.788,-5.848 -21.417,-8.737c-5.628,-2.889 -10.825,-4.333 -15.589,-4.333c-4.257,0 -7.92,1.424 -10.99,4.273c-3.069,2.849 -6.016,5.722 -8.841,8.62c-2.825,2.898 -6.07,4.346 -9.737,4.346c-2.701,0 -5.029,-0.989 -6.982,-2.966c-1.953,-1.977 -3.39,-5.31 -4.31,-9.997l-13.906,-67.13c-1.462,-7.559 -1.775,-13.197 -0.94,-16.914c0.835,-3.717 3.119,-6.322 6.852,-7.815c3.986,-1.587 7.492,-1.376 10.518,0.633c3.026,2.009 6.143,5.641 9.352,10.898c13.649,24.583 28.663,42.171 45.042,52.763c16.378,10.592 33.123,15.888 50.234,15.888Z"
|
|
||||||
style="fill-rule: nonzero"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">mifi Ventures</span>
|
|
||||||
</h1>
|
|
||||||
<p class="headline">Software Engineering Consulting</p>
|
|
||||||
<p class="subhead">
|
|
||||||
Principal: Mike Fitzpatrick — senior full-stack engineer and architect
|
|
||||||
helping teams ship reliable, accessible, high-performance web
|
|
||||||
products.
|
|
||||||
</p>
|
|
||||||
<div class="cta-group">
|
|
||||||
<a
|
|
||||||
href="https://cal.mifi.ventures/the-mifi/30min?utm_source=website&utm_medium=cta&utm_campaign=schedule_call&utm_medium=hero_cta"
|
|
||||||
class="btn btn-primary"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
|
||||||
>
|
|
||||||
Schedule a 30-minute intro call
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/assets/resume.pdf"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
download
|
|
||||||
aria-label="Download Mike Fitzpatrick's resume as PDF"
|
|
||||||
>
|
|
||||||
Download resume
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main id="main">
|
|
||||||
<!-- Experience Includes Section -->
|
|
||||||
<section
|
|
||||||
id="experience"
|
|
||||||
class="section experience-section"
|
|
||||||
aria-labelledby="experience-heading"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="experience-heading" class="section-title">
|
|
||||||
Experience includes teams at:
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<!-- Logo strip with accessible images -->
|
|
||||||
<div class="logo-strip" role="list" aria-label="Company logos">
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/atlassian.svg"
|
|
||||||
alt="Atlassian"
|
|
||||||
loading="lazy"
|
|
||||||
width="2500"
|
|
||||||
height="2500"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">Atlassian</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/tjx.svg"
|
|
||||||
alt="TJ Maxx (The TJX Companies)"
|
|
||||||
loading="lazy"
|
|
||||||
width="2500"
|
|
||||||
height="621"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">TJ Maxx</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/cargurus.svg"
|
|
||||||
alt="CarGurus"
|
|
||||||
loading="lazy"
|
|
||||||
width="2500"
|
|
||||||
height="398"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">CarGurus</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/timberland.svg"
|
|
||||||
alt="Timberland"
|
|
||||||
loading="lazy"
|
|
||||||
width="190"
|
|
||||||
height="35"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">Timberland</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/vf.svg"
|
|
||||||
alt="VF Corporation"
|
|
||||||
loading="lazy"
|
|
||||||
width="190"
|
|
||||||
height="155"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">VF Corporation</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/bottomline.svg"
|
|
||||||
alt="Bottomline Technologies"
|
|
||||||
loading="lazy"
|
|
||||||
width="2702"
|
|
||||||
height="571"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">Bottomline Technologies</span>
|
|
||||||
</div>
|
|
||||||
<div class="logo-item" role="listitem">
|
|
||||||
<img
|
|
||||||
src="/assets/logos/mfa-boston.svg"
|
|
||||||
alt="Museum of Fine Arts Boston"
|
|
||||||
loading="lazy"
|
|
||||||
width="572"
|
|
||||||
height="88"
|
|
||||||
/>
|
|
||||||
<span class="logo-fallback-text">MFA Boston</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Text-only fallback list (visible on very small screens or when images fail) -->
|
|
||||||
<ul class="logo-text-list" aria-hidden="true">
|
|
||||||
<li>Atlassian</li>
|
|
||||||
<li>TJ Maxx (The TJX Companies)</li>
|
|
||||||
<li>CarGurus</li>
|
|
||||||
<li>Timberland</li>
|
|
||||||
<li>VF Corporation</li>
|
|
||||||
<li>Bottomline Technologies</li>
|
|
||||||
<li>Museum of Fine Arts Boston</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p class="footnote">
|
|
||||||
Logos are trademarks of their respective owners.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- What We Do Section -->
|
|
||||||
<section
|
|
||||||
id="what-we-do"
|
|
||||||
class="section"
|
|
||||||
aria-labelledby="what-we-do-heading"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="what-we-do-heading" class="section-title">What We Do</h2>
|
|
||||||
<ul class="content-list">
|
|
||||||
<li>
|
|
||||||
Product-focused frontend and UI architecture for modern web
|
|
||||||
applications, with an emphasis on clarity, scalability, and
|
|
||||||
long-term maintainability.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Greenfield product builds and early-stage foundations, getting new
|
|
||||||
projects off the ground quickly with structures designed to grow,
|
|
||||||
not be rewritten.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Performance, Core Web Vitals, rendering strategy, and technical
|
|
||||||
SEO optimization focused on real-world user journeys—not just lab
|
|
||||||
scores.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Accessibility-first engineering, ensuring WCAG-compliant
|
|
||||||
interfaces with semantic markup, keyboard parity, and inclusive
|
|
||||||
interaction patterns.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Modernization and stabilization of existing systems, including
|
|
||||||
refactors, framework upgrades, and untangling overgrown frontend
|
|
||||||
codebases.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
End-to-end feature delivery with clear ownership and
|
|
||||||
documentation, spanning frontend and supporting backend work
|
|
||||||
without unnecessary complexity.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Selected Impact Section -->
|
|
||||||
<section id="impact" class="section" aria-labelledby="impact-heading">
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="impact-heading" class="section-title">Selected Impact</h2>
|
|
||||||
<ul class="content-list">
|
|
||||||
<li>
|
|
||||||
Get new products off the ground quickly by establishing durable
|
|
||||||
frontend and platform foundations—clean architecture, clear
|
|
||||||
patterns, and pragmatic defaults designed to scale with teams and
|
|
||||||
traffic.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Improve performance, Core Web Vitals, and technical SEO on
|
|
||||||
high-traffic user journeys through rendering strategy, bundle
|
|
||||||
discipline, and careful attention to real-world loading behavior.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Build accessibility into core UI systems, not as a
|
|
||||||
retrofit—semantic markup, keyboard parity, and screen reader
|
|
||||||
support baked into reusable components and design patterns.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Bring order to complex or aging codebases by simplifying
|
|
||||||
structure, reducing duplication, and clarifying ownership,
|
|
||||||
enabling teams to ship confidently without over-engineering.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Design and evolve shared component libraries and UI systems that
|
|
||||||
improve consistency, velocity, and long-term maintainability
|
|
||||||
across multiple teams.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Partner closely with product, design, and engineering leadership
|
|
||||||
(including marketing teams and non-technical organizations) to
|
|
||||||
translate goals into shippable systems, balancing speed, quality,
|
|
||||||
and technical risk.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- How We Work Section -->
|
|
||||||
<section
|
|
||||||
id="how-we-work"
|
|
||||||
class="section"
|
|
||||||
aria-labelledby="how-we-work-heading"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="how-we-work-heading" class="section-title">How We Work</h2>
|
|
||||||
<ul class="content-list">
|
|
||||||
<li>
|
|
||||||
Engagements are consulting-led and senior-driven. I work directly
|
|
||||||
with founders, product leaders, marketing teams, and engineering
|
|
||||||
teams—including organizations without in-house technical staff—to
|
|
||||||
establish direction and deliver solutions with a high degree of
|
|
||||||
autonomy.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Focused, pragmatic scope. Work is scoped to deliver real progress
|
|
||||||
quickly, with an emphasis on building the right foundation rather
|
|
||||||
than over-engineering for hypothetical futures.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Async-friendly, low-friction communication. Clear written updates,
|
|
||||||
documented decisions, and scheduled calls when they add value—not
|
|
||||||
meetings for their own sake.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Quality as a default. Accessibility, performance, and
|
|
||||||
maintainability are built into the work from the start, not added
|
|
||||||
later as cleanup.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Flexible engagement models. Hourly or fixed-scope work depending
|
|
||||||
on clarity and needs; longer-term engagements welcome when there's
|
|
||||||
ongoing product momentum.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Clean handoff. Code, documentation, and context are left in a
|
|
||||||
state where internal teams—or future vendors—can confidently
|
|
||||||
extend the work without dependency.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Recent Engagements Section -->
|
|
||||||
<section
|
|
||||||
id="engagements"
|
|
||||||
class="section"
|
|
||||||
aria-labelledby="engagements-heading"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="engagements-heading" class="section-title">
|
|
||||||
Recent Engagements
|
|
||||||
</h2>
|
|
||||||
<dl class="engagements-list">
|
|
||||||
<div class="engagement">
|
|
||||||
<dt>Atlassian — Senior UI Engineer (Enterprise SaaS)</dt>
|
|
||||||
<dd>
|
|
||||||
Frontend architecture and feature delivery for Confluence
|
|
||||||
integrations, including React 18 migration work and
|
|
||||||
standardizing end-to-end testing practices.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="engagement">
|
|
||||||
<dt>CarGurus — Principal UI Engineer (Consumer Marketplace)</dt>
|
|
||||||
<dd>
|
|
||||||
Built and maintained high-traffic frontend systems, improved
|
|
||||||
Core Web Vitals and technical SEO, and developed shared UI
|
|
||||||
platforms used across teams.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="engagement">
|
|
||||||
<dt>
|
|
||||||
The TJX Companies (TJ Maxx) — UI Engineer (Enterprise Retail)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
Delivered UX improvements for large-scale e-commerce experiences
|
|
||||||
in close partnership with design, QA, and product teams.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="engagement">
|
|
||||||
<dt>
|
|
||||||
Timberland — Senior Interactive Developer (Global Ecommerce)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
Led global web initiatives across brand and e-commerce
|
|
||||||
platforms, acting as a technical bridge between marketing,
|
|
||||||
design, and engineering.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="engagement">
|
|
||||||
<dt>
|
|
||||||
MFA Boston — Pro Bono Technical Lead (Nonprofit / Fundraising)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
Designed and built a custom auction application for the MFA's
|
|
||||||
annual Young Patrons fundraiser; subsequently iterated on and
|
|
||||||
supported the platform over multiple years as the event grew,
|
|
||||||
until it concluded during the pandemic.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Schedule Section -->
|
|
||||||
<section
|
|
||||||
id="schedule"
|
|
||||||
class="section schedule-section"
|
|
||||||
aria-labelledby="schedule-heading"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="schedule-heading" class="section-title">Let's Talk</h2>
|
|
||||||
<p class="schedule-text">Ready to discuss your project?</p>
|
|
||||||
<a
|
|
||||||
href="https://cal.mifi.ventures/the-mifi/30min?utm_source=website&utm_medium=cta&utm_campaign=schedule_call&utm_medium=schedule_section_cta"
|
|
||||||
class="btn btn-primary"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
|
||||||
>
|
|
||||||
Schedule a 30-minute intro call
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container">
|
|
||||||
<p class="copyright">
|
|
||||||
© <span id="copyright-year">2026</span> mifi Ventures, LLC · Boston,
|
|
||||||
MA
|
|
||||||
</p>
|
|
||||||
<nav class="footer-links" aria-label="Social media links">
|
|
||||||
<a
|
|
||||||
href="https://linkedin.com/in/the-mifi"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="LinkedIn profile (opens in new tab)"
|
|
||||||
>LinkedIn</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://github.com/the-mifi"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub profile (opens in new tab)"
|
|
||||||
>GitHub</a
|
|
||||||
>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="/script.js" defer></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Minimal JavaScript for mifi Ventures website
|
|
||||||
* Primary purpose: Dynamic copyright year
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Update copyright year to current year
|
|
||||||
// Script runs with defer, so DOM is always ready
|
|
||||||
const yearElement = document.getElementById('copyright-year');
|
|
||||||
if (yearElement) {
|
|
||||||
yearElement.textContent = new Date().getFullYear();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
1346
site/styles.css
689
src/app.css
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
/* ========================================
|
||||||
|
CSS Variables for Light/Dark Mode
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Light mode colors */
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-bg-alt: #faf9ff; /* subtle violet-tinted off-white */
|
||||||
|
--color-bg-subtle: #f3f1ff; /* soft surface */
|
||||||
|
|
||||||
|
--color-text: #14121a;
|
||||||
|
--color-text-secondary: #3f3a4a;
|
||||||
|
--color-text-tertiary: #625b70;
|
||||||
|
|
||||||
|
--color-border: #e4e0f2;
|
||||||
|
--color-border-strong: #c9c1e3;
|
||||||
|
|
||||||
|
/* Brand accent (links, focus, highlights) */
|
||||||
|
--color-primary: #6d28d9; /* purple */
|
||||||
|
--color-primary-hover: #5b21b6;
|
||||||
|
--color-primary-bg: #efe7ff;
|
||||||
|
|
||||||
|
--color-secondary: #3f3a4a;
|
||||||
|
--color-secondary-hover: #14121a;
|
||||||
|
|
||||||
|
/* Focus */
|
||||||
|
--color-focus: #6d28d9;
|
||||||
|
--color-focus-outline: rgba(109, 40, 217, 0.45);
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
--font-family-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
|
||||||
|
|
||||||
|
--font-size-base: 18px;
|
||||||
|
--font-size-small: 15px;
|
||||||
|
--font-size-medium: 16px;
|
||||||
|
--font-size-large: 20px;
|
||||||
|
--font-size-xl: 32px;
|
||||||
|
--font-size-xxl: 52px;
|
||||||
|
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
|
--line-height-base: 1.75;
|
||||||
|
--line-height-relaxed: 1.85;
|
||||||
|
--line-height-tight: 1.65;
|
||||||
|
--line-height-heading: 1.25;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--space-xs: 0.25rem;
|
||||||
|
--space-sm: 0.5rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 1.5rem;
|
||||||
|
--space-xl: 2rem;
|
||||||
|
--space-xxl: 3rem;
|
||||||
|
--space-xxxl: 7rem;
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
--max-width: 1100px;
|
||||||
|
--max-narrow-width: 680px;
|
||||||
|
--max-text-width: 70ch;
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
--border-radius: 6px;
|
||||||
|
--border-radius-small: 6px;
|
||||||
|
--border-radius-medium: 10px;
|
||||||
|
--border-radius-large: 16px;
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 250ms ease;
|
||||||
|
|
||||||
|
/* CTA palette (orange primary CTA; AAA in light mode with white text) */
|
||||||
|
--accent-orange: #9a3412;
|
||||||
|
--accent-orange-hover: #7c2d12;
|
||||||
|
--accent-orange-soft: #fff1e7;
|
||||||
|
|
||||||
|
/* Button tokens */
|
||||||
|
--btn-primary-bg: var(--accent-orange);
|
||||||
|
--btn-primary-bg-hover: var(--accent-orange-hover);
|
||||||
|
--btn-primary-fg: #ffffff;
|
||||||
|
|
||||||
|
--btn-secondary-bg: transparent;
|
||||||
|
--btn-secondary-fg: var(--color-text);
|
||||||
|
--btn-secondary-border: var(--color-border-strong);
|
||||||
|
|
||||||
|
--btn-ghost-bg-hover: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
/* Focus ring (purple feels more “intentional” than orange) */
|
||||||
|
--btn-focus-ring: rgba(109, 40, 217, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode - AAA contrast optimized */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-bg: #0b0b12; /* cool slate (not pure black) */
|
||||||
|
--color-bg-alt: #121226;
|
||||||
|
--color-bg-subtle: #191934;
|
||||||
|
|
||||||
|
--color-text: #f3f2ff;
|
||||||
|
--color-text-secondary: #c9c6e4;
|
||||||
|
--color-text-tertiary: #a7a2c8;
|
||||||
|
|
||||||
|
--color-border: #2a2950;
|
||||||
|
--color-border-strong: #3a3870;
|
||||||
|
|
||||||
|
/* Brand accent (purple) */
|
||||||
|
--color-primary: #a78bfa;
|
||||||
|
--color-primary-hover: #c4b5fd;
|
||||||
|
--color-primary-bg: #1a1530;
|
||||||
|
|
||||||
|
--color-secondary: #c9c6e4;
|
||||||
|
--color-secondary-hover: #f3f2ff;
|
||||||
|
|
||||||
|
--color-focus: #a78bfa;
|
||||||
|
--color-focus-outline: rgba(167, 139, 250, 0.45);
|
||||||
|
|
||||||
|
/* CTA button: keep AAA in dark mode by using dark text on bright orange */
|
||||||
|
--btn-primary-bg: #fb923c;
|
||||||
|
--btn-primary-bg-hover: #fdba74; /* still AAA with dark text */
|
||||||
|
--btn-primary-fg: #0b0b12;
|
||||||
|
|
||||||
|
--btn-secondary-fg: var(--color-text);
|
||||||
|
--btn-secondary-border: var(--color-border-strong);
|
||||||
|
|
||||||
|
--btn-ghost-bg-hover: rgba(255, 255, 255, 0.08);
|
||||||
|
--btn-focus-ring: rgba(167, 139, 250, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Base Styles
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Skip Link (Accessibility)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
clip-path: inset(100%);
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 0;
|
||||||
|
padding: var(--space-sm) var(--space-lg);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
z-index: 9999;
|
||||||
|
border-radius: 0 0 var(--border-radius-large) 0;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
top: 0;
|
||||||
|
outline: 4px solid white;
|
||||||
|
outline-offset: 3px;
|
||||||
|
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
top: 0;
|
||||||
|
outline: 4px solid white;
|
||||||
|
outline-offset: 3px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Focus Styles (Strong, Accessible)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Strong focus indicators for keyboard navigation (WCAG 2.2 AAA) */
|
||||||
|
:focus {
|
||||||
|
outline: 3px solid var(--color-focus);
|
||||||
|
outline-offset: 3px;
|
||||||
|
transition: outline-offset var(--transition-fast);
|
||||||
|
|
||||||
|
&:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 0 8px var(--color-focus-outline);
|
||||||
|
|
||||||
|
*& {
|
||||||
|
outline-style: solid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img& {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Typography
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0 0 var(--space-lg) 0;
|
||||||
|
line-height: var(--line-height-heading);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--font-size-xxl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading font (keeps layout intact; just typography) */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-family-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 var(--space-md) 0;
|
||||||
|
max-width: var(--max-text-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
text-decoration-skip-ink: auto;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary-hover);
|
||||||
|
border-bottom-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 3px solid var(--color-focus);
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Layout Containers
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--space-xxxl) 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: var(--color-bg-alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Buttons
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
min-height: 48px;
|
||||||
|
min-width: 120px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PRIMARY CTA
|
||||||
|
Use the CTA/button tokens (defined in BOTH modes) to guarantee contrast.
|
||||||
|
This fixes the dark-mode purple/white contrast violation without changing your purple brand accents. */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--btn-primary-bg);
|
||||||
|
color: var(--btn-primary-fg);
|
||||||
|
border-color: var(--btn-primary-bg);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-primary-bg-hover);
|
||||||
|
border-color: var(--btn-primary-bg-hover);
|
||||||
|
color: var(--btn-primary-fg);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 8px var(--color-focus-outline),
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SECONDARY CTA
|
||||||
|
Keep it outlined and “lighter touch” (more professional than flipping to a heavy block).
|
||||||
|
Uses existing tokens only; works in both modes. */
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--btn-secondary-bg);
|
||||||
|
color: var(--btn-secondary-fg);
|
||||||
|
border-color: var(--btn-secondary-border);
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-orange-soft);
|
||||||
|
color: var(--btn-secondary-fg);
|
||||||
|
border-color: var(--btn-primary-bg);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: rgba(251, 146, 60, 0.12);
|
||||||
|
border-color: var(--btn-primary-bg);
|
||||||
|
color: var(--btn-secondary-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 8px var(--color-focus-outline),
|
||||||
|
0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Section Titles
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: var(--line-height-heading);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Content Lists
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.content-list {
|
||||||
|
max-width: var(--max-text-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
& li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '→';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1;
|
||||||
|
top: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Nav Item and Footer Links Common Styles
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.footer-links,
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
& a {
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
margin: calc(-1 * var(--space-xs)) calc(-1 * var(--space-sm));
|
||||||
|
min-height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-bottom-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
box-shadow: 0 0 0 8px var(--color-focus-outline);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
&:after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Responsive Design
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--font-size-base: 17px;
|
||||||
|
--font-size-small: 14px;
|
||||||
|
--font-size-medium: 15px;
|
||||||
|
--font-size-large: 19px;
|
||||||
|
--font-size-xl: 28px;
|
||||||
|
--font-size-xxl: 40px;
|
||||||
|
--space-lg: 2rem;
|
||||||
|
--space-xl: 3rem;
|
||||||
|
--space-xxl: 4.5rem;
|
||||||
|
--space-xxxl: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--space-xxl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
:root {
|
||||||
|
--font-size-base: 16px;
|
||||||
|
--font-size-small: 13px;
|
||||||
|
--font-size-medium: 14px;
|
||||||
|
--font-size-large: 18px;
|
||||||
|
--font-size-xl: 24px;
|
||||||
|
--font-size-xxl: 34px;
|
||||||
|
--space-xl: 2.5rem;
|
||||||
|
--space-xxl: 3.5rem;
|
||||||
|
--space-xxxl: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--space-xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
High Contrast Mode Support
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
:root {
|
||||||
|
--color-primary: #0047b3;
|
||||||
|
--color-border: #000000;
|
||||||
|
--color-text: #000000;
|
||||||
|
--color-text-secondary: #1a1a1a;
|
||||||
|
|
||||||
|
/* Maintain button contrast in high contrast mode (same tokens, same names) */
|
||||||
|
--btn-primary-bg: #000000;
|
||||||
|
--btn-primary-bg-hover: #000000;
|
||||||
|
--btn-primary-fg: #ffffff;
|
||||||
|
|
||||||
|
--btn-secondary-border: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-primary: #66b3ff;
|
||||||
|
--color-border: #ffffff;
|
||||||
|
--color-text: #ffffff;
|
||||||
|
--color-text-secondary: #e0e0e0;
|
||||||
|
|
||||||
|
--btn-primary-bg: #ffffff;
|
||||||
|
--btn-primary-bg-hover: #ffffff;
|
||||||
|
--btn-primary-fg: #000000;
|
||||||
|
|
||||||
|
--btn-secondary-border: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-width: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus,
|
||||||
|
:focus-visible {
|
||||||
|
outline-width: 4px;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Print Styles (Accessibility)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Show all content clearly for printing */
|
||||||
|
body {
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand all sections */
|
||||||
|
.section {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-bottom: 1pt solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show URLs for external links */
|
||||||
|
a[href^='http']:after {
|
||||||
|
content: ' (' attr(href) ')';
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide interactive elements that don't make sense in print */
|
||||||
|
.btn,
|
||||||
|
.cta-group {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure good contrast */
|
||||||
|
* {
|
||||||
|
color: #000 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
dt {
|
||||||
|
color: #000 !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
11
src/app.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
src/lib/components/EngagementsSection.svelte
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { engagements } from '$lib/data/engagements';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="engagements" class="section" aria-labelledby="engagements-heading">
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="engagements-heading" class="section-title">Recent Engagements</h2>
|
||||||
|
<dl class="engagements-list">
|
||||||
|
{#each engagements as engagement (engagement.title)}
|
||||||
|
<div class="engagement">
|
||||||
|
<dt>{engagement.title}</dt>
|
||||||
|
<dd>{engagement.description}</dd>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.engagements-list {
|
||||||
|
max-width: var(--max-text-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
padding-bottom: var(--space-lg);
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
border-left: 3px solid var(--color-border);
|
||||||
|
transition: border-color var(--transition-base);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
border-left-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-left: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& dt {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
& dd {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
276
src/lib/components/ExperienceSection.svelte
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { experienceLogos, experienceTextList } from '$lib/data/experience';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="experience"
|
||||||
|
class="section experience-section"
|
||||||
|
aria-labelledby="experience-heading"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="experience-heading" class="section-title">
|
||||||
|
Experience includes teams at:
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="logo-strip" role="list" aria-label="Company logos">
|
||||||
|
{#each experienceLogos as logo (logo.alt)}
|
||||||
|
<div class="logo-item" role="listitem">
|
||||||
|
<img
|
||||||
|
src={logo.src}
|
||||||
|
alt={logo.alt}
|
||||||
|
loading="lazy"
|
||||||
|
width={logo.width}
|
||||||
|
height={logo.height}
|
||||||
|
/>
|
||||||
|
<span class="logo-fallback-text">{logo.alt}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="logo-text-list" aria-hidden="true">
|
||||||
|
{#each experienceTextList as name (name)}
|
||||||
|
<li>{name}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="footnote">Logos are trademarks of their respective owners.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.experience-section {
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-lg) 0;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) and (min-width: 481px) {
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: var(--space-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-item {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 160px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-xs);
|
||||||
|
|
||||||
|
/* Make logo containers keyboard focusable for screen reader users */
|
||||||
|
&:focus-within {
|
||||||
|
outline: 3px solid var(--color-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) and (min-width: 481px) {
|
||||||
|
min-width: 110px;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 50px;
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: 0.75;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
filter: grayscale(100%) contrast(1.15);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
filter: grayscale(25%) contrast(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[alt]:not([src]),
|
||||||
|
&[alt][src=''],
|
||||||
|
&[alt]:not([src*='.svg']):not([src*='.png']):not([src*='.jpg']) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) and (min-width: 481px) {
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode logo adaptations */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
filter: grayscale(100%) brightness(0) invert(1) contrast(1.25);
|
||||||
|
opacity: 0.65;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
filter: grayscale(50%) brightness(1) invert(1) contrast(1.1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
opacity: 1;
|
||||||
|
filter: contrast(1.6);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
filter: brightness(0) invert(1) contrast(1.9);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
opacity: 1;
|
||||||
|
filter: none;
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback text (shown when image fails to load or on very small screens) */
|
||||||
|
.logo-fallback-text {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-item:has(img[alt]:not([src])) .logo-fallback-text,
|
||||||
|
.logo-item:has(img[alt][src='']) .logo-fallback-text {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
clip: auto;
|
||||||
|
white-space: normal;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg-alt);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-only list (hidden by default, shown on very small screens) */
|
||||||
|
.logo-text-list {
|
||||||
|
display: none;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& li {
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg-subtle);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footnote {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
font-style: italic;
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.logo-item img {
|
||||||
|
opacity: 1;
|
||||||
|
filter: contrast(1.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.logo-item img {
|
||||||
|
filter: brightness(0) invert(1) contrast(1.9);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text-list li {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p class="copyright">
|
||||||
|
© <span id="copyright-year">2026</span> mifi Ventures, LLC · Boston, MA
|
||||||
|
</p>
|
||||||
|
<nav class="footer-links" aria-label="Social media links">
|
||||||
|
<a
|
||||||
|
href="https://linkedin.com/in/the-mifi"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="LinkedIn profile (opens in new tab)">LinkedIn</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/the-mifi"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="GitHub profile (opens in new tab)">GitHub</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.footer {
|
||||||
|
padding: var(--space-xxl) 0 var(--space-xl) 0;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
82
src/lib/components/Hero.svelte
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Logo from './Logo.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header id="header" class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<Logo />
|
||||||
|
<p class="headline">Software Engineering Consulting</p>
|
||||||
|
<p class="subhead">
|
||||||
|
Principal: Mike Fitzpatrick — senior full-stack engineer and architect helping
|
||||||
|
teams ship reliable, accessible, high-performance web products.
|
||||||
|
</p>
|
||||||
|
<div class="cta-group">
|
||||||
|
<a
|
||||||
|
href="https://cal.mifi.ventures/the-mifi/30min?utm_source=website&utm_medium=cta&utm_campaign=schedule_call&utm_medium=hero_cta"
|
||||||
|
class="btn btn-primary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
||||||
|
>
|
||||||
|
Schedule a 30-minute intro call
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/downloads/resume.pdf"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
download
|
||||||
|
aria-label="Download Mike Fitzpatrick's resume as PDF"
|
||||||
|
>
|
||||||
|
Download resume
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
padding: var(--space-xxxl) 0 var(--space-xxl) 0;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: var(--space-xxl) 0 var(--space-xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: var(--space-xl) 0 var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
font-family: var(--font-family-heading);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subhead {
|
||||||
|
max-width: var(--max-narrow-width);
|
||||||
|
margin: 0 auto var(--space-xl) auto;
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-md);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/lib/components/HowWeWork.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { howWeWorkItems } from '$lib/data/content';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="how-we-work" class="section" aria-labelledby="how-we-work-heading">
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="how-we-work-heading" class="section-title">How We Work</h2>
|
||||||
|
<ul class="content-list">
|
||||||
|
{#each howWeWorkItems as item (item)}
|
||||||
|
<li>{item}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
14
src/lib/components/ImpactSection.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { impactItems } from '$lib/data/content';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="impact" class="section" aria-labelledby="impact-heading">
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="impact-heading" class="section-title">Selected Impact</h2>
|
||||||
|
<ul class="content-list">
|
||||||
|
{#each impactItems as item (item)}
|
||||||
|
<li>{item}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
18
src/lib/components/Logo.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Wordmark from './Wordmark.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="logo">
|
||||||
|
<Wordmark />
|
||||||
|
<span class="sr-only">mifi Ventures</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.logo {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family-heading);
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 350px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
319
src/lib/components/Navigation.svelte
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Wordmark from './Wordmark.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="nav" class="nav" aria-label="Main navigation">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="nav-toggle"
|
||||||
|
class="nav-toggle-input"
|
||||||
|
aria-hidden="true"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<div class="mobile-nav-header">
|
||||||
|
<span class="mobile nav-header-logo">
|
||||||
|
<Wordmark />
|
||||||
|
</span>
|
||||||
|
<label for="nav-toggle" class="nav-toggle" aria-label="Toggle navigation">
|
||||||
|
<span class="nav-toggle-inner">
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="nav-menu container">
|
||||||
|
<span class="nav-header-logo desktop">
|
||||||
|
<Wordmark />
|
||||||
|
</span>
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#what-we-do" class="nav-link">Services</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#impact" class="nav-link">Impact</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#how-we-work" class="nav-link">Process</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#schedule" class="nav-link">Contact</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="nav-item nav-back-to-top">
|
||||||
|
<a href="#header" class="nav-link">Back to top</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
|
||||||
|
padding: var(--space-md) 0;
|
||||||
|
position: sticky;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container,
|
||||||
|
.nav-menu {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav-header {
|
||||||
|
anchor-name: --mobile-nav-header;
|
||||||
|
display: none;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-header-logo {
|
||||||
|
color: var(--color-text);
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 250px;
|
||||||
|
padding-left: var(--space-md);
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
&.desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger toggle: mobile only, animates to X when open */
|
||||||
|
.nav-toggle-input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
& ~ .mobile-nav-header .nav-toggle .nav-toggle-line {
|
||||||
|
&:nth-child(1) {
|
||||||
|
transform: translateY(8px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
transform: translateY(-8px) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .nav-menu {
|
||||||
|
max-height: 80vh;
|
||||||
|
opacity: 1;
|
||||||
|
padding-top: var(--space-md);
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
width: calc(24px + var(--space-md) + var(--space-md));
|
||||||
|
height: calc(31px + var(--space-sm) + var(--space-sm));
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--space-xs);
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 4px solid var(--color-focus);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 24px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle-line {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: currentColor;
|
||||||
|
border-radius: 1px;
|
||||||
|
transform-origin: center;
|
||||||
|
transition:
|
||||||
|
transform 0.25s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition-duration: 0.01ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
margin: 0 var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to top + mobile nav logo: hidden until page is scrolled (CSS scroll-driven animation) */
|
||||||
|
.nav-back-to-top,
|
||||||
|
.nav-header-logo {
|
||||||
|
/* Fallback when scroll-driven animations aren’t supported: always visible */
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: show toggle, collapse menu until opened; menu overlays content via anchor */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-top: none;
|
||||||
|
transition:
|
||||||
|
max-height 0.3s ease,
|
||||||
|
opacity 0.25s ease,
|
||||||
|
padding 0.25s ease;
|
||||||
|
|
||||||
|
& .nav-list,
|
||||||
|
& .nav-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .nav-list {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .nav-item {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& a:hover {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .nav-item a,
|
||||||
|
& .nav-back-to-top a {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (top: anchor(bottom)) {
|
||||||
|
position: fixed;
|
||||||
|
position-anchor: --mobile-nav-header;
|
||||||
|
top: anchor(--mobile-nav-header bottom);
|
||||||
|
left: anchor(--mobile-nav-header left);
|
||||||
|
right: anchor(--mobile-nav-header right);
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition-duration: 0.01ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
.nav-back-to-top,
|
||||||
|
.nav-header-logo {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: nav-reveal-on-scroll linear;
|
||||||
|
animation-timeline: scroll(root block);
|
||||||
|
animation-range: 300px 400px;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nav-reveal-on-scroll {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.nav-back-to-top,
|
||||||
|
.nav-header-logo {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/lib/components/ScheduleSection.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<section
|
||||||
|
id="schedule"
|
||||||
|
class="section schedule-section"
|
||||||
|
aria-labelledby="schedule-heading"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="schedule-heading" class="section-title">Let's Talk</h2>
|
||||||
|
<p class="schedule-text">Ready to discuss your project?</p>
|
||||||
|
<a
|
||||||
|
href="https://cal.mifi.ventures/the-mifi/30min?utm_source=website&utm_medium=cta&utm_campaign=schedule_call&utm_medium=schedule_section_cta"
|
||||||
|
class="btn btn-primary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
||||||
|
>
|
||||||
|
Schedule a 30-minute intro call
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schedule-section {
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--color-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-text {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: var(--line-height-relaxed);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/lib/components/WhatWeDo.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { whatWeDoItems } from '$lib/data/content';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="what-we-do" class="section" aria-labelledby="what-we-do-heading">
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="what-we-do-heading" class="section-title">What We Do</h2>
|
||||||
|
<ul class="content-list">
|
||||||
|
{#each whatWeDoItems as item (item)}
|
||||||
|
<li>{item}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
61
src/lib/components/Wordmark.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const { color = 'currentColor' } = $props<{ color?: string }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 3934 513"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xml:space="preserve"
|
||||||
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||||
|
><g
|
||||||
|
><path
|
||||||
|
fill={color}
|
||||||
|
d="M0,504.667l0,-362.333l82.5,0l0,83.5l-9.5,-13.5c6.444,-26.222 19.75,-45.778 39.917,-58.667c20.167,-12.889 43.806,-19.333 70.917,-19.333c29.556,0 55.694,7.694 78.417,23.083c22.722,15.389 37.417,35.861 44.083,61.417l-25,2.167c11.222,-29.222 27.917,-50.972 50.083,-65.25c22.167,-14.278 47.75,-21.417 76.75,-21.417c25.667,0 48.611,5.778 68.833,17.333c20.222,11.556 36.194,27.611 47.917,48.167c11.722,20.556 17.583,44.333 17.583,71.333l0,233.5l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-13.889,0 -26.139,3.194 -36.75,9.583c-10.611,6.389 -18.833,15.333 -24.667,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-14,0 -26.278,3.194 -36.833,9.583c-10.556,6.389 -18.75,15.333 -24.583,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M614.5,504.667l0,-362.333l87.5,0l0,362.333l-87.5,0Zm0,-403.333l0,-93.333l87.5,0l0,93.333l-87.5,0Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M823.5,504.667l0,-284.833l-63.833,0l0,-77.5l63.833,0l0,-12c0,-27.889 5.639,-51.472 16.917,-70.75c11.278,-19.278 27.194,-34.028 47.75,-44.25c20.556,-10.222 44.778,-15.333 72.667,-15.333c5.444,0 11.389,0.333 17.833,1c6.444,0.667 11.778,1.444 16,2.333l0,75.333c-4.111,-0.889 -8.083,-1.444 -11.917,-1.667c-3.833,-0.222 -7.361,-0.333 -10.583,-0.333c-19.333,0 -34.361,4.361 -45.083,13.083c-10.722,8.722 -16.083,22.25 -16.083,40.583l0,12l158.667,0l0,77.5l-158.667,0l0,284.833l-87.5,0Zm213.667,0l0,-362.333l87.5,0l0,362.333l-87.5,0Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M1474.74,50.297c0,-3.892 1.365,-6.915 4.096,-9.068c2.731,-2.153 6.86,-3.229 12.388,-3.229l106.333,0c5.83,0 10.083,1.052 12.76,3.156c2.677,2.104 4.016,5.056 4.016,8.854c0,2.844 -0.913,5.219 -2.74,7.125c-1.826,1.906 -5.043,3.74 -9.651,5.5l-13.286,4.479c-6.694,2.681 -12.347,6.961 -16.958,12.841c-4.611,5.88 -8.964,15.216 -13.057,28.008l-114.453,337.807c-2.128,6.608 -3.694,12.082 -4.695,16.424c-1.002,4.342 -1.503,8.846 -1.503,13.513l0,15.141c0,4.351 -1.242,7.741 -3.727,10.172c-2.484,2.431 -5.817,3.646 -9.997,3.646l-71.854,0c-4.354,0 -7.762,-1.215 -10.224,-3.646c-2.462,-2.431 -3.693,-5.965 -3.693,-10.604l0,-14.969c0,-3.542 -0.508,-7.257 -1.523,-11.146c-1.016,-3.889 -2.428,-8.453 -4.237,-13.693l-125.854,-363.062c-2.097,-6.191 -4.4,-10.66 -6.909,-13.406c-2.509,-2.747 -5.987,-4.977 -10.435,-6.693l-14.141,-4.193c-7.497,-2.809 -11.245,-7.128 -11.245,-12.958c0,-3.892 1.394,-6.915 4.182,-9.068c2.788,-2.153 7.002,-3.229 12.641,-3.229l151.687,0c5.75,0 9.968,1.076 12.654,3.229c2.686,2.153 4.029,5.175 4.029,9.068c0,3.128 -1.044,5.646 -3.133,7.552c-2.089,1.906 -5.221,3.55 -9.398,4.932l-25.328,4.427c-5.542,1.573 -8.939,4.237 -10.193,7.992c-1.253,3.755 -0.444,9.841 2.427,18.258l124.146,361.656l-26.328,20.599l125.198,-369.797c3.542,-10.618 4.107,-18.966 1.695,-25.044c-2.411,-6.078 -9.02,-10.76 -19.826,-14.044l-21.573,-4.193c-3.972,-1.382 -7.014,-2.978 -9.125,-4.789c-2.111,-1.811 -3.167,-4.327 -3.167,-7.549Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M1905.721,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.062,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M2076.711,205.234l0,253.599c0,6.017 0.997,10.479 2.99,13.385c1.993,2.906 4.998,4.97 9.016,6.193l14.234,3.453c6.413,2.337 9.62,6.03 9.62,11.078c0,7.816 -5.082,11.724 -15.245,11.724l-124.286,0c-5.035,0 -8.755,-1.004 -11.161,-3.013c-2.406,-2.009 -3.609,-4.721 -3.609,-8.138c0,-2.733 0.865,-5.056 2.596,-6.969c1.731,-1.913 4.438,-3.458 8.122,-4.635l15.234,-3.5c4.031,-1.222 7.04,-3.262 9.026,-6.12c1.986,-2.858 2.979,-7.281 2.979,-13.271l0,-204.677c0,-4.844 -0.782,-8.342 -2.346,-10.495c-1.564,-2.153 -4.126,-3.467 -7.685,-3.943l-20.542,-1c-3.559,-0.667 -6.109,-1.819 -7.651,-3.456c-1.542,-1.637 -2.312,-3.734 -2.312,-6.289c0,-2.972 0.918,-5.387 2.753,-7.245c1.835,-1.858 5.192,-3.66 10.07,-5.406l62.24,-21.5c6.941,-2.556 12.56,-4.39 16.857,-5.503c4.297,-1.113 8.263,-1.669 11.898,-1.669c5.688,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.375 4.336,12.667Zm-9.385,71.365l-12.724,-13.036l13.526,-11.927c26.417,-23.639 49.119,-40.515 68.107,-50.628c18.988,-10.113 37.15,-15.169 54.487,-15.169c26.26,0 46.657,8.724 61.19,26.172c14.533,17.448 23.444,41.141 26.732,71.078l20.208,174.552c0.729,6.417 2.038,11.217 3.927,14.401c1.889,3.184 4.993,5.387 9.313,6.609l13.427,3.26c3.684,1.16 6.391,2.697 8.122,4.612c1.731,1.915 2.596,4.246 2.596,6.992c0,3.417 -1.175,6.129 -3.526,8.138c-2.351,2.009 -6.115,3.013 -11.292,3.013l-125.594,0c-10.198,0 -15.297,-3.908 -15.297,-11.724c-0,-5.017 3.177,-8.71 9.531,-11.078l14.896,-3.453c4.448,-1.222 7.823,-3.425 10.125,-6.609c2.302,-3.184 3.087,-7.905 2.354,-14.161l-18.995,-162.974c-2.476,-20.635 -7.75,-36.081 -15.823,-46.336c-8.073,-10.255 -19.927,-15.383 -35.562,-15.383c-9.844,0 -20.199,2.655 -31.065,7.966c-10.866,5.311 -22.546,13.293 -35.039,23.945l-13.625,11.74Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M2387.845,220.714l-18.271,-4.667c-4.927,-1.479 -8.352,-3.2 -10.273,-5.161c-1.922,-1.962 -2.883,-4.276 -2.883,-6.943c0,-3.51 1.223,-6.199 3.669,-8.065c2.446,-1.866 5.704,-2.799 9.773,-2.799l21.99,0c5.101,-0 9.294,-0.87 12.581,-2.609c3.286,-1.74 6.416,-4.936 9.388,-9.589l34.552,-51.276c3.59,-4.91 7.104,-8.504 10.542,-10.784c3.438,-2.28 6.943,-3.419 10.516,-3.419c3.878,0 6.89,1.22 9.034,3.659c2.144,2.439 3.216,5.905 3.216,10.398l0,288.479c0,15.747 3.147,27.707 9.44,35.88c6.293,8.174 15.034,12.26 26.221,12.26c7.67,0 13.736,-1.373 18.198,-4.12c4.462,-2.747 8.049,-6.002 10.763,-9.766c2.714,-3.764 5.304,-7.236 7.771,-10.417c2.467,-3.181 5.447,-5.205 8.94,-6.073c2.733,-0.177 4.901,0.624 6.505,2.404c1.604,1.78 2.375,4.773 2.312,8.982c-0.507,11.427 -4.431,21.975 -11.773,31.643c-7.342,9.668 -17.384,17.448 -30.125,23.339c-12.741,5.891 -27.367,8.836 -43.878,8.836c-26.191,0 -46.827,-6.628 -61.909,-19.883c-15.082,-13.255 -22.622,-33.345 -22.622,-60.268l0,-192.13c0,-5.128 -1.021,-9.007 -3.063,-11.635c-2.042,-2.628 -5.58,-4.72 -10.615,-6.276Zm61.109,-0.854l0.281,-26.781l106.25,0c4.444,-0 7.866,0.858 10.266,2.573c2.399,1.715 3.599,4.257 3.599,7.625c0,4.733 -2.383,8.68 -7.148,11.841c-4.766,3.161 -12.326,4.742 -22.68,4.742l-90.568,0Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M2855.8,465.307l0,-21.885l-2.146,-1.526l0,-187.313c0,-4.847 -0.782,-8.346 -2.346,-10.497c-1.564,-2.151 -4.126,-3.464 -7.685,-3.94l-20.542,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.917,-5.382 2.75,-7.24c1.833,-1.858 5.189,-3.661 10.068,-5.411l62.24,-21.495c6.924,-2.559 12.538,-4.394 16.844,-5.505c4.306,-1.111 8.276,-1.667 11.911,-1.667c5.687,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.373 4.336,12.661l0,253.365c0,6.017 0.997,10.503 2.99,13.456c1.993,2.953 4.998,4.994 9.016,6.122l14.516,3.312c3.812,1.16 6.609,2.701 8.388,4.622c1.78,1.922 2.669,4.312 2.669,7.169c0,3.417 -1.223,6.129 -3.669,8.138c-2.446,2.009 -6.258,3.013 -11.435,3.013l-65.932,0c-10.229,0 -18.6,-3.578 -25.112,-10.734c-6.512,-7.156 -9.768,-16.698 -9.768,-28.625Zm-208.76,-50.839l0,-159.885c0,-4.847 -0.786,-8.346 -2.357,-10.497c-1.571,-2.151 -4.143,-3.464 -7.716,-3.94l-20.547,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.918,-5.382 2.753,-7.24c1.835,-1.858 5.19,-3.661 10.065,-5.411l62.286,-21.495c7.226,-2.653 12.964,-4.511 17.214,-5.576c4.25,-1.064 7.908,-1.596 10.974,-1.596c5.972,0 10.428,1.576 13.367,4.729c2.939,3.153 4.409,7.373 4.409,12.661l0,197.422c0,20.92 5.023,36.531 15.07,46.833c10.047,10.302 23.452,15.453 40.216,15.453c10.382,0 21.431,-2.572 33.146,-7.716c11.715,-5.144 23.986,-13.207 36.813,-24.19l13.62,-11.74l12.724,13.031l-13.526,11.927c-26.653,24.323 -49.98,41.37 -69.982,51.141c-20.002,9.771 -38.973,14.656 -56.914,14.656c-27.354,0 -49.469,-8.744 -66.344,-26.232c-16.875,-17.488 -25.313,-41.35 -25.313,-71.586Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M3130.82,330.943c0,-31.67 4.576,-58.287 13.729,-79.852c9.153,-21.564 21.071,-37.831 35.755,-48.799c14.684,-10.969 30.319,-16.453 46.906,-16.453c20.118,0 35.681,5.683 46.69,17.049c11.009,11.366 16.513,27.369 16.513,48.008c0,17.24 -3.655,30.185 -10.964,38.836c-7.309,8.651 -16.743,12.977 -28.302,12.977c-11.556,0 -20.418,-3.171 -26.586,-9.513c-6.168,-6.342 -9.284,-15.235 -9.346,-26.68l-0.047,-11.573c-0.111,-7.236 -1.802,-12.64 -5.073,-16.211c-3.271,-3.571 -8.644,-5.357 -16.12,-5.357c-8.635,0 -16.97,3.531 -25.003,10.594c-8.033,7.062 -14.597,17.733 -19.693,32.01c-5.095,14.278 -7.643,32.392 -7.643,54.344l-10.818,0.62Zm6.812,-125.427l4.005,81.208l0,171.87c0,5.497 1.199,9.661 3.596,12.495c2.398,2.833 6.598,4.719 12.602,5.656l29.714,4.427c4.462,0.701 7.769,2.029 9.922,3.982c2.153,1.953 3.229,4.694 3.229,8.221c0,3.51 -1.299,6.27 -3.896,8.279c-2.597,2.009 -6.398,3.013 -11.401,3.013l-147.318,0c-5.083,0 -8.836,-1.013 -11.258,-3.039c-2.422,-2.026 -3.633,-4.739 -3.633,-8.138c0,-2.747 0.885,-5.085 2.656,-7.016c1.771,-1.931 4.514,-3.483 8.229,-4.656l15.068,-3.406c4.035,-1.128 7.044,-3.137 9.029,-6.026c1.984,-2.889 2.977,-7.295 2.977,-13.219l0,-204.391c0,-4.861 -0.779,-8.379 -2.336,-10.555c-1.557,-2.175 -4.114,-3.503 -7.669,-3.982l-20.667,-1c-3.51,-0.667 -6.04,-1.818 -7.589,-3.453c-1.549,-1.635 -2.323,-3.724 -2.323,-6.266c0,-2.955 0.942,-5.393 2.826,-7.315c1.884,-1.922 5.232,-3.709 10.044,-5.362l61.26,-20.76c8.799,-3.306 15.307,-5.466 19.526,-6.482c4.219,-1.016 7.575,-1.523 10.068,-1.523c4.08,0 7.156,1.345 9.229,4.036c2.073,2.691 3.443,7.158 4.109,13.401Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M3622.771,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.063,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/><path
|
||||||
|
fill={color}
|
||||||
|
d="M3813.044,487.698c15.937,0 28.422,-4.111 37.453,-12.333c9.031,-8.222 13.547,-18.745 13.547,-31.568c0,-8.125 -1.861,-15.469 -5.583,-22.031c-3.722,-6.562 -10.543,-12.562 -20.464,-17.997c-9.92,-5.436 -24.125,-10.471 -42.615,-15.107c-31.625,-7.188 -56.268,-15.988 -73.93,-26.401c-17.661,-10.413 -30.004,-22.363 -37.029,-35.849c-7.024,-13.486 -10.536,-28.356 -10.536,-44.609c0,-29.288 10.358,-52.604 31.073,-69.948c20.715,-17.344 50.299,-26.016 88.75,-26.016c14.302,0 26.049,1.234 35.24,3.703c9.191,2.469 16.712,4.961 22.562,7.477c5.851,2.516 10.873,3.773 15.068,3.773c4.382,0 7.961,-1.258 10.737,-3.773c2.776,-2.516 5.492,-5.031 8.148,-7.547c2.656,-2.516 5.993,-3.773 10.01,-3.773c2.781,0 5.272,0.905 7.471,2.716c2.2,1.811 4.03,5.162 5.492,10.055l22.667,71.646c2.066,6.226 2.702,11.364 1.909,15.414c-0.793,4.05 -3.287,6.918 -7.482,8.602c-4.097,1.556 -7.667,1.551 -10.708,-0.013c-3.042,-1.564 -5.937,-4.451 -8.687,-8.659c-10.062,-18.646 -20.847,-33.42 -32.354,-44.323c-11.507,-10.903 -23.586,-18.714 -36.237,-23.435c-12.651,-4.72 -25.85,-7.081 -39.596,-7.081c-19.764,0 -34.641,4.182 -44.633,12.547c-9.991,8.365 -14.987,19.563 -14.987,33.594c0,8.41 2.122,16.051 6.367,22.924c4.245,6.873 12.054,13.202 23.427,18.987c11.373,5.785 27.584,11.288 48.633,16.51c27.465,6.378 49.29,14.37 65.474,23.974c16.184,9.604 27.82,21.081 34.909,34.43c7.089,13.349 10.633,28.883 10.633,46.602c0,18.205 -4.623,34.268 -13.87,48.19c-9.247,13.922 -22.141,24.768 -38.682,32.539c-16.542,7.771 -35.79,11.656 -57.745,11.656c-13.83,0 -25.069,-1.468 -33.719,-4.404c-8.649,-2.936 -15.788,-5.848 -21.417,-8.737c-5.628,-2.889 -10.825,-4.333 -15.589,-4.333c-4.257,0 -7.92,1.424 -10.99,4.273c-3.069,2.849 -6.016,5.722 -8.841,8.62c-2.825,2.898 -6.07,4.346 -9.737,4.346c-2.701,0 -5.029,-0.989 -6.982,-2.966c-1.953,-1.977 -3.39,-5.31 -4.31,-9.997l-13.906,-67.13c-1.462,-7.559 -1.775,-13.197 -0.94,-16.914c0.835,-3.717 3.119,-6.322 6.852,-7.815c3.986,-1.587 7.492,-1.376 10.518,0.633c3.026,2.009 6.143,5.641 9.352,10.898c13.649,24.583 28.663,42.171 45.042,52.763c16.378,10.592 33.123,15.888 50.234,15.888Z"
|
||||||
|
style="fill-rule:nonzero;"
|
||||||
|
/></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
8
src/lib/copyright-year.test.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { getCurrentYear } from './copyright-year';
|
||||||
|
|
||||||
|
describe('getCurrentYear', () => {
|
||||||
|
it('returns the current calendar year', () => {
|
||||||
|
expect(getCurrentYear()).toBe(new Date().getFullYear());
|
||||||
|
});
|
||||||
|
});
|
||||||
7
src/lib/copyright-year.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Returns the current calendar year (for testing and any server use).
|
||||||
|
* The client-side footer year is updated by static/copyright-year.js.
|
||||||
|
*/
|
||||||
|
export function getCurrentYear(): number {
|
||||||
|
return new Date().getFullYear();
|
||||||
|
}
|
||||||
26
src/lib/data/content.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export const whatWeDoItems = [
|
||||||
|
'Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability.',
|
||||||
|
'Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten.',
|
||||||
|
'Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys—not just lab scores.',
|
||||||
|
'Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns.',
|
||||||
|
'Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases.',
|
||||||
|
'End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity.',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const impactItems = [
|
||||||
|
'Get new products off the ground quickly by establishing durable frontend and platform foundations—clean architecture, clear patterns, and pragmatic defaults designed to scale with teams and traffic.',
|
||||||
|
'Improve performance, Core Web Vitals, and technical SEO on high-traffic user journeys through rendering strategy, bundle discipline, and careful attention to real-world loading behavior.',
|
||||||
|
'Build accessibility into core UI systems, not as a retrofit—semantic markup, keyboard parity, and screen reader support baked into reusable components and design patterns.',
|
||||||
|
'Bring order to complex or aging codebases by simplifying structure, reducing duplication, and clarifying ownership, enabling teams to ship confidently without over-engineering.',
|
||||||
|
'Design and evolve shared component libraries and UI systems that improve consistency, velocity, and long-term maintainability across multiple teams.',
|
||||||
|
'Partner closely with product, design, and engineering leadership (including marketing teams and non-technical organizations) to translate goals into shippable systems, balancing speed, quality, and technical risk.',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const howWeWorkItems = [
|
||||||
|
'Engagements are consulting-led and senior-driven. I work directly with founders, product leaders, marketing teams, and engineering teams—including organizations without in-house technical staff—to establish direction and deliver solutions with a high degree of autonomy.',
|
||||||
|
'Focused, pragmatic scope. Work is scoped to deliver real progress quickly, with an emphasis on building the right foundation rather than over-engineering for hypothetical futures.',
|
||||||
|
'Async-friendly, low-friction communication. Clear written updates, documented decisions, and scheduled calls when they add value—not meetings for their own sake.',
|
||||||
|
'Quality as a default. Accessibility, performance, and maintainability are built into the work from the start, not added later as cleanup.',
|
||||||
|
"Flexible engagement models. Hourly or fixed-scope work depending on clarity and needs; longer-term engagements welcome when there's ongoing product momentum.",
|
||||||
|
'Clean handoff. Code, documentation, and context are left in a state where internal teams—or future vendors—can confidently extend the work without dependency.',
|
||||||
|
];
|
||||||
27
src/lib/data/engagements.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const engagements = [
|
||||||
|
{
|
||||||
|
title: 'Atlassian — Senior UI Engineer (Enterprise SaaS)',
|
||||||
|
description:
|
||||||
|
'Frontend architecture and feature delivery for Confluence integrations, including React 18 migration work and standardizing end-to-end testing practices.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CarGurus — Principal UI Engineer (Consumer Marketplace)',
|
||||||
|
description:
|
||||||
|
'Built and maintained high-traffic frontend systems, improved Core Web Vitals and technical SEO, and developed shared UI platforms used across teams.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'The TJX Companies (TJ Maxx) — UI Engineer (Enterprise Retail)',
|
||||||
|
description:
|
||||||
|
'Delivered UX improvements for large-scale e-commerce experiences in close partnership with design, QA, and product teams.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Timberland — Senior Interactive Developer (Global Ecommerce)',
|
||||||
|
description:
|
||||||
|
'Led global web initiatives across brand and e-commerce platforms, acting as a technical bridge between marketing, design, and engineering.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'MFA Boston — Pro Bono Technical Lead (Nonprofit / Fundraising)',
|
||||||
|
description:
|
||||||
|
"Designed and built a custom auction application for the MFA's annual Young Patrons fundraiser; subsequently iterated on and supported the platform over multiple years as the event grew, until it concluded during the pandemic.",
|
||||||
|
},
|
||||||
|
];
|
||||||
26
src/lib/data/experience.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export const experienceLogos = [
|
||||||
|
{ src: '/assets/logos/atlassian.svg', alt: 'Atlassian', width: 2500, height: 2500 },
|
||||||
|
{
|
||||||
|
src: '/assets/logos/tjx.svg',
|
||||||
|
alt: 'TJ Maxx (The TJX Companies)',
|
||||||
|
width: 2500,
|
||||||
|
height: 621,
|
||||||
|
},
|
||||||
|
{ src: '/assets/logos/cargurus.svg', alt: 'CarGurus', width: 2500, height: 398 },
|
||||||
|
{ src: '/assets/logos/timberland.svg', alt: 'Timberland', width: 190, height: 35 },
|
||||||
|
{ src: '/assets/logos/vf.svg', alt: 'VF Corporation', width: 190, height: 155 },
|
||||||
|
{
|
||||||
|
src: '/assets/logos/bottomline.svg',
|
||||||
|
alt: 'Bottomline Technologies',
|
||||||
|
width: 2702,
|
||||||
|
height: 571,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/assets/logos/mfa-boston.svg',
|
||||||
|
alt: 'Museum of Fine Arts Boston',
|
||||||
|
width: 572,
|
||||||
|
height: 88,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const experienceTextList = experienceLogos.map((l) => l.alt);
|
||||||
9
src/lib/data/home-meta.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { PageMeta } from '$lib/seo';
|
||||||
|
import { defaultJsonLdGraph } from './json-ld';
|
||||||
|
|
||||||
|
export const homeMeta: PageMeta = {
|
||||||
|
title: 'mifi Ventures — Software Engineering Consulting | Boston, MA',
|
||||||
|
description:
|
||||||
|
'Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications. Specializing in frontend architecture, performance optimization, and modern web development.',
|
||||||
|
jsonLd: defaultJsonLdGraph,
|
||||||
|
};
|
||||||
150
src/lib/data/json-ld.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Default JSON-LD graph nodes (Organization, Person, WebSite, WebPage, OfferCatalog).
|
||||||
|
* Used for the home page; other pages can add or override via meta.jsonLd.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE = 'https://mifi.ventures';
|
||||||
|
|
||||||
|
export const defaultJsonLdGraph: Record<string, unknown>[] = [
|
||||||
|
{
|
||||||
|
'@type': 'Organization',
|
||||||
|
'@id': `${BASE}/#organization`,
|
||||||
|
name: 'mifi Ventures, LLC',
|
||||||
|
legalName: 'mifi Ventures, LLC',
|
||||||
|
url: `${BASE}/`,
|
||||||
|
logo: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
|
||||||
|
description:
|
||||||
|
'Software engineering consulting specializing in product-focused frontend architecture, performance optimization, and accessibility-first engineering.',
|
||||||
|
founder: { '@id': `${BASE}/#principal` },
|
||||||
|
address: {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
addressLocality: 'Boston',
|
||||||
|
addressRegion: 'MA',
|
||||||
|
addressCountry: 'US',
|
||||||
|
},
|
||||||
|
geo: { '@type': 'GeoCoordinates', latitude: 42.360082, longitude: -71.05888 },
|
||||||
|
areaServed: { '@type': 'Country', name: 'United States' },
|
||||||
|
hasOfferCatalog: { '@id': `${BASE}/#services` },
|
||||||
|
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Person',
|
||||||
|
'@id': `${BASE}/#principal`,
|
||||||
|
name: 'Mike Fitzpatrick',
|
||||||
|
jobTitle: 'Principal Software Engineer and Architect',
|
||||||
|
description:
|
||||||
|
'Senior full-stack engineer and architect helping teams ship reliable, accessible, high-performance web products.',
|
||||||
|
url: `${BASE}/`,
|
||||||
|
worksFor: { '@id': `${BASE}/#organization` },
|
||||||
|
knowsAbout: [
|
||||||
|
'Frontend Architecture',
|
||||||
|
'UI Architecture',
|
||||||
|
'React Development',
|
||||||
|
'Web Performance Optimization',
|
||||||
|
'Core Web Vitals',
|
||||||
|
'Technical SEO',
|
||||||
|
'Web Accessibility (WCAG)',
|
||||||
|
'Component Libraries',
|
||||||
|
'Design Systems',
|
||||||
|
'JavaScript',
|
||||||
|
'TypeScript',
|
||||||
|
'Modern Web Development',
|
||||||
|
'Greenfield Product Development',
|
||||||
|
'Legacy System Modernization',
|
||||||
|
'Code Refactoring',
|
||||||
|
],
|
||||||
|
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': `${BASE}/#website`,
|
||||||
|
url: `${BASE}/`,
|
||||||
|
name: 'mifi Ventures',
|
||||||
|
description: 'Software Engineering Consulting — Boston, MA',
|
||||||
|
publisher: { '@id': `${BASE}/#organization` },
|
||||||
|
potentialAction: {
|
||||||
|
'@type': 'ReserveAction',
|
||||||
|
target: {
|
||||||
|
'@type': 'EntryPoint',
|
||||||
|
urlTemplate: 'https://cal.mifi.ventures/the-mifi',
|
||||||
|
},
|
||||||
|
name: 'Schedule a 30-minute intro call',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': `${BASE}/#webpage`,
|
||||||
|
url: `${BASE}/`,
|
||||||
|
name: 'mifi Ventures — Software Engineering Consulting | Boston, MA',
|
||||||
|
description:
|
||||||
|
'Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications.',
|
||||||
|
isPartOf: { '@id': `${BASE}/#website` },
|
||||||
|
about: { '@id': `${BASE}/#organization` },
|
||||||
|
mainEntity: { '@id': `${BASE}/#organization` },
|
||||||
|
primaryImageOfPage: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'OfferCatalog',
|
||||||
|
'@id': `${BASE}/#services`,
|
||||||
|
name: 'Software Engineering Consulting Services',
|
||||||
|
description: 'Consulting services offered by mifi Ventures',
|
||||||
|
numberOfItems: 6,
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Frontend and UI Architecture',
|
||||||
|
description:
|
||||||
|
'Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Greenfield Product Development',
|
||||||
|
description:
|
||||||
|
'Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Performance Optimization',
|
||||||
|
description:
|
||||||
|
'Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Accessibility Engineering',
|
||||||
|
description:
|
||||||
|
'Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'System Modernization',
|
||||||
|
description:
|
||||||
|
'Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
itemOffered: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'End-to-End Feature Delivery',
|
||||||
|
description:
|
||||||
|
'End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
53
src/lib/seo.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* SEO / meta: site-wide defaults, page-meta type, and merge helper.
|
||||||
|
* Layout renders <head> from merged meta; each route can export meta from +page.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SEO_DEFAULTS = {
|
||||||
|
siteName: 'mifi Ventures',
|
||||||
|
baseUrl: 'https://mifi.ventures',
|
||||||
|
defaultOgImage: '/assets/og-image.png',
|
||||||
|
ogImageWidth: 1200,
|
||||||
|
ogImageHeight: 630,
|
||||||
|
locale: 'en_US',
|
||||||
|
twitterCard: 'summary_large_image' as const,
|
||||||
|
themeColorLight: '#0052cc',
|
||||||
|
themeColorDark: '#4da6ff',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface PageMeta {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
canonical?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
ogType?: string;
|
||||||
|
twitterTitle?: string;
|
||||||
|
twitterDescription?: string;
|
||||||
|
/** JSON-LD graph nodes (merged with defaults in layout) */
|
||||||
|
jsonLd?: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MergedMeta extends PageMeta {
|
||||||
|
canonical: string;
|
||||||
|
ogImage: string;
|
||||||
|
ogImageAlt: string;
|
||||||
|
jsonLdGraph: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merge page meta with site defaults for rendering. */
|
||||||
|
export function mergeMeta(meta: PageMeta, path: string = '/'): MergedMeta {
|
||||||
|
const baseUrl = SEO_DEFAULTS.baseUrl;
|
||||||
|
const canonical = meta.canonical ?? `${baseUrl}${path === '/' ? '' : path}`;
|
||||||
|
const ogImage = meta.ogImage?.startsWith('http')
|
||||||
|
? meta.ogImage
|
||||||
|
: `${baseUrl}${meta.ogImage?.startsWith('/') ? meta.ogImage : SEO_DEFAULTS.defaultOgImage}`;
|
||||||
|
const ogImageAlt = meta.title;
|
||||||
|
const jsonLdGraph = meta.jsonLd ?? [];
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
canonical,
|
||||||
|
ogImage,
|
||||||
|
ogImageAlt,
|
||||||
|
jsonLdGraph,
|
||||||
|
};
|
||||||
|
}
|
||||||
146
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { mergeMeta, SEO_DEFAULTS } from '$lib/seo';
|
||||||
|
import { homeMeta } from '$lib/data/home-meta';
|
||||||
|
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
const meta = $derived(page.data?.meta ?? homeMeta);
|
||||||
|
const path = $derived(page.url?.pathname ?? '/');
|
||||||
|
const merged = $derived(mergeMeta(meta, path));
|
||||||
|
|
||||||
|
const jsonLdScript = $derived(
|
||||||
|
merged.jsonLdGraph.length > 0
|
||||||
|
? JSON.stringify({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': merged.jsonLdGraph,
|
||||||
|
})
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
const jsonLdHtml = $derived(
|
||||||
|
jsonLdScript
|
||||||
|
? '<script type="application/ld+json">' + jsonLdScript + '</scr' + 'ipt>'
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"></script>
|
||||||
|
<script defer src="/assets/js/ga-init.js"></script>
|
||||||
|
|
||||||
|
<title>{merged.title}</title>
|
||||||
|
<meta name="description" content={merged.description ?? ''} />
|
||||||
|
<link rel="canonical" href={merged.canonical} />
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/fraunces-v38-latin-600.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/fraunces-v38-latin-700.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/inter-v20-latin-regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/inter-v20-latin-italic.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/inter-v20-latin-500.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/inter-v20-latin-600.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/assets/fonts/inter-v20-latin-700.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="robots"
|
||||||
|
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||||
|
/>
|
||||||
|
<meta name="author" content="Mike Fitzpatrick" />
|
||||||
|
<meta name="geo.region" content="US-MA" />
|
||||||
|
<meta name="geo.placename" content="Boston" />
|
||||||
|
<meta name="geo.position" content="42.360082;-71.058880" />
|
||||||
|
<meta name="ICBM" content="42.360082, -71.058880" />
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content={SEO_DEFAULTS.themeColorLight}
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content={SEO_DEFAULTS.themeColorDark}
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<meta property="og:type" content={merged.ogType ?? 'website'} />
|
||||||
|
<meta property="og:url" content={merged.canonical} />
|
||||||
|
<meta property="og:site_name" content={SEO_DEFAULTS.siteName} />
|
||||||
|
<meta property="og:title" content={merged.twitterTitle ?? merged.title} />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={merged.twitterDescription ?? merged.description ?? ''}
|
||||||
|
/>
|
||||||
|
<meta property="og:image" content={merged.ogImage} />
|
||||||
|
<meta property="og:image:width" content={String(SEO_DEFAULTS.ogImageWidth)} />
|
||||||
|
<meta property="og:image:height" content={String(SEO_DEFAULTS.ogImageHeight)} />
|
||||||
|
<meta property="og:image:alt" content={merged.ogImageAlt} />
|
||||||
|
<meta property="og:locale" content={SEO_DEFAULTS.locale} />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content={SEO_DEFAULTS.twitterCard} />
|
||||||
|
<meta name="twitter:url" content={merged.canonical} />
|
||||||
|
<meta name="twitter:title" content={merged.twitterTitle ?? merged.title} />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content={merged.twitterDescription ?? merged.description ?? ''}
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content={merged.ogImage} />
|
||||||
|
<meta name="twitter:image:alt" content={merged.ogImageAlt} />
|
||||||
|
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||||
|
|
||||||
|
{#if jsonLdHtml}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD -->
|
||||||
|
{@html jsonLdHtml}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<script src="/assets/scripts/copyright-year.js" defer></script>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<a href="#main" class="skip-link">Skip to main content</a>
|
||||||
|
{@render children()}
|
||||||
3
src/routes/+layout.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const prerender = true;
|
||||||
|
export const ssr = true;
|
||||||
|
export const csr = false;
|
||||||
23
src/routes/+page.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Navigation from '$lib/components/Navigation.svelte';
|
||||||
|
import Hero from '$lib/components/Hero.svelte';
|
||||||
|
import ExperienceSection from '$lib/components/ExperienceSection.svelte';
|
||||||
|
import WhatWeDo from '$lib/components/WhatWeDo.svelte';
|
||||||
|
import ImpactSection from '$lib/components/ImpactSection.svelte';
|
||||||
|
import HowWeWork from '$lib/components/HowWeWork.svelte';
|
||||||
|
import EngagementsSection from '$lib/components/EngagementsSection.svelte';
|
||||||
|
import ScheduleSection from '$lib/components/ScheduleSection.svelte';
|
||||||
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navigation />
|
||||||
|
<Hero />
|
||||||
|
<main id="main">
|
||||||
|
<ExperienceSection />
|
||||||
|
<WhatWeDo />
|
||||||
|
<ImpactSection />
|
||||||
|
<HowWeWork />
|
||||||
|
<EngagementsSection />
|
||||||
|
<ScheduleSection />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
6
src/routes/+page.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { PageLoad } from './$types';
|
||||||
|
import { homeMeta } from '$lib/data/home-meta';
|
||||||
|
|
||||||
|
export const load: PageLoad = () => {
|
||||||
|
return { meta: homeMeta };
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
8
static/assets/js/ga-init.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){ window.dataLayer.push(arguments); }
|
||||||
|
|
||||||
|
gtag("js", new Date());
|
||||||
|
gtag("config", "G-36F29PDKRT", {
|
||||||
|
// optional, but often helpful:
|
||||||
|
anonymize_ip: true,
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 885 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
5
static/assets/scripts/copyright-year.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var el = document.getElementById('copyright-year');
|
||||||
|
if (el) el.textContent = new Date().getFullYear();
|
||||||
|
})();
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
25
stylelint.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
extends: ['stylelint-config-standard'],
|
||||||
|
rules: {
|
||||||
|
'color-hex-length': null,
|
||||||
|
'custom-property-empty-line-before': null,
|
||||||
|
'color-function-notation': null,
|
||||||
|
'color-function-alias-notation': null,
|
||||||
|
'alpha-value-notation': null,
|
||||||
|
'value-keyword-case': null,
|
||||||
|
'property-no-deprecated': null,
|
||||||
|
'declaration-block-no-duplicate-properties': null,
|
||||||
|
'selector-not-notation': null,
|
||||||
|
'media-feature-range-notation': null,
|
||||||
|
'no-descending-specificity': null,
|
||||||
|
'media-feature-name-value-no-unknown': null,
|
||||||
|
'selector-pseudo-element-colon-notation': null,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte'],
|
||||||
|
customSyntax: 'postcss-html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ignoreFiles: ['dist/**', 'node_modules/**', '.svelte-kit/**'],
|
||||||
|
};
|
||||||
18
svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
pages: 'dist',
|
||||||
|
assets: 'dist',
|
||||||
|
fallback: undefined,
|
||||||
|
precompress: false,
|
||||||
|
strict: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
12
tests/visual.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('visual regression', () => {
|
||||||
|
test('home page matches snapshot', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page).toHaveTitle(/mifi Ventures/);
|
||||||
|
await expect(page.locator('h1')).toBeVisible();
|
||||||
|
await expect(page.locator('#main')).toBeVisible();
|
||||||
|
await expect(page.locator('.footer')).toBeVisible();
|
||||||
|
await expect(page).toHaveScreenshot('home.png', { fullPage: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
tests/visual.spec.ts-snapshots/home-chromium-darwin.png
Normal file
|
After Width: | Height: | Size: 591 KiB |
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
12
vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
server: {
|
||||||
|
host: true, // listen on 0.0.0.0 so reachable from host (e.g. dev container, port forward)
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
7
vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
|
||||||
|
},
|
||||||
|
});
|
||||||