Compare commits

...

20 Commits

Author SHA1 Message Date
c963e34766 Proper 410/404 pages
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-16 01:18:38 -03:00
f91531b5fa Even stupider typo... I'm tired.
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-12 01:37:45 -03:00
b0146992c2 Silly secret typo
Some checks failed
ci/woodpecker/push/deploy unknown status
ci/woodpecker/push/ci Pipeline failed
2026-02-12 01:36:48 -03:00
7a01cbd2c9 Final notifications setup 2026-02-12 01:35:22 -03:00
fe8cf26a29 Channel ID typo?
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-12 01:22:39 -03:00
a519df1016 Stupid typo
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-12 01:14:01 -03:00
ceeb76663b Test with posts API and bot
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/deploy unknown status
2026-02-12 01:12:10 -03:00
14edd403eb Let the bot be the notifier and fix for missing svelte kit in unit tests
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-12 00:48:52 -03:00
9f43bf7879 One last tweak
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/deploy unknown status
2026-02-12 00:35:57 -03:00
2d0a4935a5 Pipeline fixes
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline failed
2026-02-12 00:32:52 -03:00
e4929a4699 Trigger pipelines
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/deploy unknown status
2026-02-12 00:17:00 -03:00
af705efc17 Pipeline fixes
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/deploy unknown status
2026-02-11 23:43:14 -03:00
f73b7822a5 Pipeline Notification Updates (swap Discord to Mattermost) 2026-02-11 23:40:39 -03:00
c094cc29ea TJX Logo removal (legal request) (#5) (#6)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Remove logo at request of TJX Companies

Reviewed-on: #5
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>

Reviewed-on: #6
2026-02-11 00:59:39 +00:00
4cfe2c5da0 Add pipeline notifications
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-07 12:08:33 -03:00
864c9a735c Minify JS and resolve accessibility issues (back to 100% in Lighthouse) (#4)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
Reviewed-on: #4
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>
2026-02-07 04:41:31 +00:00
11ff3dcff3 Minify JS output
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-07 00:46:35 -03:00
0f423f0677 Fix for deploy failure
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2026-02-06 19:51:04 -03:00
aaea4169f9 Tweaks to Icons and Such (#3)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline failed
Just some post launch cleanup

Reviewed-on: #3
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>
2026-02-06 22:40:45 +00:00
e76f0f79e8 Update snapshots and .well-known handling (#2)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
This should resolve a few issues...

Reviewed-on: #2
Co-authored-by: mifi <badmf@mifi.dev>
Co-committed-by: mifi <badmf@mifi.dev>
2026-02-02 13:51:57 +00:00
30 changed files with 991 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
# CI workflow: one clone, one workspace — install → lint → build → test. # CI workflow: one clone, one workspace — install → lint → build → unit test - e2e test.
# Runs on pull requests, push/tag/manual on main, or manual from any branch. # Runs on pull requests, push/tag/manual on main, or manual from any branch.
# Deploy workflow depends on this (ci) and runs only on main. # Deploy workflow depends on this (ci) and runs only on main.
when: when:
@@ -19,22 +19,86 @@ steps:
image: node:20-alpine image: node:20-alpine
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm run lint - pnpm run lint
- pnpm run lint:css - pnpm run lint:css
depends_on:
- install
- 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
when:
- status: [failure]
- name: unit test
image: node:20-alpine
commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm exec svelte-kit sync
- pnpm test
depends_on:
- lint
- name: Send Unit Test 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] Unit test 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:
- unit test
when:
- status: [failure]
- name: build - name: build
image: node:20-alpine image: node:20-alpine
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm run build - pnpm run build
depends_on:
- unit test
- name: test - name: Send Test Build Status Notification (failure)
image: node:20-alpine 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: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - |
- pnpm test BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Test 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: test e2e - name: e2e test
image: mcr.microsoft.com/playwright:v1.58.0-noble image: mcr.microsoft.com/playwright:v1.58.0-noble
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
@@ -43,3 +107,45 @@ steps:
- npx serve dist -p 4173 & - npx serve dist -p 4173 &
- sleep 2 - sleep 2
- CI=1 pnpm run test:e2e - CI=1 pnpm run test:e2e
depends_on:
- build
- name: Send E2E Test 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] E2E test 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:
- e2e test
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
- lint
- unit test
- build
- e2e test
when:
- status: [success]

View File

@@ -2,8 +2,10 @@
# Runs on push to main, tag, or manual (only when on main). # Runs on push to main, tag, or manual (only when on main).
# Waits for ci workflow (install → lint → build → test) to succeed first. # Waits for ci workflow (install → lint → build → test) to succeed first.
when: when:
branch: main - branch: main
event: [push, tag, manual] event: [push, tag, manual]
- event: deployment
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
depends_on: depends_on:
- ci - ci
@@ -12,6 +14,7 @@ steps:
- name: 'Docker image build' - name: 'Docker image build'
image: docker:latest image: docker:latest
environment: environment:
DOCKER_API_VERSION: '1.43'
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
@@ -29,9 +32,46 @@ steps:
. .
- echo "✓ Docker image built successfully" - echo "✓ Docker image built successfully"
- name: Send 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] 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:
- 'Docker image build'
when:
- status: [success]
- 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_pushes_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:
- Docker image build
when:
- status: [failure]
- name: 'Push to registry' - name: 'Push to registry'
image: docker:latest image: docker:latest
environment: environment:
DOCKER_API_VERSION: '1.43'
REGISTRY_URL: git.mifi.dev REGISTRY_URL: git.mifi.dev
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
REGISTRY_USERNAME: REGISTRY_USERNAME:
@@ -55,6 +95,42 @@ steps:
depends_on: depends_on:
- 'Docker image build' - 'Docker image build'
- name: Send Push 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 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]
- name: 'Trigger Portainer stack redeploy' - name: 'Trigger Portainer stack redeploy'
image: curlimages/curl:latest image: curlimages/curl:latest
environment: environment:
@@ -74,3 +150,39 @@ steps:
echo "✓ Portainer redeploy triggered (HTTP $code)" echo "✓ Portainer redeploy triggered (HTTP $code)"
depends_on: depends_on:
- 'Push to registry' - 'Push to registry'
- 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]

