diff --git a/e2e/blog.spec.ts b/e2e/blog.spec.ts
index 1f180c284..29eb8e2aa 100644
--- a/e2e/blog.spec.ts
+++ b/e2e/blog.spec.ts
@@ -60,4 +60,42 @@ test.describe('blog', () => {
await expect(page.locator('h1').first()).toHaveText('Blog');
}
});
+
+ test('searches correctly', async () => {
+ const input = page.getByRole('textbox');
+
+ await input.fill('Vault');
+
+ const title = page.locator('[data-testid="post-title"]', {
+ hasText: 'Getting started with aws-vault',
+ });
+
+ await expect(title).toHaveText('Getting started with aws-vault');
+
+ const otherBlogPost = page.locator('[data-testid="post-title"]', {
+ hasText: 'Code linters and formatters',
+ });
+
+ await expect(otherBlogPost).not.toBeVisible();
+
+ await expect(page).toHaveURL(`${baseUrl}/blog?title=Vault`);
+ });
+
+ test('searches correctly via visting URL param', async () => {
+ await page.goto(`${baseUrl}/blog?title=playwright`);
+ const input = page.getByRole('textbox');
+
+ await expect(input).toHaveValue('playwright');
+
+ const playwrightBlogPost = page.locator('[data-testid="post-title"]', {
+ hasText: 'Getting started with Playwright UI testing',
+ });
+
+ await expect(playwrightBlogPost).toBeVisible();
+
+ const otherBlogPost = page.locator('[data-testid="post-title"]', {
+ hasText: 'Code linters and formatters',
+ });
+ await expect(otherBlogPost).not.toBeVisible();
+ });
});
diff --git a/next-env.d.ts b/next-env.d.ts
index 40c3d6809..1b3be0840 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index cc754f945..ccbd5dede 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"next-themes": "^0.4.4",
"nodemon": "^3.1.7",
"nprogress": "^0.2.0",
+ "nuqs": "^2.2.3",
"parse-numeric-range": "^1.3.0",
"pino": "^9.5.0",
"prism-react-renderer": "^2.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 40a378212..331a7416d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -193,7 +193,7 @@ importers:
version: 5.0.0(@types/react@19.0.1)(acorn@8.14.0)(react@19.0.0)
next-sanity:
specifier: 9.8.25
- version: 9.8.25(@sanity/client@6.24.1)(@sanity/icons@3.5.2(react@19.0.0))(@sanity/types@3.66.1)(@sanity/ui@2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)))(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sanity@3.66.1(@emotion/is-prop-valid@1.3.1)(@types/node@22.10.2)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@15.0.1)(@types/react@19.0.1)(encoding@0.1.13)(react@19.0.0))(react@19.0.0)(sass@1.82.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.37.0))(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
+ version: 9.8.25(@sanity/client@6.24.1)(@sanity/icons@3.5.2(react@19.0.0))(@sanity/types@3.66.1(debug@4.4.0))(@sanity/ui@2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)))(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sanity@3.66.1(@emotion/is-prop-valid@1.3.1)(@types/node@22.10.2)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@15.0.1)(@types/react@19.0.1)(encoding@0.1.13)(react@19.0.0))(react@19.0.0)(sass@1.82.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.37.0))(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
next-seo:
specifier: ^6.6.0
version: 6.6.0(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -206,6 +206,9 @@ importers:
nprogress:
specifier: ^0.2.0
version: 0.2.0
+ nuqs:
+ specifier: ^2.2.3
+ version: 2.2.3(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react@19.0.0)
parse-numeric-range:
specifier: ^1.3.0
version: 1.3.0
@@ -425,7 +428,7 @@ importers:
version: 8.57.0
eslint-config-airbnb:
specifier: ^19.0.4
- version: 19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.2(eslint@8.57.0))(eslint@8.57.0)
+ version: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.2(eslint@8.57.0))(eslint@8.57.0)
eslint-config-next:
specifier: 15.1.0
version: 15.1.0(eslint@8.57.0)(typescript@5.7.2)
@@ -434,7 +437,7 @@ importers:
version: 9.1.0(eslint@8.57.0)
eslint-import-resolver-typescript:
specifier: ^3.6.3
- version: 3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0)
+ version: 3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0)
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
@@ -5384,9 +5387,6 @@ packages:
'@types/node@16.18.11':
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
- '@types/node@22.10.1':
- resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
-
'@types/node@22.10.2':
resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==}
@@ -9851,6 +9851,9 @@ packages:
resolution: {integrity: sha512-7PujJ3Te6GGg9lG1nfw5jYCPV6/BsoAT0nCQwb6w+ROuromXYxI6jc/CQSlD82Z/OUMSBX1SoaqhTE+vXiLQzQ==}
engines: {node: '>=4.0.0'}
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@@ -10130,6 +10133,21 @@ packages:
nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
+ nuqs@2.2.3:
+ resolution: {integrity: sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA==}
+ peerDependencies:
+ '@remix-run/react': '>=2'
+ next: '>=14.2.0'
+ react: '>=18.2.0 || ^19.0.0-0'
+ react-router-dom: '>=6'
+ peerDependenciesMeta:
+ '@remix-run/react':
+ optional: true
+ next:
+ optional: true
+ react-router-dom:
+ optional: true
+
nwsapi@2.2.7:
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
@@ -16447,7 +16465,7 @@ snapshots:
dependencies:
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
'@types/yargs': 15.0.19
chalk: 4.1.2
optional: true
@@ -18404,7 +18422,7 @@ snapshots:
'@sanity/comlink': 2.0.1
'@sanity/icons': 3.5.2(react@19.0.0)
'@sanity/logos': 2.1.13(@sanity/color@3.0.6)(react@19.0.0)
- '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1)
+ '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1(debug@4.4.0))
'@sanity/ui': 2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@sanity/uuid': 3.0.2
fast-deep-equal: 3.1.3
@@ -18440,7 +18458,7 @@ snapshots:
optionalDependencies:
react: 19.0.0
- '@sanity/preview-url-secret@2.0.5(@sanity/client@6.24.1)':
+ '@sanity/preview-url-secret@2.0.5(@sanity/client@6.24.1(debug@4.4.0))':
dependencies:
'@sanity/client': 6.24.1(debug@4.4.0)
'@sanity/uuid': 3.0.2
@@ -18561,7 +18579,7 @@ snapshots:
dependencies:
'@sanity/comlink': 2.0.1
'@sanity/mutate': 0.11.0-canary.3(xstate@5.19.0)
- '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1)
+ '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1(debug@4.4.0))
'@vercel/stega': 0.1.2
get-random-values-esm: 1.0.2
react: 19.0.0
@@ -18998,7 +19016,7 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
optional: true
'@types/body-scroll-lock@3.1.2': {}
@@ -19017,7 +19035,7 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
optional: true
'@types/conventional-commits-parser@5.0.1':
@@ -19052,7 +19070,7 @@ snapshots:
'@types/express-serve-static-core@5.0.2':
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
'@types/qs': 6.9.17
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -19167,14 +19185,10 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
'@types/node@16.18.11': {}
- '@types/node@22.10.1':
- dependencies:
- undici-types: 6.20.0
-
'@types/node@22.10.2':
dependencies:
undici-types: 6.20.0
@@ -19237,13 +19251,13 @@ snapshots:
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
optional: true
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
'@types/send': 0.17.4
optional: true
@@ -20531,7 +20545,7 @@ snapshots:
chrome-launcher@0.15.2:
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
escape-string-regexp: 4.0.0
is-wsl: 2.2.0
lighthouse-logger: 1.4.2
@@ -20542,7 +20556,7 @@ snapshots:
chromium-edge-launcher@0.2.0:
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
escape-string-regexp: 4.0.0
is-wsl: 2.2.0
lighthouse-logger: 1.4.2
@@ -21748,7 +21762,7 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
- eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0)(eslint@8.57.0):
+ eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
confusing-browser-globals: 1.0.11
eslint: 8.57.0
@@ -21757,10 +21771,10 @@ snapshots:
object.entries: 1.1.7
semver: 6.3.1
- eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.2(eslint@8.57.0))(eslint@8.57.0):
+ eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.2(eslint@8.57.0))(eslint@8.57.0):
dependencies:
eslint: 8.57.0
- eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0)(eslint@8.57.0)
+ eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0)
eslint-plugin-react: 7.37.2(eslint@8.57.0)
@@ -21806,7 +21820,7 @@ snapshots:
debug: 4.3.7(supports-color@5.5.0)
enhanced-resolve: 5.17.1
eslint: 8.57.0
- eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+ eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -21819,7 +21833,26 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
+ eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0):
+ dependencies:
+ '@nolyfill/is-core-module': 1.0.39
+ debug: 4.3.7(supports-color@5.5.0)
+ enhanced-resolve: 5.17.1
+ eslint: 8.57.0
+ eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
+ fast-glob: 3.3.2
+ get-tsconfig: 4.8.1
+ is-bun-module: 1.2.1
+ is-glob: 4.0.3
+ optionalDependencies:
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+ transitivePeerDependencies:
+ - '@typescript-eslint/parser'
+ - eslint-import-resolver-node
+ - eslint-import-resolver-webpack
+ - supports-color
+
+ eslint-module-utils@2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -21830,14 +21863,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
+ eslint-module-utils@2.11.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.12.2(eslint@8.57.0)(typescript@5.7.2)
+ eslint: 8.57.0
+ eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.12.2(eslint@8.57.0)(typescript@5.7.2)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.0)
+ eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
@@ -21852,7 +21895,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.12.2(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -23732,7 +23775,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 22.10.1
+ '@types/node': 22.10.2
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -25008,6 +25051,8 @@ snapshots:
stream-each: 1.2.3
through2: 3.0.2
+ mitt@3.0.1: {}
+
mkdirp-classic@0.5.3: {}
mkdirp@0.5.6:
@@ -25098,14 +25143,14 @@ snapshots:
next: 15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0)
react: 19.0.0
- next-sanity@9.8.25(@sanity/client@6.24.1)(@sanity/icons@3.5.2(react@19.0.0))(@sanity/types@3.66.1)(@sanity/ui@2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)))(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sanity@3.66.1(@emotion/is-prop-valid@1.3.1)(@types/node@22.10.2)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@15.0.1)(@types/react@19.0.1)(encoding@0.1.13)(react@19.0.0))(react@19.0.0)(sass@1.82.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.37.0))(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
+ next-sanity@9.8.25(@sanity/client@6.24.1)(@sanity/icons@3.5.2(react@19.0.0))(@sanity/types@3.66.1(debug@4.4.0))(@sanity/ui@2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)))(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sanity@3.66.1(@emotion/is-prop-valid@1.3.1)(@types/node@22.10.2)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@15.0.1)(@types/react@19.0.1)(encoding@0.1.13)(react@19.0.0))(react@19.0.0)(sass@1.82.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.37.0))(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0)):
dependencies:
'@portabletext/react': 3.2.0(react@19.0.0)
'@sanity/client': 6.24.1(debug@4.4.0)
'@sanity/icons': 3.5.2(react@19.0.0)
'@sanity/next-loader': 1.2.3(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react@19.0.0)
'@sanity/preview-kit': 5.1.23(@sanity/client@6.24.1)(react@19.0.0)
- '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1)
+ '@sanity/preview-url-secret': 2.0.5(@sanity/client@6.24.1(debug@4.4.0))
'@sanity/types': 3.66.1(debug@4.4.0)
'@sanity/ui': 2.10.5(@emotion/is-prop-valid@1.3.1)(react-dom@19.0.0(react@19.0.0))(react-is@18.3.1)(react@19.0.0)(styled-components@6.1.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@sanity/visual-editing': 2.10.5(@sanity/client@6.24.1)(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -25286,6 +25331,13 @@ snapshots:
nullthrows@1.1.1: {}
+ nuqs@2.2.3(next@15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0))(react@19.0.0):
+ dependencies:
+ mitt: 3.0.1
+ react: 19.0.0
+ optionalDependencies:
+ next: 15.1.0(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.82.0)
+
nwsapi@2.2.7: {}
ob1@0.81.0:
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 90db80d2f..888311496 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -91,7 +91,6 @@ export default async function AboutPage() {
))}
-
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index 901921e63..d6c63030c 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -50,6 +50,10 @@ export default async function PostPage({ params }: Props) {
placeholder="blur"
blurDataURL={imageService.urlFor(post.image.asset) ?? undefined}
alt={post.image.alt ?? post.title}
+ style={{
+ maxWidth: '100%',
+ height: 'auto',
+ }}
/>
{
+ test('renders posts', async () => {
+ mockUseSearchParams.mockReturnValue(new ReadonlyURLSearchParams('/blog'));
+
+ render();
+
+ expect(screen.getByText(posts[0].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[0].intro)).toBeInTheDocument();
+
+ expect(screen.getByText(posts[1].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[1].intro)).toBeInTheDocument();
+
+ expect(screen.getByText(posts[2].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[2].intro)).toBeInTheDocument();
+ });
+
+ test('typing in input adds to query param and filters posts', async () => {
+ const push = jest.fn();
+
+ // @ts-expect-error - we don't need to mock all the properties but TS isn't happy about that
+ mockUseRouter.mockReturnValue({ push });
+
+ mockUseSearchParams.mockReturnValue(new ReadonlyURLSearchParams('/blog'));
+
+ render();
+
+ expect(screen.getByText(posts[0].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[0].intro)).toBeInTheDocument();
+
+ expect(screen.getByText(posts[1].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[1].intro)).toBeInTheDocument();
+
+ expect(screen.getByText(posts[2].title)).toBeInTheDocument();
+ expect(screen.getByText(posts[2].intro)).toBeInTheDocument();
+
+ await userEvent.type(screen.getByRole('textbox'), 'vault');
+
+ // vault post
+ expect(screen.queryByText(posts[2].title)).toBeInTheDocument();
+ expect(screen.queryByText(posts[2].intro)).toBeInTheDocument();
+
+ // rest of posts
+ expect(screen.queryByText(posts[0].title)).not.toBeInTheDocument();
+ expect(screen.queryByText(posts[0].intro)).not.toBeInTheDocument();
+
+ expect(screen.queryByText(posts[1].title)).not.toBeInTheDocument();
+ expect(screen.queryByText(posts[1].intro)).not.toBeInTheDocument();
+ });
+});
diff --git a/src/app/blog/page.client.tsx b/src/app/blog/page.client.tsx
new file mode 100644
index 000000000..c1efe1c9b
--- /dev/null
+++ b/src/app/blog/page.client.tsx
@@ -0,0 +1,112 @@
+'use client';
+
+import Box from '@frontend/components/Box';
+import Heading from '@frontend/components/Heading';
+import Input from '@frontend/components/Input';
+import PostItem from '@frontend/components/PostItem';
+import Spacer from '@frontend/components/Spacer';
+import { Post } from '@frontend/types/sanity';
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { ChangeEvent, useCallback, useState } from 'react';
+
+interface Props {
+ posts: Post[];
+}
+
+export default function PostsClient({ posts }: Props) {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const pathname = usePathname();
+ const [query, setQuery] = useState({
+ title: searchParams.get('title') || '',
+ });
+
+ const createQueryString = useCallback(
+ (name: string, value: string) => {
+ const params = new URLSearchParams(searchParams.toString());
+ if (value) {
+ params.set(name, value);
+ } else {
+ params.delete(name);
+ }
+
+ return params.toString();
+ },
+ [searchParams],
+ );
+
+ const handleInputChange = (e: ChangeEvent) => {
+ const { name, value } = e.target;
+
+ setQuery(prevState => ({
+ ...prevState,
+ [name]: value,
+ }));
+
+ const queryString = createQueryString(name, value);
+
+ router.push(`${pathname}?${queryString}`);
+ };
+
+ const filteredPosts = posts
+ .filter(post => {
+ return post.title.toLowerCase().includes(query.title.toLowerCase());
+ })
+ .sort((a, b) => {
+ if (a.publishedAt < b.publishedAt) {
+ return 1;
+ }
+
+ if (a.publishedAt > b.publishedAt) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ const postsByYear: Record = {};
+
+ filteredPosts.forEach(post => {
+ const year = new Date(post.publishedAt).getFullYear();
+
+ if (!postsByYear[year]) {
+ postsByYear[year] = [];
+ }
+
+ postsByYear[year].push(post);
+ });
+
+ const sortedYears = Object.keys(postsByYear).sort(
+ (a, b) => Number(b) - Number(a),
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ {sortedYears.map(year => (
+
+
+ {year}
+
+
+ {postsByYear[year].map(post => (
+
+ ))}
+
+ ))}
+
+ >
+ );
+}
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index f4f2542b9..ea679374b 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -1,12 +1,12 @@
import Box from '@frontend/components/Box';
import Heading from '@frontend/components/Heading';
import Page from '@frontend/components/Page';
-import PostItem from '@frontend/components/PostItem';
import Spacer from '@frontend/components/Spacer';
import Text from '@frontend/components/Text';
import postService from '@frontend/services/postService';
-import { Post } from '@frontend/types/sanity';
import { Metadata } from 'next';
+import { Suspense } from 'react';
+import PostsClient from './page.client';
export const revalidate = 1800;
@@ -19,34 +19,6 @@ export const metadata: Metadata = {
export default async function BlogPage() {
const posts = await postService.getAllPosts();
- const allPosts = posts.sort((a, b) => {
- if (a.publishedAt < b.publishedAt) {
- return 1;
- }
-
- if (a.publishedAt > b.publishedAt) {
- return -1;
- }
-
- return 0;
- });
-
- const postsByYear: Record = {};
-
- allPosts.forEach(post => {
- const year = new Date(post.publishedAt).getFullYear();
-
- if (!postsByYear[year]) {
- postsByYear[year] = [];
- }
-
- postsByYear[year].push(post);
- });
-
- const sortedYears = Object.keys(postsByYear).sort(
- (a, b) => Number(b) - Number(a),
- );
-
return (
@@ -59,19 +31,9 @@ export default async function BlogPage() {
-
- {sortedYears.map(year => (
-
-
- {year}
-
-
- {postsByYear[year].map(post => (
-
- ))}
-
- ))}
-
+
+
+
);
}
diff --git a/src/app/projects/[slug]/page.tsx b/src/app/projects/[slug]/page.tsx
index fa009cba8..027063431 100644
--- a/src/app/projects/[slug]/page.tsx
+++ b/src/app/projects/[slug]/page.tsx
@@ -49,6 +49,10 @@ export default async function ProjectPage({ params }: Props) {
placeholder="blur"
blurDataURL={imageService.urlFor(project.image.asset)}
alt={project.image.alt ?? project.title}
+ style={{
+ maxWidth: '100%',
+ height: 'auto',
+ }}
/>
;
+
+export default function Input(props: InputProps) {
+ return ;
+}
diff --git a/src/components/NowPlaying/index.tsx b/src/components/NowPlaying/index.tsx
index 70c5465b8..68c6057ca 100644
--- a/src/components/NowPlaying/index.tsx
+++ b/src/components/NowPlaying/index.tsx
@@ -37,11 +37,12 @@ export default function NowPlaying() {
blurDataURL={data.albumImageUrl}
placeholder="blur"
alt="Album cover"
- layout="intrinsic"
width={65}
height={65}
style={{
borderRadius: '7px',
+ maxWidth: '100%',
+ height: 'auto',
}}
/>
diff --git a/src/components/ProjectItem/index.tsx b/src/components/ProjectItem/index.tsx
index 8d65a7526..6e8315adf 100644
--- a/src/components/ProjectItem/index.tsx
+++ b/src/components/ProjectItem/index.tsx
@@ -57,7 +57,6 @@ export default function ProjectItem({ project }: Props) {
>
-
{
height={300}
style={{
marginBottom: variables.spacing.sm,
+ maxWidth: '100%',
+ height: 'auto',
}}
/>
)}
diff --git a/src/test/appRouterMock.tsx b/src/test/appRouterMock.tsx
new file mode 100644
index 000000000..be3726d09
--- /dev/null
+++ b/src/test/appRouterMock.tsx
@@ -0,0 +1,33 @@
+// app-router-context-provider-mock.tsx
+
+import {
+ AppRouterContext,
+ AppRouterInstance,
+} from 'next/dist/shared/lib/app-router-context.shared-runtime';
+import React from 'react';
+
+export type AppRouterContextProviderMockProps = {
+ router: Partial;
+ children: React.ReactNode;
+};
+
+export const AppRouterContextProviderMock = ({
+ router,
+ children,
+}: AppRouterContextProviderMockProps): React.ReactNode => {
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
+ const mockedRouter: AppRouterInstance = {
+ back: jest.fn(),
+ forward: jest.fn(),
+ push: jest.fn(),
+ replace: jest.fn(),
+ refresh: jest.fn(),
+ prefetch: jest.fn(),
+ ...router,
+ };
+ return (
+
+ {children}
+
+ );
+};
diff --git a/test/setupGlobals.js b/test/setupGlobals.js
index 8b1e831ad..b4ea870c1 100644
--- a/test/setupGlobals.js
+++ b/test/setupGlobals.js
@@ -10,7 +10,7 @@ const trueForNonBoolProp2 = /Warning: Received*/;
const radixUiDialog =
/`DialogContent` requires a `DialogTitle` for the component to be accessible for screen reader users./;
const radixUiDialogWarn =
- /Warning: Missing `Description` or `aria-describedby={undefined}` for {DialogContent}./;
+ /Warning: Missing `Description` or `aria-describedby={undefined}` for {DialogContent}*/;
beforeAll(() => {
console.error = (...args) => {
@@ -19,14 +19,20 @@ beforeAll(() => {
forwardRefWarning.test(args[0]) ||
trueForNonBoolProp.test(args[0]) ||
trueForNonBoolProp2.test(args[0]) ||
- radixUiDialog.test(args[0]) ||
- radixUiDialogWarn.test(args[0])
+ radixUiDialog.test(args[0])
) {
return;
}
originalError.call(console, ...args);
};
+ console.warn = (...args) => {
+ if (radixUiDialogWarn.test(args[0])) {
+ return;
+ }
+ originalError.call(console, ...args);
+ };
+
Element.prototype.scrollIntoView = jest.fn();
window.scroll = jest.fn();
diff --git a/test/setupTests.js b/test/setupTests.js
index 76ab6b555..e67dfca16 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -13,24 +13,12 @@ jest.mock('nanoid', () => {
};
});
-// jest.mock('next/navigation', () => ({
-// __esModule: true,
-// useRouter: () => ({
-// push: jest.fn(),
-// replace: jest.fn(),
-// prefetch: jest.fn(),
-// isFallback: false,
-// }),
-// useSearchParams: () => ({
-// get: () => {},
-// }),
-// }));
-
if (typeof window !== 'undefined') {
// fetch polyfill for making API calls.
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('cross-fetch');
}
+
global.Request = jest.requireActual('node-fetch').Request;
global.Response = jest.requireActual('node-fetch').Response;