diff --git a/.env.example b/.env.example
index 87d5aef..59fa437 100644
--- a/.env.example
+++ b/.env.example
@@ -1,46 +1,24 @@
-# Web3Forms access keys.
+# Web3Forms access key.
#
-# These values are PUBLISHABLE, not secret. They carry the NEXT_PUBLIC_ prefix,
-# which means Next inlines them into the client bundle at build time, and anyone
-# viewing source on the deployed site can read them. Keeping them out of the
-# source tree is hygiene, not confidentiality: it stops the repository shipping a
-# ready-made endpoint, and it keeps one environment's configuration out of
-# another's. It does not make the values private, and nothing here should be
-# described as though it does.
+# Generate at https://web3forms.com. The key belongs to one Web3Forms form, and
+# that form's recipient mailbox is set in its dashboard.
#
-# What that means in practice:
+# Set this in Vercel's Environment Variables (Production + Preview) and in a
+# local .env.local for development. Never commit a real key.
#
-# - The realistic risk is spam and quota consumption, not disclosure.
-# - Web3Forms offers a domain restriction under Settings > Security Settings.
-# It is enforced on a request header that any client can set, so it raises
-# the effort required and does not prevent abuse. Set it; do not rely on it.
-# - There is NO way to rotate a key. The access key is the form identifier.
-# Deleting the form is the only way to invalidate one, and that almost
-# certainly destroys the form's submission history, so export first.
-# - Values are inlined at BUILD time. Changing one in the hosting dashboard
-# does nothing to a deployment that already exists. Redeploy, or the old
-# value keeps being served.
-# - Paste carefully. A stray leading tab or trailing newline travels into the
-# bundle with the key, and the service then rejects it as a malformed UUID.
-#
-# Each key belongs to one Web3Forms form, and a form's recipient mailbox is set
-# in its dashboard, so routing enquiries to different mailboxes means one form
-# per mailbox.
+# NOTE: NEXT_PUBLIC_ values are inlined at BUILD time. Adding or changing one
+# after a deployment requires a redeploy before it takes effect. Paste the value
+# carefully — a stray leading tab or trailing newline makes Web3Forms reject
+# the key as a malformed UUID.
-# The fallback for every route below. Note that once all seven per-mailbox keys
-# are set, nothing reads this one, so a fault in it is invisible to testing.
+# Required. Used by every form on the site: /contact, /enquiries, and the
+# testimonial form on /testimonials.
+# Recipient: contact@invisionsolutions.co.uk
NEXT_PUBLIC_WEB3FORMS_KEY=
-# Per-mailbox keys used by /enquiries. Any left unset fall back to the key above.
-NEXT_PUBLIC_WEB3FORMS_KEY_SALES=
-NEXT_PUBLIC_WEB3FORMS_KEY_CONTACT=
-NEXT_PUBLIC_WEB3FORMS_KEY_SUPPORT=
-NEXT_PUBLIC_WEB3FORMS_KEY_BILLING=
-NEXT_PUBLIC_WEB3FORMS_KEY_INVOICE=
-NEXT_PUBLIC_WEB3FORMS_KEY_ADMIN=
-
-# Used by /enquiries and, read directly, by the testimonial form on /testimonials.
-NEXT_PUBLIC_WEB3FORMS_KEY_HELLO=
-
-# /contact reads NEXT_PUBLIC_WEB3FORMS_KEY_CONTACT, the same key as the General
-# enquiry desk, so the form it submits to is exercised on every /enquiries test.
+# There were previously seven per-mailbox keys (NEXT_PUBLIC_WEB3FORMS_KEY_SALES,
+# _CONTACT, _SUPPORT, _BILLING, _INVOICE, _ADMIN, _HELLO), one per Web3Forms
+# form. Those forms have been deleted from the account and nothing in the code
+# reads those variables any more. Delete them from Vercel too: a variable
+# naming a form that no longer exists is the failure that took /contact down in
+# August, and leaving them set invites someone to wire one back up.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d47f89..a6becde 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -60,31 +60,6 @@ jobs:
- name: Build
run: npm run build
- # The brand assets in public/brand/ are generated, committed, and served
- # to the public from /brand. Nothing verified that the generator still
- # produced them until this step existed.
- #
- # That gap is not hypothetical. `npm run brand` was broken on main and
- # stayed broken through two merges, because no job ran it: the generator
- # located its webfont by CSS *filename*, and Next emits one hash-named
- # stylesheet carrying every family. A manual run days later is what
- # surfaced it.
- #
- # `git diff --exit-code` is the assertion. Regenerating is not enough on
- # its own: the generator can run to completion and still emit different
- # bytes, which is exactly the risk when the imaging library underneath it
- # moves a major version.
- - name: Brand assets reproduce byte-identically
- run: |
- npm run brand
- git diff --exit-code public/brand/ || {
- echo "::error::Regenerating produced different bytes from the committed brand assets."
- echo "Either the generator changed behaviour, or a dependency under it did."
- echo "If the change is intended, commit the regenerated files in the same PR."
- git diff --stat public/brand/
- exit 1
- }
-
secrets:
name: Secret scan
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 32dcb5a..1e9c247 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,10 @@ Open [http://localhost:3000](http://localhost:3000).
| `npm run start` | Serve the production build |
| `npm run lint` | ESLint |
| `npm run check:install-scripts` | Fail if a dependency ships an install script not on the allowlist |
-| `npm run brand` | Regenerate the downloadable brand assets (run `build` first) |
## Environment variables
-Copy `.env.example` to `.env.local` and fill in the Web3Forms access keys:
+Copy `.env.example` to `.env.local` and fill in the Web3Forms access key:
```bash
cp .env.example .env.local
@@ -34,6 +33,10 @@ Web3Forms access keys are **publishable, not secret**. They are prefixed `NEXT_P
Deployment and mailbox configuration is documented privately.
+There is one Web3Forms form and one key. `/contact`, `/enquiries` and the testimonial form on `/testimonials` all submit with `NEXT_PUBLIC_WEB3FORMS_KEY`, which delivers to `contact@invisionsolutions.co.uk`. Each form sets its own subject line, so mail rules can still tell submissions apart.
+
+This was previously seven forms and eight keys, one per department mailbox. Those forms have been deleted from the Web3Forms account, and nothing reads the old `NEXT_PUBLIC_WEB3FORMS_KEY_*` variables any more. Delete them from the hosting dashboard too: a variable naming a form that no longer exists is the fault that stopped `/contact` delivering in August, and leaving them set invites someone to wire one back up.
+
### When a submission fails
Three unrelated causes used to produce one identical message, which made a failure report carry no information. The form now names the cause on screen, so a screenshot is enough to tell them apart without asking anyone to open a browser console:
@@ -51,22 +54,15 @@ The mark is a cloud drawn as one continuous line: the stroke traces the silhouet
There is **one version and no variants**. No dark colourway, no inverted form, and no heavier stand-in at small sizes. The tile, favicon and app icon all carry the same artwork on a white field. On anything other than white or near-white, use the white tile rather than recolouring the mark.
-Geometry and colour roles live in `src/lib/brand.ts` and are mirrored in `scripts/generate-brand-assets.mjs`. Change both together.
+Geometry and colour roles live in `src/lib/brand.ts`, which is the single source for the mark. `src/components/logo.tsx`, `src/app/icon.tsx` and `src/app/opengraph-image.tsx` all draw from it.
The mark and the wordmark are trademarked material. See `LICENSE.md`.
-### Regenerating the downloadable files
-
-```bash
-npm run build # must run first; the generator reads Next's webfont files
-npm run brand
-```
-
-This writes 15 files to `public/brand/`, surfaced on `/brand`. Wordmark type is converted to outlines so the SVGs render correctly without Space Grotesk installed.
+There is no public brand-assets page and no downloadable logo files. There was a `/brand` route serving 15 generated files from `public/brand/`, produced by `scripts/generate-brand-assets.mjs`; the page, the files and the generator have all been removed. Send anyone who needs artwork a file directly.
### Two things worth knowing before editing the lockup
-**The descriptor is width-matched.** "SOLUTIONS" is tracked so it spans exactly the width of "INVISION" above it. Size is the lever, not tracking: at 25% of the wordmark it would need 1.28em of tracking to reach that width, which scatters the letters, so it sits at 45% and needs 0.4615em. The generator re-solves the value from real font metrics rather than reading the constant, so the two cannot drift apart. Both figures are recorded in `WORDMARK` in `src/lib/brand.ts`.
+**The descriptor is width-matched.** "SOLUTIONS" is tracked so it spans exactly the width of "INVISION" above it. Size is the lever, not tracking: at 25% of the wordmark it would need 1.28em of tracking to reach that width, which scatters the letters, so it sits at 45% and needs 0.4615em. Both figures are recorded in `WORDMARK` in `src/lib/brand.ts`. They were previously re-solved from real font metrics by the generator, which no longer exists, so the constants are now the only record — check them against rendered output if the wordmark is ever retyped.
**`src/app/_fonts/` exists for the Open Graph image.** `next/font` ships Space Grotesk as a single variable file covering all three weights, and Satori does not apply variable axes, so `ImageResponse` silently falls back to a generic sans. Those three static font instances are loaded explicitly in `src/app/opengraph-image.tsx` to stop that happening. Do not delete them.
@@ -82,14 +78,14 @@ HSTS is sent with `max-age` and `includeSubDomains` but deliberately **without `
Routes, one folder per page under `src/app/`:
-`/` · `/services` · `/approach` · `/case-studies` · `/testimonials` · `/enquiries` · `/contact` · `/brand` · `/payreckon`
+`/` · `/services` · `/approach` · `/case-studies` · `/testimonials` · `/enquiries` · `/contact` · `/payreckon`
- `src/app/icon.tsx`, `src/app/opengraph-image.tsx`: favicon and social card, generated at build time
- `src/app/globals.css`: design tokens (colour, font, focus states) as CSS custom properties, mapped into Tailwind's `@theme`
- `src/components/*`: header, footer, logo, surface primitives, verification-ledger motif, scroll-reveal wrapper, social icons, Web3Forms form
- `src/lib/brand.ts`: mark geometry, colour roles, wordmark metrics
- `src/lib/content.ts`: services and case study copy, shared between the Home teasers and the full pages
-- `src/lib/mailboxes.ts`: the department mailboxes behind `/enquiries`
+- `src/lib/mailboxes.ts`: the single Web3Forms access key and the addresses the site publishes
- `src/lib/metadata.ts`: canonical URL and per-page metadata helper
- `src/lib/payreckon.ts`: PayReckon product copy, with sourcing rules in the file header
diff --git a/package-lock.json b/package-lock.json
index c8a1993..8aeee21 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,11 +21,8 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.3.3",
- "fontkit": "^2.0.4",
- "sharp": "^0.35.4",
"tailwindcss": "^4",
- "typescript": "^5",
- "wawoff2": "^2.0.1"
+ "typescript": "^5"
}
},
"node_modules/@alloc/quick-lru": {
@@ -297,7 +294,6 @@
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -529,8 +525,8 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"engines": {
"node": ">=18"
}
@@ -542,7 +538,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -565,7 +560,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -585,7 +579,6 @@
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
"integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -605,7 +598,6 @@
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
- "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
@@ -625,7 +617,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -642,7 +633,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -659,7 +649,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -676,7 +665,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -693,7 +681,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -710,7 +697,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -727,7 +713,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -744,7 +729,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -761,7 +745,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -778,7 +761,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -795,7 +777,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -818,7 +799,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -841,7 +821,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -864,7 +843,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -887,7 +865,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -910,7 +887,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -933,7 +909,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -956,7 +931,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -979,7 +953,6 @@
"cpu": [
"wasm32"
],
- "dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
@@ -996,7 +969,6 @@
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
- "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
@@ -1016,7 +988,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1036,7 +1007,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1056,7 +1026,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2693,27 +2662,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
"node_modules/baseline-browser-mapping": {
"version": "2.11.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.11.tgz",
@@ -2750,16 +2698,6 @@
"node": ">=8"
}
},
- "node_modules/brotli": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
- "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.1.2"
- }
- },
"node_modules/browserslist": {
"version": "4.28.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
@@ -2897,16 +2835,6 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
- "node_modules/clone": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
- "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3095,13 +3023,6 @@
"node": ">=8"
}
},
- "node_modules/dfa": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
- "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3911,24 +3832,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/fontkit": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
- "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@swc/helpers": "^0.5.12",
- "brotli": "^1.3.2",
- "clone": "^2.1.2",
- "dfa": "^1.2.0",
- "fast-deep-equal": "^3.1.3",
- "restructure": "^3.0.0",
- "tiny-inflate": "^1.0.3",
- "unicode-properties": "^1.4.0",
- "unicode-trie": "^2.0.0"
- }
- },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -5716,13 +5619,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/pako": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
- "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5990,13 +5886,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
- "node_modules/restructure": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
- "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -6156,8 +6045,8 @@
"version": "0.35.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
- "devOptional": true,
"license": "Apache-2.0",
+ "optional": true,
"dependencies": {
"@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
@@ -6206,8 +6095,8 @@
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "devOptional": true,
"license": "ISC",
+ "optional": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -6551,13 +6440,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/tiny-inflate": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
- "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -6819,28 +6701,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/unicode-properties": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
- "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.0",
- "unicode-trie": "^2.0.0"
- }
- },
- "node_modules/unicode-trie": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
- "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "pako": "^0.2.5",
- "tiny-inflate": "^1.0.0"
- }
- },
"node_modules/unrs-resolver": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
@@ -6920,20 +6780,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/wawoff2": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz",
- "integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "woff2_compress.js": "bin/woff2_compress.js",
- "woff2_decompress.js": "bin/woff2_decompress.js"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 393db94..2ca4cc5 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
- "check:install-scripts": "node scripts/check-install-scripts.mjs",
- "brand": "node scripts/generate-brand-assets.mjs"
+ "check:install-scripts": "node scripts/check-install-scripts.mjs"
},
"dependencies": {
"lucide-react": "^1.28.0",
@@ -24,10 +23,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.3.3",
- "fontkit": "^2.0.4",
- "sharp": "^0.35.4",
"tailwindcss": "^4",
- "typescript": "^5",
- "wawoff2": "^2.0.1"
+ "typescript": "^5"
}
}
diff --git a/public/brand/invision-favicon.svg b/public/brand/invision-favicon.svg
deleted file mode 100644
index 08d2855..0000000
--- a/public/brand/invision-favicon.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
\ No newline at end of file
diff --git a/public/brand/invision-icon-1024.png b/public/brand/invision-icon-1024.png
deleted file mode 100644
index b1409a0..0000000
Binary files a/public/brand/invision-icon-1024.png and /dev/null differ
diff --git a/public/brand/invision-icon-2048.png b/public/brand/invision-icon-2048.png
deleted file mode 100644
index 177faff..0000000
Binary files a/public/brand/invision-icon-2048.png and /dev/null differ
diff --git a/public/brand/invision-icon-256.png b/public/brand/invision-icon-256.png
deleted file mode 100644
index 38a7c1f..0000000
Binary files a/public/brand/invision-icon-256.png and /dev/null differ
diff --git a/public/brand/invision-icon-512.png b/public/brand/invision-icon-512.png
deleted file mode 100644
index ba91ba0..0000000
Binary files a/public/brand/invision-icon-512.png and /dev/null differ
diff --git a/public/brand/invision-icon-square-1024.png b/public/brand/invision-icon-square-1024.png
deleted file mode 100644
index 606935f..0000000
Binary files a/public/brand/invision-icon-square-1024.png and /dev/null differ
diff --git a/public/brand/invision-icon-square-256.png b/public/brand/invision-icon-square-256.png
deleted file mode 100644
index d513946..0000000
Binary files a/public/brand/invision-icon-square-256.png and /dev/null differ
diff --git a/public/brand/invision-icon-square-512.png b/public/brand/invision-icon-square-512.png
deleted file mode 100644
index f40005d..0000000
Binary files a/public/brand/invision-icon-square-512.png and /dev/null differ
diff --git a/public/brand/invision-icon-square.svg b/public/brand/invision-icon-square.svg
deleted file mode 100644
index 08d2855..0000000
--- a/public/brand/invision-icon-square.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
\ No newline at end of file
diff --git a/public/brand/invision-icon.svg b/public/brand/invision-icon.svg
deleted file mode 100644
index 4fbafc4..0000000
--- a/public/brand/invision-icon.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/public/brand/invision-logo-2048.png b/public/brand/invision-logo-2048.png
deleted file mode 100644
index 5e25c94..0000000
Binary files a/public/brand/invision-logo-2048.png and /dev/null differ
diff --git a/public/brand/invision-logo-4096.png b/public/brand/invision-logo-4096.png
deleted file mode 100644
index f5e4708..0000000
Binary files a/public/brand/invision-logo-4096.png and /dev/null differ
diff --git a/public/brand/invision-logo.svg b/public/brand/invision-logo.svg
deleted file mode 100644
index 7e28af9..0000000
--- a/public/brand/invision-logo.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/public/brand/invision-wordmark-2048.png b/public/brand/invision-wordmark-2048.png
deleted file mode 100644
index 4a336d4..0000000
Binary files a/public/brand/invision-wordmark-2048.png and /dev/null differ
diff --git a/public/brand/invision-wordmark.svg b/public/brand/invision-wordmark.svg
deleted file mode 100644
index 9db5fbf..0000000
--- a/public/brand/invision-wordmark.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/scripts/check-install-scripts.mjs b/scripts/check-install-scripts.mjs
index 55f20c9..2fd6ac6 100644
--- a/scripts/check-install-scripts.mjs
+++ b/scripts/check-install-scripts.mjs
@@ -31,19 +31,17 @@ const HOOKS = ["preinstall", "install", "postinstall"];
* `verified` records how that was established, because reading a script's name
* is not verification.
*/
+// sharp was listed here while the tree carried sharp 0.34.x, which shipped an
+// install script (install/check.js, deciding whether to build against a global
+// libvips). 0.35.x dropped that script entirely, so from the 0.34.5 -> 0.35.4
+// bump onward there has been nothing to permit. sharp is still installed, as an
+// optional dependency of next; it simply no longer runs anything on install.
+//
+// The entry is removed rather than kept "just in case". This is a permit list:
+// an unused permit is not harmless, because if a future sharp reintroduces an
+// install script it should fail this check and be re-reviewed, not sail through
+// on a rationale written for a script that no longer exists.
const ALLOWED = {
- sharp: {
- reason:
- "Arrives as a dependency of next, which uses it for image optimisation. " +
- "The install script checks whether a system-wide libvips should be used " +
- "and, if so, exits non-zero to trigger a source build. On a normal " +
- "install it is a no-op and the prebuilt platform binary is used instead, " +
- "so skipping it costs nothing.",
- verified:
- "Read node_modules/sharp/install/check.js. It requires ../lib/libvips, " +
- "calls useGlobalLibvips(), and exits 1 only when a global libvips or " +
- "npm_config_build_from_source is present. It writes nothing.",
- },
"unrs-resolver": {
reason:
"Development only. Arrives via eslint-config-next through " +
diff --git a/scripts/generate-brand-assets.mjs b/scripts/generate-brand-assets.mjs
deleted file mode 100644
index 7da2084..0000000
--- a/scripts/generate-brand-assets.mjs
+++ /dev/null
@@ -1,303 +0,0 @@
-/**
- * Generates the downloadable brand assets in public/brand/.
- *
- * Run with: npm run brand
- *
- * Wordmark type is converted to outlines rather than left as
- Every Invision Solutions logo file, ready to download. SVG scales to
- any size without losing quality — use it wherever it is accepted. The
- PNGs are transparent and go up to 4096px for print and large
- displays.
-
- The mark is a cloud drawn as one continuous line. The stroke traces
- the silhouette in gold, runs back along the base, then turns inward
- in deep gold and stops. The outward pass is the platform — the
- estate running somewhere you cannot point at. The return is the
- consultant coming back through it. One line, two tones, no second
- object.
-
- There is one version and no variants. Where the mark cannot sit on
- white — an app icon, an avatar, a dark surface — the white tile
- carries it, rather than the artwork being recoloured or swapped for
- a heavier one.
-
- Mark and wordmark together. The default choice wherever there is room
- for it.
-
- The mark on its own — app icons, favicons, social avatars, and
- anywhere the full logo would be too small to read.
-
- The full name set as type, for places that already carry the mark —
- letterheads, invoice footers, email signatures.
-
- Gold changes between uses — the bright tone lacks contrast as text on
- a light background, so the deeper one is used there.
-
- Brand assets
-
-
- Full logo
-
-
- Icon
-
-
- Wordmark
-
-
- Colours
-
-
- Using the logo
-
-
-
-
- Have something more specific — pricing, invoicing, contracts, or - support on live work?{" "} - - Send it to the right desk - {" "} - and it will reach the correct mailbox first time. + {PUBLIC_CONTACT_ADDRESS} + + .
diff --git a/src/app/enquiries/page.tsx b/src/app/enquiries/page.tsx index 6eebf2d..c8e0c9c 100644 --- a/src/app/enquiries/page.tsx +++ b/src/app/enquiries/page.tsx @@ -2,13 +2,17 @@ import type { Metadata } from "next"; import { pageMetadata } from "@/lib/metadata"; import { Reveal } from "@/components/reveal"; import { Web3Form, type Field } from "@/components/web3forms"; -import { Section, Eyebrow, SurfaceCard } from "@/components/ui"; -import { MAILBOXES } from "@/lib/mailboxes"; +import { Section, Eyebrow } from "@/components/ui"; +import { + CONTACT_ACCESS_KEY, + CONTACT_KEY_NAME, + PUBLIC_CONTACT_ADDRESS, +} from "@/lib/mailboxes"; export const metadata: Metadata = pageMetadata({ title: "Enquiries", description: - "Send your enquiry straight to the right desk at Invision Solutions — sales, support, billing, invoices, contracts, or partnerships.", + "Send a general enquiry to Invision Solutions. Every message reaches David Levi directly.", path: "/enquiries", }); @@ -16,57 +20,46 @@ const FIELDS: Field[] = [ { name: "name", label: "Name", required: true }, { name: "email", label: "Email", type: "email", required: true }, { name: "company", label: "Company" }, - { name: "message", label: "Message", type: "textarea", required: true, rows: 4 }, + { name: "message", label: "Message", type: "textarea", required: true, full: true }, ]; export default function EnquiriesPage() { return ( - <> -- Each form below goes to its own mailbox, so your enquiry lands where - it belongs instead of queuing behind everything else. It still - reaches David — this only decides which inbox it arrives in. +
+ Sales, pricing, invoicing, contracts, support on live work, or an + introduction — it all comes to the same place, and it comes to David + directly. +
++ Prefer email? Send it to{" "} + + {PUBLIC_CONTACT_ADDRESS} + + .
- {box.description} -
-- {box.address} -
-Pricing question? Day rates and retainer costs go to{" "} - {SALES_ADDRESS} + {PUBLIC_CONTACT_ADDRESS} .
diff --git a/src/app/testimonials/page.tsx b/src/app/testimonials/page.tsx index 6ac8243..457a726 100644 --- a/src/app/testimonials/page.tsx +++ b/src/app/testimonials/page.tsx @@ -3,7 +3,11 @@ import { pageMetadata } from "@/lib/metadata"; import { Reveal } from "@/components/reveal"; import { Web3Form, type Field } from "@/components/web3forms"; import { Section, Eyebrow, SurfaceCard } from "@/components/ui"; -import { TESTIMONIALS_ADDRESS } from "@/lib/mailboxes"; +import { + CONTACT_ACCESS_KEY, + CONTACT_KEY_NAME, + PUBLIC_CONTACT_ADDRESS, +} from "@/lib/mailboxes"; export const metadata: Metadata = pageMetadata({ title: "Testimonials", @@ -150,10 +154,10 @@ export default function TestimonialsPage() {Prefer email? Send it to{" "} - {TESTIMONIALS_ADDRESS} + {PUBLIC_CONTACT_ADDRESS} .
@@ -161,7 +165,8 @@ export default function TestimonialsPage() {- Session: closed — log {year} — access: granted -
+© {year} Invision Solutions Ltd. All rights reserved.
diff --git a/src/lib/mailboxes.ts b/src/lib/mailboxes.ts index 85df429..ef08020 100644 --- a/src/lib/mailboxes.ts +++ b/src/lib/mailboxes.ts @@ -1,119 +1,42 @@ /** - * Enquiry routing. + * Contact routing. * - * Web3Forms binds one access key to one form, and a form's recipients are set - * in its dashboard — there is no client-side recipient override (that would - * make the endpoint an open relay). So routing an enquiry to a specific mailbox - * means a separate Web3Forms form, with its own access key, per mailbox. + * There is one Web3Forms form behind this site, and one access key for it: + * NEXT_PUBLIC_WEB3FORMS_KEY. Every form on the site — /contact, /enquiries and + * the testimonial form on /testimonials — submits to it, and it delivers to + * PUBLIC_CONTACT_ADDRESS. * - * Each entry reads its own NEXT_PUBLIC_WEB3FORMS_KEY_* variable and falls back - * to the general key when that variable is unset. The fallback delivers to - * info@, which is the catch-all, so the page works before every form exists and - * gets more precise as keys are added. Nothing silently disappears. + * This used to be seven forms and eight keys, one per mailbox, because + * Web3Forms binds one access key to one form and sets recipients per form in + * its dashboard rather than from the client (a client-side recipient override + * would make the endpoint an open relay). Those forms have been deleted from + * the Web3Forms account, so the per-mailbox NEXT_PUBLIC_WEB3FORMS_KEY_* + * variables now name forms that no longer exist. * - * NEXT_PUBLIC_ values are inlined at build time, so process.env cannot be - * indexed dynamically here — each key must be written out literally. + * Nothing here reads those variables any more, deliberately. A stale + * NEXT_PUBLIC_WEB3FORMS_KEY_* left set in Vercel is therefore inert rather than + * live: it cannot be picked up and silently pointed at a deleted form. That is + * the exact shape of the August /contact outage, where a key resolved to a + * form that had been removed and submissions stopped delivering without any + * visible error. + * + * Submissions are told apart by subject line, set per form at the call site. */ -export type Mailbox = { - slug: string; - address: string; - label: string; - description: string; - accessKey: string | undefined; - /** The variable this mailbox's key comes from, for diagnostics. */ - keyName: string; -}; - -const GENERAL = process.env.NEXT_PUBLIC_WEB3FORMS_KEY; +/** The access key every form on the site submits with. */ +export const CONTACT_ACCESS_KEY = process.env.NEXT_PUBLIC_WEB3FORMS_KEY; -export const MAILBOXES: Mailbox[] = [ - { - slug: "sales", - address: "sales@invisionsolutions.co.uk", - label: "Sales & pricing", - description: - "Day rates, retainer pricing, and what a given scope is likely to cost.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_SALES ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_SALES", - }, - { - slug: "contact", - address: "contact@invisionsolutions.co.uk", - label: "General enquiry", - description: - "Anything that does not fit the other routes — including introductions.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_CONTACT ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_CONTACT", - }, - { - slug: "support", - address: "support@invisionsolutions.co.uk", - label: "Client support", - description: - "Existing engagements: questions on delivered work, handover, or documentation.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_SUPPORT ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_SUPPORT", - }, - { - slug: "billing", - address: "billing@invisionsolutions.co.uk", - label: "Billing", - description: "Payment terms, purchase orders, and billing arrangements.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_BILLING ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_BILLING", - }, - { - slug: "invoice", - address: "invoice@invisionsolutions.co.uk", - label: "Invoices", - description: "Invoice queries, copies, remittance advice, and corrections.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_INVOICE ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_INVOICE", - }, - { - slug: "admin", - address: "admin@invisionsolutions.co.uk", - label: "Contracts & admin", - description: - "Supplier onboarding, NDAs, insurance certificates, and compliance paperwork.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_ADMIN ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_ADMIN", - }, - { - slug: "hello", - address: "hello@invisionsolutions.co.uk", - label: "Partnerships & press", - description: - "Collaborations, speaking, podcasts, and anything media-related.", - accessKey: process.env.NEXT_PUBLIC_WEB3FORMS_KEY_HELLO ?? GENERAL, - keyName: "NEXT_PUBLIC_WEB3FORMS_KEY_HELLO", - }, -]; +/** Named in the error state when CONTACT_ACCESS_KEY resolves to nothing. */ +export const CONTACT_KEY_NAME = "NEXT_PUBLIC_WEB3FORMS_KEY"; /** - * The key the /contact form uses. - * - * It reuses the contact desk's form rather than reading the general key - * directly, because every mailbox above has its own key set in production. That - * means the `?? GENERAL` fallbacks never fire, and the general key was left - * exercised by exactly one page: /contact. A fault in it was therefore invisible - * on /enquiries, which is how the contact form came to fail while all seven - * enquiry forms passed. + * The address that key delivers to, and the only address the site publishes. * - * Pointing /contact at the contact desk removes that blind spot. The form it - * submits to is now the same one /enquiries exercises on every test, and it - * delivers to PUBLIC_CONTACT_ADDRESS below, which is the address the contact - * page already gives out. + * Every `mailto:` link on the site points here too. /services previously + * published sales@ and /testimonials published hello@; both mailboxes are being + * retired along with the per-department forms, so advertising them would leave + * dead addresses on a live site. A mailto does not touch the Web3Forms account + * at all, so this is about where mail can actually be read, not about the + * linked-email allowance. */ -export const CONTACT_ACCESS_KEY = - MAILBOXES.find((box) => box.slug === "contact")?.accessKey ?? GENERAL; - -/** Named in the error state when CONTACT_ACCESS_KEY resolves to nothing. */ -export const CONTACT_KEY_NAME = - "NEXT_PUBLIC_WEB3FORMS_KEY_CONTACT (or NEXT_PUBLIC_WEB3FORMS_KEY)"; - -/** Address shown in the footer and as the general-purpose public address. */ export const PUBLIC_CONTACT_ADDRESS = "contact@invisionsolutions.co.uk"; -export const SALES_ADDRESS = "sales@invisionsolutions.co.uk"; -export const TESTIMONIALS_ADDRESS = "hello@invisionsolutions.co.uk";