75
AGENTS.md Normal file
View File

@@ -0,0 +1,75 @@
# Agent guide: mifi Ventures landing
This file helps LLM agents work in this repo without introducing anti-patterns. Follow the architecture and conventions below.
## Purpose
- **Audience**: LLM agents (e.g. Cursor, Codex) making code or content changes.
- **Goal**: Preserve a minimal static site: SvelteKit prerender, no client-side app JS, shared theming, critical CSS inlining, and clear separation between app routes and static error pages.
## Stack and architecture
- **Framework**: SvelteKit with **adapter-static**. All routes are prerendered; there is no client-side router or hydration (`csr: false` in `src/routes/+layout.ts`).
- **Build**: `pnpm run build` = `vite build``node scripts/critters.mjs``node scripts/minify-static-js.mjs``node scripts/copy-410-paths.mjs`. Output is `dist/` (static files only).
- **Runtime**: nginx serves `dist/` (mounted as `/usr/share/nginx/html` in the container). No Node at runtime.
- **Theming**: CSS only. Light/dark follows **system preference** via `@media (prefers-color-scheme: dark)` in `src/app.css`. There is no JS theme toggle or `data-theme`; do not add one unless explicitly requested.
- **Fonts**: **Local only.** Inter and Fraunces are served from `static/assets/fonts/` (e.g. `inter-v20-latin-*.woff2`, `fraunces-v38-latin-*.woff2`). Preloads are in `src/routes/+layout.svelte`. Do not add Google Fonts or other external font URLs for the main site or error pages.
## Key paths
| Path | Role |
|------|------|
| `src/app.css` | Single global stylesheet: CSS variables (light/dark), base styles, components. Source of truth for theme tokens. |
| `src/app.html` | SvelteKit HTML shell. Rarely edited. |
| `src/routes/+layout.svelte` | Root layout: head (meta, fonts, favicon, scripts), skip link, slot. Imports `app.css`. |
| `src/routes/+layout.ts` | Exports `prerender = true`, `ssr = true`, `csr = false`. Do not enable CSR. |
| `src/routes/+page.svelte` | Home page; composes sections from `src/lib/components/`. |
| `src/lib/data/*.ts` | Content and meta (home-meta, content, experience, engagements, json-ld). Edit here for copy or SEO. |
| `src/lib/seo.ts` | SEO defaults (baseUrl, theme colors, etc.) and `mergeMeta()`. |
| `static/` | Copied as-is into `dist/` by SvelteKit. Favicon, robots.txt, fonts, images, **404.html**, **410.html**, and **assets/error-pages.css** live here. |
| `scripts/critters.mjs` | Post-build: inlines critical CSS into **every** `dist/*.html` (including 404 and 410). Resolves stylesheet URLs relative to `dist/`. |
| `scripts/minify-static-js.mjs` | Post-build: minifies JS in `dist/assets/`. |
| `scripts/copy-410-paths.mjs` | Post-build: copies `410.html` to each 410 URL path as `index.html` so static preview (e.g. `serve dist`) shows the 410 page at those URLs; nginx still returns 410 via explicit location blocks. |
| `nginx.conf` | Serves static files; `try_files $uri $uri/ /index.html` for SPA-style fallback; `error_page 404 /404.html` and `error_page 410 /410.html` for custom error pages. |
## Static error pages (404, 410)
- **Files**: `static/404.html`, `static/410.html`. They are **standalone HTML** (not Svelte). Do not convert them to Svelte routes.
- **Styling**: Both link to **one** shared stylesheet: `<link rel="stylesheet" href="/assets/error-pages.css">`. All error-page CSS lives in **`static/assets/error-pages.css`** (theme variables for light/dark, local `@font-face` for Inter/Fraunces, layout for `.error-page`). Do not duplicate theme tokens or add inline `<style>` in the HTML; keep a single source in `error-pages.css`.
- **Critical CSS**: The build runs Critters on all `dist/*.html`. Critters inlines critical CSS from linked stylesheets (including `/assets/error-pages.css`) into 404.html and 410.html. So:
- **Do** keep the `<link rel="stylesheet" href="/assets/error-pages.css">` in 404.html and 410.html; Critters will inline it at build time.
- **Do not** add error-page-only CSS in `src/app.css`; the app bundle is not loaded on error pages.
- **Theme alignment**: When changing light/dark colors or typography in `src/app.css`, update **`static/assets/error-pages.css`** so 404/410 stay visually consistent (same `--ep-*` tokens and, if needed, `@media (prefers-color-scheme: dark)`).
- **Local fonts**: Error pages use the same font paths as the main site (`/assets/fonts/...`) via `@font-face` in `error-pages.css`. Do not use Google Fonts or other external font URLs on error pages.
- **Preview vs production**: In preview (`serve dist`), the 410 URLs (e.g. `/pt/`, `/feed/`) are served by copying `410.html` to each path as `index.html` (see `scripts/copy-410-paths.mjs`). In production, nginx returns HTTP 410 for those paths and serves the same content via `error_page 410 /410.html`. If you add or remove 410 paths, update both `nginx.conf` and the `PATHS` array in `scripts/copy-410-paths.mjs`.
## Anti-patterns to avoid
1. **Enabling client-side rendering**
Do not set `csr: true` or add a client-side router. The site is intentionally static and JS-minimal.
2. **Adding app JavaScript for the main shell**
The only scripts are small, purposeful ones (e.g. `copyright-year.js`, `ga-init.js`, `mobile-menu-helper.js`) in `static/assets/js/`. Do not introduce a Svelte hydration bundle or large runtime for the main pages.
3. **External fonts**
Do not add `<link>` to Google Fonts (or similar) in layout or error pages. Use local fonts in `static/assets/fonts/` and reference them via preload (layout) or `@font-face` (error-pages.css).
4. **Skipping Critters for new HTML**
Any new `.html` in `static/` is copied to `dist/` and **must** be processed by Critters (the script already runs on all `dist/*.html`). Do not add static HTML that bypasses the build or that uses only inline styles without a linked stylesheet (linked styles get inlined by Critters).
5. **Diverging error page theme**
Do not change 404/410 styling in a way that ignores `static/assets/error-pages.css` or that duplicates theme tokens from `src/app.css` in ad-hoc form. Keep one error-page stylesheet and align its variables with `app.css` when you change the main theme.
6. **Breaking static export**
Do not add routes or behavior that require server-side rendering at request time (e.g. dynamic routes without prerender). The app is fully prerendered and served as static files.
7. **Scattering SEO or theme defaults**
Keep SEO defaults and theme-color values in `src/lib/seo.ts` and in layout/error pages that need them. Do not duplicate or hardcode them in many places.
## Quick reference
- **Change copy or structure (home)**: `src/lib/data/*.ts`, `src/lib/components/*.svelte`, `src/routes/+page.svelte`.
- **Change global styles or theme**: `src/app.css`. Then sync **`static/assets/error-pages.css`** if tokens or dark mode change.
- **Change error page copy or structure**: `static/404.html`, `static/410.html`. Style changes: **`static/assets/error-pages.css`** only.
- **Add a new static HTML page**: Add it under `static/`, link to `/assets/error-pages.css` (or a dedicated stylesheet that Critters can inline). Ensure `scripts/critters.mjs` runs over all `dist/*.html` (it already does).
- **Change nginx behavior**: `nginx.conf` (e.g. cache headers, `error_page`, `try_files`).

