# mifi.dev landing One-page static Linktree-style site for [mifi.dev](https://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. 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 ```bash 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 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 build:full` with 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. ```bash 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](https://fonts.google.com/specimen/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](https://github.com/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`): ```bash # 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: ```bash 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' ``` 3. **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 `` 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](https://fonts.google.com/specimen/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`): ```bash # 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: ```bash 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' ``` 3. **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.