Compare commits

9 Commits

Author SHA1 Message Date
fef29bd5a1 This is why templates and layouts are better...
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-17 13:33:18 -03:00
aa6d24be5a Wrong pixel URI
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-17 12:47:23 -03:00
4bc40b2be7 Umami tracking
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-17 12:18:55 -03:00
9a2d7ae222 Setup umami analytics
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-17 11:01:30 -03:00
c71ec612bb Accessibility fixes
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-16 12:57:44 -03:00
635594aea8 robots.txt and sitemap.xml
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-16 11:08:07 -03:00
115d63768c Slight tweaks and JSON-LD
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-16 11:00:51 -03:00
6bef0f8254 Updates to readme
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-13 18:19:29 -03:00
455bc18b3b Add pipelines
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-13 18:12:11 -03:00
17 changed files with 1094 additions and 364 deletions

View File

@@ -1,8 +1,8 @@
{ {
"semi": false, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "all",
"overrides": [ "overrides": [
{ {
"files": "*.yml", "files": "*.yml",

159
.woodpecker/build.yml Normal file
View 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
View 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
View 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]

122
README.md
View File

@@ -1,23 +1,129 @@
# mail-landing # mail-landing
Static landing site for mail.mifi.holdings (HTML/CSS/JS), built to `dist/` and served with nginx in production. 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.
## Dev Container (local development and preview) ---
## 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** 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). In Cursor/VS Code: **Command Palette****Dev Containers: Reopen in Container** (or **Clone Repository in Container Volume** when opening the repo).
The first time will build the container (Node 22 + pnpm) and run `pnpm install`. First time: builds the container (Node 22 + pnpm), runs `pnpm install`.
2. **Preview the site** 2. **Preview the site**
In the container terminal: In the container terminal:
- **Quick preview** (serves `src/` as-is, no build): - **Quick preview** (serves `src/` as-is, no build):
`pnpm preview` `pnpm preview`
- **Production-like preview** (build then serve `dist/`): - **Production-like** (build then serve `dist/`):
`pnpm preview:prod` `pnpm preview:prod`
Port **3000** is forwarded; open **http://localhost:3000** (or use the “Preview” port notification).
Port **3000** is forwarded; open **http://localhost:3000** in the host browser (or use the “Preview” port notification).
3. **Other commands** 3. **Other commands**
- `pnpm build` — build `src/``dist/` (minify JS/CSS, inline critical CSS) - `pnpm build` — build `src/``dist/` (minify JS/CSS, inline critical CSS)
- `pnpm lint` / `pnpm format` — lint and format - `pnpm lint` / `pnpm format` — lint and format
- `pnpm docker:build` — build production image - `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 repos `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. Lets 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) |

View File

@@ -1,4 +1,4 @@
import prettierConfig from 'eslint-config-prettier/flat' import prettierConfig from 'eslint-config-prettier/flat';
export default [ export default [
{ {
@@ -9,12 +9,12 @@ export default [
globals: { globals: {
window: 'readonly', window: 'readonly',
document: 'readonly', document: 'readonly',
dataLayer: 'writable' dataLayer: 'writable',
} },
}, },
rules: { rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }] 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
}
}, },
prettierConfig },
] prettierConfig,
];

View File

