Tweaks and improvements

This commit is contained in:
2026-02-13 14:50:12 -03:00
parent 1a1f068301
commit db75809bd0
11 changed files with 284 additions and 113 deletions

144
README.md
View File

@@ -1 +1,143 @@
# Simple Package (Docker)
# mifi.holdings — Landing Page
Static landing site for **mifi.holdings** (and www). Plain HTML/CSS, no framework; served by **nginx** in Docker behind **Traefik**, with CI/CD via **Woodpecker** and deployment via **Portainer**.
---
## Quick reference
| What | Where |
| ----------------------- | -------------------------------------------- |
| **Site** | `mifi.holdings`, `www.mifi.holdings` (HTTPS) |
| **Runtime** | nginx (Alpine) in Docker |
| **Reverse proxy / TLS** | Traefik (Let's Encrypt) |
| **CI/CD** | Woodpecker (ci → build → deploy) |
| **Registry** | `git.mifi.dev/mifi-holdings/landing` |
| **Package manager** | pnpm |
---
## Architecture
```
┌─────────────────┐
│ Traefik │ (routing, TLS, websecure)
└────────┬────────┘
┌────────▼────────┐
│ Docker container │ mifi-holdings-landing
│ nginx:alpine │ port 80
│ /usr/share/ │
│ nginx/html ← │ static files from image
└─────────────────┘
```
- **Build**: Docker image = `nginx:alpine` + project `nginx/conf.d/` + `src/` copied into `/usr/share/nginx/html`.
- **Run**: Single service on external network `marina-net`; Traefik routes `mifi.holdings` and `www.mifi.holdings` to this container (HTTPS, `security-prison@file` middleware).
---
## Repo structure
```
.
├── src/ # Static site (served as-is)
│ ├── index.html
│ └── css/
│ └── style.css
├── nginx/
│ └── conf.d/
│ └── default.conf # nginx server config (cache rules, SPA fallback)
├── .woodpecker/
│ ├── ci.yml # Lint + format check (PR + push to main)
│ ├── build.yml # Build image, push to registry (main)
│ └── deploy.yml # Trigger Portainer redeploy (main)
├── docker-compose.yml # Service + Traefik labels for production
├── Dockerfile # nginx + config + src
├── package.json # pnpm scripts: format, lint, docker build/push
└── README.md
```
---
## Tech stack
- **Frontend**: Static HTML + CSS only (no JS build step).
- **Server**: nginx (Alpine) in Docker.
- **Tooling**: **pnpm** (format with Prettier, lint with yamllint for `.woodpecker/*.yml` and `docker-compose.yml`).
- **Deployment**: Docker image → Gitea registry → Portainer stack redeploy.
---
## Local development
- **Dependencies**: `pnpm install` (only devDependencies: Prettier, yaml-lint).
- **Format**: `pnpm format` / `pnpm format:check`.
- **Lint**: `pnpm lint` (yamllint on Woodpecker and docker-compose YAML).
- **Run locally (Docker)**:
- Build: `pnpm docker:build` (or `docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .`).
- Run: use `docker-compose up` **only** on a host that has `marina-net` and Traefik; otherwise run the image with a port map and open `http://localhost:<port>`.
There is no dev server in this repo; edit `src/` and refresh the browser or rebuild the image to test.
---
## Docker
- **Dockerfile**: Copies `nginx/conf.d/` and `src/` into an `nginx:alpine` image. No multi-stage build; the image is the final runtime.
- **Image**: Tagged as `git.mifi.dev/mifi-holdings/landing:latest` (and `:<commit-sha>` in CI).
- **Local build/push**: `pnpm docker:build`, `pnpm docker:push` (requires login to `git.mifi.dev`).
---
## CI/CD (Woodpecker)
Three pipelines:
1. **ci** (`.woodpecker/ci.yml`)
- **When**: Every PR and every push to `main`.
- **Steps**: Install deps → Prettier format check → yamllint (Woodpecker + docker-compose). Mattermost notifications on success/failure.
2. **build** (`.woodpecker/build.yml`)
- **When**: Push/tag/manual on `main` (and deployment to production); **depends_on: ci**.
- **Steps**: Build Docker image (linux/amd64, up to 3 retries) → push `:latest` and `:<CI_COMMIT_SHA>` to `git.mifi.dev/mifi-holdings/landing`. Mattermost notifications.
3. **deploy** (`.woodpecker/deploy.yml`)
- **When**: Same as build (main / production deploy); **depends_on: ci**.
- **Steps**: Call Portainer webhook to redeploy the stack. Mattermost notifications.
Secrets used: `gitea_registry_username`, `gitea_package_token`, `portainer_webhook_url`, `mattermost_*` (bot token, channel IDs, API URL).
---
## Deployment (production)
- **Orchestration**: Stack defined in `docker-compose.yml`, deployed via **Portainer** (webhook triggered by Woodpecker deploy pipeline).
- **Network**: Container joins external network `marina-net` (shared with Traefik).
- **Traefik**:
- Hosts: `mifi.holdings`, `www.mifi.holdings`.
- Entrypoint: `websecure` (HTTPS).
- TLS: Let's Encrypt (`tls.certresolver=letsencrypt`).
- Middleware: `security-prison@file`.
- Backend: this service, port 80.
- **Healthcheck**: `wget --spider -q http://localhost/` every 20s (timeout 3s, 3 retries).
To deploy manually: pull the latest image and redeploy the stack in Portainer, or trigger the Portainer webhook (e.g. same URL as in `portainer_webhook_url`).
---
## nginx behavior
- **Root**: `/usr/share/nginx/html` (contents of `src/`).
- **HTML**: `Cache-Control: public, no-cache` so updates are visible quickly.
- **JS/CSS**: Long cache, `immutable` for hashed/versioned assets.
- **Images/fonts**: Cached (30d / 1y).
- **SPA-style fallback**: `/` tries `$uri`, `$uri/`, then `index.html`, then 404.
---
## Summary
- **What it is**: Static landing for mifi.holdings.
- **How it runs**: nginx in Docker, fronted by Traefik on `marina-net`.
- **How its updated**: Push to `main` → Woodpecker runs ci, build (image + push), deploy (Portainer webhook); Mattermost reports status.

View File

@@ -1,34 +1,24 @@
services:
service:
image: git.mifi.dev/...:${IMAGE_TAG:-latest}
container_name: service
environment:
- ENV_NAME=value
mifi-holdings-landing:
image: git.mifi.dev/mifi-holdings/landing:latest
container_name: mifi-holdings-landing
networks:
- marina-net
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=marina-net'
- 'traefik.http.routers.holdings-landing.rule=Host(`mifi.holdings`) || Host(`www.mifi.holdings`)'
- 'traefik.http.routers.holdings-landing.entrypoints=websecure'
- 'traefik.http.routers.holdings-landing.middlewares=ecurity-supermax-with-analytics@@file,redirect-www-to-non-www@file'
- 'traefik.http.routers.holdings-landing.tls=true'
- 'traefik.http.routers.holdings-landing.tls.certresolver=letsencrypt'
- 'traefik.http.services.holdings-landing.loadbalancer.server.port=80'
healthcheck:
test:
[
'CMD',
'/usr/local/bin/healthcheck.sh',
'--connect',
'--innodb_initialized'
]
retries: 10
start_period: 20s
networks:
- network
volumes:
- volume:/var/lib/...
- other_volume:/var/lib/...
depends_on:
- other service
restart: unless-stopped
test: ['CMD-SHELL', 'wget --spider -q http://localhost/ || exit 1']
interval: 20s
timeout: 3s
retries: 3
networks:
network:
marina-net:
external: true
volumes:
volume:
external: true
other_volume:
external: false

View File

@@ -1,14 +1,13 @@
{
"name": "...",
"name": "mifi-holdings-landing",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@10.29.3",
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml",
"docker:build": "docker build --platform linux/amd64 -t git.mifi.dev/.../...:latest .",
"docker:push": "docker push git.mifi.dev/.../...:latest"
"docker:build": "docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .",
"docker:push": "docker push git.mifi.dev/mifi-holdings/landing:latest"
},
"devDependencies": {
"prettier": "^3.4.2",
@@ -16,6 +15,6 @@
},
"repository": {
"type": "git",
"url": "https://github.com/.../...git"
"url": "https://github.com/mifi-holdings/landing.git"
}
}

