diff --git a/.gitignore b/.gitignore index 2431b8c..27d4360 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* next-env.d.ts .tern-port + +# playwright-mcp browser sessions +.playwright-mcp/ diff --git a/next.config.js b/next.config.js deleted file mode 100644 index b0a4510..0000000 --- a/next.config.js +++ /dev/null @@ -1,30 +0,0 @@ -/** @type {import('next').NextConfig} */ -const url = require("url"); - -const nextConfig = { - images: { - dangerouslyAllowSVG: true, - remotePatterns: [ - { - protocol: "https", - hostname: url.parse(process.env.NEXT_PUBLIC_FONTDUE_URL).hostname, - }, - { - protocol: "https", - hostname: "*.fontdue.com", - }, - ], - }, - logging: { - fetches: { - fullUrl: true, - hmrRefreshes: true, - }, - }, - // Treat every user agent as HTML-limited so metadata rendering blocks the - // response. Streamed metadata locks in a 200 status before - // generateMetadata's notFound() can produce a real 404. - htmlLimitedBots: /.*/, -}; - -module.exports = nextConfig; diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..d6b4fd4 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,17 @@ +import { withFontdue } from "fontdue-js/next/config"; + +// withFontdue installs everything the storefront needs — the rewrites that +// route requests to the right Fontdue site, image settings, and metadata +// workarounds — driven by NEXT_PUBLIC_FONTDUE_URL (see .env.local). +// Anything in here composes with it like a normal Next config. +export default withFontdue({ + // Emit a self-contained server bundle (`.next/standalone`) so the app can + // run as `node server.js` on any Node host. Harmless on Vercel. + output: "standalone", + logging: { + fetches: { + fullUrl: true, + hmrRefreshes: true, + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 5a7ef9b..f8f45c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@graphql-tools/import": "^7.1.14", - "fontdue-js": "^2.28.0", + "fontdue-js": "3.0.0-alpha14", "graphql": "^16.14.2", "html-react-parser": "^5", "next": "^15.5.14", @@ -373,6 +373,40 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "license": "MIT", @@ -1601,7 +1635,6 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1618,6 +1651,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@next/env": { "version": "15.5.14", "license": "MIT" @@ -1781,6 +1832,336 @@ "node": ">= 8" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.124.0.tgz", + "integrity": "sha512-+R9zCafSL8ovjokdPtorUp3sXrh8zQ2AC2L0ivXNvlLR0WS+5WdPkNVrnENq5UvzagM4Xgl0NPsJKz3Hv9+y8g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.124.0.tgz", + "integrity": "sha512-ULHC/gVZ+nP4pd3kNNQTYaQ/e066BW/KuY5qUsvwkVWwOUQGDg+WpfyVOmQ4xfxoue6cMlkKkJ+ntdzfDXpNlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.124.0.tgz", + "integrity": "sha512-fGJ2hw7bnbUYn6UvTjp0m4WJ9zXz3cohgcwcgeo7gUZehpPNpvcVEVeIVHNmHnAuAw/ysf4YJR8DA1E+xCA4Lw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.124.0.tgz", + "integrity": "sha512-j0+re9pgps5BH2Tk3fm59Hi3QuLP3C4KhqXi6A+wRHHHJWDFR8mc/KI9mBrfk2JRT+15doGo+zv1eN75/9DuOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.124.0.tgz", + "integrity": "sha512-0k5mS0npnrhKy72UfF51lpOZ2ESoPWn6gdFw+RdeRWcokraDW1O2kSx3laQ+yk7cCEavQdJSpWCYS/GvBbUCXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.124.0.tgz", + "integrity": "sha512-P/i4eguRWvAUfGdfhQYg1jpwYkyUV6D3gefIH7HhmRl1Ph6P4IqTIEVcyJr1i/3vr1V5OHU4wonH6/ue/Qzvrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.124.0.tgz", + "integrity": "sha512-/ameqFQH5fFP+66Atr8Ynv/2rYe4utcU7L4MoWS5JtrFLVO78g4qDLavyIlJxa6caSwYOvG/eO3c/DXqY5/6Rw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.124.0.tgz", + "integrity": "sha512-gNeyEcXTtfrRCbj2EfxWU85Fs0wIX3p44Y3twnvuMfkWlLrb9M1Z25AYNSKjJM+fdAjeeQCjw0on47zFuBYwQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.124.0.tgz", + "integrity": "sha512-uvG7v4Tz9S8/PVqY0SP0DLHxo4hZGe+Pv2tGVnwcsjKCCUPjplbrFVvDzXq+kOaEoUkiCY0Kt1hlZ6FDJ1LKNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.124.0.tgz", + "integrity": "sha512-t7KZaaUhfp2au0MRpoENEFqwLKYDdptEry6V7pTAVdPEcFG4P6ii8yeGU9m6p5vb+b8WEKmdpGMNXBEYy7iJdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.124.0.tgz", + "integrity": "sha512-eurGGaxHZiIQ+fBSageS8TAkRqZgdOiBeqNrWAqAPup9hXBTmQ0WcBjwsLElf+3jvDL9NhnX0dOgOqPfsjSjdg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.124.0.tgz", + "integrity": "sha512-d1V7/ll1i/LhqE/gZy6Wbz6evlk0egh2XKkwMI3epiojtbtUwQSLIER0Y3yDBBocPuWOjJdvmjtEmPTTLXje/w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.124.0.tgz", + "integrity": "sha512-w1+cBvriUteOpox6ATqCFVkpGL47PFdcfCPGmgUZbd78Fw44U0gQkc+kVGvAOTvGrptMYgwomD1c6OTVvkrpGg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.124.0.tgz", + "integrity": "sha512-RRB1evQiXRtMCsQQiAh9U0H3HzguLpE0ytfStuhRgmOj7tqUCOVxkHsvM9geZjAax6NqVRj7VXx32qjjkZPsBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.124.0.tgz", + "integrity": "sha512-asVYN0qmSHlCU8H9Q47SmeJ/Z5EG4IWCC+QGxkfFboI5qh15aLlJnHmnrV61MwQRPXGnVC/sC3qKhrUyqGxUqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.124.0.tgz", + "integrity": "sha512-nhwuxm6B8pn9lzAzMUfa571L5hCXYwQo8C8cx5aGOuHWCzruR8gPJnRRXGBci+uGaIIQEZDyU/U6HDgrSp/JlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.124.0.tgz", + "integrity": "sha512-LWuq4Dl9tff7n+HjJcqoBjDlVCtruc0shgtdtGM+rTUIE9aFxHA/P+wCYR+aWMjN8m9vNaRME/sKXErmhmeKrA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.124.0.tgz", + "integrity": "sha512-aOh3Lf3AeH0dgzT4yBXcArFZ8VhqNXwZ/xlN0GqBtgVaGoHOOqL2YHlcVIgT+ghsXPVR2PTtYgBiQ1CNK7jp5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.124.0.tgz", + "integrity": "sha512-sib5xC0nz/+SCpaETBuHBz4SXS02KuG5HtyOcHsO/SK5ZvLRGhOZx0elDKawjb6adFkD7dQCqpXUS25wY6ELKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.124.0.tgz", + "integrity": "sha512-UgojtjGUgZgAZQYt7SC6VO65OVdxEkRe2q+2vbHJO//18qw3Hrk6UvHGQKldsQKgbVcIBT/YBrt85YberiYIPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "dev": true, @@ -1924,40 +2305,18 @@ "react": ">=16.8" } }, - "node_modules/@react-hook/passive-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", - "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "node_modules/@react-hook/throttle": { + "version": "2.2.0", "license": "MIT", + "dependencies": { + "@react-hook/latest": "^1.0.2" + }, "peerDependencies": { "react": ">=16.8" } }, - "node_modules/@react-hook/resize-observer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-2.0.2.tgz", - "integrity": "sha512-tzKKzxNpfE5TWmxuv+5Ae3IF58n0FQgQaWJmcbYkjXTRZATXxClnTprQ2uuYygYTpu1pqbBskpwMpj6jpT1djA==", - "license": "MIT", - "dependencies": { - "@react-hook/latest": "^1.0.2", - "@react-hook/passive-layout-effect": "^1.2.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@react-hook/throttle": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "@react-hook/latest": "^1.0.2" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/window-size": { - "version": "3.1.1", + "node_modules/@react-hook/window-size": { + "version": "3.1.1", "license": "MIT", "dependencies": { "@react-hook/debounce": "^3.0.0", @@ -1973,6 +2332,270 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT", + "peer": true + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "dev": true, @@ -2090,9 +2713,18 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/estree": { "version": "1.0.8", - "dev": true, "license": "MIT" }, "node_modules/@types/js-yaml": { @@ -2912,12 +3544,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/clean-stack": { "version": "2.2.0", "dev": true, @@ -3314,7 +3940,6 @@ }, "node_modules/detect-libc": { "version": "2.0.4", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4383,6 +5008,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "dev": true, @@ -4617,18 +5251,16 @@ "license": "ISC" }, "node_modules/fontdue-js": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/fontdue-js/-/fontdue-js-2.28.0.tgz", - "integrity": "sha512-zz7DubLeWNE+f2lYfzdcbpHHyudnZuiMaliJ8KieNliyQH9Wq0U3MKNAg4kUEKPvB3ONHCOTNr/gK5PCX89zNA==", + "version": "3.0.0-alpha14", + "resolved": "https://registry.npmjs.org/fontdue-js/-/fontdue-js-3.0.0-alpha14.tgz", + "integrity": "sha512-HJp9Igz153lCLGmkuYrsvZ877VRiHDZqx+KZi1H2BmCNFgbX5l608vqf87V9MZKSJ8wqy1yWP2dZ/WtYn+UQqA==", "license": "MIT", "dependencies": { "@emotion/react": "^11.14.0", - "@react-hook/resize-observer": "^2", "@react-hook/window-size": "^3.0.7", "@sentry/react": "^10.17.0", "@stripe/react-stripe-js": "^3.8.1", "@stripe/stripe-js": "^7.7.0", - "classnames": "^2.2.5", "draft-js": "^0.11.7", "fast-deep-equal": "^2.0.1", "fontfaceobserver": "^2.0.13", @@ -4640,19 +5272,233 @@ "redux": "^5.0.0", "relay-runtime": "^20.0.0", "store": "^2.0.12", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "vite-plugin-cjs-interop": "^3.3.0" }, "peerDependencies": { "react": "18 || 19", "react-dom": "18 || 19" } }, + "node_modules/fontdue-js/node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/fontdue-js/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/fontdue-js/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/fontdue-js/node_modules/fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "license": "MIT" }, + "node_modules/fontdue-js/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/fontdue-js/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fontdue-js/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/fontdue-js/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/fontdue-js/node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/fontdue-js/node_modules/vite-plugin-cjs-interop": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-cjs-interop/-/vite-plugin-cjs-interop-3.3.0.tgz", + "integrity": "sha512-Gz4A1NxldSaq8v1UJTkhnZrq4P9V6+06+DJOGt2V8mneUwe6VfZF5VsZ6dK9HOPFQu5gn6CMxb+1p5qrbe7OYg==", + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21", + "minimatch": "^10.2.5", + "oxc-parser": "^0.124.0" + }, + "engines": { + "node": "20 || 22 || 24 || 25" + }, + "peerDependencies": { + "vite": "~6.4 || ~7.3 || 8" + } + }, + "node_modules/fontdue-js/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/fontfaceobserver": { "version": "2.3.0", "license": "BSD-2-Clause" @@ -4671,6 +5517,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -5895,94 +6756,355 @@ "jsonify": "^0.0.1" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-to-pretty-yaml": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "remedial": "^1.0.7", + "remove-trailing-spaces": "^1.0.6" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-to-pretty-yaml": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "remedial": "^1.0.7", - "remove-trailing-spaces": "^1.0.6" - }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, "engines": { - "node": ">= 0.2.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "bin": { - "json5": "lib/cli.js" - }, "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsonify": { - "version": "0.0.1", - "dev": true, - "license": "Public Domain", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, "engines": { - "node": ">=4.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -6161,6 +7283,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/map-cache": { "version": "0.2.2", "dev": true, @@ -6263,7 +7394,9 @@ "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -6764,6 +7897,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.124.0.tgz", + "integrity": "sha512-h07SFj/tp2U3cf3+LFX6MmOguQiM9ahwpGs0ZK5CGhgL8p4kk24etrJKsEzhXAvo7mfvoKTZooZ5MLKAPRmJ1g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.124.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.124.0", + "@oxc-parser/binding-android-arm64": "0.124.0", + "@oxc-parser/binding-darwin-arm64": "0.124.0", + "@oxc-parser/binding-darwin-x64": "0.124.0", + "@oxc-parser/binding-freebsd-x64": "0.124.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.124.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.124.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.124.0", + "@oxc-parser/binding-linux-arm64-musl": "0.124.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.124.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.124.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.124.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.124.0", + "@oxc-parser/binding-linux-x64-gnu": "0.124.0", + "@oxc-parser/binding-linux-x64-musl": "0.124.0", + "@oxc-parser/binding-openharmony-arm64": "0.124.0", + "@oxc-parser/binding-wasm32-wasi": "0.124.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.124.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.124.0", + "@oxc-parser/binding-win32-x64-msvc": "0.124.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/p-limit": { "version": "3.1.0", "dev": true, @@ -7360,6 +8539,40 @@ "dev": true, "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "dev": true, @@ -8216,6 +9429,54 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/title-case": { "version": "3.0.3", "dev": true, @@ -8438,6 +9699,14 @@ "node": ">=0.10.0" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/unixify": { "version": "1.0.0", "dev": true, diff --git a/package.json b/package.json index c21e252..691ca84 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@graphql-tools/import": "^7.1.14", - "fontdue-js": "^2.28.0", + "fontdue-js": "3.0.0-alpha14", "graphql": "^16.14.2", "html-react-parser": "^5", "next": "^15.5.14", diff --git a/src/app/[...rest]/page.tsx b/src/app/[...rest]/page.tsx new file mode 100644 index 0000000..a2bbb15 --- /dev/null +++ b/src/app/[...rest]/page.tsx @@ -0,0 +1,7 @@ +import { notFound } from "next/navigation"; + +// Catch-all so every unmatched path renders the site's not-found page +// (with its nav and footer) instead of a bare global 404. +export default function CatchAll(): never { + notFound(); +} diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 2261e9b..e10e0b6 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -1,9 +1,6 @@ -import fs from "fs/promises"; -import path from "path"; import FontdueHTML from "@/components/FontdueHTML"; import { fetchGraphql } from "@/lib/graphql"; -import { notEmpty } from "@/lib/utils"; -import { PagePathsQuery, PageQuery, PageQueryVariables } from "@graphql"; +import { PageQuery, PageQueryVariables } from "@graphql"; import { notFound } from "next/navigation"; import { Metadata } from "next"; @@ -21,10 +18,8 @@ async function getPage(slug: string) { return viewer.slug?.page; } -export async function generateMetadata({ - params, -}: PageProps): Promise { - const { slug } = await params; +export async function generateMetadata(props: PageProps): Promise { + const { slug } = await props.params; const page = await getPage(slug); if (!page) notFound(); return { @@ -34,8 +29,8 @@ export async function generateMetadata({ }; } -export default async function Page({ params }: PageProps) { - const { slug } = await params; +export default async function Page(props: PageProps) { + const { slug } = await props.params; const page = await getPage(slug); if (!page) notFound(); @@ -48,21 +43,4 @@ export default async function Page({ params }: PageProps) { ); } -export async function generateStaticParams(): Promise<{ slug: string }[]> { - // Solves an issue with Next.js when these [slug]/page.js clash with named - // name/page.js routes - const dirs = ( - await fs.readdir(path.resolve(__dirname, ".."), { withFileTypes: true }) - ) - .filter((dir) => dir.isDirectory()) - .map((dir) => dir.name); - - const data = await fetchGraphql("PagePaths.graphql"); - const slugs = - data.viewer.pages?.edges - ?.map((edge) => edge?.node?.slug?.name) - .filter(notEmpty) - .filter((slug) => !dirs.includes(slug)) ?? []; - - return slugs.map((slug) => ({ slug })); -} +export { pageParams as generateStaticParams } from "@/lib/static-params"; diff --git a/src/app/api/preview/route.ts b/src/app/api/preview/route.ts new file mode 100644 index 0000000..e37865d --- /dev/null +++ b/src/app/api/preview/route.ts @@ -0,0 +1,28 @@ +import { draftMode } from "next/headers"; +import { handlePreviewRequest } from "fontdue-js/preview"; + +// Preview mode entry/exit. A logged-in admin brokers a short-lived +// token (via the createAdminToken GraphQL mutation) and POSTs it here. +// handlePreviewRequest (from fontdue-js/preview) implements the portable +// cookie contract — it sets an httpOnly token cookie plus a non-httpOnly +// marker the client toolbar reads (and clears both on DELETE). On top of that +// we toggle Next draft mode, so server renders forward the token and reveal +// hidden (unpublished) fonts site-wide. The public never has these cookies, so +// their renders stay static and cached. + +// Secure cookies in production. Next can sit behind a proxy where the request +// URL isn't https, so set this explicitly rather than relying on the helper's +// request-protocol default. +const previewCookieOptions = { secure: process.env.NODE_ENV === "production" }; + +export async function POST(request: Request) { + const response = await handlePreviewRequest(request, previewCookieOptions); + if (response.ok) (await draftMode()).enable(); + return response; +} + +export async function DELETE(request: Request) { + const response = await handlePreviewRequest(request, previewCookieOptions); + (await draftMode()).disable(); + return response; +} diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index eaef2b4..f0de40a 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -1,7 +1,3 @@ -import { NextRequest, NextResponse } from "next/server"; -import { revalidateTag } from "next/cache"; - -export async function POST(_request: NextRequest) { - revalidateTag("graphql"); - return NextResponse.json({ revalidated: true, now: Date.now() }); -} +// Fontdue's deploy hook calls this when the site's content changes, purging +// every cached page and GraphQL response so the next request re-renders. +export { POST } from "fontdue-js/next/revalidate"; diff --git a/src/app/article/[slug]/page.tsx b/src/app/article/[slug]/page.tsx index 89283a0..4e6d33e 100644 --- a/src/app/article/[slug]/page.tsx +++ b/src/app/article/[slug]/page.tsx @@ -1,22 +1,15 @@ import Carousel from "@/components/Carousel"; import { fetchGraphql } from "@/lib/graphql"; -import { - ArticlePathsQuery, - ArticleQuery, - ArticleQueryVariables, -} from "@graphql"; +import { ArticleQuery, ArticleQueryVariables } from "@graphql"; import { notFound } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; import { Fragment } from "react"; import FontdueHTML from "@/components/FontdueHTML"; -import { notEmpty } from "@/lib/utils"; import { Metadata } from "next"; interface ArticleProps { - params: Promise<{ - slug: string; - }>; + params: Promise<{ slug: string }>; } async function getData(slug: string) { @@ -25,10 +18,8 @@ async function getData(slug: string) { }); } -export async function generateMetadata({ - params, -}: ArticleProps): Promise { - const { slug } = await params; +export async function generateMetadata(props: ArticleProps): Promise { + const { slug } = await props.params; const data = await getData(slug); const article = data.viewer.slug?.article; if (!article) notFound(); @@ -40,8 +31,8 @@ export async function generateMetadata({ }; } -export default async function Article({ params }: ArticleProps) { - const { slug } = await params; +export default async function Article(props: ArticleProps) { + const { slug } = await props.params; const data = await getData(slug); const article = data.viewer.slug?.article; if (!article) notFound(); @@ -89,12 +80,4 @@ export default async function Article({ params }: ArticleProps) { ); } -export async function generateStaticParams(): Promise<{ slug: string }[]> { - const data = await fetchGraphql("ArticlePaths.graphql"); - const slugs = - data.viewer.articles?.edges - ?.map((edge) => edge?.node?.slug?.name) - .filter(notEmpty) ?? []; - - return slugs.map((slug) => ({ slug })); -} +export { articleParams as generateStaticParams } from "@/lib/static-params"; diff --git a/src/app/articles/tag/[tag]/page.tsx b/src/app/articles/tag/[tag]/page.tsx index 02d6dfd..44ae885 100644 --- a/src/app/articles/tag/[tag]/page.tsx +++ b/src/app/articles/tag/[tag]/page.tsx @@ -1,8 +1,5 @@ import { Metadata } from "next"; -import { fetchGraphql } from "@/lib/graphql"; import ArticlesIndex from "@/components/ArticlesIndex"; -import { ArticleTagsQuery } from "@graphql"; -import { notEmpty } from "@/lib/utils"; interface ArticlesProps { params: Promise<{ tag: string }>; @@ -19,19 +16,9 @@ export async function generateMetadata({ } export default async function Articles(props: ArticlesProps) { - const params = await props.params; - - const { - tag - } = params; + const { tag } = await props.params; return ; } -export async function generateStaticParams(): Promise<{ tag: string }[]> { - const data = await fetchGraphql("ArticleTags.graphql"); - - return ( - data.viewer.articlesTags?.filter(notEmpty).map((tag) => ({ tag })) ?? [] - ); -} +export { articleTagParams as generateStaticParams } from "@/lib/static-params"; diff --git a/src/app/customer-login/page.tsx b/src/app/customer-login/page.tsx index d6fe59d..7df2fcc 100644 --- a/src/app/customer-login/page.tsx +++ b/src/app/customer-login/page.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { export default async function CustomerLoginPage() { const data = await fetchGraphql( "Page.graphql", - { slug: "customer-login" } + { slug: "customer-login" }, ); const page = data.viewer.slug?.page; diff --git a/src/app/fonts/[slug]/page.tsx b/src/app/fonts/[slug]/page.tsx index 335d761..0460d87 100644 --- a/src/app/fonts/[slug]/page.tsx +++ b/src/app/fonts/[slug]/page.tsx @@ -1,7 +1,6 @@ import React from "react"; -import { FontPathsQuery, FontQuery, FontQueryVariables } from "@graphql"; +import { FontQuery, FontQueryVariables } from "@graphql"; import { fetchGraphql } from "@/lib/graphql"; -import { notEmpty } from "@/lib/utils"; import FontDetail from "@/components/FontDetail"; import { notFound } from "next/navigation"; import { Metadata } from "next"; @@ -16,10 +15,8 @@ async function getData(slug: string) { }); } -export async function generateMetadata({ - params, -}: FontProps): Promise { - const { slug } = await params; +export async function generateMetadata(props: FontProps): Promise { + const { slug } = await props.params; const { viewer } = await getData(slug); const font = viewer.slug?.fontCollection; if (!font) notFound(); @@ -31,8 +28,8 @@ export async function generateMetadata({ }; } -export default async function Font({ params }: FontProps) { - const { slug } = await params; +export default async function Font(props: FontProps) { + const { slug } = await props.params; const data = await getData(slug); const collection = data.viewer.slug?.fontCollection; if (!collection) notFound(); @@ -40,12 +37,4 @@ export default async function Font({ params }: FontProps) { return ; } -export async function generateStaticParams(): Promise<{ slug: string }[]> { - const data = await fetchGraphql("FontPaths.graphql"); - const slugs = - data.viewer.fontCollections?.edges - ?.map((edge) => edge?.node?.slug?.name) - .filter(notEmpty) ?? []; - - return slugs.map((slug) => ({ slug })); -} +export { fontParams as generateStaticParams } from "@/lib/static-params"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 88e37e3..b1c8546 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,10 +5,9 @@ import parse from "html-react-parser"; import { Metadata } from "next"; import "fontdue-js/fontdue.css"; import Image from "next/image"; -import "../styles/main.scss"; +import "@/styles/main.scss"; import { RootLayoutQuery } from "@graphql"; import { fetchGraphql } from "@/lib/graphql"; -import { fallbackSiteUrl } from "@/lib/utils"; import ActiveLink from "@/components/ActiveLink"; import PreloadWebfonts from "@/components/PreloadWebfonts"; import FontdueHTML from "@/components/FontdueHTML"; @@ -26,6 +25,10 @@ function styleFamilyName( return `"${style.cssFamily} ${style.name}"`; } +interface LayoutProps { + children: React.ReactNode; +} + async function getData() { return fetchGraphql("RootLayout.graphql"); } @@ -34,7 +37,10 @@ export async function generateMetadata(): Promise { const { viewer } = await getData(); return { - metadataBase: new URL(viewer.url ?? fallbackSiteUrl), + // The canonical site URL set in the Fontdue admin (Settings → Website + // settings). The store API host is usually not the site's public host, + // so there is deliberately no env-var fallback. + metadataBase: viewer.url ? new URL(viewer.url) : null, title: { template: `%s | ${viewer.settings?.title}`, default: viewer.settings?.title ?? "", @@ -43,11 +49,8 @@ export async function generateMetadata(): Promise { }; } -export default async function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function RootLayout(props: LayoutProps) { + const { children } = props; const { viewer } = await getData(); const pages = viewer.pages?.edges?.map((edge) => edge!.node!); @@ -73,6 +76,7 @@ export default async function RootLayout({ /> ; } @@ -20,10 +15,8 @@ async function getData(slug: string) { }); } -export async function generateMetadata({ - params, -}: FontProps): Promise { - const { slug } = await params; +export async function generateMetadata(props: LicenseProps): Promise { + const { slug } = await props.params; const data = await getData(slug); const license = data.viewer.slug?.license; if (!license) notFound(); @@ -33,8 +26,8 @@ export async function generateMetadata({ }; } -export default async function License({ params }: FontProps) { - const { slug } = await params; +export default async function License(props: LicenseProps) { + const { slug } = await props.params; const data = await getData(slug); const license = data.viewer.slug?.license; @@ -53,12 +46,4 @@ export default async function License({ params }: FontProps) { ); } -export async function generateStaticParams(): Promise<{ slug: string }[]> { - const data = await fetchGraphql("LicensePaths.graphql"); - const slugs = - data.viewer.licenses - ?.map((license) => license.slug?.name) - .filter(notEmpty) ?? []; - - return slugs.map((slug) => ({ slug })); -} +export { licenseParams as generateStaticParams } from "@/lib/static-params"; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 7c19d35..efba3de 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,6 +1,10 @@ export default function NotFound() { return (
+ {/* Upstream Next.js bug: notFound() responses carry a 200 status when + the tree contains Suspense / a root layout (vercel/next.js#82041). + Until that's fixed, noindex keeps these soft-404s out of search. */} +

