130 lines
8.0 KiB
Markdown
130 lines
8.0 KiB
Markdown
# 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)
|
||
|
||
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).
|
||
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** (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 (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) |
|