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
Option A: Dev container (recommended)
Use the same Linux environment as CI so Playwright snapshots match.
- Open the repo in VS Code or Cursor.
- When prompted, click Reopen in Container (or run Dev Containers: Reopen in Container from the command palette).
- Wait for the container to build; postCreateCommand runs
pnpm installandpnpm exec playwright install chromium --with-depsso dependencies and the Playwright browser are ready. - 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 (requires Chromium: pnpm exec puppeteer browsers install chromium) |
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 timestatic/– Static assetsscripts/– Post-build scripts (e.g. critical CSS)e2e/– Playwright e2e testsbuild/– Output afterpnpm 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(runpnpm build:fullwith Chromium installed)
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.ymldefines one service with nginx and Traefik labels formifi.dev,www.mifi.dev,mifi.bio, andwww.mifi.bioon networkmarina-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:
-
Download the font
Google Fonts → Plus Jakarta Sans → “Download family” (ZIP). Unzip; use the Bold static file (e.g.PlusJakartaSans-Bold.ttforPlusJakartaText-Bold.otffrom thestaticfolder). Or clone Tokotype/PlusJakartaSans and usefonts/ttf/PlusJakartaSans-Bold.ttf(or the OTF equivalent). -
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'
- Switch back to self-hosted
Put the new woff2 instatic/assets/fonts/. Insrc/lib/fonts.css, uncomment the Plus Jakarta Sans@font-faceand set itsurl()toplus-jakarta-sans-700-liga.woff2. Insrc/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.
-
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). -
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'
- Use it in the app
Insrc/lib/fonts.css, replace the Fraunces@font-facewith one that points at the variable woff2 and afont-weightrange (e.g. 100 900) so the browser can use the wght axis. See the comment infonts.cssfor the exact block.
CSP
CSP is set via Traefik middleware, not in app code.