Compare commits
11 Commits
feature/la
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
fef29bd5a1
|
|||
|
aa6d24be5a
|
|||
|
4bc40b2be7
|
|||
|
9a2d7ae222
|
|||
|
c71ec612bb
|
|||
|
635594aea8
|
|||
|
115d63768c
|
|||
|
6bef0f8254
|
|||
|
455bc18b3b
|
|||
|
05a27fc19c
|
|||
|
36ff5d0c1a
|
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"
|
||||||
|
}
|
||||||
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Avoid sending secrets or dev tooling into the build context
|
||||||
|
# config/ and plugins/ are included (no secrets; PHP configs read from ENV at runtime)
|
||||||
|
.devcontainer
|
||||||
|
.pnpm-store
|
||||||
|
.vscode
|
||||||
|
.woodpecker
|
||||||
|
node_modules
|
||||||
|
scripts
|
||||||
|
.git
|
||||||
|
.prettierrc
|
||||||
|
.prettierignore
|
||||||
|
*.md
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
stylelint.config.js
|
||||||
|
eslint.config.js
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
dist
|
||||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.pnpm-store
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
dist
|
||||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 4,
|
||||||
|
"proseWrap": "preserve"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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/mail-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/mail-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]
|
||||||
4
Dockerfile
Normal file
4
Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY nginx/conf.d/ /etc/nginx/conf.d/
|
||||||
|
COPY dist/ /usr/share/nginx/html/
|
||||||
129
README.md
129
README.md
@@ -0,0 +1,129 @@
|
|||||||
|
# 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) |
|
||||||
|
|||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
mail-landing:
|
||||||
|
image: git.mifi.dev/mifi-holdings/mail-landing:latest
|
||||||
|
container_name: mifi-mail-landing
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- marina-net
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.docker.network=marina-net'
|
||||||
|
- 'traefik.http.routers.mail-landing.rule=Host(`mail.mifi.holdings`)'
|
||||||
|
- 'traefik.http.routers.mail-landing.entrypoints=websecure'
|
||||||
|
- 'traefik.http.routers.mail-landing.middlewares=gzip@file,security-supermax-with-analytics@file'
|
||||||
|
- 'traefik.http.routers.mail-landing.tls=true'
|
||||||
|
- 'traefik.http.routers.mail-landing.tls.certresolver=letsencrypt'
|
||||||
|
- 'traefik.http.services.mail-landing.loadbalancer.server.port=80'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'wget --spider -q http://localhost/ || exit 1']
|
||||||
|
interval: 20s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
marina-net:
|
||||||
|
external: true
|
||||||
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,
|
||||||
|
];
|
||||||
40
nginx/conf.d/default.conf
Normal file
40
nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# HTML: revalidate so content updates are seen quickly
|
||||||
|
location ~* \.html?$ {
|
||||||
|
add_header Cache-Control "public, no-cache";
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Versioned/hashed assets: long cache, immutable
|
||||||
|
location ~* \.(js|css)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Images and media
|
||||||
|
location ~* \.(ico|png|jpg|jpeg|gif|webp|svg|avif)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
location ~* \.(woff2?|ttf|otf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default: serve static files, then try directory/index, then 404
|
||||||
|
location / {
|
||||||
|
add_header Cache-Control "public, no-cache";
|
||||||
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "mail-landing",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.29.3",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"docker:build": "docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/mail-landing:latest .",
|
||||||
|
"docker:push": "docker push git.mifi.dev/mifi-holdings/mail-landing:latest",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"lint": "pnpm run lint:yaml && pnpm run lint:js && pnpm run lint:css",
|
||||||
|
"lint:css": "stylelint \"src/**/*.css\"",
|
||||||
|
"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": {
|
||||||
|
"beasties": "^0.4.1",
|
||||||
|
"clean-css": "^5.3.3",
|
||||||
|
"eslint": "^10.0.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mifi-holdings/mail-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 @@
|
|||||||
|
{}
|
||||||
372
src/assets/css/site.css
Normal file
372
src/assets/css/site.css
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
:root {
|
||||||
|
--accent: #4f46e5;
|
||||||
|
--accent-light: #6366f1;
|
||||||
|
--background: #f7fafc;
|
||||||
|
--button-bg: var(--accent);
|
||||||
|
--button-hover: var(--accent-light);
|
||||||
|
--button-text: var(--background);
|
||||||
|
--radius: 1.25rem;
|
||||||
|
--shadow: 0 2px 12px 0 rgb(20 30 60 / 9%);
|
||||||
|
--surface: rgb(255 255 255 / 94%);
|
||||||
|
--text-main: #23243a;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--table-bg: transparent;
|
||||||
|
--accordion-bg: #fff;
|
||||||
|
--content-bg: #f8fafc;
|
||||||
|
--faq-a: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--accent: #a5b4fc;
|
||||||
|
--accent-light: #818cf8;
|
||||||
|
--background: #171923;
|
||||||
|
--shadow: 0 2px 16px 0 rgb(8 8 24 / 24%);
|
||||||
|
--surface: rgb(30 34 42 / 90%);
|
||||||
|
--text-main: #f6f7fa;
|
||||||
|
--text-muted: #aab2bd;
|
||||||
|
--table-bg: #252745;
|
||||||
|
--accordion-bg: #24264a;
|
||||||
|
--content-bg: #21223a;
|
||||||
|
--faq-a: #b7badf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: Inter, 'Segoe UI', Arial, sans-serif;
|
||||||
|
font-size: 17px;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.94em;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.footer {
|
||||||
|
color: #aab2bd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--button-text);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 100;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
transform: translateY(0);
|
||||||
|
outline: 2px solid var(--accent-light);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
max-width: 370px;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 400px) {
|
||||||
|
.container {
|
||||||
|
padding: 2rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.faq {
|
||||||
|
margin: 2.5rem auto 1.5rem;
|
||||||
|
max-width: 580px;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li:not(:last-child)::after {
|
||||||
|
content: '›';
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb [aria-current='page'] {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-text);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
box-shadow: 0 1px 4px 0 rgb(43 196 250 / 11%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings {
|
||||||
|
background: var(--content-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.2rem 1.1rem 1.1rem;
|
||||||
|
margin-bottom: 2.1rem;
|
||||||
|
box-shadow: 0 1px 6px 0 rgb(90 100 140 / 6%);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings h2 {
|
||||||
|
font-size: 1.14rem;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
background: var(--table-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.38em 0.5em;
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
width: 44%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tip colors chosen for WCAG 2.2 AAA (≥7:1 contrast) */
|
||||||
|
.tip {
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #3730a3;
|
||||||
|
border-radius: 0.7em;
|
||||||
|
font-size: 0.98em;
|
||||||
|
padding: 0.48em 0.8em;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.3em 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.tip {
|
||||||
|
background: #232555;
|
||||||
|
color: #c7d2fe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
margin: 1.5rem 0 1rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 6px 0 rgb(90 100 140 / 6%);
|
||||||
|
background: var(--accordion-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-section {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
background: var(--accordion-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.accordion-section {
|
||||||
|
border-top: 1px solid #28284b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-section:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.12rem;
|
||||||
|
padding: 1.1rem 1.2rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
color: var(--text-main);
|
||||||
|
outline: none;
|
||||||
|
gap: 0.6em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-trigger:hover,
|
||||||
|
.accordion-trigger:focus {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-trigger .icon {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
font-size: 1.3em;
|
||||||
|
opacity: 0.86;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-trigger[aria-expanded='true'] .icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--content-bg);
|
||||||
|
transition: max-height 0.3s cubic-bezier(0.7, 0, 0.3, 1);
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-section.open .accordion-content {
|
||||||
|
padding: 1.2rem 1.5rem 1.3rem;
|
||||||
|
max-height: 1000px;
|
||||||
|
transition: max-height 0.5s cubic-bezier(0.7, 0, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-q {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.8em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-a {
|
||||||
|
margin: 0.1em 0 0.6em 0.3em;
|
||||||
|
color: var(--faq-a);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.container {
|
||||||
|
padding: 1.1rem 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.36rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings {
|
||||||
|
padding: 0.8rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-trigger {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.93rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-section.open .accordion-content {
|
||||||
|
padding: 0.7rem 0.8rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-size: 0.98em;
|
||||||
|
}
|
||||||
|
}
|
||||||
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 |
37
src/assets/js/accordion.js
Normal file
37
src/assets/js/accordion.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Native accessible accordion
|
||||||
|
document.querySelectorAll('.accordion-trigger').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const section = btn.closest('.accordion-section');
|
||||||
|
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
||||||
|
document.querySelectorAll('.accordion-section').forEach((s) => {
|
||||||
|
if (s === section) {
|
||||||
|
s.classList.toggle('open', !expanded);
|
||||||
|
btn.setAttribute('aria-expanded', String(!expanded));
|
||||||
|
const content = btn.nextElementSibling;
|
||||||
|
content.style.maxHeight = !expanded
|
||||||
|
? content.scrollHeight + 40 + 'px'
|
||||||
|
: '0px';
|
||||||
|
} else {
|
||||||
|
s.classList.remove('open');
|
||||||
|
s.querySelector('.accordion-trigger').setAttribute(
|
||||||
|
'aria-expanded',
|
||||||
|
'false',
|
||||||
|
);
|
||||||
|
s.querySelector('.accordion-content').style.maxHeight = '0px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow arrow navigation
|
||||||
|
btn.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
const triggers = Array.from(
|
||||||
|
document.querySelectorAll('.accordion-trigger'),
|
||||||
|
);
|
||||||
|
let idx = triggers.indexOf(e.target);
|
||||||
|
if (e.key === 'ArrowDown') idx = (idx + 1) % triggers.length;
|
||||||
|
else idx = (idx - 1 + triggers.length) % triggers.length;
|
||||||
|
triggers[idx].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
4
src/assets/js/current-year.js
Normal file
4
src/assets/js/current-year.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
(function () {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
document.getElementById('current-year').textContent = `–${year}`;
|
||||||
|
})();
|
||||||
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,364 +1,402 @@
|
|||||||
<!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">
|
<script
|
||||||
<title>Welcome to Email from mifi Ventures</title>
|
async
|
||||||
<meta name="description" content="Setup help for users of Email from mifi Ventures">
|
src="https://www.googletagmanager.com/gtag/js?id=G-NF64QMKWX6"
|
||||||
<style>
|
></script>
|
||||||
:root {
|
<script
|
||||||
--accent: #4f46e5;
|
defer
|
||||||
--accent-light: #6366f1;
|
src="/assets/js/ga-init.js"
|
||||||
--background: #f7fafc;
|
data-ga-id="G-NF64QMKWX6"
|
||||||
--surface: #fff;
|
></script>
|
||||||
--text-main: #1a202c;
|
<script
|
||||||
--text-muted: #64748b;
|
defer
|
||||||
--radius: 1.25rem;
|
src="https://analytics.mifi.holdings/script.js"
|
||||||
--shadow: 0 2px 12px 0 rgba(20,30,60,0.09);
|
data-website-id="80f4013d-dd3f-4a10-af75-2e788090990d"
|
||||||
--table-bg: transparent;
|
></script>
|
||||||
--accordion-bg: #fff;
|
|
||||||
--content-bg: #f8fafc;
|
|
||||||
--faq-a: #333;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--accent: #a5b4fc;
|
|
||||||
--accent-light: #818cf8;
|
|
||||||
--background: #171923;
|
|
||||||
--surface: #22243a;
|
|
||||||
--text-main: #e7eaf8;
|
|
||||||
--text-muted: #b4b9d1;
|
|
||||||
--shadow: 0 2px 16px 0 rgba(8,8,24,0.24);
|
|
||||||
--table-bg: #252745;
|
|
||||||
--accordion-bg: #24264a;
|
|
||||||
--content-bg: #21223a;
|
|
||||||
--faq-a: #b7badf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
|
|
||||||
font-size: 17px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 580px;
|
|
||||||
margin: 2.5rem auto 1.5rem auto;
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
padding: 2.5rem 2rem 2rem 2rem;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
.intro {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1.7rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.general-settings {
|
|
||||||
background: var(--content-bg);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1.2rem 1.1rem 1.1rem 1.1rem;
|
|
||||||
margin-bottom: 2.1rem;
|
|
||||||
box-shadow: 0 1px 6px 0 rgba(90,100,140,0.06);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.general-settings h2 {
|
|
||||||
font-size: 1.14rem;
|
|
||||||
margin: 0 0 0.6rem 0;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
background: var(--table-bg);
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 0.38em 0.5em;
|
|
||||||
border: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
td:first-child {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
width: 44%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.tip {
|
|
||||||
background: #eef2ff;
|
|
||||||
color: var(--accent-light);
|
|
||||||
border-radius: 0.7em;
|
|
||||||
font-size: 0.98em;
|
|
||||||
padding: 0.48em 0.8em;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0.3em 0 0.2em 0;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.tip { background: #232555; color: #a5b4fc; }
|
|
||||||
}
|
|
||||||
.accordion {
|
|
||||||
margin: 1.5rem 0 1rem 0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 1px 6px 0 rgba(90,100,140,0.06);
|
|
||||||
background: var(--accordion-bg);
|
|
||||||
}
|
|
||||||
.accordion-section {
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
background: var(--accordion-bg);
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.accordion-section {
|
|
||||||
border-top: 1px solid #28284b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.accordion-section:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
.accordion-trigger {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.12rem;
|
|
||||||
padding: 1.1rem 1.2rem;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: background 0.2s;
|
|
||||||
color: var(--text-main);
|
|
||||||
outline: none;
|
|
||||||
gap: 0.6em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.accordion-trigger:hover, .accordion-trigger:focus {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
.accordion-trigger .icon {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
font-size: 1.3em;
|
|
||||||
opacity: 0.86;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
.accordion-trigger[aria-expanded="true"] .icon {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
.accordion-content {
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--content-bg);
|
|
||||||
transition: max-height 0.3s cubic-bezier(.7,0,.3,1);
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
.accordion-section.open .accordion-content {
|
|
||||||
padding: 1.2rem 1.5rem 1.3rem 1.5rem;
|
|
||||||
max-height: 1000px;
|
|
||||||
transition: max-height 0.5s cubic-bezier(.7,0,.3,1);
|
|
||||||
}
|
|
||||||
.faq-q {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 0.8em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
.faq-a {
|
|
||||||
margin: 0.1em 0 0.6em 0.3em;
|
|
||||||
color: var(--faq-a);
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
margin-top: 2.5em;
|
|
||||||
text-align: center;
|
|
||||||
color: #bbb;
|
|
||||||
font-size: 0.94em;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { padding: 1.1rem 0.5rem 1rem 0.5rem;}
|
|
||||||
h1 { font-size: 1.36rem; }
|
|
||||||
.general-settings { padding: 0.8rem 0.6rem 0.8rem 0.6rem; }
|
|
||||||
.general-settings h2 { font-size: 1rem; }
|
|
||||||
.accordion-trigger { font-size: 1rem; padding: 0.93rem 0.8rem;}
|
|
||||||
.accordion-section.open .accordion-content { padding: 0.7rem 0.8rem 0.9rem 0.8rem; }
|
|
||||||
td { font-size: 0.98em;}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Welcome to Email from mifi Ventures</h1>
|
|
||||||
<div class="intro">
|
|
||||||
<strong>Let’s get your inbox ready! 📬</strong><br>
|
|
||||||
<p>Friendly help for setting up your email—works with Outlook, Apple Mail, Thunderbird, phones, and more.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="general-settings" aria-label="General Email Settings">
|
<meta charset="UTF-8" />
|
||||||
<h2>General Settings (All Clients)</h2>
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<table>
|
<title>mifi Holdings — Email Setup & Help</title>
|
||||||
<tr><td>Email Address</td><td>your.name@yourdomain.com</td></tr>
|
<meta
|
||||||
<tr><td>Username</td><td>your.name@yourdomain.com</td></tr>
|
name="description"
|
||||||
<tr><td>Password</td><td>(your email password)</td></tr>
|
content="Setup help for users of Email from mifi Holdings"
|
||||||
<tr><td>Incoming Server</td><td><b>mail.mifi.holdings</b></td></tr>
|
/>
|
||||||
<tr><td>Outgoing Server</td><td><b>mail.mifi.holdings</b></td></tr>
|
<link rel="canonical" href="https://mail.mifi.holdings/help" />
|
||||||
<tr><td>IMAP Port</td><td>993 (SSL/TLS)</td></tr>
|
<meta name="robots" content="index, follow" />
|
||||||
<tr><td>POP3 Port</td><td>995 (SSL/TLS)</td></tr>
|
<meta name="author" content="Mike Fitzpatrick (mifi)" />
|
||||||
<tr><td>SMTP Port</td><td>587 (STARTTLS) or 465 (SSL/TLS)</td></tr>
|
|
||||||
<tr><td>Authentication</td><td>Required (use same as incoming)</td></tr>
|
|
||||||
<tr><td>Encryption</td><td>SSL/TLS or STARTTLS</td></tr>
|
|
||||||
</table>
|
|
||||||
<span class="tip">Tip: Always use your <b>full email address</b> as your username!</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="accordion" id="helpAccordion">
|
<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"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="/assets/css/site.css" />
|
||||||
|
|
||||||
<!-- Outlook -->
|
<script type="application/ld+json">
|
||||||
<section class="accordion-section">
|
{
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
"@context": "https://schema.org",
|
||||||
<span class="icon">▶</span> Microsoft Outlook
|
"@type": "FAQPage",
|
||||||
</button>
|
"mainEntity": [
|
||||||
<div class="accordion-content">
|
{
|
||||||
<ol>
|
"@type": "Question",
|
||||||
<li>Go to <b>File → Add Account</b></li>
|
"name": "My email won't send?",
|
||||||
<li>Enter your full email address</li>
|
"acceptedAnswer": {
|
||||||
<li>Choose <b>Advanced options</b> → check “Set up manually”</li>
|
"@type": "Answer",
|
||||||
<li>Select <b>IMAP</b> (recommended) or POP</li>
|
"text": "Check that you're using your full email address for both incoming and outgoing username, and that the port is 587 or 465."
|
||||||
<li>Incoming server: <code>mail.mifi.holdings</code>, port <b>993</b> (SSL/TLS)</li>
|
}
|
||||||
<li>Outgoing server: <code>mail.mifi.holdings</code>, port <b>587</b> (STARTTLS) or <b>465</b> (SSL/TLS)</li>
|
},
|
||||||
<li>Username: full email address; Password: your password</li>
|
{
|
||||||
<li>Click <b>Connect</b></li>
|
"@type": "Question",
|
||||||
</ol>
|
"name": "SSL/TLS errors?",
|
||||||
<span class="tip">If sending fails, make sure “Require logon using SPA” is <b>unchecked</b>.</span>
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Ensure SSL or STARTTLS is enabled for both incoming and outgoing mail."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "Still stuck?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Contact postmaster@mifi.holdings. Please include any error messages, your mail app, and a screenshot if you can!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Home",
|
||||||
|
"item": "https://mail.mifi.holdings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "Email Setup & Help",
|
||||||
|
"item": "https://mail.mifi.holdings/help"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
class="skip-link"
|
||||||
|
data-umami-event="skip to main content"
|
||||||
|
>Skip to main content</a
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<main id="main-content" class="container faq">
|
||||||
|
<nav aria-label="Breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
data-umami-event="breadcrumb"
|
||||||
|
data-umami-event-label="home"
|
||||||
|
>Home</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li aria-current="page">Email Setup & Help</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1 class="text-center">Welcome to Email from mifi Ventures</h1>
|
||||||
|
<div class="intro">
|
||||||
|
<strong>Let's get your inbox ready! 📬</strong><br />
|
||||||
|
<p>
|
||||||
|
Friendly help for setting up your email—works with
|
||||||
|
Outlook, Apple Mail, Thunderbird, phones, and more.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="general-settings"
|
||||||
|
aria-label="General Email Settings"
|
||||||
|
>
|
||||||
|
<h2>General Settings (All Clients)</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Email Address</td>
|
||||||
|
<td>your.name@yourdomain.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Username</td>
|
||||||
|
<td>your.name@yourdomain.com</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Password</td>
|
||||||
|
<td>(your email password)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Incoming Server</td>
|
||||||
|
<td><b>mail.mifi.holdings</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Outgoing Server</td>
|
||||||
|
<td><b>mail.mifi.holdings</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IMAP Port</td>
|
||||||
|
<td>993 (SSL/TLS)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>POP3 Port</td>
|
||||||
|
<td>995 (SSL/TLS)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>SMTP Port</td>
|
||||||
|
<td>587 (STARTTLS) or 465 (SSL/TLS)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Authentication</td>
|
||||||
|
<td>Required (use same as incoming)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Encryption</td>
|
||||||
|
<td>SSL/TLS or STARTTLS</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<span class="tip"
|
||||||
|
>Tip: Always use your <b>full email address</b> as your
|
||||||
|
username!</span
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="accordion" id="helpAccordion">
|
||||||
|
<!-- Outlook -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="microsoft outlook"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> Microsoft Outlook
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ol>
|
||||||
|
<li>Go to <b>File → Add Account</b></li>
|
||||||
|
<li>Enter your full email address</li>
|
||||||
|
<li>
|
||||||
|
Choose <b>Advanced options</b> → check “Set
|
||||||
|
up manually”
|
||||||
|
</li>
|
||||||
|
<li>Select <b>IMAP</b> (recommended) or POP</li>
|
||||||
|
<li>
|
||||||
|
Incoming server:
|
||||||
|
<code>mail.mifi.holdings</code>, port
|
||||||
|
<b>993</b> (SSL/TLS)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Outgoing server:
|
||||||
|
<code>mail.mifi.holdings</code>, port
|
||||||
|
<b>587</b> (STARTTLS) or
|
||||||
|
<b>465</b> (SSL/TLS)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Username: full email address; Password: your
|
||||||
|
password
|
||||||
|
</li>
|
||||||
|
<li>Click <b>Connect</b></li>
|
||||||
|
</ol>
|
||||||
|
<span class="tip"
|
||||||
|
>If sending fails, make sure “Require logon
|
||||||
|
using SPA” is <b>unchecked</b>.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Apple Mail -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="apple mail (macos, ios)"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> Apple Mail (macOS, iOS)
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Add Account → <b>Other Mail Account</b>
|
||||||
|
</li>
|
||||||
|
<li>Enter your name, email, and password</li>
|
||||||
|
<li>
|
||||||
|
Incoming/Outgoing server:
|
||||||
|
<code>mail.mifi.holdings</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
IMAP port: <b>993</b> (SSL); SMTP port:
|
||||||
|
<b>587</b> (STARTTLS) or <b>465</b> (SSL)
|
||||||
|
</li>
|
||||||
|
<li>Use full email address for username</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Thunderbird -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="thunderbird"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> Thunderbird
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Menu → Account Settings → Add Mail Account
|
||||||
|
</li>
|
||||||
|
<li>Fill in your name, email, and password</li>
|
||||||
|
<li>
|
||||||
|
Click “Configure manually” and use settings
|
||||||
|
above
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Mobile -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="ios / android mail / gmail app"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> iOS / Android Mail /
|
||||||
|
Gmail App
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ul>
|
||||||
|
<li>Add Account → Other</li>
|
||||||
|
<li>Enter your email and password</li>
|
||||||
|
<li>
|
||||||
|
Manual setup:
|
||||||
|
<code>mail.mifi.holdings</code>, correct
|
||||||
|
ports, SSL/TLS required
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Gmail app: tap profile → Add account →
|
||||||
|
Other, fill in details, use IMAP
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FAQ -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="faq / troubleshooting"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> FAQ / Troubleshooting
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<div class="faq-q">Q: My email won’t send?</div>
|
||||||
|
<div class="faq-a">
|
||||||
|
Check that you’re using your full email address
|
||||||
|
for both incoming and outgoing username, and
|
||||||
|
that the port is 587 or 465.
|
||||||
|
</div>
|
||||||
|
<div class="faq-q">Q: SSL/TLS errors?</div>
|
||||||
|
<div class="faq-a">
|
||||||
|
Ensure SSL or STARTTLS is enabled for both
|
||||||
|
incoming and outgoing mail.
|
||||||
|
</div>
|
||||||
|
<div class="faq-q">Q: Still stuck?</div>
|
||||||
|
<div class="faq-a">
|
||||||
|
Contact
|
||||||
|
<a href="mailto:postmaster@mifi.holdings"
|
||||||
|
>postmaster@mifi.holdings</a
|
||||||
|
>.<br />
|
||||||
|
Please include any error messages, your mail
|
||||||
|
app, and a screenshot if you can!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pro Tips -->
|
||||||
|
<section class="accordion-section">
|
||||||
|
<button
|
||||||
|
class="accordion-trigger"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-umami-event="accordion"
|
||||||
|
data-umami-event-label="pro tips & advanced"
|
||||||
|
>
|
||||||
|
<span class="icon">▶</span> Pro Tips & Advanced
|
||||||
|
</button>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>IMAP syncs</b> your mail
|
||||||
|
everywhere—choose IMAP unless you know you
|
||||||
|
want POP3.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Your login is always your
|
||||||
|
<b>full email address</b>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Check your Spam/Junk folder for misfiled
|
||||||
|
good emails.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Advanced: IMAP path prefix =
|
||||||
|
<b>(leave blank)</b>; SMTP authentication is
|
||||||
|
always required.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<footer class="footer">
|
||||||
|
Email from mifi Holdings · Help Page · ©
|
||||||
<!-- Apple Mail -->
|
2025<span id="current-year"></span>
|
||||||
<section class="accordion-section">
|
<a
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
href="https://mifi.ventures"
|
||||||
<span class="icon">▶</span> Apple Mail (macOS, iOS)
|
data-umami-event="footer link"
|
||||||
</button>
|
data-umami-event-label="mifi ventures"
|
||||||
<div class="accordion-content">
|
>mifi Ventures, LLC</a
|
||||||
<ol>
|
>
|
||||||
<li>Add Account → <b>Other Mail Account</b></li>
|
</footer>
|
||||||
<li>Enter your name, email, and password</li>
|
<img
|
||||||
<li>Incoming/Outgoing server: <code>mail.mifi.holdings</code></li>
|
src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
|
||||||
<li>IMAP port: <b>993</b> (SSL); SMTP port: <b>587</b> (STARTTLS) or <b>465</b> (SSL)</li>
|
alt=""
|
||||||
<li>Use full email address for username</li>
|
width="1"
|
||||||
</ol>
|
height="1"
|
||||||
</div>
|
role="presentation"
|
||||||
</section>
|
loading="eager"
|
||||||
|
/>
|
||||||
<!-- Thunderbird -->
|
<script defer src="/assets/js/current-year.js"></script>
|
||||||
<section class="accordion-section">
|
<script defer src="/assets/js/accordion.js"></script>
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
</body>
|
||||||
<span class="icon">▶</span> Thunderbird
|
|
||||||
</button>
|
|
||||||
<div class="accordion-content">
|
|
||||||
<ol>
|
|
||||||
<li>Menu → Account Settings → Add Mail Account</li>
|
|
||||||
<li>Fill in your name, email, and password</li>
|
|
||||||
<li>Click “Configure manually” and use settings above</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Mobile -->
|
|
||||||
<section class="accordion-section">
|
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
|
||||||
<span class="icon">▶</span> iOS / Android Mail / Gmail App
|
|
||||||
</button>
|
|
||||||
<div class="accordion-content">
|
|
||||||
<ul>
|
|
||||||
<li>Add Account → Other</li>
|
|
||||||
<li>Enter your email and password</li>
|
|
||||||
<li>Manual setup: <code>mail.mifi.holdings</code>, correct ports, SSL/TLS required</li>
|
|
||||||
<li>Gmail app: tap profile → Add account → Other, fill in details, use IMAP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FAQ -->
|
|
||||||
<section class="accordion-section">
|
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
|
||||||
<span class="icon">▶</span> FAQ / Troubleshooting
|
|
||||||
</button>
|
|
||||||
<div class="accordion-content">
|
|
||||||
<div class="faq-q">Q: My email won’t send?</div>
|
|
||||||
<div class="faq-a">Check that you’re using your full email address for both incoming and outgoing username, and that the port is 587 or 465.</div>
|
|
||||||
<div class="faq-q">Q: SSL/TLS errors?</div>
|
|
||||||
<div class="faq-a">Ensure SSL or STARTTLS is enabled for both incoming and outgoing mail.</div>
|
|
||||||
<div class="faq-q">Q: Still stuck?</div>
|
|
||||||
<div class="faq-a">Contact <a href="mailto:postmaster@mifi.holdings">postmaster@mifi.holdings</a>.<br>
|
|
||||||
Please include any error messages, your mail app, and a screenshot if you can!</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Pro Tips -->
|
|
||||||
<section class="accordion-section">
|
|
||||||
<button class="accordion-trigger" aria-expanded="false">
|
|
||||||
<span class="icon">▶</span> Pro Tips & Advanced
|
|
||||||
</button>
|
|
||||||
<div class="accordion-content">
|
|
||||||
<ul>
|
|
||||||
<li><b>IMAP syncs</b> your mail everywhere—choose IMAP unless you know you want POP3.</li>
|
|
||||||
<li>Your login is always your <b>full email address</b>.</li>
|
|
||||||
<li>Check your Spam/Junk folder for misfiled good emails.</li>
|
|
||||||
<li>Advanced: IMAP path prefix = <b>(leave blank)</b>; SMTP authentication is always required.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
Email from mifi Ventures · Help Page – © 2025 mifi Ventures, LLC
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
// Native accessible accordion
|
|
||||||
document.querySelectorAll('.accordion-trigger').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const section = btn.closest('.accordion-section');
|
|
||||||
const expanded = btn.getAttribute('aria-expanded') === "true";
|
|
||||||
document.querySelectorAll('.accordion-section').forEach(s => {
|
|
||||||
if (s === section) {
|
|
||||||
s.classList.toggle('open', !expanded);
|
|
||||||
btn.setAttribute('aria-expanded', String(!expanded));
|
|
||||||
const content = btn.nextElementSibling;
|
|
||||||
content.style.maxHeight = !expanded ? (content.scrollHeight+40) + "px" : "0px";
|
|
||||||
} else {
|
|
||||||
s.classList.remove('open');
|
|
||||||
s.querySelector('.accordion-trigger').setAttribute('aria-expanded', "false");
|
|
||||||
s.querySelector('.accordion-content').style.maxHeight = "0px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Allow arrow navigation
|
|
||||||
btn.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
||||||
const triggers = Array.from(document.querySelectorAll('.accordion-trigger'));
|
|
||||||
let idx = triggers.indexOf(e.target);
|
|
||||||
if (e.key === "ArrowDown") idx = (idx + 1) % triggers.length;
|
|
||||||
else idx = (idx - 1 + triggers.length) % triggers.length;
|
|
||||||
triggers[idx].focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
223
src/index.html
223
src/index.html
@@ -1,103 +1,124 @@
|
|||||||
<!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
|
||||||
<title>mail.mifi.holdings</title>
|
async
|
||||||
<style>
|
src="https://www.googletagmanager.com/gtag/js?id=G-NF64QMKWX6"
|
||||||
:root {
|
></script>
|
||||||
--background: #f7fafc;
|
<script
|
||||||
--surface: rgba(255,255,255,0.94);
|
defer
|
||||||
--text-main: #23243a;
|
src="/assets/js/ga-init.js"
|
||||||
--text-muted: #64748b;
|
data-ga-id="G-NF64QMKWX6"
|
||||||
--button-bg: #2bc4fa;
|
></script>
|
||||||
--button-hover: #22a0ca;
|
<script
|
||||||
--button-text: #181a20;
|
defer
|
||||||
--shadow: 0 2px 24px 0 rgba(0,0,0,0.11);
|
src="https://analytics.mifi.holdings/script.js"
|
||||||
}
|
data-website-id="80f4013d-dd3f-4a10-af75-2e788090990d"
|
||||||
@media (prefers-color-scheme: dark) {
|
></script>
|
||||||
:root {
|
|
||||||
--background: #15181c;
|
<meta charset="UTF-8" />
|
||||||
--surface: rgba(30,34,42,0.9);
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
--text-main: #f6f7fa;
|
<title>mifi Holdings — Mail Services</title>
|
||||||
--text-muted: #aab2bd;
|
<meta
|
||||||
--button-bg: #2bc4fa;
|
name="description"
|
||||||
--button-hover: #22a0ca;
|
content="The mifi Holdings mail services hub—here you can find help with setting up your email, changing your password, and accessing webmail."
|
||||||
--button-text: #181a20;
|
/>
|
||||||
--shadow: 0 2px 24px 0 rgba(0,0,0,0.18);
|
<link rel="canonical" href="https://mail.mifi.holdings" />
|
||||||
}
|
<meta name="robots" content="index, follow" />
|
||||||
}
|
<meta name="author" content="Mike Fitzpatrick (mifi)" />
|
||||||
html,body {
|
|
||||||
height: 100%;
|
<link
|
||||||
margin: 0;
|
rel="icon"
|
||||||
padding: 0;
|
type="image/svg+xml"
|
||||||
background: var(--background);
|
href="/assets/images/favicon.svg"
|
||||||
color: var(--text-main);
|
/>
|
||||||
font-family: 'Inter', Arial, sans-serif;
|
<link
|
||||||
}
|
rel="icon"
|
||||||
body {
|
type="image/x-icon"
|
||||||
display: flex;
|
href="/assets/images/favicon.ico"
|
||||||
align-items: center;
|
/>
|
||||||
justify-content: center;
|
<link
|
||||||
min-height: 100vh;
|
rel="apple-touch-icon"
|
||||||
}
|
sizes="180x180"
|
||||||
.container {
|
href="/assets/images/apple-touch-icon.png"
|
||||||
text-align: center;
|
/>
|
||||||
background: var(--surface);
|
<link rel="stylesheet" href="/assets/css/site.css" />
|
||||||
padding: 3rem 2rem;
|
|
||||||
border-radius: 1.5rem;
|
<script type="application/ld+json">
|
||||||
box-shadow: var(--shadow);
|
{
|
||||||
max-width: 370px;
|
"@context": "https://schema.org",
|
||||||
margin: 1rem;
|
"@type": "WebSite",
|
||||||
}
|
"name": "mifi Holdings Mail",
|
||||||
.emoji {
|
"url": "https://mail.mifi.holdings",
|
||||||
font-size: 3rem;
|
"description": "The mifi Holdings mail services hub—here you can find help with setting up your email, changing your password, and accessing webmail.",
|
||||||
margin-bottom: 1rem;
|
"publisher": {
|
||||||
}
|
"@type": "Organization",
|
||||||
h1 {
|
"name": "mifi Ventures",
|
||||||
font-size: 2rem;
|
"url": "https://mifi.ventures",
|
||||||
margin-bottom: 0.5rem;
|
"email": "postmaster@mifi.holdings"
|
||||||
font-weight: 700;
|
}
|
||||||
letter-spacing: -1px;
|
}
|
||||||
}
|
</script>
|
||||||
p {
|
</head>
|
||||||
color: var(--text-muted);
|
<body>
|
||||||
margin-bottom: 2rem;
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||||
font-size: 1.1rem;
|
<div class="content">
|
||||||
line-height: 1.5;
|
<main id="main-content" class="container text-center">
|
||||||
}
|
<div class="emoji">📮</div>
|
||||||
a {
|
<h1>This is just a mailbox.</h1>
|
||||||
display: block;
|
<p>
|
||||||
padding: 0.75rem 1.5rem;
|
You've reached <b>mail.mifi.holdings</b>.<br />
|
||||||
margin-bottom: 0.75rem;
|
There's nothing exciting here's nothing exciting
|
||||||
background: var(--button-bg);
|
here—just some gears whirring and mail being sorted.<br />
|
||||||
color: var(--button-text);
|
Looking for your messages?
|
||||||
border-radius: 999px;
|
</p>
|
||||||
font-weight: 600;
|
<a
|
||||||
text-decoration: none;
|
class="button"
|
||||||
transition: background 0.2s;
|
href="/help"
|
||||||
box-shadow: 0 1px 4px 0 rgba(43,196,250,0.11);
|
data-umami-event="button"
|
||||||
}
|
data-umami-event-label="email setup help"
|
||||||
a:hover {
|
>Email Setup Help</a
|
||||||
background: var(--button-hover);
|
>
|
||||||
}
|
<a
|
||||||
@media (max-width: 400px) {
|
class="button"
|
||||||
.container { padding: 2rem 0.5rem; }
|
href="https://webmail.mifi.holdings"
|
||||||
}
|
target="_blank"
|
||||||
</style>
|
data-umami-event="button"
|
||||||
</head>
|
data-umami-event-label="webmail"
|
||||||
<body>
|
>
|
||||||
<div class="container">
|
Go to Webmail
|
||||||
<div class="emoji">📮</div>
|
</a>
|
||||||
<h1>This is just a mailbox.</h1>
|
<a
|
||||||
<p>
|
class="button"
|
||||||
You’ve reached <b>mail.mifi.holdings</b>.<br>
|
href="https://postmaster.mifi.holdings/users/login.php"
|
||||||
There’s nothing exciting here—just some gears whirring and mail being sorted.<br>
|
target="_blank"
|
||||||
Looking for your messages?
|
data-umami-event="button"
|
||||||
</p>
|
data-umami-event-label="change/forgot password"
|
||||||
<a href="/help">Email Setup Help</a>
|
>
|
||||||
<a href="https://webmail.mifi.holdings">Go to Webmail</a>
|
Change/Forgot Password
|
||||||
<a href="https://postmaster.mifi.holdings/users/login.php">Change/Forgot Password</a>
|
</a>
|
||||||
</div>
|
</main>
|
||||||
</body>
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
Email from mifi Holdings · © 2025<span
|
||||||
|
id="current-year"
|
||||||
|
></span>
|
||||||
|
<a
|
||||||
|
href="https://mifi.ventures"
|
||||||
|
data-umami-event="footer link"
|
||||||
|
data-umami-event-label="mifi ventures"
|
||||||
|
>mifi Ventures, LLC</a
|
||||||
|
>
|
||||||
|
</footer>
|
||||||
|
<img
|
||||||
|
src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
|
||||||
|
alt=""
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
role="presentation"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<script defer src="/assets/js/current-year.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4
src/robots.txt
Normal file
4
src/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://mail.mifi.holdings/sitemap.xml
|
||||||
13
src/sitemap.xml
Normal file
13
src/sitemap.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://mail.mifi.holdings/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://mail.mifi.holdings/help</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</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