@@ -8,71 +8,71 @@ import {
readFileSync, readFileSync,
writeFileSync, writeFileSync,
cpSync, cpSync,
readdirSync readdirSync,
} from 'fs' } from 'fs';
import { join, dirname, extname } from 'path' import { join, dirname, extname } from 'path';
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url';
import Beasties from 'beasties' import Beasties from 'beasties';
import { minify as minifyJs } from 'terser' import { minify as minifyJs } from 'terser';
import CleanCSS from 'clean-css' import CleanCSS from 'clean-css';
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..') const root = join(__dirname, '..');
const srcDir = join(root, 'src') const srcDir = join(root, 'src');
const distDir = join(root, 'dist') const distDir = join(root, 'dist');
function getFiles(dir, files = []) { function getFiles(dir, files = []) {
const entries = readdirSync(dir, { withFileTypes: true }) const entries = readdirSync(dir, { withFileTypes: true });
for (const e of entries) { for (const e of entries) {
const full = join(dir, e.name) const full = join(dir, e.name);
if (e.isDirectory()) getFiles(full, files) if (e.isDirectory()) getFiles(full, files);
else files.push(full) else files.push(full);
} }
return files return files;
} }
async function main() { async function main() {
// 1. Clean and copy src → dist // 1. Clean and copy src → dist
rmSync(distDir, { recursive: true, force: true }) rmSync(distDir, { recursive: true, force: true });
mkdirSync(distDir, { recursive: true }) mkdirSync(distDir, { recursive: true });
cpSync(srcDir, distDir, { recursive: true }) cpSync(srcDir, distDir, { recursive: true });
const distFiles = getFiles(distDir) const distFiles = getFiles(distDir);
// 2. Minify JS // 2. Minify JS
const jsFiles = distFiles.filter((f) => extname(f) === '.js') const jsFiles = distFiles.filter((f) => extname(f) === '.js');
for (const f of jsFiles) { for (const f of jsFiles) {
const code = readFileSync(f, 'utf8') const code = readFileSync(f, 'utf8');
const result = await minifyJs(code, { format: { comments: false } }) const result = await minifyJs(code, { format: { comments: false } });
if (result.code) writeFileSync(f, result.code) if (result.code) writeFileSync(f, result.code);
} }
// 3. Minify CSS // 3. Minify CSS
const cleanCss = new CleanCSS({ level: 2 }) const cleanCss = new CleanCSS({ level: 2 });
const cssFiles = distFiles.filter((f) => extname(f) === '.css') const cssFiles = distFiles.filter((f) => extname(f) === '.css');
for (const f of cssFiles) { for (const f of cssFiles) {
const code = readFileSync(f, 'utf8') const code = readFileSync(f, 'utf8');
const result = cleanCss.minify(code) const result = cleanCss.minify(code);
if (!result.errors.length) writeFileSync(f, result.styles) if (!result.errors.length) writeFileSync(f, result.styles);
} }
// 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI) // 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI)
const htmlFiles = distFiles.filter((f) => extname(f) === '.html') const htmlFiles = distFiles.filter((f) => extname(f) === '.html');
const beasties = new Beasties({ const beasties = new Beasties({
path: distDir, path: distDir,
preload: 'default', preload: 'default',
logLevel: 'warn' logLevel: 'warn',
}) });
for (const htmlFile of htmlFiles) { for (const htmlFile of htmlFiles) {
const html = readFileSync(htmlFile, 'utf8') const html = readFileSync(htmlFile, 'utf8');
const inlined = await beasties.process(html) const inlined = await beasties.process(html);
writeFileSync(htmlFile, inlined) writeFileSync(htmlFile, inlined);
} }
console.log('Build complete: dist/') console.log('Build complete: dist/');
} }
main().catch((err) => { main().catch((err) => {
console.error(err) console.error(err);
process.exit(1) process.exit(1);
}) });

View File

@@ -0,0 +1 @@
{}

View File

