Compare commits

...

14 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
14 changed files with 479 additions and 42 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

@@ -35,12 +35,16 @@ steps:
- name: Send Build Status Notification (success) - name: Send Build Status Notification (success)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Build success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- 'Docker image build' - 'Docker image build'
when: when:
@@ -49,12 +53,16 @@ steps:
- name: Send Build Status Notification (failure) - name: Send Build Status Notification (failure)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Build failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- Docker image build - Docker image build
when: when:
@@ -90,12 +98,16 @@ steps:
- name: Send Push Status Notification (success) - name: Send Push Status Notification (success)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- 'Push to registry' - 'Push to registry'
when: when:
@@ -104,12 +116,16 @@ steps:
- name: Send Push Status Notification (failure) - name: Send Push Status Notification (failure)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- 'Push to registry' - 'Push to registry'
when: when:
@@ -138,12 +154,16 @@ steps:
- name: Send Deploy Status Notification (success) - name: Send Deploy Status Notification (success)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- 'Trigger Portainer stack redeploy' - 'Trigger Portainer stack redeploy'
when: when:
@@ -152,12 +172,16 @@ steps:
- name: Send Deploy Status Notification (failure) - name: Send Deploy Status Notification (failure)
image: curlimages/curl image: curlimages/curl
environment: environment:
DISCORD_WEBHOOK_URL: MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: discord_webhook_url 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: commands:
- | - |
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER") 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" "$DISCORD_WEBHOOK_URL" curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on: depends_on:
- Trigger Portainer stack redeploy - Trigger Portainer stack redeploy
when: when:

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,

View File

@@ -108,6 +108,25 @@ http {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# 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.) # Allow .well-known (security.txt, ACME challenge, etc.)
location ^~ /.well-known/ { location ^~ /.well-known/ {
add_header Cache-Control "public, max-age=86400"; add_header Cache-Control "public, max-age=86400";
@@ -120,7 +139,11 @@ http {
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 && node scripts/minify-static-js.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",

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

@@ -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

@@ -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;

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>

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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

After

Width:  |  Height:  |  Size: 577 KiB