Compare commits
1 Commits
develop
...
3bb6782d95
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb6782d95 |
@@ -1,4 +1,4 @@
|
|||||||
# CI workflow: one clone, one workspace — install → lint → build → unit test - e2e test.
|
# CI workflow: one clone, one workspace — install → lint → build → 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,86 +19,22 @@ 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: Send Test Build Status Notification (failure)
|
- name: test
|
||||||
image: curlimages/curl
|
image: node:20-alpine
|
||||||
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
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Test build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
- pnpm test
|
||||||
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: e2e test
|
- name: test e2e
|
||||||
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
|
||||||
@@ -107,45 +43,3 @@ 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]
|
|
||||||
|
|||||||
@@ -35,16 +35,12 @@ steps:
|
|||||||
- name: Send Build Status Notification (success)
|
- name: Send Build Status Notification (success)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Build success 🎉"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- 'Docker image build'
|
- 'Docker image build'
|
||||||
when:
|
when:
|
||||||
@@ -53,16 +49,12 @@ steps:
|
|||||||
- name: Send Build Status Notification (failure)
|
- name: Send Build Status Notification (failure)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Build failure 💩"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- Docker image build
|
- Docker image build
|
||||||
when:
|
when:
|
||||||
@@ -98,16 +90,12 @@ steps:
|
|||||||
- name: Send Push Status Notification (success)
|
- name: Send Push Status Notification (success)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry success 🎉"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- 'Push to registry'
|
- 'Push to registry'
|
||||||
when:
|
when:
|
||||||
@@ -116,16 +104,12 @@ steps:
|
|||||||
- name: Send Push Status Notification (failure)
|
- name: Send Push Status Notification (failure)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry failure 💩"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- 'Push to registry'
|
- 'Push to registry'
|
||||||
when:
|
when:
|
||||||
@@ -154,16 +138,12 @@ steps:
|
|||||||
- name: Send Deploy Status Notification (success)
|
- name: Send Deploy Status Notification (success)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy success 🎉"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- 'Trigger Portainer stack redeploy'
|
- 'Trigger Portainer stack redeploy'
|
||||||
when:
|
when:
|
||||||
@@ -172,16 +152,12 @@ steps:
|
|||||||
- name: Send Deploy Status Notification (failure)
|
- name: Send Deploy Status Notification (failure)
|
||||||
image: curlimages/curl
|
image: curlimages/curl
|
||||||
environment:
|
environment:
|
||||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
DISCORD_WEBHOOK_URL:
|
||||||
from_secret: mattermost_bot_access_token
|
from_secret: discord_webhook_url
|
||||||
MATTERMOST_CHANNEL_ID:
|
|
||||||
from_secret: mattermost_pushes_channel_id
|
|
||||||
MATTERMOST_POST_API_URL:
|
|
||||||
from_secret: mattermost_post_api_url
|
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy failure 💩"}' "$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
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
depends_on:
|
depends_on:
|
||||||
- Trigger Portainer stack redeploy
|
- Trigger Portainer stack redeploy
|
||||||
when:
|
when:
|
||||||
|
|||||||
75
AGENTS.md
75
AGENTS.md
@@ -1,75 +0,0 @@
|
|||||||
# 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`).
|
|
||||||
@@ -14,7 +14,6 @@ export default [
|
|||||||
'site/**',
|
'site/**',
|
||||||
'static/**',
|
'static/**',
|
||||||
'build.mjs',
|
'build.mjs',
|
||||||
'playwright-report/**',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|||||||
27
nginx.conf
27
nginx.conf
@@ -108,25 +108,6 @@ 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";
|
||||||
@@ -139,11 +120,7 @@ http {
|
|||||||
log_not_found off;
|
log_not_found off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Custom 404 page (for missing static assets; SPA routes still try index.html first via try_files)
|
# 404 falls back to index.html for SPA-style routing
|
||||||
error_page 404 /404.html;
|
error_page 404 /index.html;
|
||||||
location = /404.html {
|
|
||||||
add_header Cache-Control "no-cache, must-revalidate";
|
|
||||||
add_header Content-Type "text/html; charset=utf-8";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc",
|
"packageManager": "pnpm@10.28.2",
|
||||||
"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 && node scripts/copy-410-paths.mjs",
|
"build": "vite build && node scripts/critters.mjs && node scripts/minify-static-js.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",
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<!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. We’re not sure it’s coming back.</p>
|
|
||||||
<p><a href="/">Back to mifi Ventures →</a></p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<!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. We’ve moved on—and so should you.</p>
|
|
||||||
<p><a href="/">Back to mifi Ventures →</a></p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user