View File

@@ -14,6 +14,7 @@ export default [
'site/**', 'site/**',
'static/**', 'static/**',
'build.mjs', 'build.mjs',
'playwright-report/**',
], ],
}, },
js.configs.recommended, js.configs.recommended,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -108,14 +108,42 @@ http {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Deny access to hidden files (.git, .env, etc.) # 410 Gone: permanently removed URLs (tells crawlers to deindex)
error_page 410 /410.html;
location = /410.html {
add_header Cache-Control "no-cache, must-revalidate";
add_header Content-Type "text/html; charset=utf-8";
}
location = /2024/02/18/hello-world/ { return 410; }
location = /2024/02/18/hello-world { return 410; }
location = /pt/ { return 410; }
location = /pt { return 410; }
location = /feed/ { return 410; }
location = /feed { return 410; }
location = /category/uncategorized/feed/ { return 410; }
location = /category/uncategorized/feed { return 410; }
location = /category/uncategorized/ { return 410; }
location = /category/uncategorized { return 410; }
location = /comments/feed/ { return 410; }
location = /comments/feed { return 410; }
# Allow .well-known (security.txt, ACME challenge, etc.)
location ^~ /.well-known/ {
add_header Cache-Control "public, max-age=86400";
}
# Deny access to other hidden files (.git, .env, etc.)
location ~ /\. { location ~ /\. {
deny all; deny all;
access_log off; access_log off;
log_not_found off; log_not_found off;
} }
# 404 falls back to index.html for SPA-style routing # Custom 404 page (for missing static assets; SPA routes still try index.html first via try_files)
error_page 404 /index.html; error_page 404 /404.html;
location = /404.html {
add_header Cache-Control "no-cache, must-revalidate";
add_header Content-Type "text/html; charset=utf-8";
}
} }
} }

View File

@@ -3,11 +3,11 @@
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"repository": "https://git.mifi.dev/mifi-ventures/landing.git", "repository": "https://git.mifi.dev/mifi-ventures/landing.git",
"packageManager": "pnpm@10.28.2", "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc",
"description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining", "description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "vite build && node scripts/critters.mjs", "build": "vite build && node scripts/critters.mjs && node scripts/minify-static-js.mjs && node scripts/copy-410-paths.mjs",
"dev": "vite dev", "dev": "vite dev",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -34,6 +34,7 @@
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0", "@typescript-eslint/parser": "^8.54.0",
"critters": "^0.0.24", "critters": "^0.0.24",
"esbuild": "^0.24.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",

View File

