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 (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 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 run critical-css:installonce, thenpnpm 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.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.
Trusted Types: This app is not compatible with require-trusted-types-for 'script'. Svelte’s runtime assigns to DOM sinks (e.g. innerHTML) during hydration, which that directive blocks. The Traefik middleware used for mifi.dev/mifi.bio must not include require-trusted-types-for 'script' (or the site will break with "This assignment requires a TrustedHTML").