Page not found

diff --git a/src/app/robots.ts b/src/app/robots.ts index c1692cb..ed56789 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from "next"; import { fetchGraphql } from "@/lib/graphql"; import { SiteUrlQuery } from "@graphql"; -import { fallbackSiteUrl } from "@/lib/utils"; +import { requireSiteUrl } from "@/lib/site-url"; export default async function robots(): Promise { const { viewer } = await fetchGraphql("SiteUrl.graphql"); @@ -12,9 +12,6 @@ export default async function robots(): Promise { allow: "/", disallow: "/api/", }, - sitemap: new URL( - "/sitemap.xml", - viewer.url ?? fallbackSiteUrl, - ).toString(), + sitemap: new URL("/sitemap.xml", requireSiteUrl(viewer.url)).toString(), }; } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 0e0b3fc..2d7e172 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,11 +1,12 @@ import { MetadataRoute } from "next"; import { fetchGraphql } from "@/lib/graphql"; import { SitemapQuery } from "@graphql"; -import { notEmpty, fallbackSiteUrl } from "@/lib/utils"; +import { notEmpty } from "@/lib/utils"; +import { requireSiteUrl } from "@/lib/site-url"; -// Utility pages (login, font trials) are intentionally left out — they're -// reachable from the nav but aren't search-relevant content. -const EXCLUDED_PAGE_SLUGS = new Set(["customer-login", "test-fonts"]); +// The login page is intentionally left out — it's reachable from the nav +// but isn't search-relevant content. +const EXCLUDED_PAGE_SLUGS = new Set(["customer-login"]); export default async function sitemap(): Promise { const { viewer } = await fetchGraphql("Sitemap.graphql"); @@ -40,11 +41,13 @@ export default async function sitemap(): Promise { ...(articles.length ? ["/articles"] : []), ...articles.map((slug) => `/article/${slug}`), ...pages.map((slug) => `/${slug}`), + "/test-fonts", "/licenses", ...licenses.map((slug) => `/licenses/${slug}`), ]); + const base = requireSiteUrl(viewer.url); return Array.from(paths).map((path) => ({ - url: new URL(path, viewer.url ?? fallbackSiteUrl).toString(), + url: new URL(path, base).toString(), })); } diff --git a/src/app/test-fonts/page.tsx b/src/app/test-fonts/page.tsx index cee086e..9d3738d 100644 --- a/src/app/test-fonts/page.tsx +++ b/src/app/test-fonts/page.tsx @@ -9,10 +9,10 @@ export const metadata: Metadata = { alternates: { canonical: "/test-fonts" }, }; -export default async function CustomerLoginPage() { +export default async function TestFontsPage() { const data = await fetchGraphql( "Page.graphql", - { slug: "test-fonts" } + { slug: "test-fonts" }, ); const page = data.viewer.slug?.page; diff --git a/src/components/FontDetail.tsx b/src/components/FontDetail.tsx index 2b20d0f..ec9061a 100644 --- a/src/components/FontDetail.tsx +++ b/src/components/FontDetail.tsx @@ -22,7 +22,7 @@ type VariableInstance = { function instanceCSS(instance: VariableInstance): React.CSSProperties { const settings = instance.coordinates.map( - (coordinate: Coordinate) => `'${coordinate.axis}' ${coordinate.value}` + (coordinate: Coordinate) => `'${coordinate.axis}' ${coordinate.value}`, ); return { @@ -31,23 +31,23 @@ function instanceCSS(instance: VariableInstance): React.CSSProperties { } function showBuyButton( - collection: FontDetailFragment | FontDetailCollectionFragment + collection: FontDetailFragment | FontDetailCollectionFragment, ): boolean { if (collection.sku) return true; const hasFontStylesSKU = collection.fontStyles?.some( - (style) => style.sku !== null + (style) => style.sku !== null, ); if (hasFontStylesSKU) return true; const hasBundlesSKU = collection.bundles?.some( - (bundle) => bundle.sku !== null + (bundle) => bundle.sku !== null, ); if (hasBundlesSKU) return true; if ("children" in collection) { const hasChildrenSKU = collection.children?.some((child) => - showBuyButton(child) + showBuyButton(child), ); if (hasChildrenSKU) return true; } @@ -61,7 +61,7 @@ interface CollectionStyles_props { } function groupVariableInstances( - fontInstances: VariableInstance[] | undefined | null + fontInstances: VariableInstance[] | undefined | null, ): VariableInstance[][] | undefined { if (!fontInstances) return; const groupedFontInstances: { [key: string]: VariableInstance[] } = {}; @@ -99,20 +99,23 @@ function groupVariableInstances( } function familyStylesGrouped< - T extends { cssFamily: string | null; cssWeight: string | null } + T extends { cssFamily: string | null; cssWeight: string | null }, >(fontStyles: (T | null)[] | null): T[][] | null { if (!fontStyles) return null; - const groups = fontStyles.filter(notEmpty).reduce((groups, style) => { - const key = `${style.cssFamily}-${style.cssWeight}`; + const groups = fontStyles.filter(notEmpty).reduce( + (groups, style) => { + const key = `${style.cssFamily}-${style.cssWeight}`; - if (!groups[key]) { - groups[key] = []; - } + if (!groups[key]) { + groups[key] = []; + } - groups[key].push(style); + groups[key].push(style); - return groups; - }, {} as Record); + return groups; + }, + {} as Record, + ); return Object.values(groups); } diff --git a/src/components/FontdueHTML.tsx b/src/components/FontdueHTML.tsx index c193448..1867538 100644 --- a/src/components/FontdueHTML.tsx +++ b/src/components/FontdueHTML.tsx @@ -42,6 +42,7 @@ export default function FontdueHTML({ html }: FontdueHTML_props) { return ; } if (domNode.name === "fontdue-character-viewer") { + // @ts-ignore return ; } if (domNode.name === "fontdue-type-tester") { @@ -52,6 +53,7 @@ export default function FontdueHTML({ html }: FontdueHTML_props) { return ; } if (domNode.name === "fontdue-buy-button") { + // @ts-ignore return ; } if (domNode.name === "fontdue-cart-button") { diff --git a/src/components/PreloadWebfonts.tsx b/src/components/PreloadWebfonts.tsx index 196f918..33962e2 100644 --- a/src/components/PreloadWebfonts.tsx +++ b/src/components/PreloadWebfonts.tsx @@ -37,7 +37,7 @@ export default function PreloadWebfonts({ }) { if (!style) return null; const source = style.webfontSources?.find( - (source) => source?.format === "woff2" + (source) => source?.format === "woff2", ); if (source?.url) { ReactDOM.preload(source.url, { as: "font" }); diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts index 606491f..ee0167e 100644 --- a/src/lib/graphql.ts +++ b/src/lib/graphql.ts @@ -1,9 +1,9 @@ import path from "path"; +import { notFound } from "next/navigation"; +import { createFontdueFetch, FontdueNotFoundError } from "fontdue-js/server"; import { processImport } from "@graphql-tools/import"; import { print } from "graphql"; -const ENDPOINT = `${process.env.NEXT_PUBLIC_FONTDUE_URL}/graphql`; - const getStaticQuery = async (queryName: string) => { // Resolve any `#import` statements so the query sent to the server is // self-contained — shared fragments live once in fragments.graphql. @@ -17,32 +17,22 @@ const getStaticQuery = async (queryName: string) => { const fetchGraphql = async ( queryName: string, - variables: V | void, + variables?: V, ): Promise => { const query = await getStaticQuery(queryName); - const response = await fetch(`${ENDPOINT}?query=${queryName}`, { - method: "POST", - body: JSON.stringify({ query, variables }), - headers: { - "content-type": "application/json", - }, - next: { - tags: ["graphql"], - }, - }); - if (response.status !== 200) { - throw new Error("Fontdue request failed"); + // One transport for every fetch: createFontdueFetch targets the site and, + // while an admin is previewing, carries the token that reveals hidden fonts. + const fetchFontdue = createFontdueFetch(); + + try { + return await fetchFontdue(queryName, query, variables); + } catch (error) { + // The Fontdue server 404s when the requested host doesn't resolve to a + // site — surface that as the page's 404 rather than an error. + if (error instanceof FontdueNotFoundError) notFound(); + throw error; } - - const json = await response.json(); - - const errorMessage = json.errors?.[0]?.message; - if (errorMessage) { - throw new Error(`Fontdue graphql request error: ${errorMessage}`); - } - - return json.data; }; export { fetchGraphql, getStaticQuery }; diff --git a/src/lib/site-url.ts b/src/lib/site-url.ts new file mode 100644 index 0000000..04630ed --- /dev/null +++ b/src/lib/site-url.ts @@ -0,0 +1,13 @@ +// The site's canonical URL comes from the Fontdue admin (Settings → +// Website settings). Absolute URLs — sitemap entries, the robots.txt +// sitemap pointer — need it, and NEXT_PUBLIC_FONTDUE_URL is not a +// substitute: the Fontdue API often lives on a different host (e.g. +// store.example.com) than the site itself (www.example.com). +export function requireSiteUrl(url: string | null | undefined): string { + if (!url) { + throw new Error( + "Site URL is not set: enter your site's URL in the Fontdue admin under Settings → Website settings.", + ); + } + return url; +} diff --git a/src/lib/static-params.ts b/src/lib/static-params.ts new file mode 100644 index 0000000..72586f4 --- /dev/null +++ b/src/lib/static-params.ts @@ -0,0 +1,73 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { + ArticlePathsQuery, + ArticleTagsQuery, + FontPathsQuery, + LicensePathsQuery, + PagePathsQuery, +} from "@graphql"; +import { fetchGraphql } from "@/lib/graphql"; +import { notEmpty } from "@/lib/utils"; + +// Build-time route params: every font, article, page and license is +// prerendered at build and revalidated by the deploy hook. + +export async function fontParams(): Promise<{ slug: string }[]> { + const data = await fetchGraphql("FontPaths.graphql"); + const slugs = + data.viewer.fontCollections?.edges + ?.map((edge) => edge?.node?.slug?.name) + .filter(notEmpty) ?? []; + + return slugs.map((slug) => ({ slug })); +} + +export async function articleParams(): Promise<{ slug: string }[]> { + const data = await fetchGraphql("ArticlePaths.graphql"); + const slugs = + data.viewer.articles?.edges + ?.map((edge) => edge?.node?.slug?.name) + .filter(notEmpty) ?? []; + + return slugs.map((slug) => ({ slug })); +} + +export async function articleTagParams(): Promise<{ tag: string }[]> { + const data = await fetchGraphql("ArticleTags.graphql"); + + return ( + data.viewer.articlesTags?.filter(notEmpty).map((tag) => ({ tag })) ?? [] + ); +} + +export async function pageParams(): Promise<{ slug: string }[]> { + // A page slug that collides with a named route (articles, licenses, …) + // is served by the named route, so don't claim it for [slug]. + const dirs = ( + await fs.readdir(path.resolve(process.cwd(), "src", "app"), { + withFileTypes: true, + }) + ) + .filter((dir) => dir.isDirectory()) + .map((dir) => dir.name); + + const data = await fetchGraphql("PagePaths.graphql"); + const slugs = + data.viewer.pages?.edges + ?.map((edge) => edge?.node?.slug?.name) + .filter(notEmpty) + .filter((slug) => !dirs.includes(slug)) ?? []; + + return slugs.map((slug) => ({ slug })); +} + +export async function licenseParams(): Promise<{ slug: string }[]> { + const data = await fetchGraphql("LicensePaths.graphql"); + const slugs = + data.viewer.licenses + ?.map((license) => license.slug?.name) + .filter(notEmpty) ?? []; + + return slugs.map((slug) => ({ slug })); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 91ec932..08902a5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,13 +1,14 @@ -// Used when the Fontdue site URL setting (viewer.url) is empty. -export const fallbackSiteUrl = "http://localhost:3000"; - export function notEmpty( - value: TValue | null | undefined + value: TValue | null | undefined, ): value is TValue { return value !== null && value !== undefined; } -export function pluralize(singular: string, plural: string, count: number): string { +export function pluralize( + singular: string, + plural: string, + count: number, +): string { return count === 1 ? singular : plural; }