Tweaks and improvements
This commit is contained in:
144
README.md
144
README.md
@@ -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 it’s updated**: Push to `main` → Woodpecker runs ci, build (image + push), deploy (Portainer webhook); Mattermost reports status.
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
services:
|
||||
service:
|
||||
image: git.mifi.dev/...:${IMAGE_TAG:-latest}
|
||||
container_name: service
|
||||
environment:
|
||||
- ENV_NAME=value
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD',
|
||||
'/usr/local/bin/healthcheck.sh',
|
||||
'--connect',
|
||||
'--innodb_initialized'
|
||||
]
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
mifi-holdings-landing:
|
||||
image: git.mifi.dev/mifi-holdings/landing:latest
|
||||
container_name: mifi-holdings-landing
|
||||
networks:
|
||||
- network
|
||||
volumes:
|
||||
- volume:/var/lib/...
|
||||
- other_volume:/var/lib/...
|
||||
depends_on:
|
||||
- other service
|
||||
restart: unless-stopped
|
||||
- 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-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
|
||||
@@ -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
62
src/assets/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
src/assets/images/apple-touch-icon.png
Normal file
BIN
src/assets/images/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/favicon.ico
Normal file
BIN
src/assets/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
9
src/assets/images/favicon.svg
Normal file
9
src/assets/images/favicon.svg
Normal 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
11
src/assets/js/ga-init.js
Normal 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 });
|
||||
})();
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,20 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="emoji">🛸</div>
|
||||
<h1>Nothing to See Here</h1>
|
||||
<p>
|
||||
You'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's nothing here for you.<br>
|
||||
Go on. Shoo. Scram. Or just keep wondering.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
<head>
|
||||
<!-- 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>
|
||||
<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'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's nothing here for you.<br />
|
||||
Go on. Shoo. Scram. Or just keep wondering.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user