mifi a52938f6cf
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Mat now we will have interactivity?
2026-02-06 20:36:38 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 19:09:48 -03:00
2026-02-06 20:12:05 -03:00
2026-02-06 19:29:49 -03:00
2026-02-06 19:09:48 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 19:09:48 -03:00
2026-02-06 20:24:15 -03:00
2026-02-06 19:29:49 -03:00
2026-02-06 19:29:49 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 19:10:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 19:10:27 -03:00
2026-02-06 15:28:27 -03:00
2026-02-06 15:28:27 -03:00

mifi.dev landing

One-page static Linktree-style site for mifi.dev. Built with SvelteKit (static adapter), TypeScript, pnpm, PostCSS, and critical CSS.

Prerequisites

  • Node.js 22+
  • pnpm (enable via corepack enable && corepack prepare pnpm@latest --activate)

Quick start

Use the same Linux environment as CI so Playwright snapshots match.

  1. Open the repo in VS Code or Cursor.
  2. When prompted, click Reopen in Container (or run Dev Containers: Reopen in Container from the command palette).
  3. Wait for the container to build; postCreateCommand runs pnpm install and pnpm exec playwright install chromium --with-deps so dependencies and the Playwright browser are ready.
  4. Run the scripts below inside the container.

Option B: Local

pnpm install
pnpm dev          # dev server at http://localhost:5173
pnpm build        # output in build/
pnpm preview      # preview build at http://localhost:4173

Scripts

Script Description
pnpm dev:bio Start Vite dev server for mifi.bio
pnpm dev:dev Start Vite dev server for mifi.dev
pnpm build Build static site to build/
pnpm build:full Build + inline critical CSS (run pnpm run critical-css:install once to install Chromium)
pnpm critical-css:install Install Chromium for critical CSS (required once before first build:full)
pnpm preview Serve build/ locally
pnpm check Run svelte-kit sync and svelte-check
pnpm lint ESLint + Stylelint
pnpm format Prettier (write)
pnpm format:check Prettier (check only)
pnpm test Vitest (watch)
pnpm test:run Vitest (single run)
pnpm test:coverage Vitest with coverage
pnpm test:e2e Playwright e2e (starts preview, then runs tests)
pnpm test:e2e:ui Playwright e2e in UI mode

Project layout

  • src/ SvelteKit app (routes, layout, global CSS)
  • src/lib/data/ JSON content (e.g. links.json) loaded at build time
  • static/ Static assets
  • scripts/ Post-build scripts (e.g. critical CSS)
  • e2e/ Playwright e2e tests
  • build/ Output after pnpm build (gitignored)

Tooling

  • Package manager: pnpm
  • Language: TypeScript
  • Lint: ESLint, Stylelint
  • Format: Prettier
  • Unit tests: Vitest
  • E2E / visual regression: Playwright (use same Linux build in dev container and CI)
  • Critical CSS: Post-build step via critical (run pnpm run critical-css:install once, then pnpm build:full)

Build and run with Docker

One image contains both variants (mifi.dev and mifi.bio). The Dockerfile runs two SvelteKit builds (CONTENT_VARIANT=dev and CONTENT_VARIANT=bio) and nginx serves by host.

docker build -t mifi-links:local .
docker run --rm -p 8080:80 mifi-links:local

Then open http://localhost:8080 with Host: mifi.dev or Host: mifi.bio (e.g. add 127.0.0.1 mifi.dev to /etc/hosts and visit http://mifi.dev:8080).

For production, the Portainer stack uses the image from the package registry. To run the stack locally with the built image, use the same docker-compose.yml and either point it at your local tag or run docker compose build then docker compose up.

Deploy

  • Repo: git.mifi.dev/mifi-holdings/mifi-links
  • One image (both mifi.dev and mifi.bio); deploy via webhook to Portainer stack.
  • docker-compose.yml defines one service with nginx and Traefik labels for mifi.dev, www.mifi.dev, mifi.bio, and www.mifi.bio on network marina-net.

Fonts

Fonts live in static/assets/fonts/. The wordmark uses Plus Jakarta Sans (700). The self-hosted “latin” woff2 subsets omit ligature (GSUB) tables, so the fi ligature in “mifi” does not appear with them.

Current setup: Plus Jakarta Sans 700 is loaded from Google Fonts in the layout so the wordmark fi ligature works. All other fonts (Fraunces, Inter, etc.) are self-hosted.

To self-host the wordmark font with ligatures instead of Google Fonts:

  1. Download the font
    Google Fonts → Plus Jakarta Sans → “Download family” (ZIP). Unzip; use the Bold static file (e.g. PlusJakartaSans-Bold.ttf or PlusJakartaText-Bold.otf from the static folder). Or clone Tokotype/PlusJakartaSans and use fonts/ttf/PlusJakartaSans-Bold.ttf (or the OTF equivalent).

  2. Subset with ligatures (from the repo root, with fonttools installed: pip install fonttools brotli):

# Replace PATH_TO_BOLD with the path to the Bold TTF/OTF (e.g. ~/Downloads/Plus_Jakarta_Sans/static/PlusJakartaSans-Bold.ttf)
# Use --layout-features='*' to keep all layout features (including liga); or 'liga','clig'
pyftsubset PATH_TO_BOLD \
  --output-file=static/assets/fonts/plus-jakarta-sans-700-liga.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes='U+0020-007F,U+00A0-00FF,U+FB01,U+FB02'

Example if the ZIP is in your Downloads folder:

pyftsubset ~/Downloads/Plus_Jakarta_Sans/static/PlusJakartaSans-Bold.ttf \
  --output-file=static/assets/fonts/plus-jakarta-sans-700-liga.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes='U+0020-007F,U+00A0-00FF,U+FB01,U+FB02'
  1. Switch back to self-hosted
    Put the new woff2 in static/assets/fonts/. In src/lib/fonts.css, uncomment the Plus Jakarta Sans @font-face and set its url() to plus-jakarta-sans-700-liga.woff2. In src/routes/+layout.svelte, remove the Google Fonts <link> for Plus Jakarta Sans.

Fraunces variable (headings with opsz)

Headings use Fraunces with font-variation-settings: "opsz" 36. That only works with the variable font (opsz + wght axes). Static instances (e.g. fraunces-v38-latin-500.woff2) ignore opsz.

  1. Download the variable TTF
    Google Fonts → Fraunces → “Download family”. In the ZIP, use the file in the variable folder: Fraunces-VariableFont_opsz,wght.ttf (or similar name).

  2. Subset to woff2 (from repo root; pip install fonttools brotli):

# Replace PATH_TO_VARIANT with the path to Fraunces-VariableFont_opsz,wght.ttf
pyftsubset PATH_TO_VARIANT \
  --output-file=static/assets/fonts/fraunces-variable-opsz-wght.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes='U+0020-007F,U+00A0-00FF'

Example if the ZIP is in Downloads:

pyftsubset ~/Downloads/Fraunces/fraunces-variable-opsz-wght.ttf \
  --output-file=static/assets/fonts/fraunces-variable-opsz-wght.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes='U+0020-007F,U+00A0-00FF'
  1. Use it in the app
    In src/lib/fonts.css, replace the Fraunces @font-face with one that points at the variable woff2 and a font-weight range (e.g. 100 900) so the browser can use the wght axis. See the comment in fonts.css for the exact block.

CSP

CSP is set via Traefik middleware, not in app code.

Description
It's like Linktree or Gravatar's bio page, but of my own design and deployment.
Readme 11 MiB
Languages
Svelte 44.8%
TypeScript 33%
CSS 11.2%
JavaScript 9.3%
Dockerfile 1.4%
Other 0.3%