From bc325285519c6d6f9e7e44f4aaac71856831f8c9 Mon Sep 17 00:00:00 2001 From: mifi Date: Fri, 30 Jan 2026 23:29:39 -0300 Subject: [PATCH] =?UTF-8?q?The=20Svelte=205=20SSG=20migration=E2=80=94we?= =?UTF-8?q?=20brought=20sexy=20back...=20or=20to=20it=3F=20Or=20something.?= =?UTF-8?q?..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 11 +- .gitignore | 10 + .prettierignore | 5 + .prettierrc | 9 + .woodpecker.yml => .woodpecker/deploy.yaml | 55 +- .woodpecker/pr.yaml | 16 + Dockerfile | 10 +- README.md | 226 +- SEO-CHECKLIST.md | 164 - build.mjs | 48 - DEPLOYMENT.md => docs/DEPLOYMENT.md | 22 +- eslint.config.js | 33 + package.json | 65 +- playwright.config.ts | 23 + pnpm-lock.yaml | 5112 +++++++++++++---- postcss.config.js | 17 + scripts/critters.mjs | 71 + site/index.html | 768 --- site/script.js | 15 - site/styles.css | 1064 ---- src/app.css | 640 +++ src/app.d.ts | 13 + src/app.html | 11 + src/lib/components/EngagementsSection.svelte | 64 + src/lib/components/ExperienceSection.svelte | 276 + src/lib/components/Footer.svelte | 82 + src/lib/components/Hero.svelte | 82 + src/lib/components/HowWeWork.svelte | 14 + src/lib/components/ImpactSection.svelte | 14 + src/lib/components/Logo.svelte | 18 + src/lib/components/ScheduleSection.svelte | 35 + src/lib/components/WhatWeDo.svelte | 14 + src/lib/components/Wordmark.svelte | 61 + src/lib/copyright-year.test.ts | 8 + src/lib/copyright-year.ts | 7 + src/lib/data/content.ts | 26 + src/lib/data/engagements.ts | 27 + src/lib/data/experience.ts | 26 + src/lib/data/home-meta.ts | 9 + src/lib/data/json-ld.ts | 150 + src/lib/seo.ts | 53 + src/routes/+layout.svelte | 141 + src/routes/+layout.ts | 3 + src/routes/+page.svelte | 21 + src/routes/+page.ts | 6 + {site => static}/assets/apple-touch-icon.png | Bin {site => static}/assets/avatar.png | Bin {site => static}/assets/favicon-cutout.svg | 0 .../assets/fonts/fraunces-v38-latin-500.woff2 | Bin .../fonts/fraunces-v38-latin-500italic.woff2 | Bin .../assets/fonts/fraunces-v38-latin-600.woff2 | Bin .../fonts/fraunces-v38-latin-600italic.woff2 | Bin .../assets/fonts/fraunces-v38-latin-700.woff2 | Bin .../fonts/fraunces-v38-latin-italic.woff2 | Bin .../fonts/fraunces-v38-latin-regular.woff2 | Bin .../assets/fonts/inter-v20-latin-500.woff2 | Bin .../fonts/inter-v20-latin-500italic.woff2 | Bin .../assets/fonts/inter-v20-latin-600.woff2 | Bin .../fonts/inter-v20-latin-600italic.woff2 | Bin .../assets/fonts/inter-v20-latin-700.woff2 | Bin .../fonts/inter-v20-latin-700italic.woff2 | Bin .../assets/fonts/inter-v20-latin-italic.woff2 | Bin .../fonts/inter-v20-latin-regular.woff2 | Bin .../plus-jakarta-sans-v12-latin-500.woff2 | Bin ...lus-jakarta-sans-v12-latin-500italic.woff2 | Bin .../plus-jakarta-sans-v12-latin-600.woff2 | Bin ...lus-jakarta-sans-v12-latin-600italic.woff2 | Bin .../plus-jakarta-sans-v12-latin-700.woff2 | Bin ...lus-jakarta-sans-v12-latin-700italic.woff2 | Bin .../plus-jakarta-sans-v12-latin-italic.woff2 | Bin .../plus-jakarta-sans-v12-latin-regular.woff2 | Bin {site => static}/assets/logos/atlassian.svg | 0 {site => static}/assets/logos/bottomline.svg | 0 {site => static}/assets/logos/cargurus.svg | 0 {site => static}/assets/logos/mfa-boston.svg | 0 {site => static}/assets/logos/timberland.svg | 0 {site => static}/assets/logos/tjx.svg | 0 {site => static}/assets/logos/vf.svg | 0 {site => static}/assets/og-image.png | Bin static/assets/scripts/copyright-year.js | 5 + {site => static}/assets/wordmark.png | Bin {site => static}/assets/wordmark.svg | 0 {site/assets => static/downloads}/resume.pdf | Bin {site => static}/favicon.ico | Bin {site => static}/favicon.svg | 0 {site => static}/robots.txt | 0 stylelint.config.js | 25 + svelte.config.js | 18 + tests/visual.spec.ts | 12 + .../home-chromium-darwin.png | Bin 0 -> 600629 bytes tsconfig.json | 16 + vite.config.ts | 12 + vitest.config.ts | 7 + 93 files changed, 6409 insertions(+), 3231 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc rename .woodpecker.yml => .woodpecker/deploy.yaml (54%) create mode 100644 .woodpecker/pr.yaml delete mode 100644 SEO-CHECKLIST.md delete mode 100644 build.mjs rename DEPLOYMENT.md => docs/DEPLOYMENT.md (88%) create mode 100644 eslint.config.js create mode 100644 playwright.config.ts create mode 100644 postcss.config.js create mode 100644 scripts/critters.mjs delete mode 100644 site/index.html delete mode 100644 site/script.js delete mode 100644 site/styles.css create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/lib/components/EngagementsSection.svelte create mode 100644 src/lib/components/ExperienceSection.svelte create mode 100644 src/lib/components/Footer.svelte create mode 100644 src/lib/components/Hero.svelte create mode 100644 src/lib/components/HowWeWork.svelte create mode 100644 src/lib/components/ImpactSection.svelte create mode 100644 src/lib/components/Logo.svelte create mode 100644 src/lib/components/ScheduleSection.svelte create mode 100644 src/lib/components/WhatWeDo.svelte create mode 100644 src/lib/components/Wordmark.svelte create mode 100644 src/lib/copyright-year.test.ts create mode 100644 src/lib/copyright-year.ts create mode 100644 src/lib/data/content.ts create mode 100644 src/lib/data/engagements.ts create mode 100644 src/lib/data/experience.ts create mode 100644 src/lib/data/home-meta.ts create mode 100644 src/lib/data/json-ld.ts create mode 100644 src/lib/seo.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/+page.ts rename {site => static}/assets/apple-touch-icon.png (100%) rename {site => static}/assets/avatar.png (100%) rename {site => static}/assets/favicon-cutout.svg (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-500.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-500italic.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-600.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-600italic.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-700.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-italic.woff2 (100%) rename {site => static}/assets/fonts/fraunces-v38-latin-regular.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-500.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-500italic.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-600.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-600italic.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-700.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-700italic.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-italic.woff2 (100%) rename {site => static}/assets/fonts/inter-v20-latin-regular.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-500.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-500italic.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-600.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-600italic.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-700.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-700italic.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-italic.woff2 (100%) rename {site => static}/assets/fonts/plus-jakarta-sans-v12-latin-regular.woff2 (100%) rename {site => static}/assets/logos/atlassian.svg (100%) rename {site => static}/assets/logos/bottomline.svg (100%) rename {site => static}/assets/logos/cargurus.svg (100%) rename {site => static}/assets/logos/mfa-boston.svg (100%) rename {site => static}/assets/logos/timberland.svg (100%) rename {site => static}/assets/logos/tjx.svg (100%) rename {site => static}/assets/logos/vf.svg (100%) rename {site => static}/assets/og-image.png (100%) create mode 100644 static/assets/scripts/copyright-year.js rename {site => static}/assets/wordmark.png (100%) rename {site => static}/assets/wordmark.svg (100%) rename {site/assets => static/downloads}/resume.pdf (100%) rename {site => static}/favicon.ico (100%) rename {site => static}/favicon.svg (100%) rename {site => static}/robots.txt (100%) create mode 100644 stylelint.config.js create mode 100644 svelte.config.js create mode 100644 tests/visual.spec.ts create mode 100644 tests/visual.spec.ts-snapshots/home-chromium-darwin.png create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9800903..aaf80b9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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": [ diff --git a/.gitignore b/.gitignore index 97e2f12..e6eceff 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d78805e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist/ +build/ +.svelte-kit/ +node_modules/ +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f842a73 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "all", + "printWidth": 90, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/.woodpecker.yml b/.woodpecker/deploy.yaml similarity index 54% rename from .woodpecker.yml rename to .woodpecker/deploy.yaml index 15de89d..70c9c40 100644 --- a/.woodpecker.yml +++ b/.woodpecker/deploy.yaml @@ -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. diff --git a/.woodpecker/pr.yaml b/.woodpecker/pr.yaml new file mode 100644 index 0000000..63d0fba --- /dev/null +++ b/.woodpecker/pr.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile index a42d5eb..e4f32b6 100644 --- a/Dockerfile +++ b/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 diff --git a/README.md b/README.md index 9b50acf..4fb8fb3 100644 --- a/README.md +++ b/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 - -``` +- **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**: `
`, `
`, `