The Svelte 5 SSG Migration #1
@@ -3,14 +3,17 @@
|
||||
"dockerFile": "Dockerfile",
|
||||
"workspaceFolder": "/workspaces/mifi-ventures-landing",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/mifi-ventures-landing,type=bind",
|
||||
"forwardPorts": [3000],
|
||||
"forwardPorts": [5173, 4173],
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Site",
|
||||
"5173": {
|
||||
"label": "Dev (Vite)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"4173": {
|
||||
"label": "Preview (Vite)",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
"postStartCommand": "nohup npx -y serve site -l 3000 > /tmp/serve.log 2>&1 & sleep 1",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
||||
10
.gitignore
vendored
@@ -22,6 +22,16 @@ pnpm-debug.log*
|
||||
dist/
|
||||
build/
|
||||
|
||||
# SvelteKit / Vite
|
||||
.svelte-kit/
|
||||
.vite/
|
||||
|
||||
# Test outputs
|
||||
test-results/
|
||||
playwright-report/
|
||||
coverage/
|
||||
.playwright/
|
||||
|
||||
# Environment variables (NEVER commit secrets)
|
||||
.env
|
||||
.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" } }]
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
# 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
|
||||
# Deploy pipeline: lint, test, build, then Docker image → registry → Portainer webhook.
|
||||
# Runs on push to main, tag, or manual run.
|
||||
# See pr.yaml for PR-only (lint + test + build).
|
||||
when:
|
||||
branch: main
|
||||
event: [push, tag, manual]
|
||||
|
||||
steps:
|
||||
# ============================================
|
||||
# Stage 1: Build Docker Image
|
||||
# ============================================
|
||||
- name: build-and-test
|
||||
image: node:20-alpine
|
||||
commands:
|
||||
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||
- pnpm install --frozen-lockfile || pnpm install
|
||||
- pnpm run lint
|
||||
- pnpm run lint:css
|
||||
- pnpm run build
|
||||
- pnpm test
|
||||
|
||||
- name: build
|
||||
image: docker:latest
|
||||
environment:
|
||||
@@ -18,7 +23,7 @@ steps:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
commands:
|
||||
- set -e # Exit on error
|
||||
- set -e
|
||||
- echo "=== Building Docker image ==="
|
||||
- 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"'
|
||||
- 'echo "Registry repo: $REGISTRY_REPO"'
|
||||
@@ -30,10 +35,9 @@ steps:
|
||||
--label "git.branch=${CI_COMMIT_BRANCH}" \
|
||||
.
|
||||
- echo "✓ Docker image built successfully"
|
||||
depends_on:
|
||||
- build-and-test
|
||||
|
||||
# ============================================
|
||||
# Stage 2: Push to Registry
|
||||
# ============================================
|
||||
- name: push
|
||||
image: docker:latest
|
||||
environment:
|
||||
@@ -46,7 +50,7 @@ steps:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
commands:
|
||||
- set -e # Exit on error
|
||||
- set -e
|
||||
- echo "=== Pushing to registry ==="
|
||||
- 'echo "Registry: $REGISTRY_URL"'
|
||||
- 'echo "Repository: $REGISTRY_REPO"'
|
||||
@@ -60,9 +64,6 @@ steps:
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# ============================================
|
||||
# Stage 3: Trigger Portainer stack redeploy (webhook)
|
||||
# ============================================
|
||||
- name: deploy
|
||||
image: curlimages/curl:latest
|
||||
environment:
|
||||
@@ -82,25 +83,3 @@ steps:
|
||||
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.
|
||||
16
.woodpecker/pr.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# PR pipeline: lint, test, and test build on the branch.
|
||||
# Runs when a pull request is opened or updated.
|
||||
# Does not build Docker image or deploy.
|
||||
when:
|
||||
event: pull_request
|
||||
|
||||
steps:
|
||||
- name: lint-and-build
|
||||
image: node:20-alpine
|
||||
commands:
|
||||
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||
- pnpm install --frozen-lockfile || pnpm install
|
||||
- pnpm run lint
|
||||
- pnpm run lint:css
|
||||
- pnpm run build
|
||||
- pnpm test
|
||||
10
Dockerfile
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
|
||||
WORKDIR /app
|
||||
@@ -12,8 +12,10 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install --frozen-lockfile || pnpm install
|
||||
|
||||
COPY build.mjs ./
|
||||
COPY site/ ./site/
|
||||
COPY svelte.config.js vite.config.ts tsconfig.json postcss.config.js ./
|
||||
COPY src/ ./src/
|
||||
COPY static/ ./static/
|
||||
COPY scripts/ ./scripts/
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
226
README.md
@@ -4,7 +4,8 @@ A minimal, production-ready static website for mifi Ventures, LLC — a software
|
||||
|
||||
## 🏗️ 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)
|
||||
- **Containerization**: Docker
|
||||
- **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
|
||||
- ✅ **SEO optimized** with Open Graph, Twitter Cards, JSON-LD structured data
|
||||
- ✅ **Performance optimized** with nginx gzip compression and cache headers
|
||||
- ✅ **Zero frameworks** — pure HTML/CSS/JS for maximum speed and simplicity
|
||||
- ✅ **Minimal JS** — only a tiny copyright-year script; no Svelte runtime or app bundle
|
||||
|
||||
## 🚀 Local Development
|
||||
|
||||
This project uses **pnpm** as the package manager. After cloning, run `pnpm install` (or ensure Corepack is enabled so `pnpm` is available).
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm install` | Install dependencies |
|
||||
| `pnpm run dev` | Serve `site/` at http://localhost:3000 with **live reload** (watcher) |
|
||||
| `pnpm run build` | Copy `site/` → `dist/` and inline critical CSS in `index.html` |
|
||||
| `pnpm run preview` | Serve built `dist/` to test production output |
|
||||
| Command | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------ |
|
||||
| `pnpm install` | Install dependencies |
|
||||
| `pnpm run dev` | SvelteKit dev server at http://localhost:5173 with **live reload** |
|
||||
| `pnpm run build` | SvelteKit build → `dist/`, then Critters inlines critical CSS |
|
||||
| `pnpm run preview` | Serve `dist/` (Critters-processed) at http://localhost:4173 — same as deployed |
|
||||
| `pnpm test` | Run unit tests (Vitest) |
|
||||
| `pnpm run lint` | ESLint (JS/TS/Svelte) |
|
||||
| `pnpm run lint:css` | Stylelint (global CSS + Svelte styles) |
|
||||
| `pnpm run format` | Prettier (JS/TS/Svelte/CSS/JSON) |
|
||||
|
||||
### Option 1: pnpm dev (recommended for editing)
|
||||
|
||||
@@ -39,27 +44,18 @@ From the project root:
|
||||
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
|
||||
# Python 3
|
||||
cd site
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Node (if you prefer not to use pnpm dev)
|
||||
cd site
|
||||
pnpm exec serve .
|
||||
|
||||
# PHP
|
||||
cd site
|
||||
php -S localhost:8000
|
||||
pnpm run build
|
||||
pnpm run preview
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -76,7 +72,7 @@ pnpm install
|
||||
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)
|
||||
|
||||
@@ -91,53 +87,51 @@ Then visit: `http://localhost:8080`. Stop with `docker stop mifi-ventures-landin
|
||||
|
||||
## 📝 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
|
||||
<!--
|
||||
EDITABLE CONSTANTS:
|
||||
- DOMAIN
|
||||
- ORG_NAME
|
||||
- PRINCIPAL_NAME
|
||||
- CAL_LINK
|
||||
- RESUME_PATH
|
||||
- LINKEDIN_URL
|
||||
- GITHUB_URL
|
||||
-->
|
||||
```
|
||||
- **Meta/SEO**: `src/lib/seo.ts`, `src/lib/data/home-meta.ts`, and per-route `+page.ts` load functions
|
||||
- **Section copy**: `src/lib/data/content.ts`, `src/lib/data/experience.ts`, `src/lib/data/engagements.ts`
|
||||
- **JSON-LD**: `src/lib/data/json-ld.ts`
|
||||
- **Layout and sections**: `src/routes/+layout.svelte`, `src/routes/+page.svelte`, and components in `src/lib/components/`
|
||||
|
||||
Update these values directly in `site/index.html` to modify:
|
||||
- Company information
|
||||
- Calendar booking link
|
||||
- Social media links
|
||||
- Resume file path
|
||||
To change company info, calendar link, social links, or resume path, edit the data modules and `src/lib/data/home-meta.ts` (or the relevant route’s meta).
|
||||
|
||||
## 🗂️ Project Structure
|
||||
|
||||
```
|
||||
mifi-ventures-landing/
|
||||
├── .devcontainer/ # Dev container for local development
|
||||
│ ├── devcontainer.json # Dev container config (port 3000, extensions)
|
||||
│ └── Dockerfile # Dev container image (Node + serve)
|
||||
├── .woodpecker.yml # CI/CD pipeline configuration
|
||||
│ ├── devcontainer.json # Dev container config (extensions)
|
||||
│ └── Dockerfile # Dev container image (Node)
|
||||
├── .woodpecker/ # CI/CD pipelines (see below)
|
||||
│ ├── pr.yaml # PR: lint, test, build (no deploy)
|
||||
│ └── deploy.yaml # main: lint, test, build, Docker, push, webhook
|
||||
├── Dockerfile # Production container (nginx:alpine)
|
||||
├── nginx.conf # nginx web server configuration
|
||||
├── README.md # This file
|
||||
├── .gitignore # Git ignore rules
|
||||
└── site/ # Static website files
|
||||
├── index.html # Main HTML file
|
||||
├── styles.css # CSS styles (light/dark mode)
|
||||
├── script.js # Minimal JavaScript (dynamic year)
|
||||
├── robots.txt # Search engine directives
|
||||
├── favicon.svg # Site favicon
|
||||
└── assets/
|
||||
├── resume.pdf # Resume download (placeholder)
|
||||
└── logos/ # Company logo SVGs
|
||||
├── atlassian.svg
|
||||
├── tjx.svg
|
||||
├── cargurus.svg
|
||||
├── timberland.svg
|
||||
└── mfa-boston.svg
|
||||
├── svelte.config.js # SvelteKit config (adapter-static)
|
||||
├── vite.config.ts # Vite config
|
||||
├── postcss.config.js # PostCSS (autoprefixer)
|
||||
├── scripts/critters.mjs # Post-build critical CSS inlining
|
||||
├── static/ # Static assets (copied to dist as-is)
|
||||
│ ├── favicon.svg, favicon.ico, robots.txt
|
||||
│ ├── copyright-year.js # Minimal client script (footer year)
|
||||
│ └── assets/ # Fonts, images, logos, resume.pdf, og-image.png
|
||||
├── src/
|
||||
│ ├── app.css # Global tokens + base styles
|
||||
│ ├── app.html # HTML shell for SvelteKit
|
||||
│ ├── app.d.ts # SvelteKit types
|
||||
│ ├── routes/
|
||||
│ │ ├── +layout.ts # Prerender, csr: false
|
||||
│ │ ├── +layout.svelte # Shell, head, skip link, slot
|
||||
│ │ ├── +page.ts # Home page meta (load)
|
||||
│ │ └── +page.svelte # Home page content (components)
|
||||
│ └── lib/
|
||||
│ ├── seo.ts # Meta defaults, mergeMeta, PageMeta type
|
||||
│ ├── copyright-year.ts
|
||||
│ ├── data/ # home-meta, json-ld, content, experience, engagements
|
||||
│ └── components/ # Hero, sections, Footer, Logo, etc.
|
||||
├── tests/ # Playwright visual regression
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🚢 CI/CD Deployment (Woodpecker + Gitea)
|
||||
@@ -146,9 +140,12 @@ mifi-ventures-landing/
|
||||
|
||||
### Pipeline Overview
|
||||
|
||||
The `.woodpecker.yml` pipeline automates deployment on push to `main`:
|
||||
Woodpecker uses two workflows (`.woodpecker/pr.yaml` and `.woodpecker/deploy.yaml`):
|
||||
|
||||
- **Pull requests**: Opening or updating a PR runs **lint** (ESLint + Stylelint), **tests** (Vitest), and a **test build** (SvelteKit + Critters) on the branch. No Docker image or deploy.
|
||||
- **Push to main** (or tag / manual run): Runs the same lint, test, and build, then:
|
||||
1. **Build** — Builds Docker image tagged with commit SHA + `latest`
|
||||
|
||||
1. **Build** — Builds Docker image tagged with commit SHA + `latest`
|
||||
2. **Push** — Pushes images to private Docker registry
|
||||
3. **Deploy** — SSH to Linode VPS, pulls latest image, restarts container with health checks
|
||||
|
||||
@@ -158,15 +155,16 @@ The `.woodpecker.yml` pipeline automates deployment on push to `main`:
|
||||
|
||||
Navigate to your repository → Settings → Secrets and add:
|
||||
|
||||
| Secret Name | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| `registry_password` | Docker registry password or token | `dckr_pat_xxxxx` |
|
||||
| `deploy_host` | Linode VPS hostname or IP | `vps.example.com` or `192.0.2.1` |
|
||||
| `deploy_username` | SSH username | `deploy` or `root` |
|
||||
| `deploy_ssh_key` | Private SSH key (multi-line) | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||
| `deploy_port` | SSH port | `22` (default) |
|
||||
| Secret Name | Description | Example |
|
||||
| ------------------- | --------------------------------- | ---------------------------------------- |
|
||||
| `registry_password` | Docker registry password or token | `dckr_pat_xxxxx` |
|
||||
| `deploy_host` | Linode VPS hostname or IP | `vps.example.com` or `192.0.2.1` |
|
||||
| `deploy_username` | SSH username | `deploy` or `root` |
|
||||
| `deploy_ssh_key` | Private SSH key (multi-line) | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||
| `deploy_port` | SSH port | `22` (default) |
|
||||
|
||||
**Generate SSH key for deployment:**
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
|
||||
# Add public key to server: ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub user@host
|
||||
@@ -177,13 +175,13 @@ ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
|
||||
|
||||
Set these as repository or organization-level variables:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `REGISTRY_URL` | Docker registry base URL | `registry.example.com` |
|
||||
| `REGISTRY_REPO` | Full image repository path | `registry.example.com/mifi-ventures-landing` |
|
||||
| `REGISTRY_USERNAME` | Registry username | `myusername` |
|
||||
| `CONTAINER_NAME` | Container name on server | `mifi-ventures-landing` |
|
||||
| `APP_PORT` | Host port to expose | `8080` (or `80` if direct) |
|
||||
| Variable | Description | Example |
|
||||
| ------------------- | -------------------------- | -------------------------------------------- |
|
||||
| `REGISTRY_URL` | Docker registry base URL | `registry.example.com` |
|
||||
| `REGISTRY_REPO` | Full image repository path | `registry.example.com/mifi-ventures-landing` |
|
||||
| `REGISTRY_USERNAME` | Registry username | `myusername` |
|
||||
| `CONTAINER_NAME` | Container name on server | `mifi-ventures-landing` |
|
||||
| `APP_PORT` | Host port to expose | `8080` (or `80` if direct) |
|
||||
|
||||
#### Example Configuration
|
||||
|
||||
@@ -191,22 +189,21 @@ Set these as repository or organization-level variables:
|
||||
|
||||
```yaml
|
||||
# Secrets (Values tab)
|
||||
registry_password: "your-registry-token"
|
||||
deploy_host: "123.45.67.89"
|
||||
deploy_username: "deploy"
|
||||
registry_password: 'your-registry-token'
|
||||
deploy_host: '123.45.67.89'
|
||||
deploy_username: 'deploy'
|
||||
deploy_ssh_key: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
deploy_port: "22"
|
||||
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
deploy_port: '22'
|
||||
# Environment Variables (Variables tab)
|
||||
REGISTRY_URL: "registry.example.com"
|
||||
REGISTRY_REPO: "registry.example.com/mifi-ventures-landing"
|
||||
REGISTRY_USERNAME: "myuser"
|
||||
CONTAINER_NAME: "mifi-ventures-landing"
|
||||
APP_PORT: "8080"
|
||||
REGISTRY_URL: 'registry.example.com'
|
||||
REGISTRY_REPO: 'registry.example.com/mifi-ventures-landing'
|
||||
REGISTRY_USERNAME: 'myuser'
|
||||
CONTAINER_NAME: 'mifi-ventures-landing'
|
||||
APP_PORT: '8080'
|
||||
```
|
||||
|
||||
### Pipeline Features
|
||||
@@ -221,15 +218,21 @@ APP_PORT: "8080"
|
||||
### Troubleshooting
|
||||
|
||||
**Build fails:**
|
||||
|
||||
```bash
|
||||
# Build locally first (must succeed before Docker)
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
# Check Dockerfile syntax
|
||||
docker build -t test .
|
||||
|
||||
# Verify files are present
|
||||
ls -la site/
|
||||
# Verify source is present
|
||||
ls -la src/ static/
|
||||
```
|
||||
|
||||
**Push fails:**
|
||||
|
||||
```bash
|
||||
# Test registry login locally
|
||||
echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
||||
@@ -238,6 +241,7 @@ echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
|
||||
```
|
||||
|
||||
**Deploy fails:**
|
||||
|
||||
```bash
|
||||
# Test SSH connection
|
||||
ssh -i ~/.ssh/key user@host "docker ps"
|
||||
@@ -250,6 +254,7 @@ ssh user@host "docker --version"
|
||||
```
|
||||
|
||||
**Container fails health check:**
|
||||
|
||||
```bash
|
||||
# SSH to server and check logs
|
||||
ssh user@host "docker logs mifi-ventures-landing"
|
||||
@@ -285,6 +290,7 @@ EOF
|
||||
The custom `nginx.conf` provides optimized static file delivery:
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
- **HTML files**: `no-cache, must-revalidate` (always fresh from server)
|
||||
- **CSS/JS**: `max-age=31536000, immutable` (1 year, content-addressed)
|
||||
- **Images** (JPG, PNG, WebP, AVIF): `max-age=2592000` (30 days)
|
||||
@@ -295,7 +301,9 @@ The custom `nginx.conf` provides optimized static file delivery:
|
||||
- **favicon.svg**: `max-age=2592000` (30 days)
|
||||
|
||||
### Gzip Compression
|
||||
|
||||
Enabled for all text-based content with compression level 6:
|
||||
|
||||
- HTML, CSS, JavaScript
|
||||
- JSON, XML
|
||||
- SVG images
|
||||
@@ -303,6 +311,7 @@ Enabled for all text-based content with compression level 6:
|
||||
Minimum size: 256 bytes (avoids compressing tiny files)
|
||||
|
||||
### Other Features
|
||||
|
||||
- **Server tokens**: Disabled for security
|
||||
- **Access logs**: Disabled for static assets (performance)
|
||||
- **Hidden files**: Denied (.git, .env, etc.)
|
||||
@@ -310,6 +319,7 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
||||
- **Health check**: Available on port 80 for container orchestration
|
||||
|
||||
### Security Headers
|
||||
|
||||
**Note**: Security headers (CSP, HSTS, X-Frame-Options, etc.) are handled upstream by Traefik and are NOT included in this nginx configuration to avoid duplication.
|
||||
|
||||
## 🎯 SEO & Performance
|
||||
@@ -317,6 +327,7 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
||||
### Current Optimizations
|
||||
|
||||
#### On-Page SEO
|
||||
|
||||
- **Title tag**: Includes business name, service, and location
|
||||
- **Meta description**: Natural, compelling copy (155 characters) emphasizing Boston location and services
|
||||
- **Canonical URL**: Set to `https://mifi.ventures/` to prevent duplicate content issues
|
||||
@@ -327,17 +338,20 @@ Minimum size: 256 bytes (avoids compressing tiny files)
|
||||
- **Language declaration**: `lang="en-US"` for US English
|
||||
|
||||
#### Social Media Share Previews
|
||||
|
||||
- **Open Graph tags**: Complete OG implementation for Facebook, LinkedIn
|
||||
- Site name, title, description, URL, image
|
||||
- Image dimensions (1200x630px) and alt text
|
||||
- Locale set to `en_US`
|
||||
- Site name, title, description, URL, image
|
||||
- Image dimensions (1200x630px) and alt text
|
||||
- Locale set to `en_US`
|
||||
- **Twitter Cards**: `summary_large_image` card with full metadata
|
||||
- Creator and site handles (update with actual Twitter)
|
||||
- Image with alt text for accessibility
|
||||
- Creator and site handles (update with actual Twitter)
|
||||
- Image with alt text for accessibility
|
||||
- **Theme colors**: Dynamic based on light/dark mode preference
|
||||
|
||||
#### Structured Data (JSON-LD)
|
||||
|
||||
Comprehensive @graph structure with interconnected entities:
|
||||
|
||||
- **Organization** (`#organization`): mifi Ventures, LLC with Boston address, geo coordinates, and service catalog
|
||||
- **Person** (`#principal`): Mike Fitzpatrick as "Principal Software Engineer and Architect" with worksFor relationship and knowsAbout expertise areas
|
||||
- **WebSite** (`#website`): Site-level metadata with ReserveAction pointing to Cal.com scheduling
|
||||
@@ -347,6 +361,7 @@ Comprehensive @graph structure with interconnected entities:
|
||||
- **No email or phone**: Complies with privacy requirements
|
||||
|
||||
#### Technical SEO
|
||||
|
||||
- **robots.txt**: Properly configured for full site crawling
|
||||
- **Lazy loading**: Images load on-demand for performance
|
||||
- **Minimal JavaScript**: Only essential scripts (copyright year)
|
||||
@@ -358,6 +373,7 @@ Comprehensive @graph structure with interconnected entities:
|
||||
### Action Items
|
||||
|
||||
Before launch, update these placeholders:
|
||||
|
||||
1. Create OG image: 1200x630px PNG at `/assets/og-image.png`
|
||||
2. Update Twitter handles in meta tags (lines 57-58) if you have a Twitter presence
|
||||
3. Update GitHub URL in footer and constants if you want to include it (currently optional)
|
||||
@@ -365,6 +381,7 @@ Before launch, update these placeholders:
|
||||
### SEO Testing & Validation
|
||||
|
||||
Before going live, validate with these tools:
|
||||
|
||||
- **Google Search Console**: Submit site, monitor indexing
|
||||
- **Rich Results Test**: Verify JSON-LD structured data
|
||||
- **Facebook Sharing Debugger**: Test OG tags preview
|
||||
@@ -374,6 +391,7 @@ Before going live, validate with these tools:
|
||||
- **PageSpeed Insights**: Check Core Web Vitals
|
||||
|
||||
Key metrics to monitor post-launch:
|
||||
|
||||
- Indexing status in Google Search Console
|
||||
- Click-through rates (CTR) from search results
|
||||
- Share engagement on social platforms
|
||||
@@ -398,6 +416,7 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
||||
### Implemented Features
|
||||
|
||||
#### Keyboard Navigation
|
||||
|
||||
- **Skip link**: Visible on keyboard focus, jumps directly to main content (`#main`)
|
||||
- **Logical tab order**: All interactive elements follow natural reading order
|
||||
- **No keyboard traps**: Users can navigate through and exit all interactive regions
|
||||
@@ -405,36 +424,42 @@ 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
|
||||
|
||||
#### Semantic Structure
|
||||
|
||||
- **Proper landmarks**: `<header>`, `<main>`, `<footer>`, and `<nav>` for clear page regions
|
||||
- **Single H1**: One `<h1>` element ("mifi Ventures") with logical H2 nesting for all sections
|
||||
- **ARIA labelledby**: All sections connected to their headings via `aria-labelledby` attributes
|
||||
- **Language declaration**: `lang="en-US"` attribute on `<html>` element
|
||||
|
||||
#### Visual & Color
|
||||
|
||||
- **AAA contrast ratios**: All text meets AAA standards (7:1 for normal text, 4.5:1 for large text)
|
||||
- Light mode: `#1a1a1a` text on `#ffffff` background (16.1:1 ratio)
|
||||
- Dark mode: `#f5f5f5` text on `#0a0a0a` background (18.4:1 ratio)
|
||||
- Light mode: `#1a1a1a` text on `#ffffff` background (16.1:1 ratio)
|
||||
- Dark mode: `#f5f5f5` text on `#0a0a0a` background (18.4:1 ratio)
|
||||
- **Color independence**: No information conveyed by color alone
|
||||
- **High contrast mode**: Enhanced borders, outlines, and contrast for users with `prefers-contrast: high`
|
||||
|
||||
#### Interactive Elements
|
||||
|
||||
- **Adequate touch targets**: All buttons and links meet minimum 44x44px size (AAA requirement)
|
||||
- **Descriptive link text**: All links have meaningful text or enhanced ARIA labels
|
||||
- **External link warnings**: Links opening in new tabs clearly labeled "(opens in new tab)"
|
||||
- **Button spacing**: Generous gaps between CTAs prevent accidental activation
|
||||
|
||||
#### Motion & Animation
|
||||
|
||||
- **Respects `prefers-reduced-motion`**: All animations and transforms disabled when users prefer reduced motion
|
||||
- **Safe default animations**: Subtle hover effects that don't cause vestibular issues
|
||||
- **No auto-playing content**: No carousels, videos, or content that moves automatically
|
||||
|
||||
#### Images & Media
|
||||
|
||||
- **Descriptive alt text**: All images have clear, concise alternative text
|
||||
- **Text fallbacks**: Logo strip includes visually-hidden text that appears if images fail
|
||||
- **Mobile text list**: On small screens, logo images replaced with accessible text list
|
||||
- **Decorative images marked**: Images that don't convey content use appropriate ARIA attributes
|
||||
|
||||
#### Screen Reader Support
|
||||
|
||||
- **Clear labels**: All form controls, buttons, and navigation have proper labels
|
||||
- **ARIA landmarks**: Supplementary ARIA roles for enhanced screen reader navigation
|
||||
- **Visually-hidden content**: Important text available to screen readers but hidden visually where appropriate
|
||||
@@ -443,6 +468,7 @@ This site is built to meet **WCAG 2.2 Level AAA** standards wherever applicable
|
||||
### Testing Recommendations
|
||||
|
||||
For best results, test with:
|
||||
|
||||
- **Keyboard only**: Tab through entire page without mouse
|
||||
- **Screen readers**: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
|
||||
- **Browser extensions**: axe DevTools, WAVE, Lighthouse accessibility audit
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -34,9 +34,11 @@ Portainer pulls `git.mifi.dev/mifi-ventures/landing:latest`. If that image has n
|
||||
|
||||
**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
|
||||
pnpm install
|
||||
pnpm run build # SvelteKit → dist/; Critters inlines critical CSS
|
||||
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 .
|
||||
```
|
||||
@@ -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) |
|
||||
| `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
|
||||
|
||||
#### 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
|
||||
# Clone repo
|
||||
git clone https://git.mifi.dev/mifi-ventures/landing.git
|
||||
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 .
|
||||
|
||||
# Run locally
|
||||
@@ -109,6 +119,8 @@ curl http://localhost:8080
|
||||
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
|
||||
```bash
|
||||
# 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
|
||||
33
eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
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: ['**/*.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'
|
||||
}
|
||||
}
|
||||
];
|
||||
65
package.json
@@ -1,17 +1,52 @@
|
||||
{
|
||||
"name": "mifi-ventures-landing",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"repository": "https://git.mifi.dev/mifi-ventures/landing.git",
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"description": "mifi Ventures landing site — static build with critical CSS inlining",
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"preview": "npx serve dist",
|
||||
"dev": "live-server site --port=3000 --open=/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"critters": "^0.0.24",
|
||||
"live-server": "^1.2.2"
|
||||
}
|
||||
"name": "mifi-ventures-landing",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"repository": "https://git.mifi.dev/mifi-ventures/landing.git",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && node scripts/critters.mjs",
|
||||
"preview": "serve dist -p 4173",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:all": "vitest run && playwright test",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint .",
|
||||
"lint:css": "stylelint \"src/**/*.css\" \"src/**/*.svelte\"",
|
||||
"format": "prettier --write \"src/**/*.{ts,js,svelte,css,json}\""
|
||||
},
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
});
|
||||
5112
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: 'swap',
|
||||
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
640
src/app.css
Normal file
@@ -0,0 +1,640 @@
|
||||
/* ========================================
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
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>
|
||||
64
src/lib/components/EngagementsSection.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<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>
|
||||
82
src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<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%;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</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"
|
||||
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>
|
||||
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"
|
||||
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">
|
||||
let { color = 'currentColor' }: { color?: string } = $props();
|
||||
</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,
|
||||
};
|
||||
}
|
||||
141
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,141 @@
|
||||
<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>
|
||||
<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}
|
||||
{@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;
|
||||
21
src/routes/+page.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
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>
|
||||
|
||||
<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 |
|
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: 587 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'],
|
||||
},
|
||||
});
|
||||