diff --git a/README.md b/README.md index 1e29962..bca101c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,129 @@ # mail-landing -Static landing site for mail.mifi.holdings (HTML/CSS/JS), built to `dist/` and served with nginx in production. +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. -## Dev Container (local development and preview) +--- + +## 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) + +1. **Develop** in `src/` (HTML/CSS/JS). No bundler; structure mirrors output. +2. **Build** (`pnpm build`): `src/` → `dist/` (copy, minify JS/CSS, inline critical CSS via Beasties). +3. **Docker image**: `Dockerfile` copies `nginx/conf.d/` and `dist/` into `nginx:alpine`; no Node in the image. +4. **Run**: Container serves `/usr/share/nginx/html` on port 80. Traefik (external) terminates TLS for `mail.mifi.holdings`, applies gzip and security middlewares, and routes to this service on `marina-net`. +5. **Deploy**: Woodpecker runs **ci** → **build** (site + image + push) → **deploy** (Portainer webhook redeploy + Mattermost). + +--- + +## Local development (Dev Container) 1. **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). - The first time will build the container (Node 22 + pnpm) and run `pnpm install`. + 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), runs `pnpm install`. 2. **Preview the site** In the container terminal: - **Quick preview** (serves `src/` as-is, no build): `pnpm preview` - - **Production-like preview** (build then serve `dist/`): - `pnpm preview:prod` - - Port **3000** is forwarded; open **http://localhost:3000** in the host browser (or use the “Preview” port notification). + - **Production-like** (build then serve `dist/`): + `pnpm preview:prod` + Port **3000** is forwarded; open **http://localhost:3000** (or use the “Preview” port notification). 3. **Other commands** - `pnpm build` — build `src/` → `dist/` (minify JS/CSS, inline critical CSS) - `pnpm lint` / `pnpm format` — lint and format - - `pnpm docker:build` — build production image + - `pnpm docker:build` — build production image (for local testing; image: `git.mifi.dev/mifi-holdings/mail-landing:latest`) + +--- + +## Build pipeline (what `pnpm build` does) + +1. **Clean & copy** — `dist/` is removed; `src/` is copied recursively to `dist/`. +2. **Minify JS** — All `.js` files in `dist/` are minified with Terser (no comments). +3. **Minify CSS** — All `.css` files are minified with clean-css (level 2). +4. **Inline critical CSS** — Beasties runs on every `.html` file in `dist/` (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.yml` defines one service, `marina-net`, Traefik labels for `Host(\`mail.mifi.holdings\`)`, TLS, gzip, and security middlewares; healthcheck via `wget`on`/`. +- **nginx**: Serves `/usr/share/nginx/html`; HTML no-cache, JS/CSS long-lived cache; static assets and directory/index handling as in `nginx/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) |