22 KiB
mifi Ventures Landing Site
A minimal, production-ready static website for mifi Ventures, LLC — a software engineering consulting business.
🏗️ Technology Stack
- Frontend: SvelteKit (Svelte 5) with adapter-static — prerendered HTML/CSS, zero app JS (no hydration)
- Build: Vite, PostCSS (autoprefixer), Critters (critical CSS inlining)
- Server: nginx (Alpine Linux)
- Containerization: Docker
- CI/CD: Woodpecker CI
- Deployment: Linode VPS
🎨 Features
- ✅ Single-page design with anchored sections
- ✅ Responsive and mobile-friendly
- ✅ Light/Dark mode via
prefers-color-scheme - ✅ WCAG 2.2 AAA oriented with strong focus states, keyboard navigation, semantic markup
- ✅ SEO optimized with Open Graph, Twitter Cards, JSON-LD structured data
- ✅ Performance optimized with nginx gzip compression and cache headers
- ✅ Minimal JS — only a tiny copyright-year script; no Svelte runtime or app bundle
🚀 Local Development
This project uses pnpm as the package manager. After cloning, run pnpm install (or ensure Corepack is enabled so pnpm is available).
| Command | Description |
|---|---|
pnpm install |
Install dependencies |
pnpm run dev |
SvelteKit dev server at http://localhost:5173 with live reload |
pnpm run build |
SvelteKit build → dist/, then Critters inlines critical CSS |
pnpm run preview |
Serve dist/ (Critters-processed) at http://localhost:4173 — same as deployed |
pnpm test |
Run unit tests (Vitest) |
pnpm run test:e2e |
Run Playwright visual regression (e2e) |
pnpm run test:e2e:update-snapshots |
Regenerate e2e snapshots (in devcontainer = CI; see Visual regression) |
pnpm run lint |
ESLint (JS/TS/Svelte) |
pnpm run lint:css |
Stylelint (global CSS + Svelte styles) |
pnpm run format |
Prettier (JS/TS/Svelte/CSS/JSON) |
Option 1: pnpm dev (recommended for editing)
From the project root:
pnpm run dev
Opens http://localhost:5173 with live reload when you change files in src/ or static/.
Option 2: Preview production build
After building, serve the static output:
pnpm run build
pnpm run preview
Preview uses serve dist so you see the same HTML/CSS as in production (including critical CSS).
Option 3: Dev Container
Open the project in a dev container for a consistent local environment. The devcontainer uses the same image as CI (mcr.microsoft.com/playwright:v1.58.0-noble), so e2e snapshots generated inside it match CI.
- Open in Cursor or VS Code with the Dev Containers extension installed.
- Reopen in Container: Command Palette (
Cmd/Ctrl+Shift+P) → Dev Containers: Reopen in Container. - Wait for the container to build and start. (If you already had a devcontainer open, use Rebuild Container once to pick up the Playwright Noble image.)
Inside the container, run:
pnpm install
pnpm run dev
The site is served at http://localhost:5173 (or the port shown) with live reload (port forwarded automatically). Run pnpm run test:e2e:update-snapshots here to regenerate the Linux snapshot when the layout changes.
Visual regression (e2e)
E2e tests use Playwright and compare full-page screenshots to committed snapshots. CI and the devcontainer both use mcr.microsoft.com/playwright:v1.58.0-noble, so snapshots generated in the devcontainer match CI.
To update the Linux snapshot locally (devcontainer):
- Rebuild the devcontainer once (so it uses the Playwright Noble image; see Option 3 below).
- Run
pnpm run test:e2e:update-snapshots(no Docker needed — same environment as CI). - Commit the updated file(s) under
tests/visual.spec.ts-snapshots/.
If you’re not using the devcontainer: run the update-e2e-snapshots workflow manually in Woodpecker (requires a git_push_token secret), or run pnpm run test:e2e:update-snapshots on a host with Docker.
Local pnpm run test:e2e on macOS uses the Darwin snapshot; the Linux snapshot is used in CI and in the devcontainer.
Option 4: Docker (Production-like Test)
To test the production nginx image locally (same as deployed):
docker build -t mifi-ventures-landing .
docker run -d -p 8080:80 --name mifi-ventures-landing mifi-ventures-landing
Then visit: http://localhost:8080. Stop with docker stop mifi-ventures-landing && docker rm mifi-ventures-landing.
📝 Content Updates
Content and links are driven by data and components:
- Meta/SEO:
src/lib/seo.ts,src/lib/data/home-meta.ts, and per-route+page.tsload 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 insrc/lib/components/
To change company info, calendar link, social links, or resume path, edit the data modules and src/lib/data/home-meta.ts (or the relevant route’s meta).
🗂️ Project Structure
mifi-ventures-landing/
├── .devcontainer/ # Dev container for local development
│ ├── devcontainer.json # Dev container config (extensions)
│ └── Dockerfile # Dev container image (Node)
├── .woodpecker/ # CI/CD pipelines (see below)
│ ├── ci.yaml # one clone/workspace: install → lint → build → test
│ └── deploy.yaml # Docker → push → webhook (main only, after ci)
├── Dockerfile # Production container (nginx:alpine)
├── nginx.conf # nginx web server configuration
├── svelte.config.js # SvelteKit config (adapter-static)
├── vite.config.ts # Vite config
├── postcss.config.js # PostCSS (autoprefixer)
├── scripts/critters.mjs # Post-build critical CSS inlining
├── static/ # Static assets (copied to dist as-is)
│ ├── favicon.svg, favicon.ico, robots.txt
│ ├── copyright-year.js # Minimal client script (footer year)
│ └── assets/ # Fonts, images, logos, resume.pdf, og-image.png
├── src/
│ ├── app.css # Global tokens + base styles
│ ├── app.html # HTML shell for SvelteKit
│ ├── app.d.ts # SvelteKit types
│ ├── routes/
│ │ ├── +layout.ts # Prerender, csr: false
│ │ ├── +layout.svelte # Shell, head, skip link, slot
│ │ ├── +page.ts # Home page meta (load)
│ │ └── +page.svelte # Home page content (components)
│ └── lib/
│ ├── seo.ts # Meta defaults, mergeMeta, PageMeta type
│ ├── copyright-year.ts
│ ├── data/ # home-meta, json-ld, content, experience, engagements
│ └── components/ # Hero, sections, Footer, Logo, etc.
├── tests/ # Playwright visual regression
└── README.md # This file
🚢 CI/CD Deployment (Woodpecker + Gitea)
📖 Full deployment guide: See DEPLOYMENT.md for step-by-step setup instructions, troubleshooting, and examples.
Pipeline Overview
Woodpecker uses two workflows (.woodpecker/ci.yaml, deploy.yaml):
- Pull requests (and push/tag/manual on main): ci runs install → lint → build → test in one workspace (one clone, one install). No Docker or deploy on PRs.
- Push to main (or tag / manual on main): After ci succeeds, deploy runs:
- Build — Builds Docker image tagged with commit SHA +
latest
- Build — Builds Docker image tagged with commit SHA +
- Push — Pushes images to private Docker registry
- Deploy — SSH to Linode VPS, pulls latest image, restarts container with health checks
Required Configuration
Secrets (Configure in Woodpecker UI)
Navigate to your repository → Settings → Secrets and add:
| Secret Name | Description | Example |
|---|---|---|
registry_password |
Docker registry password or token | dckr_pat_xxxxx |
deploy_host |
Linode VPS hostname or IP | vps.example.com or 192.0.2.1 |
deploy_username |
SSH username | deploy or root |
deploy_ssh_key |
Private SSH key (multi-line) | -----BEGIN OPENSSH PRIVATE KEY-----... |
deploy_port |
SSH port | 22 (default) |
git_push_token |
(Optional) Repo push token for manual update-e2e-snapshots workflow (fallback when not using devcontainer) | Gitea/Forgejo personal access token |
Generate SSH key for deployment:
ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/woodpecker_deploy
# Add public key to server: ssh-copy-id -i ~/.ssh/woodpecker_deploy.pub user@host
# Copy private key content to Woodpecker secret
Environment Variables (Configure in Woodpecker)
Set these as repository or organization-level variables:
| Variable | Description | Example |
|---|---|---|
REGISTRY_URL |
Docker registry base URL | registry.example.com |
REGISTRY_REPO |
Full image repository path | registry.example.com/mifi-ventures-landing |
REGISTRY_USERNAME |
Registry username | myusername |
CONTAINER_NAME |
Container name on server | mifi-ventures-landing |
APP_PORT |
Host port to expose | 8080 (or 80 if direct) |
Example Configuration
In Woodpecker UI (Repository Settings):
# Secrets (Values tab)
registry_password: 'your-registry-token'
deploy_host: '123.45.67.89'
deploy_username: 'deploy'
deploy_ssh_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
-----END OPENSSH PRIVATE KEY-----
deploy_port: '22'
# Environment Variables (Variables tab)
REGISTRY_URL: 'registry.example.com'
REGISTRY_REPO: 'registry.example.com/mifi-ventures-landing'
REGISTRY_USERNAME: 'myuser'
CONTAINER_NAME: 'mifi-ventures-landing'
APP_PORT: '8080'
Pipeline Features
- ✅ Deterministic builds — Commit SHA tagging ensures reproducibility
- ✅ Fail-fast — Exits immediately on any error (
set -e) - ✅ Health checks — Verifies container starts and responds before completing
- ✅ Automatic cleanup — Prunes old images older than 72 hours
- ✅ Zero-downtime — Old container runs until new one is healthy
- ✅ Detailed logging — Clear output at each stage
Troubleshooting
Build fails:
# Build locally first (must succeed before Docker)
pnpm install
pnpm run build
# Check Dockerfile syntax
docker build -t test .
# Verify source is present
ls -la src/ static/
Push fails:
# Test registry login locally
echo "PASSWORD" | docker login registry.example.com -u username --password-stdin
# Verify registry URL and credentials
Deploy fails:
# Test SSH connection
ssh -i ~/.ssh/key user@host "docker ps"
# Check if Docker is installed on server
ssh user@host "docker --version"
# Verify environment variables are passed
# Check Woodpecker build logs for "REGISTRY_URL" values
Container fails health check:
# SSH to server and check logs
ssh user@host "docker logs mifi-ventures-landing"
# Check if port is already in use
ssh user@host "netstat -tulpn | grep :80"
Manual Deployment
For emergency deployments or testing:
# Build and push manually
docker build -t registry.example.com/mifi-ventures-landing:latest .
docker push registry.example.com/mifi-ventures-landing:latest
# Deploy manually via SSH
ssh user@host << 'EOF'
docker pull registry.example.com/mifi-ventures-landing:latest
docker stop mifi-ventures-landing || true
docker rm mifi-ventures-landing || true
docker run -d \
--name mifi-ventures-landing \
--restart unless-stopped \
-p 8080:80 \
registry.example.com/mifi-ventures-landing:latest
EOF
🔧 nginx Configuration
The custom nginx.conf provides optimized static file delivery:
Caching Strategy
- HTML files:
no-cache, must-revalidate(always fresh from server) - CSS/JS:
max-age=31536000, immutable(1 year, content-addressed) - Images (JPG, PNG, WebP, AVIF):
max-age=2592000(30 days) - SVG images:
max-age=2592000(30 days) - Fonts:
max-age=31536000, immutable(1 year) - Documents (PDF):
max-age=2592000(30 days) - robots.txt:
max-age=86400(1 day) - favicon.svg:
max-age=2592000(30 days)
Gzip Compression
Enabled for all text-based content with compression level 6:
- HTML, CSS, JavaScript
- JSON, XML
- SVG images
Minimum size: 256 bytes (avoids compressing tiny files)
Other Features
- Server tokens: Disabled for security
- Access logs: Disabled for static assets (performance)
- Hidden files: Denied (.git, .env, etc.)
- 404 handling: Falls back to index.html
- Health check: Available on port 80 for container orchestration
Security Headers
Note: Security headers (CSP, HSTS, X-Frame-Options, etc.) are handled upstream by Traefik and are NOT included in this nginx configuration to avoid duplication.
🎯 SEO & Performance
Current Optimizations
On-Page SEO
- Title tag: Includes business name, service, and location
- Meta description: Natural, compelling copy (155 characters) emphasizing Boston location and services
- Canonical URL: Set to
https://mifi.ventures/to prevent duplicate content issues - Robots meta:
index, followwith enhanced directives for snippet and image preview control - Semantic HTML5: Proper heading hierarchy (single H1, logical H2 structure)
- Geographic metadata: Boston, MA coordinates for local SEO
- Author attribution: Mike Fitzpatrick properly credited
- Language declaration:
lang="en-US"for US English
Social Media Share Previews
- Open Graph tags: Complete OG implementation for Facebook, LinkedIn
- Site name, title, description, URL, image
- Image dimensions (1200x630px) and alt text
- Locale set to
en_US
- Twitter Cards:
summary_large_imagecard with full metadata- Creator and site handles (update with actual Twitter)
- Image with alt text for accessibility
- Theme colors: Dynamic based on light/dark mode preference
Structured Data (JSON-LD)
Comprehensive @graph structure with interconnected entities:
- Organization (
#organization): mifi Ventures, LLC with Boston address, geo coordinates, and service catalog - Person (
#principal): Mike Fitzpatrick as "Principal Software Engineer and Architect" with worksFor relationship and knowsAbout expertise areas - WebSite (
#website): Site-level metadata with ReserveAction pointing to Cal.com scheduling - WebPage (
#webpage): Page-level metadata with inLanguage and primaryImageOfPage - OfferCatalog (
#services): Six service offerings aligned with "What We Do" section - LinkedIn profile: https://linkedin.com/in/the-mifi
- No email or phone: Complies with privacy requirements
Technical SEO
- robots.txt: Properly configured for full site crawling
- Lazy loading: Images load on-demand for performance
- Minimal JavaScript: Only essential scripts (copyright year)
- System font stack: No web font loading delays
- Clean URLs: No parameters or session IDs
- Mobile-friendly: Responsive design, passes mobile-usability tests
- Fast loading: Optimized assets, gzip compression, cache headers
Action Items
Before launch, update these placeholders:
- Create OG image: 1200x630px PNG at
/assets/og-image.png - Update Twitter handles in meta tags (lines 57-58) if you have a Twitter presence
- Update GitHub URL in footer and constants if you want to include it (currently optional)
SEO Testing & Validation
Before going live, validate with these tools:
- Google Search Console: Submit site, monitor indexing
- Rich Results Test: Verify JSON-LD structured data
- Facebook Sharing Debugger: Test OG tags preview
- Twitter Card Validator: Test Twitter card appearance
- Lighthouse SEO Audit: Aim for 100/100 score
- Mobile-Friendly Test: Ensure mobile usability
- PageSpeed Insights: Check Core Web Vitals
Key metrics to monitor post-launch:
- Indexing status in Google Search Console
- Click-through rates (CTR) from search results
- Share engagement on social platforms
- Core Web Vitals (LCP, FID, CLS)
- Page load times and performance scores
Future Enhancements
- Add sitemap.xml for better crawl efficiency
- Implement Content Security Policy (CSP) headers
- Add preconnect hints for external resources (Cal.com)
- Consider blog or case studies for content marketing
- Add FAQ schema markup if adding FAQ section
- Consider breadcrumb schema for better SERP display
- Local business listings (Google Business Profile)
- Schema markup for reviews/testimonials if applicable
♿ Accessibility
This site is built to meet WCAG 2.2 Level AAA standards wherever applicable to a static informational website.
Implemented Features
Keyboard Navigation
- Skip link: Visible on keyboard focus, jumps directly to main content (
#main) - Logical tab order: All interactive elements follow natural reading order
- No keyboard traps: Users can navigate through and exit all interactive regions
- Focus indicators: 4px high-contrast outlines with 4px offset and subtle glow on all focusable elements
- Focus never removed: Outline styles are enforced with
!importantto 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-labelledbyattributes - 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:
#1a1a1atext on#ffffffbackground (16.1:1 ratio) - Dark mode:
#f5f5f5text on#0a0a0abackground (18.4:1 ratio)
- Light mode:
- Color independence: No information conveyed by color alone
- High contrast mode: Enhanced borders, outlines, and contrast for users with
prefers-contrast: high
Interactive Elements
- Adequate touch targets: All buttons and links meet minimum 44x44px size (AAA requirement)
- Descriptive link text: All links have meaningful text or enhanced ARIA labels
- External link warnings: Links opening in new tabs clearly labeled "(opens in new tab)"
- Button spacing: Generous gaps between CTAs prevent accidental activation
Motion & Animation
- Respects
prefers-reduced-motion: All animations and transforms disabled when users prefer reduced motion - Safe default animations: Subtle hover effects that don't cause vestibular issues
- No auto-playing content: No carousels, videos, or content that moves automatically
Images & Media
- Descriptive alt text: All images have clear, concise alternative text
- Text fallbacks: Logo strip includes visually-hidden text that appears if images fail
- Mobile text list: On small screens, logo images replaced with accessible text list
- Decorative images marked: Images that don't convey content use appropriate ARIA attributes
Screen Reader Support
- Clear labels: All form controls, buttons, and navigation have proper labels
- ARIA landmarks: Supplementary ARIA roles for enhanced screen reader navigation
- Visually-hidden content: Important text available to screen readers but hidden visually where appropriate
- Logical reading order: Content structure follows visual hierarchy
Testing Recommendations
For best results, test with:
- Keyboard only: Tab through entire page without mouse
- Screen readers: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
- Browser extensions: axe DevTools, WAVE, Lighthouse accessibility audit
- High contrast mode: Windows High Contrast, macOS Increase Contrast
- Zoom: Test at 200% and 400% zoom levels
- Reduced motion: Enable in OS settings and verify animations stop
Known Limitations
- Logo images use CSS filters for dark mode adaptation (works well but may not be perfect for all logos)
- External link icons not implemented (relies on ARIA labels and "opens in new tab" text)
- No live regions or dynamic content requiring ARIA live announcements
📄 License
© 2026 mifi Ventures, LLC. All rights reserved.
📞 Contact
For inquiries, visit mifi.ventures or schedule a call.