Compare commits
20 Commits
1a1f068301
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
9752501ee5
|
|||
|
75f7f4b09c
|
|||
|
21b28ba376
|
|||
|
bbf1f0dde7
|
|||
|
7b63c0c922
|
|||
|
93bbfee7f7
|
|||
|
c81cc54d91
|
|||
|
88f0e84537
|
|||
|
502bc1765f
|
|||
|
4a79266a27
|
|||
|
27808cfd0e
|
|||
|
9a84e9003a
|
|||
|
56b2377cd1
|
|||
|
97c6462254
|
|||
|
2545dd8cb0
|
|||
|
348be469a0
|
|||
|
2452b3dec4
|
|||
|
26c31fded6
|
|||
|
bbf61cd6d5
|
|||
|
db75809bd0
|
27
.devcontainer/devcontainer.json
Normal file
27
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "mail-landing",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
|
||||||
|
"onCreateCommand": "npm install -g pnpm@10",
|
||||||
|
"postCreateCommand": "pnpm install",
|
||||||
|
"forwardPorts": [3000],
|
||||||
|
"portsAttributes": {
|
||||||
|
"3000": {
|
||||||
|
"label": "Preview",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
# Avoid sending secrets or dev tooling into the build context
|
# Avoid sending secrets or dev tooling into the build context
|
||||||
# config/ and plugins/ are included (no secrets; PHP configs read from ENV at runtime)
|
# config/ and plugins/ are included (no secrets; PHP configs read from ENV at runtime)
|
||||||
|
.devcontainer
|
||||||
|
.pnpm-store
|
||||||
|
.vscode
|
||||||
|
.woodpecker
|
||||||
node_modules
|
node_modules
|
||||||
|
scripts
|
||||||
.git
|
.git
|
||||||
.prettierrc
|
.prettierrc
|
||||||
.prettierignore
|
.prettierignore
|
||||||
*.md
|
*.md
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
stylelint.config.js
|
||||||
|
eslint.config.js
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
|
.pnpm-store
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
pnpm-lock.yaml
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
dist
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
.pnpm-store
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
dist
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 2,
|
"tabWidth": 4,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
159
.woodpecker/build.yml
Normal file
159
.woodpecker/build.yml
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Site build
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
|
- name: Docker image build
|
||||||
|
image: docker:latest
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
environment:
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Building Docker image (BuildKit) ==="
|
||||||
|
- 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"'
|
||||||
|
- 'echo "Registry repo: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
build() {
|
||||||
|
docker build \
|
||||||
|
--progress=plain \
|
||||||
|
--platform=linux/amd64 \
|
||||||
|
--tag $REGISTRY_REPO:${CI_COMMIT_SHA} \
|
||||||
|
--tag $REGISTRY_REPO:latest \
|
||||||
|
--label "git.commit=${CI_COMMIT_SHA}" \
|
||||||
|
--label "git.branch=${CI_COMMIT_BRANCH}" \
|
||||||
|
.
|
||||||
|
}
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "Build attempt $attempt/3"
|
||||||
|
if build; then
|
||||||
|
echo "✓ Docker image built successfully"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Build attempt $attempt failed, retrying in 30s..."
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
echo "All build attempts failed"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
REGISTRY_URL: git.mifi.dev
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/landing
|
||||||
|
REGISTRY_USERNAME:
|
||||||
|
from_secret: gitea_registry_username
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: gitea_package_token
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Pushing to registry ==="
|
||||||
|
- 'echo "Registry: $REGISTRY_URL"'
|
||||||
|
- 'echo "Repository: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \
|
||||||
|
-u "$REGISTRY_USERNAME" \
|
||||||
|
--password-stdin
|
||||||
|
- docker push $REGISTRY_REPO:${CI_COMMIT_SHA}
|
||||||
|
- docker push $REGISTRY_REPO:latest
|
||||||
|
- echo "✓ Images pushed successfully"
|
||||||
|
depends_on:
|
||||||
|
- Site build
|
||||||
|
- Docker image build
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
118
.woodpecker/ci.yml
Normal file
118
.woodpecker/ci.yml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# CI: lint and format check. Runs on every PR and every push to main.
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
- branch: main
|
||||||
|
event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Prettier Format Check
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm format:check
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
- name: Send Prettier Format Check Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier Format Check failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Prettier Format Check
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Lint Check
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm lint
|
||||||
|
depends_on:
|
||||||
|
- Prettier Format Check
|
||||||
|
|
||||||
|
- name: Send Lint Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Lint Check
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@10.29.2 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm build
|
||||||
|
depends_on:
|
||||||
|
- Lint Check
|
||||||
|
|
||||||
|
- name: Send Build Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- Build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Send CI Pipeline Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_tests_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
- Prettier Format Check
|
||||||
|
- Lint Check
|
||||||
|
- Build
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
70
.woodpecker/deploy.yml
Normal file
70
.woodpecker/deploy.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
skip_clone: true
|
||||||
|
# Use writable workspace when clone is skipped (no root clone step to create /woodpecker/src)
|
||||||
|
workspace:
|
||||||
|
base: /tmp
|
||||||
|
path: deploy
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger Portainer stack redeploy
|
||||||
|
image: curlimages/curl:latest
|
||||||
|
environment:
|
||||||
|
PORTAINER_WEBHOOK_URL:
|
||||||
|
from_secret: portainer_webhook_url
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Triggering Portainer stack redeploy ==="
|
||||||
|
- |
|
||||||
|
resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL")
|
||||||
|
body=$(echo "$resp" | head -n -1)
|
||||||
|
code=$(echo "$resp" | tail -n 1)
|
||||||
|
if [ "$code" != "200" ] && [ "$code" != "204" ]; then
|
||||||
|
echo "Webhook failed (HTTP $code): $body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Portainer redeploy triggered (HTTP $code)"
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||||
|
from_secret: mattermost_bot_access_token
|
||||||
|
MATTERMOST_CHANNEL_ID:
|
||||||
|
from_secret: mattermost_pushes_channel_id
|
||||||
|
MATTERMOST_POST_API_URL:
|
||||||
|
from_secret: mattermost_post_api_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
COPY nginx/conf.d/ /etc/nginx/conf.d/
|
COPY nginx/conf.d/ /etc/nginx/conf.d/
|
||||||
COPY src/ /usr/share/nginx/html/
|
COPY dist/ /usr/share/nginx/html/
|
||||||
|
|||||||
163
README.md
163
README.md
@@ -1 +1,162 @@
|
|||||||
# Simple Package (Docker)
|
# mifi.holdings — Landing Page
|
||||||
|
|
||||||
|
Static landing site for **mifi.holdings** (and www). Plain HTML/CSS/JS source; a **build step** produces minified assets and inlines critical CSS. 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 |
|
||||||
|
| **Build output** | `dist/` (gitignored) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Traefik │ (routing, TLS, websecure)
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Docker container │ mifi-holdings-landing
|
||||||
|
│ nginx:alpine │ port 80
|
||||||
|
│ /usr/share/ │
|
||||||
|
│ nginx/html ← │ built output from dist/
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Build**: `pnpm build` copies `src/` → `dist/`, minifies JS/CSS, inlines critical CSS (Critters). Docker image = `nginx:alpine` + `nginx/conf.d/` + **`dist/`** 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/ # Source (HTML, CSS, JS)
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── assets/
|
||||||
|
│ ├── css/style.css
|
||||||
|
│ ├── js/ga-init.js
|
||||||
|
│ └── images/ # favicon, etc.
|
||||||
|
├── scripts/
|
||||||
|
│ └── build.js # Build: copy, minify, inline critical CSS
|
||||||
|
├── dist/ # Build output (gitignored)
|
||||||
|
├── nginx/
|
||||||
|
│ └── conf.d/
|
||||||
|
│ └── default.conf # nginx server config (cache rules, SPA fallback)
|
||||||
|
├── .woodpecker/
|
||||||
|
│ ├── ci.yml # Lint, format, build check (PR + push to main)
|
||||||
|
│ ├── build.yml # Site build → Docker image → push (main)
|
||||||
|
│ └── deploy.yml # Trigger Portainer redeploy (main)
|
||||||
|
├── docker-compose.yml # Service + Traefik labels for production
|
||||||
|
├── Dockerfile # nginx + config + dist (not src)
|
||||||
|
├── package.json # pnpm scripts: build, format, lint, docker
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build (pnpm build)
|
||||||
|
|
||||||
|
- **Output**: `dist/` (gitignored). Do not edit `dist/`; it is generated.
|
||||||
|
- **Steps** (see `scripts/build.js`):
|
||||||
|
1. Clean and copy `src/` → `dist/`.
|
||||||
|
2. Minify all `.js` (terser) and `.css` (clean-css).
|
||||||
|
3. Inline critical CSS with **Critters** (lazy-loads the rest; no browser required, so it works in CI).
|
||||||
|
- **When**: Run before `docker build`. CI and the build pipeline both run `pnpm build` before packaging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- **Frontend**: Static HTML + CSS + JS in `src/`; production build minifies and inlines critical CSS.
|
||||||
|
- **Server**: nginx (Alpine) in Docker.
|
||||||
|
- **Tooling**: **pnpm**; Prettier (format); ESLint (JS), Stylelint (CSS), yamllint (YAML); **terser**, **clean-css**, **beasties** (build).
|
||||||
|
- **Deployment**: Docker image (from `dist/`) → Gitea registry → Portainer stack redeploy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
- **Dependencies**: `pnpm install`.
|
||||||
|
- **Format**: `pnpm format` / `pnpm format:check`.
|
||||||
|
- **Lint**: `pnpm lint` (ESLint for `src/**/*.js`, Stylelint for `src/**/*.css`, yamllint for Woodpecker + docker-compose). Use `pnpm lint:fix` to auto-fix where supported.
|
||||||
|
- **Build**: `pnpm build` → produces `dist/`. Required before building the Docker image.
|
||||||
|
- **Run locally (Docker)**:
|
||||||
|
- Build site: `pnpm build`.
|
||||||
|
- Image: `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>`.
|
||||||
|
|
||||||
|
No dev server; edit `src/` and run `pnpm build` (and optionally `docker run` or a local static server on `dist/`) to test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
- **Dockerfile**: Copies `nginx/conf.d/` and **`dist/`** (not `src/`) into an `nginx:alpine` image. Run `pnpm build` first so `dist/` exists.
|
||||||
|
- **Image**: Tagged as `git.mifi.dev/mifi-holdings/landing:latest` (and `:<commit-sha>` in CI).
|
||||||
|
- **Local build/push**: `pnpm build` → `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 → lint (ESLint, Stylelint, yamllint) → **Build** (`pnpm build`). Mattermost notifications on success/failure.
|
||||||
|
|
||||||
|
2. **build** (`.woodpecker/build.yml`)
|
||||||
|
- **When**: Push/tag/manual on `main` (and deployment to production); **depends_on: ci**.
|
||||||
|
- **Steps**: **Site build** (`pnpm install`, `pnpm build`) → Docker image build (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 **`dist/`** after build).
|
||||||
|
- **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; source in `src/`, built output in `dist/`.
|
||||||
|
- **How it runs**: nginx in Docker serving `dist/`, fronted by Traefik on `marina-net`.
|
||||||
|
- **How it’s updated**: Push to `main` → Woodpecker runs ci (lint, format, **build**), then build pipeline (**site build** → Docker image → push), then deploy (Portainer webhook); Mattermost reports status.
|
||||||
|
|||||||
@@ -1,34 +1,25 @@
|
|||||||
services:
|
services:
|
||||||
service:
|
mifi-holdings-landing:
|
||||||
image: git.mifi.dev/...:${IMAGE_TAG:-latest}
|
image: git.mifi.dev/mifi-holdings/landing:latest
|
||||||
container_name: service
|
container_name: mifi-holdings-landing
|
||||||
environment:
|
|
||||||
- ENV_NAME=value
|
|
||||||
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
|
restart: unless-stopped
|
||||||
|
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=gzip@file,security-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:
|
networks:
|
||||||
network:
|
marina-net:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
|
||||||
volume:
|
|
||||||
external: true
|
|
||||||
other_volume:
|
|
||||||
external: false
|
|
||||||
20
eslint.config.js
Normal file
20
eslint.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import prettierConfig from 'eslint-config-prettier/flat'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'script',
|
||||||
|
globals: {
|
||||||
|
window: 'readonly',
|
||||||
|
document: 'readonly',
|
||||||
|
dataLayer: 'writable'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prettierConfig
|
||||||
|
]
|
||||||
30
package.json
30
package.json
@@ -1,21 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "...",
|
"name": "mifi-holdings-landing",
|
||||||
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
|
||||||
"packageManager": "pnpm@10.29.3",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"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",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"lint": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml",
|
"lint": "pnpm run lint:yaml && pnpm run lint:js && pnpm run lint:css",
|
||||||
"docker:build": "docker build --platform linux/amd64 -t git.mifi.dev/.../...:latest .",
|
"lint:css": "stylelint \"src/**/*.css\"",
|
||||||
"docker:push": "docker push git.mifi.dev/.../...:latest"
|
"lint:js": "eslint src/",
|
||||||
|
"lint:yaml": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml",
|
||||||
|
"lint:fix": "pnpm run lint:fix:js && pnpm run lint:fix:css && pnpm run lint:fix:yaml",
|
||||||
|
"lint:fix:js": "eslint src/ --fix",
|
||||||
|
"lint:fix:css": "stylelint \"src/**/*.css\" --fix",
|
||||||
|
"lint:fix:yaml": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml --fix",
|
||||||
|
"preview": "serve src -l 3000",
|
||||||
|
"preview:prod": "pnpm build && serve dist -l 3000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"clean-css": "^5.3.3",
|
||||||
|
"beasties": "^0.4.1",
|
||||||
|
"eslint": "^10.0.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"serve": "^14.2.4",
|
||||||
|
"stylelint": "^17.3.0",
|
||||||
|
"stylelint-config-standard": "^40.0.0",
|
||||||
|
"terser": "^5.46.0",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/.../...git"
|
"url": "https://github.com/mifi-holdings/landing.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2283
pnpm-lock.yaml
generated
Normal file
2283
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
scripts/build.js
Normal file
78
scripts/build.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Build script: copy src → dist, minify JS/CSS, inline critical CSS (Beasties).
|
||||||
|
* Run with: pnpm build
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
rmSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
cpSync,
|
||||||
|
readdirSync
|
||||||
|
} from 'fs'
|
||||||
|
import { join, dirname, extname } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import Beasties from 'beasties'
|
||||||
|
import { minify as minifyJs } from 'terser'
|
||||||
|
import CleanCSS from 'clean-css'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const root = join(__dirname, '..')
|
||||||
|
const srcDir = join(root, 'src')
|
||||||
|
const distDir = join(root, 'dist')
|
||||||
|
|
||||||
|
function getFiles(dir, files = []) {
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true })
|
||||||
|
for (const e of entries) {
|
||||||
|
const full = join(dir, e.name)
|
||||||
|
if (e.isDirectory()) getFiles(full, files)
|
||||||
|
else files.push(full)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 1. Clean and copy src → dist
|
||||||
|
rmSync(distDir, { recursive: true, force: true })
|
||||||
|
mkdirSync(distDir, { recursive: true })
|
||||||
|
cpSync(srcDir, distDir, { recursive: true })
|
||||||
|
|
||||||
|
const distFiles = getFiles(distDir)
|
||||||
|
|
||||||
|
// 2. Minify JS
|
||||||
|
const jsFiles = distFiles.filter((f) => extname(f) === '.js')
|
||||||
|
for (const f of jsFiles) {
|
||||||
|
const code = readFileSync(f, 'utf8')
|
||||||
|
const result = await minifyJs(code, { format: { comments: false } })
|
||||||
|
if (result.code) writeFileSync(f, result.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Minify CSS
|
||||||
|
const cleanCss = new CleanCSS({ level: 2 })
|
||||||
|
const cssFiles = distFiles.filter((f) => extname(f) === '.css')
|
||||||
|
for (const f of cssFiles) {
|
||||||
|
const code = readFileSync(f, 'utf8')
|
||||||
|
const result = cleanCss.minify(code)
|
||||||
|
if (!result.errors.length) writeFileSync(f, result.styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI)
|
||||||
|
const htmlFiles = distFiles.filter((f) => extname(f) === '.html')
|
||||||
|
const beasties = new Beasties({
|
||||||
|
path: distDir,
|
||||||
|
preload: 'default',
|
||||||
|
logLevel: 'warn'
|
||||||
|
})
|
||||||
|
for (const htmlFile of htmlFiles) {
|
||||||
|
const html = readFileSync(htmlFile, 'utf8')
|
||||||
|
const inlined = await beasties.process(html)
|
||||||
|
writeFileSync(htmlFile, inlined)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Build complete: dist/')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
1
src/.well-known/appspecific/com.chrome.devtools.json
Normal file
1
src/.well-known/appspecific/com.chrome.devtools.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,42 +1,69 @@
|
|||||||
html,body {
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background: #70ffd7;
|
||||||
|
color: #1b1e22;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0 0 0.25rem;
|
||||||
|
transition: top 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 0;
|
||||||
|
outline: 2px solid #70ffd7;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #121212;
|
background: #121212;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
font-family: 'Inter', Arial, sans-serif;
|
font-family: Inter, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(34, 39, 44, 0.92);
|
background: rgb(34 39 44 / 92%);
|
||||||
padding: 3rem 2rem;
|
padding: 3rem 2rem;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
box-shadow: 0 2px 24px 0 rgba(0,0,0,0.13);
|
box-shadow: 0 2px 24px 0 rgb(0 0 0 / 13%);
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
letter-spacing: -1px;
|
letter-spacing: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: #babed8;
|
color: #babed8;
|
||||||
margin-bottom: 1.7rem;
|
margin-bottom: 1.7rem;
|
||||||
font-size: 1.14rem;
|
font-size: 1.14rem;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scram {
|
.scram {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.7rem 1.4rem;
|
padding: 0.7rem 1.4rem;
|
||||||
@@ -48,12 +75,16 @@ p {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
box-shadow: 0 1px 4px 0 rgba(112,255,215,0.10);
|
box-shadow: 0 1px 4px 0 rgb(112 255 215 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scram:hover {
|
.scram:hover {
|
||||||
background: #50bf9c;
|
background: #50bf9c;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@media (max-width: 430px) {
|
|
||||||
.container { padding: 2rem 0.6rem; }
|
@media (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,20 +1,94 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<!-- Google tag (gtag.js) -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<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>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://analytics.mifi.holdings/script.js"
|
||||||
|
data-website-id="ce2a7f8a-95e9-4bc5-93cb-2e6075d836b8"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
|
||||||
<title>mifi.holdings</title>
|
<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="assets/css/style.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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "mifi Holdings",
|
||||||
|
"url": "https://mifi.holdings",
|
||||||
|
"description": "This is just a landing page so something exists at the root domain of all the digital holdings of mifi.",
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "mifi Ventures",
|
||||||
|
"url": "https://mifi.ventures",
|
||||||
|
"email": "postmaster@mifi.holdings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
class="skip-link"
|
||||||
|
data-umami-event="skip to main content"
|
||||||
|
>Skip to main content</a
|
||||||
|
>
|
||||||
|
<main id="main-content" class="container" tabindex="-1">
|
||||||
<div class="emoji">🛸</div>
|
<div class="emoji">🛸</div>
|
||||||
<h1>Nothing to See Here</h1>
|
<h1>Nothing to See Here</h1>
|
||||||
<p>
|
<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>
|
You've stumbled onto <b>mifi.holdings</b> — the legendary
|
||||||
There's nothing here for you.<br>
|
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.
|
Go on. Shoo. Scram. Or just keep wondering.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</main>
|
||||||
|
<img
|
||||||
|
src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
|
||||||
|
alt=""
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
role="presentation"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4
src/robots.txt
Normal file
4
src/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://mifi.holdings/sitemap.xml
|
||||||
8
src/sitemap.xml
Normal file
8
src/sitemap.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://mifi.holdings/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
8
stylelint.config.js
Normal file
8
stylelint.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
extends: ['stylelint-config-standard'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.css']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user