mail-landing
Static landing site for mail.mifi.holdings (HTML/CSS/JS). Source lives in src/, is built to dist/, and is served in production by nginx inside a Docker container, with Traefik as the reverse proxy.
Tech stack & frameworks
| Layer | Technology |
|---|---|
| Runtime | Node 22 (dev/build only); production is static files only |
| Package manager | pnpm 10.x |
| Source | Plain HTML, CSS, JS — no framework (Vite/React/etc.) |
| Build | Custom script (scripts/build.js): copy, minify, inline critical CSS |
| Minification | Terser (JS), clean-css (CSS) |
| Critical CSS | Beasties — inlines above-the-fold CSS into HTML (no browser/headless; works in CI) |
| Lint / format | ESLint, Stylelint, Prettier, yamllint (for Woodpecker & compose) |
| Production server | nginx (Alpine) in Docker |
| Reverse proxy | Traefik (via docker-compose labels; TLS, gzip, security middlewares) |
| CI/CD | Woodpecker CI (Gitea); images pushed to git.mifi.dev registry |
Project structure
mail-landing/
├── src/ # Source (edited by hand)
│ ├── index.html
│ ├── help/index.html
│ └── assets/
│ ├── css/site.css
│ ├── js/ga-init.js
│ └── images/favicon.svg
├── dist/ # Build output (gitignored in practice; produced by pnpm build)
├── scripts/
│ └── build.js # Build: copy → minify JS/CSS → Beasties inline critical CSS
├── nginx/conf.d/ # nginx config for the container
├── docker-compose.yml # Service definition + Traefik labels for mail.mifi.holdings
├── Dockerfile # nginx:alpine + config + dist/
├── .woodpecker/
│ ├── ci.yml # Lint, format check, build (PR + push to main)
│ ├── build.yml # Build site → Docker image → push to registry
│ └── deploy.yml # Trigger Portainer webhook + Mattermost notifications
├── .devcontainer/ # Dev Container (Node 22 + pnpm) for Cursor/VS Code
└── package.json
Architecture (high level)
- Develop in
src/(HTML/CSS/JS). No bundler; structure mirrors output. - Build (
pnpm build):src/→dist/(copy, minify JS/CSS, inline critical CSS via Beasties). - Docker image:
Dockerfilecopiesnginx/conf.d/anddist/intonginx:alpine; no Node in the image. - Run: Container serves
/usr/share/nginx/htmlon port 80. Traefik (external) terminates TLS formail.mifi.holdings, applies gzip and security middlewares, and routes to this service onmarina-net. - Deploy: Woodpecker runs ci → build (site + image + push) → deploy (Portainer webhook redeploy + Mattermost).
Local development (Dev Container)
-
Open in Dev Container
In Cursor/VS Code: Command Palette → Dev Containers: Reopen in Container (or Clone Repository in Container Volume when opening the repo).
First time: builds the container (Node 22 + pnpm), runspnpm install. -
Preview the site
In the container terminal:- Quick preview (serves
src/as-is, no build):
pnpm preview - Production-like (build then serve
dist/):
pnpm preview:prod
Port 3000 is forwarded; open http://localhost:3000 (or use the “Preview” port notification).
- Quick preview (serves
-
Other commands
pnpm build— buildsrc/→dist/(minify JS/CSS, inline critical CSS)pnpm lint/pnpm format— lint and formatpnpm docker:build— build production image (for local testing; image:git.mifi.dev/mifi-holdings/mail-landing:latest)
Build pipeline (what pnpm build does)
- Clean & copy —
dist/is removed;src/is copied recursively todist/. - Minify JS — All
.jsfiles indist/are minified with Terser (no comments). - Minify CSS — All
.cssfiles are minified with clean-css (level 2). - Inline critical CSS — Beasties runs on every
.htmlfile indist/(default preload behavior; no headless browser).
Output: dist/ ready to be served or copied into the Docker image.
Deployment (Woodpecker CI/CD)
Pipelines live under .woodpecker/. Execution order:
| Pipeline | When | What |
|---|---|---|
ci (ci.yml) |
Every PR and every push to main |
Install → Prettier check → Lint (JS, CSS, YAML) → Build. Mattermost notifications on failure/success. |
build (build.yml) |
Push/tag/manual on main, or deployment event for production |
Depends on ci. Site build → Docker image build (linux/amd64, tagged with commit SHA + latest) → Push to git.mifi.dev/mifi-holdings/mail-landing. Mattermost on success/failure. |
deploy (deploy.yml) |
Same as build (runs after build) | skip_clone: true; triggers Portainer webhook to redeploy the stack. Mattermost deploy success/failure. |
Secrets (in Woodpecker): portainer_webhook_url, mattermost_*, gitea_registry_username, gitea_package_token.
Production: Stack is defined in Portainer (using this repo’s docker-compose.yml). Redeploy pulls git.mifi.dev/mifi-holdings/mail-landing:latest and restarts the service. Traefik (on marina-net) routes mail.mifi.holdings to this container with TLS (e.g. Let’s Encrypt) and middlewares (gzip, security).
Production runtime (Docker + Traefik)
- Image:
git.mifi.dev/mifi-holdings/mail-landing:latest(nginx:alpine +nginx/conf.d/+dist/). - Compose:
docker-compose.ymldefines one service,marina-net, Traefik labels forHost(\mail.mifi.holdings`), TLS, gzip, and security middlewares; healthcheck viawgeton/`. - nginx: Serves
/usr/share/nginx/html; HTML no-cache, JS/CSS long-lived cache; static assets and directory/index handling as innginx/conf.d/default.conf.
Quick reference (pnpm scripts)
| Command | Description |
|---|---|
pnpm build |
Build src/ → dist/ (minify + critical CSS) |
pnpm preview |
Serve src/ on port 3000 (no build) |
pnpm preview:prod |
Build then serve dist/ on 3000 |
pnpm lint |
Lint JS, CSS, and Woodpecker/compose YAML |
pnpm lint:fix |
Auto-fix lint where supported |
pnpm format |
Prettier write |
pnpm format:check |
Prettier check only |
pnpm docker:build |
Build Docker image (linux/amd64) |
pnpm docker:push |
Push image to registry (manual) |