62
src/assets/css/style.css Normal file
View File

@@ -0,0 +1,62 @@
html,
body {
height: 100%;
margin: 0;
padding: 0;
background: #121212;
color: #f4f4f4;
font-family: 'Inter', Arial, sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
text-align: center;
background: rgba(34, 39, 44, 0.92);
padding: 3rem 2rem;
border-radius: 1.5rem;
box-shadow: 0 2px 24px 0 rgba(0, 0, 0, 0.13);
max-width: 400px;
margin: 1rem;
}
.emoji {
font-size: 2.8rem;
margin-bottom: 1rem;
}
h1 {
font-size: 2rem;
font-weight: 800;
margin-bottom: 0.5rem;
letter-spacing: -1px;
}
p {
color: #babed8;
margin-bottom: 1.7rem;
font-size: 1.14rem;
line-height: 1.55;
}
.scram {
display: inline-block;
padding: 0.7rem 1.4rem;
background: #70ffd7;
color: #1b1e22;
border-radius: 999px;
font-weight: 700;
font-size: 1.1rem;
text-decoration: none;
transition: background 0.2s;
margin-top: 0.5rem;
box-shadow: 0 1px 4px 0 rgba(112, 255, 215, 0.1);
}
.scram:hover {
background: #50bf9c;
color: #fff;
}
@media (max-width: 430px) {
.container {
padding: 2rem 0.6rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,9 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<style>
.block { fill: #0b0b0f; }
@media (prefers-color-scheme: dark) {
.block { fill: #f2f2f2; }
}
</style>
<path class="block" d="M512,96l0,320c0,52.984 -43.016,96 -96,96l-320,0c-52.984,0 -96,-43.016 -96,-96l0,-320c0,-52.984 43.016,-96 96,-96l320,0c52.984,0 96,43.016 96,96Zm-96.011,80.389l53.127,0l0,-56.669l-53.127,0l0,56.669Zm-193.658,55.292c-4.819,-8.296 -11.558,-15.376 -20.217,-21.24c-13.796,-9.344 -29.667,-14.015 -47.612,-14.015c-16.461,0 -30.814,3.913 -43.058,11.739c-7.882,5.038 -14.038,11.753 -18.468,20.146l0,-27.027l-50.091,0l0,219.997l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.517,-12.413 14.926,-16.292c6.409,-3.879 13.864,-5.819 22.364,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.534,-12.413 14.977,-16.292c6.443,-3.879 13.881,-5.819 22.313,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-141.774c0,-16.394 -3.559,-30.831 -10.676,-43.311c-7.117,-12.481 -16.815,-22.229 -29.093,-29.245c-12.278,-7.016 -26.209,-10.524 -41.793,-10.524c-17.608,0 -33.141,4.335 -46.6,13.004c-8.624,5.555 -15.884,12.972 -21.779,22.252Zm193.658,189.599l53.127,0l0,-219.997l-53.127,0l0,219.997Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

11
src/assets/js/ga-init.js Normal file
View File

@@ -0,0 +1,11 @@
(function () {
var script = document.currentScript;
var id = script && script.getAttribute('data-ga-id');
if (!id) return;
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', id, { anonymize_ip: true });
})();

View File

@@ -1,59 +0,0 @@
html,body {
height: 100%;
margin: 0;
padding: 0;
background: #121212;
color: #f4f4f4;
font-family: 'Inter', Arial, sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
text-align: center;
background: rgba(34, 39, 44, 0.92);
padding: 3rem 2rem;
border-radius: 1.5rem;
box-shadow: 0 2px 24px 0 rgba(0,0,0,0.13);
max-width: 400px;
margin: 1rem;
}
.emoji {
font-size: 2.8rem;
margin-bottom: 1rem;
}
h1 {
font-size: 2rem;
font-weight: 800;
margin-bottom: 0.5rem;
letter-spacing: -1px;
}
p {
color: #babed8;
margin-bottom: 1.7rem;
font-size: 1.14rem;
line-height: 1.55;
}
.scram {
display: inline-block;
padding: 0.7rem 1.4rem;
background: #70ffd7;
color: #1b1e22;
border-radius: 999px;
font-weight: 700;
font-size: 1.1rem;
text-decoration: none;
transition: background 0.2s;
margin-top: 0.5rem;
box-shadow: 0 1px 4px 0 rgba(112,255,215,0.10);
}
.scram:hover {
background: #50bf9c;
color: #fff;
}
@media (max-width: 430px) {
.container { padding: 2rem 0.6rem; }
}

View File

@@ -1,18 +1,35 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4000VNMXLK"></script>
<script defer src="/assets/js/ga-init.js" data-ga-id="G-4000VNMXLK"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>mifi.holdings</title>
<link rel="stylesheet" href="css/styles.css">
<meta name="description" content="This is just a landing page so something exists at the root domain of all the digital holdings of mifi." />
<link rel="canonical" href="https://mifi.holdings" />
<meta name="robots" content="index, follow" />
<meta name="author" content="mifi" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="icon" type="image/svg+xml" href="/assets/images/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/apple-touch-icon.png" />
</head>
<body>
<div class="container">
<div class="emoji">🛸</div>
<h1>Nothing to See Here</h1>
<p>
You&apos;ve stumbled onto <b>mifi.holdings</b> — the legendary vault of digital oddities, curios, and coffee-fueled experiments belonging to a possibly-human, definitely-mysterious entity named <b>mifi</b>.<br><br>
There&apos;s nothing here for you.<br>
You&apos;ve stumbled onto <b>mifi.holdings</b> — the legendary vault of
digital oddities, curios, and coffee-fueled experiments belonging to a
possibly-human, definitely-mysterious entity named
<b>mifi</b>.<br /><br />
There&apos;s nothing here for you.<br />
Go on. Shoo. Scram. Or just keep wondering.
</p>
</div>