The Svelte 5 SSG Migration (#1)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful

- Migrates the site to Svelte 5
- Still generates a static site with inlined critical path CSS for the ultimate in performance
- Opens up future possibilities for site growth

Reviewed-on: #1
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>
This commit was merged in pull request #1.
This commit is contained in:
2026-02-01 05:50:41 +00:00
committed by Mike Fitzpatrick
parent 40b770f8b5
commit 911093f0b6
96 changed files with 6841 additions and 3647 deletions

226
README.md
View File

@@ -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 routes 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)
│ ├── 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
├── 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/ci.yaml`, `deploy.yaml`):
- **Pull requests** (and **push/tag/manual on main**): **ci** runs install → lint → build → test in one workspace (one clone, one install). No Docker or deploy on PRs.
- **Push to main** (or tag / manual on main): After ci succeeds, **deploy** runs:
1. **Build** — Builds Docker image tagged with commit SHA + `latest`
1. **Build** — Builds Docker image tagged with commit SHA + `latest`
2. **Push** — Pushes images to private Docker registry
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