@@ -44,9 +44,53 @@ body {
} }
body { body {
display: flex;
align-items: center; align-items: center;
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: space-between;
}
.content {
display: flex;
flex: 1;
justify-content: center; 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 { h1 {
@@ -89,6 +133,42 @@ p {
padding-bottom: 2rem; 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-center {
text-align: center; text-align: center;
} }
@@ -158,9 +238,10 @@ td:first-child {
white-space: nowrap; white-space: nowrap;
} }
/* Tip colors chosen for WCAG 2.2 AAA (≥7:1 contrast) */
.tip { .tip {
background: #eef2ff; background: #eef2ff;
color: var(--accent-light); color: #3730a3;
border-radius: 0.7em; border-radius: 0.7em;
font-size: 0.98em; font-size: 0.98em;
padding: 0.48em 0.8em; padding: 0.48em 0.8em;
@@ -171,7 +252,7 @@ td:first-child {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.tip { .tip {
background: #232555; background: #232555;
color: #a5b4fc; color: #c7d2fe;
} }
} }
@@ -259,14 +340,6 @@ td:first-child {
color: var(--faq-a); color: var(--faq-a);
} }
.footer {
margin-top: 2.5em;
text-align: center;
color: #bbb;
font-size: 0.94em;
letter-spacing: 0.01em;
}
@media (width <= 600px) { @media (width <= 600px) {
.container { .container {
padding: 1.1rem 0.5rem 1rem; padding: 1.1rem 0.5rem 1rem;

View 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();
}
});
});

View File

@@ -0,0 +1,4 @@
(function () {
const year = new Date().getFullYear();
document.getElementById('current-year').textContent = `${year}`;
})();

View File

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

View File

@@ -11,6 +11,11 @@
src="/assets/js/ga-init.js" src="/assets/js/ga-init.js"
data-ga-id="G-NF64QMKWX6" data-ga-id="G-NF64QMKWX6"
></script> ></script>
<script
defer
src="https://analytics.mifi.holdings/script.js"
data-website-id="80f4013d-dd3f-4a10-af75-2e788090990d"
></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
@@ -39,15 +44,89 @@
href="/assets/images/apple-touch-icon.png" href="/assets/images/apple-touch-icon.png"
/> />
<link rel="stylesheet" href="/assets/css/site.css" /> <link rel="stylesheet" href="/assets/css/site.css" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "My email won't send?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Check that you're using your full email address for both incoming and outgoing username, and that the port is 587 or 465."
}
},
{
"@type": "Question",
"name": "SSL/TLS errors?",
"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> </head>
<body> <body>
<div class="container faq"> <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> <h1 class="text-center">Welcome to Email from mifi Ventures</h1>
<div class="intro"> <div class="intro">
<strong>Let&apos;s get your inbox ready! 📬</strong><br /> <strong>Let&apos;s get your inbox ready! 📬</strong><br />
<p> <p>
Friendly help for setting up your email—works with Outlook, Friendly help for setting up your email—works with
Apple Mail, Thunderbird, phones, and more. Outlook, Apple Mail, Thunderbird, phones, and more.
</p> </p>
</div> </div>
@@ -107,7 +186,12 @@
<div class="accordion" id="helpAccordion"> <div class="accordion" id="helpAccordion">
<!-- Outlook --> <!-- Outlook -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <button
class="accordion-trigger"
aria-expanded="false"
data-umami-event="accordion"
data-umami-event-label="microsoft outlook"
>
<span class="icon"></span> Microsoft Outlook <span class="icon"></span> Microsoft Outlook
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
@@ -115,8 +199,8 @@
<li>Go to <b>File → Add Account</b></li> <li>Go to <b>File → Add Account</b></li>
<li>Enter your full email address</li> <li>Enter your full email address</li>
<li> <li>
Choose <b>Advanced options</b> → check “Set up Choose <b>Advanced options</b> → check “Set
manually” up manually”
</li> </li>
<li>Select <b>IMAP</b> (recommended) or POP</li> <li>Select <b>IMAP</b> (recommended) or POP</li>
<li> <li>
@@ -127,7 +211,8 @@
<li> <li>
Outgoing server: Outgoing server:
<code>mail.mifi.holdings</code>, port <code>mail.mifi.holdings</code>, port
<b>587</b> (STARTTLS) or <b>465</b> (SSL/TLS) <b>587</b> (STARTTLS) or
<b>465</b> (SSL/TLS)
</li> </li>
<li> <li>
Username: full email address; Password: your Username: full email address; Password: your
@@ -136,15 +221,20 @@
<li>Click <b>Connect</b></li> <li>Click <b>Connect</b></li>
</ol> </ol>
<span class="tip" <span class="tip"
>If sending fails, make sure “Require logon using >If sending fails, make sure “Require logon
SPA” is <b>unchecked</b>.</span using SPA” is <b>unchecked</b>.</span
> >
</div> </div>
</section> </section>
<!-- Apple Mail --> <!-- Apple Mail -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <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) <span class="icon"></span> Apple Mail (macOS, iOS)
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
@@ -168,12 +258,19 @@
<!-- Thunderbird --> <!-- Thunderbird -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <button
class="accordion-trigger"
aria-expanded="false"
data-umami-event="accordion"
data-umami-event-label="thunderbird"
>
<span class="icon"></span> Thunderbird <span class="icon"></span> Thunderbird
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<ol> <ol>
<li>Menu → Account Settings → Add Mail Account</li> <li>
Menu → Account Settings → Add Mail Account
</li>
<li>Fill in your name, email, and password</li> <li>Fill in your name, email, and password</li>
<li> <li>
Click “Configure manually” and use settings Click “Configure manually” and use settings
@@ -185,21 +282,27 @@
<!-- Mobile --> <!-- Mobile -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <button
<span class="icon"></span> iOS / Android Mail / Gmail class="accordion-trigger"
App 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> </button>
<div class="accordion-content"> <div class="accordion-content">
<ul> <ul>
<li>Add Account → Other</li> <li>Add Account → Other</li>
<li>Enter your email and password</li> <li>Enter your email and password</li>
<li> <li>
Manual setup: <code>mail.mifi.holdings</code>, Manual setup:
correct ports, SSL/TLS required <code>mail.mifi.holdings</code>, correct
ports, SSL/TLS required
</li> </li>
<li> <li>
Gmail app: tap profile → Add account → Other, Gmail app: tap profile → Add account →
fill in details, use IMAP Other, fill in details, use IMAP
</li> </li>
</ul> </ul>
</div> </div>
@@ -207,20 +310,25 @@
<!-- FAQ --> <!-- FAQ -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <button
class="accordion-trigger"
aria-expanded="false"
data-umami-event="accordion"
data-umami-event-label="faq / troubleshooting"
>
<span class="icon"></span> FAQ / Troubleshooting <span class="icon"></span> FAQ / Troubleshooting
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<div class="faq-q">Q: My email wont send?</div> <div class="faq-q">Q: My email wont send?</div>
<div class="faq-a"> <div class="faq-a">
Check that youre using your full email address for Check that youre using your full email address
both incoming and outgoing username, and that the for both incoming and outgoing username, and
port is 587 or 465. that the port is 587 or 465.
</div> </div>
<div class="faq-q">Q: SSL/TLS errors?</div> <div class="faq-q">Q: SSL/TLS errors?</div>
<div class="faq-a"> <div class="faq-a">
Ensure SSL or STARTTLS is enabled for both incoming Ensure SSL or STARTTLS is enabled for both
and outgoing mail. incoming and outgoing mail.
</div> </div>
<div class="faq-q">Q: Still stuck?</div> <div class="faq-q">Q: Still stuck?</div>
<div class="faq-a"> <div class="faq-a">
@@ -228,30 +336,36 @@
<a href="mailto:postmaster@mifi.holdings" <a href="mailto:postmaster@mifi.holdings"
>postmaster@mifi.holdings</a >postmaster@mifi.holdings</a
>.<br /> >.<br />
Please include any error messages, your mail app, Please include any error messages, your mail
and a screenshot if you can! app, and a screenshot if you can!
</div> </div>
</div> </div>
</section> </section>
<!-- Pro Tips --> <!-- Pro Tips -->
<section class="accordion-section"> <section class="accordion-section">
<button class="accordion-trigger" aria-expanded="false"> <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 <span class="icon"></span> Pro Tips & Advanced
</button> </button>
<div class="accordion-content"> <div class="accordion-content">
<ul> <ul>
<li> <li>
<b>IMAP syncs</b> your mail everywhere—choose <b>IMAP syncs</b> your mail
IMAP unless you know you want POP3. everywhere—choose IMAP unless you know you
want POP3.
</li> </li>
<li> <li>
Your login is always your Your login is always your
<b>full email address</b>. <b>full email address</b>.
</li> </li>
<li> <li>
Check your Spam/Junk folder for misfiled good Check your Spam/Junk folder for misfiled
emails. good emails.
</li> </li>
<li> <li>
Advanced: IMAP path prefix = Advanced: IMAP path prefix =
@@ -262,56 +376,27 @@
</div> </div>
</section> </section>
</div> </div>
<div class="footer"> </main>
Email from mifi Ventures &middot; Help Page &ndash; &copy; 2025
mifi Ventures, LLC
</div> </div>
</div> <footer class="footer">
<script> Email from mifi Holdings &middot; Help Page &middot; &copy;
// Native accessible accordion 2025<span id="current-year"></span>
document.querySelectorAll('.accordion-trigger').forEach((btn) => { <a
btn.addEventListener('click', function () { href="https://mifi.ventures"
const section = btn.closest('.accordion-section') data-umami-event="footer link"
const expanded = data-umami-event-label="mifi ventures"
btn.getAttribute('aria-expanded') === 'true' >mifi Ventures, LLC</a
document >
.querySelectorAll('.accordion-section') </footer>
.forEach((s) => { <img
if (s === section) { src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
s.classList.toggle('open', !expanded) alt=""
btn.setAttribute( width="1"
'aria-expanded', height="1"
String(!expanded) role="presentation"
) loading="eager"
const content = btn.nextElementSibling />
content.style.maxHeight = !expanded <script defer src="/assets/js/current-year.js"></script>
? content.scrollHeight + 40 + 'px' <script defer src="/assets/js/accordion.js"></script>
: '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> </body>
</html> </html>

View File

@@ -11,6 +11,11 @@
src="/assets/js/ga-init.js" src="/assets/js/ga-init.js"
data-ga-id="G-NF64QMKWX6" data-ga-id="G-NF64QMKWX6"
></script> ></script>
<script
defer
src="https://analytics.mifi.holdings/script.js"
data-website-id="80f4013d-dd3f-4a10-af75-2e788090990d"
></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
@@ -39,9 +44,27 @@
href="/assets/images/apple-touch-icon.png" href="/assets/images/apple-touch-icon.png"
/> />
<link rel="stylesheet" href="/assets/css/site.css" /> <link rel="stylesheet" href="/assets/css/site.css" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "mifi Holdings Mail",
"url": "https://mail.mifi.holdings",
"description": "The mifi Holdings mail services hub—here you can find help with setting up your email, changing your password, and accessing webmail.",
"publisher": {
"@type": "Organization",
"name": "mifi Ventures",
"url": "https://mifi.ventures",
"email": "postmaster@mifi.holdings"
}
}
</script>
</head> </head>
<body> <body>
<div class="container text-center"> <a href="#main-content" class="skip-link">Skip to main content</a>
<div class="content">
<main id="main-content" class="container text-center">
<div class="emoji">📮</div> <div class="emoji">📮</div>
<h1>This is just a mailbox.</h1> <h1>This is just a mailbox.</h1>
<p> <p>
@@ -50,15 +73,52 @@
here—just some gears whirring and mail being sorted.<br /> here—just some gears whirring and mail being sorted.<br />
Looking for your messages? Looking for your messages?
</p> </p>
<a class="button" href="/help">Email Setup Help</a> <a
<a class="button" href="https://webmail.mifi.holdings" class="button"
>Go to Webmail</a href="/help"
data-umami-event="button"
data-umami-event-label="email setup help"
>Email Setup Help</a
> >
<a <a
class="button" class="button"
href="https://postmaster.mifi.holdings/users/login.php" href="https://webmail.mifi.holdings"
>Change/Forgot Password</a target="_blank"
data-umami-event="button"
data-umami-event-label="webmail"
> >
Go to Webmail
</a>
<a
class="button"
href="https://postmaster.mifi.holdings/users/login.php"
target="_blank"
data-umami-event="button"
data-umami-event-label="change/forgot password"
>
Change/Forgot Password
</a>
</main>
</div> </div>
<footer class="footer">
Email from mifi Holdings &middot; &copy; 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> </body>
</html> </html>

4
src/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://mail.mifi.holdings/sitemap.xml

13
src/sitemap.xml Normal file
View 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>

View File

@@ -2,7 +2,7 @@ export default {
extends: ['stylelint-config-standard'], extends: ['stylelint-config-standard'],
overrides: [ overrides: [
{ {
files: ['src/**/*.css'] files: ['src/**/*.css'],
} },
] ],
} };