@@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ export default defineConfig({
testDir: 'tests', testDir: 'tests',
testMatch: /.*\.spec\.(ts|js)/,
fullyParallel: true, fullyParallel: true,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,

261
pnpm-lock.yaml generated
View File

@@ -39,6 +39,9 @@ importers:
critters: critters:
specifier: ^0.0.24 specifier: ^0.0.24
version: 0.0.24 version: 0.0.24
esbuild:
specifier: ^0.24.0
version: 0.24.2
eslint: eslint:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2 version: 9.39.2
@@ -453,126 +456,252 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.4 postcss: ^8.4
'@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
'@esbuild/android-arm64@0.24.2':
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.12': '@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@esbuild/android-arm@0.24.2':
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.12': '@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@esbuild/android-x64@0.24.2':
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.12': '@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@esbuild/darwin-arm64@0.24.2':
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.12': '@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@esbuild/darwin-x64@0.24.2':
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12': '@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@esbuild/freebsd-arm64@0.24.2':
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.12': '@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
'@esbuild/freebsd-x64@0.24.2':
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12': '@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@esbuild/linux-arm64@0.24.2':
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.12': '@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@esbuild/linux-arm@0.24.2':
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.12': '@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@esbuild/linux-ia32@0.24.2':
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.12': '@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
'@esbuild/linux-loong64@0.24.2':
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.12': '@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
'@esbuild/linux-mips64el@0.24.2':
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.12': '@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
'@esbuild/linux-ppc64@0.24.2':
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.12': '@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@esbuild/linux-riscv64@0.24.2':
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12': '@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@esbuild/linux-s390x@0.24.2':
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.12': '@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@esbuild/linux-x64@0.24.2':
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.12': '@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@esbuild/netbsd-arm64@0.24.2':
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.25.12': '@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [netbsd] os: [netbsd]
'@esbuild/netbsd-x64@0.24.2':
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12': '@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
'@esbuild/openbsd-arm64@0.24.2':
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.25.12': '@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [openbsd] os: [openbsd]
'@esbuild/openbsd-x64@0.24.2':
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12': '@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -585,24 +714,48 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [openharmony] os: [openharmony]
'@esbuild/sunos-x64@0.24.2':
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.12': '@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
'@esbuild/win32-arm64@0.24.2':
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.12': '@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@esbuild/win32-ia32@0.24.2':
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.12': '@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@esbuild/win32-x64@0.24.2':
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1376,6 +1529,11 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esbuild@0.24.2:
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.12: esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3022,81 +3180,156 @@ snapshots:
dependencies: dependencies:
postcss: 8.5.6 postcss: 8.5.6
'@esbuild/aix-ppc64@0.24.2':
optional: true
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
optional: true optional: true
'@esbuild/android-arm64@0.24.2':
optional: true
'@esbuild/android-arm64@0.25.12': '@esbuild/android-arm64@0.25.12':
optional: true optional: true
'@esbuild/android-arm@0.24.2':
optional: true
'@esbuild/android-arm@0.25.12': '@esbuild/android-arm@0.25.12':
optional: true optional: true
'@esbuild/android-x64@0.24.2':
optional: true
'@esbuild/android-x64@0.25.12': '@esbuild/android-x64@0.25.12':
optional: true optional: true
'@esbuild/darwin-arm64@0.24.2':
optional: true
'@esbuild/darwin-arm64@0.25.12': '@esbuild/darwin-arm64@0.25.12':
optional: true optional: true
'@esbuild/darwin-x64@0.24.2':
optional: true
'@esbuild/darwin-x64@0.25.12': '@esbuild/darwin-x64@0.25.12':
optional: true optional: true
'@esbuild/freebsd-arm64@0.24.2':
optional: true
'@esbuild/freebsd-arm64@0.25.12': '@esbuild/freebsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/freebsd-x64@0.24.2':
optional: true
'@esbuild/freebsd-x64@0.25.12': '@esbuild/freebsd-x64@0.25.12':
optional: true optional: true
'@esbuild/linux-arm64@0.24.2':
optional: true
'@esbuild/linux-arm64@0.25.12': '@esbuild/linux-arm64@0.25.12':
optional: true optional: true
'@esbuild/linux-arm@0.24.2':
optional: true
'@esbuild/linux-arm@0.25.12': '@esbuild/linux-arm@0.25.12':
optional: true optional: true
'@esbuild/linux-ia32@0.24.2':
optional: true
'@esbuild/linux-ia32@0.25.12': '@esbuild/linux-ia32@0.25.12':
optional: true optional: true
'@esbuild/linux-loong64@0.24.2':
optional: true
'@esbuild/linux-loong64@0.25.12': '@esbuild/linux-loong64@0.25.12':
optional: true optional: true
'@esbuild/linux-mips64el@0.24.2':
optional: true
'@esbuild/linux-mips64el@0.25.12': '@esbuild/linux-mips64el@0.25.12':
optional: true optional: true
'@esbuild/linux-ppc64@0.24.2':
optional: true
'@esbuild/linux-ppc64@0.25.12': '@esbuild/linux-ppc64@0.25.12':
optional: true optional: true
'@esbuild/linux-riscv64@0.24.2':
optional: true
'@esbuild/linux-riscv64@0.25.12': '@esbuild/linux-riscv64@0.25.12':
optional: true optional: true
'@esbuild/linux-s390x@0.24.2':
optional: true
'@esbuild/linux-s390x@0.25.12': '@esbuild/linux-s390x@0.25.12':
optional: true optional: true
'@esbuild/linux-x64@0.24.2':
optional: true
'@esbuild/linux-x64@0.25.12': '@esbuild/linux-x64@0.25.12':
optional: true optional: true
'@esbuild/netbsd-arm64@0.24.2':
optional: true
'@esbuild/netbsd-arm64@0.25.12': '@esbuild/netbsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/netbsd-x64@0.24.2':
optional: true
'@esbuild/netbsd-x64@0.25.12': '@esbuild/netbsd-x64@0.25.12':
optional: true optional: true
'@esbuild/openbsd-arm64@0.24.2':
optional: true
'@esbuild/openbsd-arm64@0.25.12': '@esbuild/openbsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/openbsd-x64@0.24.2':
optional: true
'@esbuild/openbsd-x64@0.25.12': '@esbuild/openbsd-x64@0.25.12':
optional: true optional: true
'@esbuild/openharmony-arm64@0.25.12': '@esbuild/openharmony-arm64@0.25.12':
optional: true optional: true
'@esbuild/sunos-x64@0.24.2':
optional: true
'@esbuild/sunos-x64@0.25.12': '@esbuild/sunos-x64@0.25.12':
optional: true optional: true
'@esbuild/win32-arm64@0.24.2':
optional: true
'@esbuild/win32-arm64@0.25.12': '@esbuild/win32-arm64@0.25.12':
optional: true optional: true
'@esbuild/win32-ia32@0.24.2':
optional: true
'@esbuild/win32-ia32@0.25.12': '@esbuild/win32-ia32@0.25.12':
optional: true optional: true
'@esbuild/win32-x64@0.24.2':
optional: true
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
optional: true optional: true
@@ -3826,6 +4059,34 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
esbuild@0.24.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.2
'@esbuild/android-arm': 0.24.2
'@esbuild/android-arm64': 0.24.2
'@esbuild/android-x64': 0.24.2
'@esbuild/darwin-arm64': 0.24.2
'@esbuild/darwin-x64': 0.24.2
'@esbuild/freebsd-arm64': 0.24.2
'@esbuild/freebsd-x64': 0.24.2
'@esbuild/linux-arm': 0.24.2
'@esbuild/linux-arm64': 0.24.2
'@esbuild/linux-ia32': 0.24.2
'@esbuild/linux-loong64': 0.24.2
'@esbuild/linux-mips64el': 0.24.2
'@esbuild/linux-ppc64': 0.24.2
'@esbuild/linux-riscv64': 0.24.2
'@esbuild/linux-s390x': 0.24.2
'@esbuild/linux-x64': 0.24.2
'@esbuild/netbsd-arm64': 0.24.2
'@esbuild/netbsd-x64': 0.24.2
'@esbuild/openbsd-arm64': 0.24.2
'@esbuild/openbsd-x64': 0.24.2
'@esbuild/sunos-x64': 0.24.2
'@esbuild/win32-arm64': 0.24.2
'@esbuild/win32-ia32': 0.24.2
'@esbuild/win32-x64': 0.24.2
esbuild@0.25.12: esbuild@0.25.12:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12 '@esbuild/aix-ppc64': 0.25.12

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env node
/**
* Post-build: copy 410.html to each 410-Gone URL path as index.html.
* So static preview (serve dist) shows the 410 page at those URLs.
* nginx still returns 410 for these paths via explicit location blocks.
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DIST = path.join(__dirname, '..', 'dist');
const SOURCE = path.join(DIST, '410.html');
const PATHS = [
'2024/02/18/hello-world',
'pt',
'feed',
'category/uncategorized/feed',
'category/uncategorized',
'comments/feed',
];
function main() {
if (!fs.existsSync(SOURCE)) {
console.error('dist/410.html not found. Run build first.');
process.exit(1);
}
const content = fs.readFileSync(SOURCE, 'utf8');
for (const dir of PATHS) {
const dirPath = path.join(DIST, dir);
fs.mkdirSync(dirPath, { recursive: true });
fs.writeFileSync(path.join(dirPath, 'index.html'), content, 'utf8');
}
console.log('✓ 410 page copied to', PATHS.length, 'paths for preview.');
}
main();

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Post-build: minify all JS in dist/assets/js/ (static scripts copied from static/assets/js/).
* Runs after vite build (and optionally after critters). Uses esbuild for minification.
*/
import * as esbuild from 'esbuild';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const JS_DIR = path.join(ROOT, 'dist', 'assets', 'js');
async function main() {
if (!fs.existsSync(JS_DIR)) {
console.warn('dist/assets/js/ not found; skipping static JS minify.');
return;
}
const files = fs.readdirSync(JS_DIR).filter((f) => f.endsWith('.js'));
if (files.length === 0) {
console.warn('No .js files in dist/assets/js/; skipping.');
return;
}
for (const file of files) {
const filePath = path.join(JS_DIR, file);
const code = fs.readFileSync(filePath, 'utf8');
const result = await esbuild.transform(code, {
minify: true,
target: 'es2015',
});
fs.writeFileSync(filePath, result.code, 'utf8');
console.log('✓ Minified dist/assets/js/' + file);
}
console.log('Static JS minify complete.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -31,6 +31,8 @@ else
echo "Updating snapshots in the current environment (matches CI when using the devcontainer)." echo "Updating snapshots in the current environment (matches CI when using the devcontainer)."
echo "" echo ""
pnpm run build
# Unset CI so Playwright config starts the preview server # Unset CI so Playwright config starts the preview server
unset CI unset CI
pnpm exec playwright test --update-snapshots pnpm exec playwright test --update-snapshots

View File

@@ -8,12 +8,10 @@
aria-labelledby="experience-heading" aria-labelledby="experience-heading"
> >
<div class="container"> <div class="container">
<h2 id="experience-heading" class="section-title"> <h2 id="experience-heading" class="section-title">Previously at:</h2>
Experience includes teams at:
</h2>
<div class="logo-strip" role="list" aria-label="Company logos"> <div class="logo-strip" role="list" aria-label="Company logos">
{#each experienceLogos as logo (logo.alt)} {#each experienceLogos.filter((logo) => logo.showLogo) as logo (logo.alt)}
<div class="logo-item" role="listitem"> <div class="logo-item" role="listitem">
<img <img
src={logo.src} src={logo.src}

View File

@@ -14,20 +14,27 @@
<span class="mobile nav-header-logo"> <span class="mobile nav-header-logo">
<Wordmark /> <Wordmark />
</span> </span>
<label <button
id="nav-toggle-label" type="button"
for="nav-toggle" id="nav-toggle-button"
class="nav-toggle" class="btn-clear"
aria-controls="nav-menu" aria-controls="nav-menu"
aria-label="Toggle navigation" aria-label="Toggle navigation"
aria-expanded="false" aria-expanded="false"
> >
<span class="nav-toggle-inner"> <label
<span class="nav-toggle-line"></span> id="nav-toggle-label"
<span class="nav-toggle-line"></span> for="nav-toggle"
<span class="nav-toggle-line"></span> class="nav-toggle"
</span> role="presentation"
</label> >
<span class="nav-toggle-inner">
<span class="nav-toggle-line"></span>
<span class="nav-toggle-line"></span>
<span class="nav-toggle-line"></span>
</span>
</label>
</button>
</div> </div>
<div id="nav-menu" class="nav-menu container"> <div id="nav-menu" class="nav-menu container">
<span class="nav-header-logo desktop"> <span class="nav-header-logo desktop">
@@ -149,6 +156,14 @@
} }
} }
.btn-clear {
background: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}
.nav-toggle { .nav-toggle {
display: none; display: none;
align-items: center; align-items: center;

View File

@@ -1,25 +1,52 @@
export const experienceLogos = [ export const experienceLogos = [
{ src: '/assets/logos/atlassian.svg', alt: 'Atlassian', width: 2500, height: 2500 }, {
src: '/assets/logos/atlassian.svg',
alt: 'Atlassian',
width: 2500,
height: 2500,
showLogo: true,
},
{ {
src: '/assets/logos/tjx.svg', src: '/assets/logos/tjx.svg',
alt: 'TJ Maxx (The TJX Companies)', alt: 'TJ Maxx (The TJX Companies)',
width: 2500, width: 2500,
height: 621, height: 621,
showLogo: false,
},
{
src: '/assets/logos/cargurus.svg',
alt: 'CarGurus',
width: 2500,
height: 398,
showLogo: true,
},
{
src: '/assets/logos/timberland.svg',
alt: 'Timberland',
width: 190,
height: 35,
showLogo: true,
},
{
src: '/assets/logos/vf.svg',
alt: 'VF Corporation',
width: 190,
height: 155,
showLogo: true,
}, },
{ src: '/assets/logos/cargurus.svg', alt: 'CarGurus', width: 2500, height: 398 },
{ src: '/assets/logos/timberland.svg', alt: 'Timberland', width: 190, height: 35 },
{ src: '/assets/logos/vf.svg', alt: 'VF Corporation', width: 190, height: 155 },
{ {
src: '/assets/logos/bottomline.svg', src: '/assets/logos/bottomline.svg',
alt: 'Bottomline Technologies', alt: 'Bottomline Technologies',
width: 2702, width: 2702,
height: 571, height: 571,
showLogo: true,
}, },
{ {
src: '/assets/logos/mfa-boston.svg', src: '/assets/logos/mfa-boston.svg',
alt: 'Museum of Fine Arts Boston', alt: 'Museum of Fine Arts Boston',
width: 572, width: 572,
height: 88, height: 88,
showLogo: true,
}, },
] as const; ] as const;

View File

@@ -132,7 +132,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/favicon.svg" /> <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
{#if jsonLdHtml} {#if jsonLdHtml}
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD --> <!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD -->

20
static/404.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 Not Found — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/error-pages.css">
</head>
<body class="error-page">
<main>
<div class="emoji" aria-hidden="true">🔍</div>
<h1>404 Not Found</h1>
<p>This page went off to find itself. Were not sure its coming back.</p>
<p><a href="/">Back to mifi Ventures →</a></p>
</main>
</body>
</html>

20
static/410.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>410 Gone — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/error-pages.css">
</head>
<body class="error-page">
<main>
<div class="emoji" aria-hidden="true">👋</div>
<h1>410 Gone</h1>
<p>This page has left the building. Weve moved on—and so should you.</p>
<p><a href="/">Back to mifi Ventures →</a></p>
</main>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,102 @@
/**
* Shared styles for static error pages (404, 410).
* Uses same theme tokens and local fonts as the main site.
* Linked from 404.html and 410.html; Critters inlines critical CSS at build time.
*/
/* Theme: light (default) — matches src/app.css :root */
:root {
color-scheme: light dark;
--ep-bg: #ffffff;
--ep-bg-alt: #faf9ff;
--ep-text: #14121a;
--ep-text-secondary: #3f3a4a;
--ep-primary: #6d28d9;
--ep-primary-hover: #5b21b6;
--ep-font: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--ep-font-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
--ep-size-base: 18px;
--ep-line-height: 1.75;
}
/* Theme: dark — matches src/app.css @media (prefers-color-scheme: dark) */
@media (prefers-color-scheme: dark) {
:root {
--ep-bg: #0b0b12;
--ep-bg-alt: #121226;
--ep-text: #f3f2ff;
--ep-text-secondary: #c9c6e4;
--ep-primary: #a78bfa;
--ep-primary-hover: #c4b5fd;
}
}
/* Local fonts — same paths as +layout.svelte preloads */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-600.woff2') format('woff2');
}
/* Error page layout */
.error-page {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--ep-font);
font-size: var(--ep-size-base);
line-height: var(--ep-line-height);
color: var(--ep-text);
background-color: var(--ep-bg-alt);
}
.error-page main {
text-align: center;
padding: 2rem 1.5rem;
max-width: 28rem;
}
.error-page .emoji {
font-size: 4rem;
margin-bottom: 0.5rem;
}
.error-page h1 {
font-family: var(--ep-font-heading);
font-size: 2rem;
font-weight: 600;
margin: 0 0 0.75rem;
color: var(--ep-text);
}
.error-page p {
margin: 0 0 1.5rem;
color: var(--ep-text-secondary);
}
.error-page a {
color: var(--ep-primary);
font-weight: 500;
text-decoration: none;
}
.error-page a:hover {
color: var(--ep-primary-hover);
text-decoration: underline;
}
.error-page a:focus-visible {
outline: 2px solid var(--ep-primary);
outline-offset: 2px;
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 2134 2134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Z" style="fill:#0b0b0f;"/><g><path d="M178.684,1755.333l0,-916.655l208.714,0l0,211.244l-24.034,-34.153c16.304,-66.339 49.965,-115.812 100.984,-148.419c51.019,-32.607 110.822,-48.911 179.41,-48.911c74.772,0 140.899,19.466 198.384,58.398c57.484,38.932 94.659,90.724 111.525,155.376l-63.247,5.481c28.391,-73.928 70.625,-128.953 126.704,-165.074c56.079,-36.121 120.801,-54.181 194.167,-54.181c64.933,0 122.98,14.617 174.139,43.851c51.16,29.234 91.567,69.852 121.223,121.855c29.656,52.003 44.483,112.157 44.483,180.464l0,590.724l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.137,0 -66.128,8.082 -92.973,24.245c-26.845,16.163 -47.646,38.791 -62.403,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.418,0 -66.479,8.082 -93.183,24.245c-26.704,16.163 -47.435,38.791 -62.193,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1733.286,1755.333l0,-916.655l221.363,0l0,916.655l-221.363,0Zm0,-1020.379l0,-236.121l221.363,0l0,236.121l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,8 +1,12 @@
const MOBILE_BREAKPOINT_PX = 768; const MOBILE_BREAKPOINT_PX = 768;
/** All focusable elements inside the menu (links). */
const getMenuFocusables = (menu) =>
menu.querySelectorAll('a[href]');
const mobileMenuHelper = () => { const mobileMenuHelper = () => {
const mobileMenu = document.getElementById('nav-menu'); const mobileMenu = document.getElementById('nav-menu');
const mobileMenuToggleLabel = document.getElementById('nav-toggle-label'); const mobileMenuToggleButton = document.getElementById('nav-toggle-button');
const mobileMenuToggle = document.querySelector('.nav-toggle-input'); const mobileMenuToggle = document.querySelector('.nav-toggle-input');
const menuItems = mobileMenu.querySelectorAll('.nav-item'); const menuItems = mobileMenu.querySelectorAll('.nav-item');
@@ -10,35 +14,75 @@ const mobileMenuHelper = () => {
const syncMenuAriaHidden = () => { const syncMenuAriaHidden = () => {
if (isMobile()) { if (isMobile()) {
mobileMenu.setAttribute('aria-hidden', mobileMenuToggle.checked ? 'false' : 'true'); const hidden = !mobileMenuToggle.checked;
mobileMenu.setAttribute(
'aria-hidden',
hidden ? 'true' : 'false',
);
// inert removes the subtree from the a11y tree and makes descendants non-focusable
if (hidden) {
mobileMenu.setAttribute('inert', '');
} else {
mobileMenu.removeAttribute('inert');
}
setMenuFocusablesTabIndex();
} else { } else {
mobileMenu.removeAttribute('aria-hidden'); mobileMenu.removeAttribute('aria-hidden');
mobileMenu.removeAttribute('inert');
getMenuFocusables(mobileMenu).forEach((el) => {
el.removeAttribute('tabindex');
});
} }
}; };
const syncLabelAriaExpanded = () => { const setMenuFocusablesTabIndex = () => {
mobileMenuToggleLabel.setAttribute('aria-expanded', mobileMenuToggle.checked ? 'true' : 'false'); const focusables = getMenuFocusables(mobileMenu);
const hidden = !mobileMenuToggle.checked;
focusables.forEach((el) => {
if (hidden) {
el.setAttribute('tabindex', '-1');
} else {
el.removeAttribute('tabindex');
}
});
};
const syncButtonAriaExpanded = () => {
mobileMenuToggleButton.setAttribute(
'aria-expanded',
mobileMenuToggle.checked ? 'true' : 'false',
);
}; };
menuItems.forEach((item) => { menuItems.forEach((item) => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
mobileMenuToggle.checked = false; mobileMenuToggle.checked = false;
syncLabelAriaExpanded(); syncButtonAriaExpanded();
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
}); });
mobileMenuToggle.addEventListener('change', () => { mobileMenuToggle.addEventListener('change', () => {
syncLabelAriaExpanded(); syncButtonAriaExpanded();
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
mobileMenuToggleButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
mobileMenuToggle.checked = !mobileMenuToggle.checked;
// Programmatic .checked change does not fire 'change'; sync state so menu is focusable when open
syncButtonAriaExpanded();
syncMenuAriaHidden();
}
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
syncMenuAriaHidden(); syncMenuAriaHidden();
syncLabelAriaExpanded(); syncButtonAriaExpanded();
}; };
document.addEventListener('DOMContentLoaded', mobileMenuHelper); document.addEventListener('DOMContentLoaded', mobileMenuHelper);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,15 +1,9 @@
<svg width="100%" height="100%" viewBox="0 0 2134 2134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> <svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<style> <style>
.bg { fill: #0b0b0f; } .block { fill: #0b0b0f; }
.fg { fill: #fff; } @media (prefers-color-scheme: dark) {
@media (prefers-color-scheme: dark) { .block { fill: #f2f2f2; }
.bg { fill: #f2f2f2; } }
.fg { fill: #0b0b0f; } </style>
} <path class="block" d="M512,96l0,320c0,52.984 -43.016,96 -96,96l-320,0c-52.984,0 -96,-43.016 -96,-96l0,-320c0,-52.984 43.016,-96 96,-96l320,0c52.984,0 96,43.016 96,96Zm-96.011,80.389l53.127,0l0,-56.669l-53.127,0l0,56.669Zm-193.658,55.292c-4.819,-8.296 -11.558,-15.376 -20.217,-21.24c-13.796,-9.344 -29.667,-14.015 -47.612,-14.015c-16.461,0 -30.814,3.913 -43.058,11.739c-7.882,5.038 -14.038,11.753 -18.468,20.146l0,-27.027l-50.091,0l0,219.997l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.517,-12.413 14.926,-16.292c6.409,-3.879 13.864,-5.819 22.364,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.534,-12.413 14.977,-16.292c6.443,-3.879 13.881,-5.819 22.313,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-141.774c0,-16.394 -3.559,-30.831 -10.676,-43.311c-7.117,-12.481 -16.815,-22.229 -29.093,-29.245c-12.278,-7.016 -26.209,-10.524 -41.793,-10.524c-17.608,0 -33.141,4.335 -46.6,13.004c-8.624,5.555 -15.884,12.972 -21.779,22.252Zm193.658,189.599l53.127,0l0,-219.997l-53.127,0l0,219.997Z" />
</style> </svg>
<path class="bg" d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Z"/>
<g>
<path class="fg" d="M434.867,1513.667l0,-652.2l148.5,0l0,150.3l-17.1,-24.3c11.6,-47.2 35.55,-82.4 71.85,-105.6c36.3,-23.2 78.85,-34.8 127.65,-34.8c53.2,0 100.25,13.85 141.15,41.55c40.9,27.7 67.35,64.55 79.35,110.55l-45,3.9c20.2,-52.6 50.25,-91.75 90.15,-117.45c39.9,-25.7 85.95,-38.55 138.15,-38.55c46.2,0 87.5,10.4 123.9,31.2c36.4,20.8 65.15,49.7 86.25,86.7c21.1,37 31.65,79.8 31.65,128.4l0,420.3l-157.5,0l0,-382.8c0,-29 -5.2,-53.8 -15.6,-74.4c-10.4,-20.6 -24.9,-36.65 -43.5,-48.15c-18.6,-11.5 -40.8,-17.25 -66.6,-17.25c-25,0 -47.05,5.75 -66.15,17.25c-19.1,11.5 -33.9,27.6 -44.4,48.3c-10.5,20.7 -15.75,45.45 -15.75,74.25l0,382.8l-157.5,0l0,-382.8c0,-29 -5.2,-53.8 -15.6,-74.4c-10.4,-20.6 -24.9,-36.65 -43.5,-48.15c-18.6,-11.5 -40.8,-17.25 -66.6,-17.25c-25.2,0 -47.3,5.75 -66.3,17.25c-19,11.5 -33.75,27.6 -44.25,48.3c-10.5,20.7 -15.75,45.45 -15.75,74.25l0,382.8l-157.5,0Z" style="fill-rule:nonzero;"/>
<path class="fg" d="M1540.967,1513.667l0,-652.2l157.5,0l0,652.2l-157.5,0Zm0,-726l0,-168l157.5,0l0,168l-157.5,0Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -29,11 +29,17 @@ function createNavDOM() {
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'mobile-nav-header'; header.className = 'mobile-nav-header';
const toggleButton = document.createElement('button');
toggleButton.type = 'button';
toggleButton.id = 'nav-toggle-button';
toggleButton.setAttribute('aria-controls', 'nav-menu');
toggleButton.setAttribute('aria-expanded', 'false');
const label = document.createElement('label'); const label = document.createElement('label');
label.id = 'nav-toggle-label'; label.id = 'nav-toggle-label';
label.htmlFor = 'nav-toggle'; label.htmlFor = 'nav-toggle';
label.className = 'nav-toggle'; label.className = 'nav-toggle';
header.appendChild(label); toggleButton.appendChild(label);
header.appendChild(toggleButton);
nav.appendChild(header); nav.appendChild(header);
const menu = document.createElement('div'); const menu = document.createElement('div');
@@ -53,7 +59,7 @@ function createNavDOM() {
nav.appendChild(menu); nav.appendChild(menu);
document.body.appendChild(nav); document.body.appendChild(nav);
return { nav, menu, label, checkbox, item }; return { nav, menu, toggleButton, label, checkbox, item, link };
} }
describe('mobile-menu-helper.js', () => { describe('mobile-menu-helper.js', () => {
@@ -75,7 +81,12 @@ describe('mobile-menu-helper.js', () => {
it('sets aria-hidden on menu when viewport is mobile and menu is closed', () => { it('sets aria-hidden on menu when viewport is mobile and menu is closed', () => {
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.label.getAttribute('aria-expanded')).toBe('false'); expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false');
});
it('sets inert and tabindex="-1" on links when menu is closed on mobile', () => {
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
it('sets aria-hidden to false when menu is open on mobile', () => { it('sets aria-hidden to false when menu is open on mobile', () => {
@@ -83,35 +94,55 @@ describe('mobile-menu-helper.js', () => {
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true })); dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.menu.getAttribute('aria-hidden')).toBe('false'); expect(dom.menu.getAttribute('aria-hidden')).toBe('false');
expect(dom.label.getAttribute('aria-expanded')).toBe('true'); expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true');
}); });
it('removes aria-hidden from menu when viewport is desktop', () => { it('removes inert and link tabindex when menu is open on mobile', () => {
dom.checkbox.checked = true;
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.menu.hasAttribute('inert')).toBe(false);
expect(dom.link.hasAttribute('tabindex')).toBe(false);
});
it('removes aria-hidden and inert from menu when viewport is desktop', () => {
setViewportWidth(1024); setViewportWidth(1024);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false); expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
expect(dom.menu.hasAttribute('inert')).toBe(false);
expect(dom.link.hasAttribute('tabindex')).toBe(false);
}); });
it('adds aria-hidden when resizing from desktop to mobile', () => { it('adds aria-hidden and inert when resizing from desktop to mobile', () => {
setViewportWidth(1024); setViewportWidth(1024);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false); expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
expect(dom.menu.hasAttribute('inert')).toBe(false);
setViewportWidth(400); setViewportWidth(400);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
it('closes menu and syncs aria when a menu item is clicked', () => { it('closes menu and syncs aria when a menu item is clicked', () => {
dom.checkbox.checked = true; dom.checkbox.checked = true;
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true })); dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.label.getAttribute('aria-expanded')).toBe('true'); expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true');
dom.item.click(); dom.item.click();
expect(dom.checkbox.checked).toBe(false); expect(dom.checkbox.checked).toBe(false);
expect(dom.label.getAttribute('aria-expanded')).toBe('false'); expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false');
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
// Keyboard open (Enter/Space on toggle button) is not asserted here: jsdoms KeyboardEvent
// often does not set e.key, so the keydown handler may not run. Opening and sync are covered
// by “sets aria-hidden to false when menu is open” and “removes inert and link tabindex when
// menu is open”; keyboard open can be covered in e2e.
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

@@ -5,6 +5,12 @@ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
server: { server: {
host: true, // listen on 0.0.0.0 so reachable from host (e.g. dev container, port forward) host: true, // listen on 0.0.0.0 so reachable from host (e.g. dev container, port forward)
port: 5173,
strictPort: false,
// Disable HMR WebSocket when using port forwarding (e.g. dev container); the tunnel
// often doesn't proxy WebSockets, causing repeated connection failures in the console.
// With csr: false we don't use client-side HMR anyway—refresh the page to see changes.
hmr: false,
}, },
preview: { preview: {
host: true, host: true,