Compare commits

..
Author SHA1 Message Date
Halvor Haugan d86b50ece4 Merge branch 'main' into copybutton-title 2026-08-28 12:19:05 +02:00
Halvor Haugan ae0ccea41f feat(Combobox2): small sizing for trigger, margin for desc, hideLabel, readOnly, stories cleanup 2026-08-28 10:33:30 +02:00
Halvor Haugan 6b11dfae6a chore(deps): bump storybook to 10.4.9 2026-08-27 11:51:54 +02:00
dependabot[bot] 487db1594b ⬆️ Bump dompurify from 3.4.12 to 3.4.13 (#5209)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 09:17:47 +02:00
dependabot[bot] 2df8aa4d3d ⬆️ Bump js-yaml from 5.2.3 to 5.3.0 (#5208)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-26 09:17:15 +02:00
Halvor Haugan 958e8ab9df chore(deps): bump @slack/web-api to 8.0.0 (#5210) 2026-08-25 21:15:27 +02:00
Halvor Haugan fd4993126d chore(deps): bump @google-cloud/bigquery to 9.0.2 2026-08-25 20:29:23 +02:00
Halvor Haugan b13ac9f1ea refactor(Combobox2): Item -> Option 2026-08-25 15:07:29 +02:00
Anniken GranbergandHalvor Haugan 046226a155 [Website] Add Lookup in table example (#5207)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-08-25 13:54:18 +02:00
Halvor Haugan 6c356c9708 feat(website): adjust examples desc Tag + Pagination 2026-08-25 13:42:01 +02:00
Halvor Haugan db3d267fc1 feat(core): Combobox 2.0 WIP 2026-08-25 13:37:21 +02:00
c7848d39fb Add lookup to sanity (#5203)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-08-25 09:34:55 +02:00
dependabot[bot]andHalvor Haugan a801fb0c45 ⬆️ Bump @axe-core/playwright from 4.11.0 to 4.13.0 (#5206)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-08-24 10:46:06 +02:00
Halvor Haugan 2dc8306762 feat(Listbox): new design + small variant 2026-08-21 17:03:00 +02:00
Halvor Haugan b9b63c9dbd feat(website): update descriptions of ExpansionCard examples 2026-08-21 14:13:14 +02:00
Halvor Haugan 2e39afd6d1 chore(deps): bump biome to 2.5.9 2026-08-21 10:48:26 +02:00
Halvor Haugan 53111a7a76 chore(deps): bump undici 7.28.0 -> 7.29.0 2026-08-21 10:39:23 +02:00
Halvor Haugan 20c59e3eef chore(deps): bump js-yaml 4.1.1->4.3.1 and 3.14.2->3.15.1 2026-08-21 10:37:37 +02:00
Halvor Haugan b0fea1ebdf chore(deps): bump brace-expansion to 2.1.4/5.0.9 2026-08-21 10:34:23 +02:00
Halvor Haugan babe31426a chore(deps): bump postcss to 8.5.26 (mostly) 2026-08-21 09:58:56 +02:00
Halvor Haugan 0c9fbe3991 chore(deps): bump shell-quote to 1.9.0 2026-08-21 09:54:15 +02:00
Halvor Haugan 2b22fdbfcf chore(deps): bump @modelcontextprotocol/inspector to 2.2.0 2026-08-20 15:28:51 +02:00
dependabot[bot]andHalvor Haugan 0ee0bafed9 ⬆️ Bump prettier from 3.8.3 to 3.9.6 (#5199)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-08-20 15:09:09 +02:00
Halvor Haugan 694bf00fcf build(aksel-icons): fix prettier script (#5201) 2026-08-20 11:38:51 +02:00
Halvor Haugan b12bb29992 feat(core): export useDeferredValue from /hooks 2026-08-18 15:05:29 +02:00
Halvor Haugan b8f2a7d0ef feat(Listbox): HTML attributes, forwardRef, preventDefault (#5202) 2026-08-18 15:02:48 +02:00
Halvor Haugan 54bcfcfccc types.d.ts: CRLF -> LF 2026-08-17 12:04:16 +02:00
dependabot[bot] 2bcd7a22f3 ⬆️ Bump @vitest/eslint-plugin from 1.6.23 to 1.6.27 in the vitest group (#5197)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 09:12:22 +02:00
8a1885f599 Add toggle show more in preview box in sanity and website (#5188)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-08-14 12:24:10 +02:00
Anniken Granberg b256af537b Remove APIKEY (#5193) 2026-08-13 15:48:05 +02:00
Halvor Haugan 185bbab0d7 fix(website): adjust descriptions of 5XX template 2026-08-13 11:33:02 +02:00
Halvor Haugan 68f0c6426e build(nais): add quotes around "{{ image }}" so that prettier doesn't change it (#5192) 2026-08-13 09:37:03 +02:00
Halvor Haugan bb8aa1b2ed chore(deps): bump nanoid 2026-08-12 14:12:33 +02:00
Halvor Haugan 542745540d chore(deps): bump concurrently to 10.0.4 and mcp-inspector to 2.1.0 (#5189) 2026-08-12 14:10:06 +02:00
Halvor Haugan 106eaea853 chore(deps): bump @modelcontextprotocol/inspector to v1 2026-08-12 12:42:44 +02:00
Halvor Haugan 522032bba0 fix(eslint): fix error in local eslint rule "args-check" (#5170) 2026-08-10 15:49:31 +02:00
Halvor Haugan 99c6407436 fix(website): adjust validation error messages for feedback form (#5187) 2026-08-10 14:14:14 +02:00
Halvor Haugan 1198d59048 fix(mcp): fix formatting error in app.yaml 2026-08-10 13:34:56 +02:00
dependabot[bot]andHalvor Haugan 548c449741 ⬆️ Bump the react group across 1 directory with 2 updates (#5158)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-08-10 13:02:45 +02:00
dependabot[bot] 5b08975f23 ⬆️ Bump hono from 4.12.33 to 4.12.34 (#5185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 09:30:23 +02:00
Halvor Haugan 0779430ae2 fix(skill): fix formatting errors in ds-component-restructure skill (#5180) 2026-08-10 09:28:04 +02:00
dependabot[bot] 8d3e99e3bb ⬆️ Bump nanoid from 3.3.11 to 3.3.16 (#5182)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 08:58:31 +02:00
dependabot[bot] f8951a9978 ⬆️ Bump @vitejs/plugin-react from 6.0.3 to 6.0.5 (#5183)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 08:57:53 +02:00
Halvor Haugan 387856cb92 fix(website): update descs of 404 template examples 2026-08-07 18:44:17 +02:00
Halvor Haugan e1f2ca0610 fix(website): Github -> GitHub 2026-08-07 18:41:38 +02:00
Halvor Haugan 547b1b71b6 refactor(metadata): adjust naming in parse-meta.ts 2026-08-07 18:29:06 +02:00
dependabot[bot] 02ccaf7080 ⬆️ Bump fast-uri from 3.1.4 to 3.1.5 (#5179)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 16:00:20 +02:00
dependabot[bot] 34c87357ee ⬆️ Bump hono from 4.12.32 to 4.12.33 (#5178)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 16:00:04 +02:00
dependabot[bot] 6f099025a4 ⬆️ Bump brace-expansion from 1.1.16 to 1.1.18 (#5177)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 15:59:16 +02:00
Halvor Haugan daf9d1241f Bump tar to 7.5.22 2026-08-07 15:44:07 +02:00
Anniken Granberg 853fcac461 fix link (#5176) 2026-08-05 09:53:06 +02:00
Halvor Haugan 6aa6c2fa3f ⬆️ Bump styled-components from 6.4.1 to 6.4.4 (#5174) 2026-08-04 11:59:35 +02:00
dependabot[bot] 09bacb3fae ⬆️ Bump undici from 6.27.0 to 6.28.0 (#5172)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:55:06 +02:00
dependabot[bot] 4c01c76cd4 ⬆️ Bump hono from 4.12.30 to 4.12.32 (#5171)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:53:59 +02:00
dependabot[bot]andHalvor Haugan 82419f32d9 ⬆️ Bump typescript-eslint from 8.59.0 to 8.65.0 (#5159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-08-04 09:28:03 +02:00
Halvor Haugan 45f33f5da0 fix(git-release): fix incorrect relative path after moving file 2026-08-04 09:23:16 +02:00
dependabot[bot] bf298a9335 ⬆️ Bump @hono/node-server from 1.19.14 to 1.19.17 (#5167)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:15:04 +02:00
dependabot[bot] fa9e4762f0 ⬆️ Bump ip-address from 10.2.0 to 10.3.1 (#5168)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:14:01 +02:00
dependabot[bot] caa85ce68c ⬆️ Bump js-yaml from 5.2.2 to 5.2.3 (#5169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:11:22 +02:00
Halvor Haugan ff48125546 Merge branch 'main' into copybutton-title 2026-08-03 14:32:05 +02:00
KenandCopilot Autofix powered by AI e192281e00 refactor(workspace): rename scripts-dir tooling, moved scripts to scripts dir inside tooling (#5164)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-30 14:20:04 +02:00
dependabot[bot] 26d6dd7bd4 ⬆️ Bump browserslist from 4.28.4 to 4.28.7 (#5161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 11:37:22 +02:00
dependabot[bot] 7d4fb6a908 ⬆️ Bump @portabletext/block-tools from 5.1.1 to 5.1.12 in the sanity group (#5157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 11:37:09 +02:00
Ken d8fa45c316 refactor: re-written changelog script from deno -> node (#5163) 2026-07-30 10:10:19 +02:00
Ken af0b1037cd Moved playroom to apps-dir (#5162) 2026-07-29 15:15:55 +02:00
Ken fa64defe3a MCP-server moved into /apps (#5156) 2026-07-29 14:41:33 +02:00
KenandCopilot Autofix powered by AI e333343c4c refactor: moved figma icon plugin into dedicated apps-dir (#5155)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-29 14:25:37 +02:00
Ken 2a8e403b39 [Datepicker, Monthpicker] Directory refactor (#5154) 2026-07-29 13:51:15 +02:00
github-actions[bot] 6b819a8fa8 Version Packages (#5153)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-29 11:06:55 +02:00
Ken a322476d33 chore: forced update to sync GPR release 2026-07-29 10:11:22 +02:00
Ken a19ae363bf fix: make sure playroom is ignored by changeset 2026-07-29 10:04:10 +02:00
github-actions[bot] 1aa690d35f Version Packages (#5053)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-29 09:42:22 +02:00
Ken 1cadfc6828 feat(website): added tags for endringslogg article links 2026-07-28 17:10:20 +02:00
Ken 6051e1346c refactor(website): link-card usage examples update 2026-07-28 16:48:27 +02:00
Ken 047a671794 fix(website): sync playwright versions 2026-07-28 16:40:18 +02:00
Ken e38a932812 fix(website): invalid resource request value 2026-07-28 13:14:24 +02:00
Ken 122291dec1 fix(website): fixed args extraction from examples 2026-07-28 13:08:21 +02:00
Ken 525aa0770f perf(website): re-add cachehandler, up max memory 2026-07-28 12:56:11 +02:00
dependabot[bot] 968446721f ⬆️ Bump stylelint from 17.14.0 to 17.14.1 (#5151)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 12:35:45 +02:00
dependabot[bot]andKen 5f28b8f1e7 ⬆️ Bump husky from 8.0.3 to 9.1.7 (#5149)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-28 11:54:08 +02:00
dependabot[bot]andKen 74cfcef435 ⬆️ Bump @biomejs/biome from 2.4.15 to 2.5.5 (#5148)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-28 11:48:14 +02:00
Copilot 97c8e4a7d9 fix: add explicit permissions to sync_data_with_sanity job in deploy-prod-gcp workflow (#5147)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-28 11:34:19 +02:00
Ken c6d33e5dcb fix(website): fix dependency issue 2026-07-28 11:29:56 +02:00
Ken 2f3d5c919f perf(website): optimize memory and cache for website 2026-07-28 11:27:53 +02:00
dependabot[bot]andKen c54d285d33 ⬆️ Bump react-day-picker from 9.14.0 to 10.0.1 (#5143)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-28 10:20:29 +02:00
Ken 85057d7703 chore: date-fns update 2026-07-28 10:02:17 +02:00
dependabot[bot] a2f219f70b ⬆️ Bump @navikt/oasis from 3.2.1 to 4.1.4 (#5141)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 09:59:01 +02:00
dependabot[bot]andKen 1ebe0297eb ⬆️ Bump @figma/rest-api-spec from 0.35.0 to 0.41.0 (#5144)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-28 09:58:04 +02:00
dependabot[bot] 8fea6758f6 ⬆️ Bump tsc-alias from 1.8.16 to 1.9.1 (#5146)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 15:28:26 +02:00
Ken 3799489367 fix: fix zod v4 error types 2026-07-27 15:27:41 +02:00
dependabot[bot] 0944a9b3b7 ⬆️ Bump zod from 3.25.76 to 4.4.3 (#5145)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 15:21:29 +02:00
dependabot[bot] 0bb9b6b399 ⬆️ Bump sharp from 0.35.2 to 0.35.3 (#5140)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 15:01:41 +02:00
dependabot[bot] dc25944245 ⬆️ Bump lightningcss from 1.32.0 to 1.33.0 (#5135)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 14:53:57 +02:00
dependabot[bot] 614b5e310c ⬆️ Bump swr from 2.4.1 to 2.4.2 (#5137)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 14:53:01 +02:00
dependabot[bot] c6d527fd7c ⬆️ Bump pino from 8.21.0 to 10.3.1 (#5138)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 14:52:45 +02:00
Ken 145fbbadb0 feat(core): update tokenfilter css 2026-07-27 14:50:07 +02:00
Ken 97fe3a452e feat(core): add support for tags and multiselect 2026-07-27 14:50:07 +02:00
Ken e7608d25bf feat(core): tokenfilter freetext-input 2026-07-27 14:50:06 +02:00
Ken 98284c65ed refactor(core): update propnames tokenfilter 2026-07-27 14:50:06 +02:00
Ken d0e126f4f9 refactor(core): fix tokenfilter bugs 2026-07-27 14:50:06 +02:00
dependabot[bot] f986968c48 ⬆️ Bump vite from 8.0.16 to 8.1.5 (#5139)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:42:40 +02:00
dependabot[bot] 6e9adf7614 ⬆️ Bump sharp from 0.35.1 to 0.35.2 (#5134)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:20:51 +02:00
Ken a4338b178b chore(website): update react-rx 2026-07-27 11:14:28 +02:00
Ken b5f557f4d4 Merge branch 'main' into copybutton-title 2026-07-27 11:02:03 +02:00
dependabot[bot] b740ac8601 ⬆️ Bump react-docgen-typescript from 2.2.2 to 2.4.0 (#5125)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 10:57:19 +02:00
dependabot[bot]andKen 8e62aeca68 ⬆️ Bump react-router from 7.18.0 to 8.3.0 (#5133)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-27 10:16:31 +02:00
dependabot[bot] f42730b230 ⬆️ Bump react-responsive-masonry from 2.2.0 to 2.7.2 (#5128)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 09:54:31 +02:00
dependabot[bot] e6f81f2e1f ⬆️ Bump fast-uri from 3.1.3 to 3.1.4 (#5130)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 09:53:56 +02:00
dependabot[bot] 5f647cf93f ⬆️ Bump sharp from 0.35.0 to 0.35.1 (#5131)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 09:53:45 +02:00
dependabot[bot] cbce3048d2 ⬆️ Bump tar from 7.5.21 to 7.5.22 (#5132)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 09:53:30 +02:00
dependabot[bot] 15f92a748b ⬆️ Bump pino-pretty from 10.3.1 to 13.1.3 (#5126)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 09:38:04 +02:00
dependabot[bot] b03287d3a7 ⬆️ Bump sharp from 0.33.5 to 0.35.0 (#5124)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:37:58 +02:00
dependabot[bot] ee50d0c558 ⬆️ Bump body-parser from 1.20.4 to 1.20.6 (#5122)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:36:19 +02:00
dependabot[bot] 762f537d3a ⬆️ Bump webpack-dev-server from 5.2.5 to 5.2.6 (#5121)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:36:09 +02:00
dependabot[bot] 7eea822e9c ⬆️ Bump next from 16.2.6 to 16.2.11 (#5120)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:34:27 +02:00
dependabot[bot] 928ca6be51 ⬆️ Bump axios from 1.16.1 to 1.18.1 (#5119)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:34:16 +02:00
dependabot[bot] bfdac6890f ⬆️ Bump commander from 14.0.3 to 15.0.0 (#5114)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:23:27 +02:00
dependabot[bot] d774c15fc9 ⬆️ Bump @navikt/analytics-types from 0.0.7 to 0.0.9 (#5115)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:21:32 +02:00
dependabot[bot] 9cc7ab2ae7 ⬆️ Bump colorjs.io from 0.6.1 to 0.7.0 (#5117)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:20:53 +02:00
dependabot[bot] fd6bc791de ⬆️ Bump postcss-selector-parser from 7.1.1 to 7.1.4 (#5118)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 17:20:22 +02:00
dependabot[bot] 5a6a5225a4 ⬆️ Bump the sanity group with 3 updates (#5113)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:48:50 +02:00
Ken 43d5c8fcb4 fix: dependabot workspace resolve fix 2026-07-24 16:41:36 +02:00
dependabot[bot]andKen a45cdd6390 ⬆️ Bump tsx from 4.21.0 to 4.23.1 (#5106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-24 16:34:10 +02:00
dependabot[bot]andKen 48a38c6ef0 ⬆️ Bump playwright from 1.60.0 to 1.61.1 (#5105)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-24 16:33:18 +02:00
Ken 29feacba0a fix: make dependabot update all workspaces at once 2026-07-24 16:30:48 +02:00
dependabot[bot] 31860d1ca7 ⬆️ Bump stylelint-test-rule-node from 1.1.0 to 1.2.0 (#5104)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:54:00 +02:00
dependabot[bot] a8916c14a5 ⬆️ Bump @figma/plugin-typings from 1.130.0 to 1.131.0 (#5107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:53:49 +02:00
dependabot[bot]andKen f7cd660199 ⬆️ Bump concurrently from 9.2.1 to 10.0.3 (#5102)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-24 13:31:17 +02:00
dependabot[bot] 9abb0d51fe ⬆️ Bump @changesets/cli from 2.31.0 to 2.31.1 (#5103)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:30:02 +02:00
dependabot[bot] ed02cc40ba ⬆️ Bump rolldown from 1.0.2 to 1.2.0 (#5101)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:29:33 +02:00
dependabot[bot]andKen f476d89c42 ⬆️ Bump jscodeshift from 17.3.0 to 17.4.0 (#5100)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-24 13:29:03 +02:00
Ken 4f31888662 fix: fix dependabot docker config 2026-07-24 12:23:32 +02:00
dependabot[bot] a35ae0881d ⬆️ Bump react-router from 7.15.1 to 7.18.0 (#5098)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:16:09 +02:00
dependabot[bot] b494fd686f ⬆️ Bump js-yaml from 5.2.1 to 5.2.2 (#5097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:15:38 +02:00
dependabot[bot] d903b6401e ⬆️ Bump fast-uri from 3.1.2 to 3.1.3 (#5088)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:08:14 +02:00
dependabot[bot] 7acab5259e ⬆️ Bump fast-xml-parser from 5.10.0 to 5.10.1 (#5089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:08:05 +02:00
dependabot[bot] 9e5c3fa279 ⬆️ Bump hono from 4.12.25 to 4.12.30 (#5090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:07:56 +02:00
dependabot[bot] 90e3f8d53f ⬆️ Bump tar from 7.5.20 to 7.5.21 (#5091)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:07:48 +02:00
dependabot[bot] 1f0077a210 ⬆️ Bump ws from 7.5.11 to 7.5.12 (#5092)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:07:32 +02:00
dependabot[bot] 884dea8c19 ⬆️ Bump svgo from 3.3.3 to 3.3.4 (#5093)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:07:23 +02:00
dependabot[bot] 008f8e8655 ⬆️ Bump dompurify from 3.4.11 to 3.4.12 (#5094)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:07:15 +02:00
dependabot[bot] 035571ec5e ⬆️ Bump linkify-it from 5.0.1 to 5.0.2 (#5095)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:06:59 +02:00
Ken 095d6fb4f5 Nextjs cache-components (#5087) 2026-07-23 12:30:05 +02:00
Ken c908466b52 feat: skill next-cache-component-adoption 2026-07-22 10:22:15 +02:00
dependabot[bot] d82d5036c4 ⬆️ Bump brace-expansion from 1.1.14 to 1.1.16 (#5081)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 15:24:20 +02:00
dependabot[bot] 5b7b528080 ⬆️ Bump adm-zip from 0.5.17 to 0.6.0 (#5080)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 15:23:54 +02:00
dependabot[bot]andKen d93adb00d6 ⬆️ Bump actions/setup-node from 6 to 7 in the actions group (#5079)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-21 15:22:47 +02:00
dependabot[bot] 5f6894116a ⬆️ Bump chromatic from 15.1.0 to 18.0.1 (#5078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 15:21:23 +02:00
Ken 8cc7a185ee chore(core): update floating-ui deps 2026-07-21 15:19:49 +02:00
Ken dfe8ed9fe2 chore: update tar 2026-07-21 15:16:30 +02:00
73a211a22a ⬆️ Bump the vitest group across 1 directory with 5 updates (#5075)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-21 15:15:05 +02:00
KenandCopilot Autofix powered by AI 700239f080 refactor(website): Cleanup of legacy components/types no longer used (#5084)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-21 14:23:04 +02:00
Ken b81d2a8427 chore(website): update playroom to 1.3.0 2026-07-21 14:11:18 +02:00
KenandCopilot Autofix powered by AI cf93e2452c Extracted studio to separate dir (#5083)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-21 13:40:23 +02:00
Ken 280a722f45 fix(website): removed duplicate sitemap entry 2026-07-21 12:39:55 +02:00
Ken 1cda5fc3f1 refactor(website): improved typegen and auth for sanity (#5073) 2026-07-17 10:02:27 +02:00
Ken 941810966c refactor(website): sanity structure display (#5072) 2026-07-17 10:02:16 +02:00
Ken c31f5da9c2 feat: added sanity skills 2026-07-17 08:38:43 +02:00
Ken 0c48dc3f75 chore: removed unused dep tanstack table 2026-07-17 08:29:00 +02:00
Ken 86969e7270 chore(core): removed unused dependency @tanstack/react-table 2026-07-17 08:29:00 +02:00
dependabot[bot] 1ed1bce97b ⬆️ Bump websocket-driver from 0.7.4 to 0.7.5 (#5070)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-17 08:17:38 +02:00
Ken 4c01a2fd9a chore(core): moved panel to legacy dir 2026-07-16 19:42:16 +02:00
KenandCopilot Autofix powered by AI 05c5fe71c2 Linting cleanup (#5071)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 19:40:02 +02:00
Ken c961c2dcc1 perf(core): memorization in provider 2026-07-15 16:09:00 +02:00
Ken 4100aad964 refactor(core): all stories are now pascalcase (#5069) 2026-07-15 16:02:26 +02:00
Ken 0212302041 refactor(core): simplified react tsc build (#5068) 2026-07-15 12:33:39 +02:00
Ken 227e5d313e perf(core): optimized baseprimitive render by 1.5-2x (#5066) 2026-07-15 10:18:09 +02:00
Ken efa4f05c56 perf(core): optimized Slot 2026-07-14 12:49:50 +02:00
Ken 5f8acb0115 refactor(core): re-structure Box primitive 2026-07-14 11:39:48 +02:00
Ken b9a8552b9c refactor(core): alert-setup re-structure (#5065) 2026-07-14 11:14:01 +02:00
Ken fe329758b8 feat(core): auto-scale column-width based on document font-size (#5054) 2026-07-14 10:38:07 +02:00
KenandCopilot Autofix powered by AI ef3cefef48 refactor(core): Multiple component re-structures (#5064)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 16:30:48 +02:00
KenandCopilot Autofix powered by AI d704993906 refactor(ActionMenu): Re-structure component (#5063)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 16:12:59 +02:00
6be6328ef1 ⬆️ Bump fuse.js from 7.4.1 to 7.4.2 (#5062)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-13 11:38:58 +02:00
5edae5d4f3 ⬆️ Bump js-yaml from 4.1.1 to 5.2.1 (#5061)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-07-13 11:37:15 +02:00
dependabot[bot]andKen ad095960cc ⬆️ Bump @figma/plugin-typings from 1.128.0 to 1.130.0 (#5060)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-07-13 11:33:24 +02:00
dependabot[bot]andKen d1720010a9 ⬆️ Bump lint-staged from 16.4.0 to 17.0.8 (#5059)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-07-13 11:31:40 +02:00
dependabot[bot]andKen 9d96e4db0e ⬆️ Bump stylelint from 17.9.0 to 17.14.0 (#5058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-07-13 11:29:44 +02:00
Ken 875c5fcc84 refactor(Accordion): Update structure and import/exports (#5057) 2026-07-13 11:27:11 +02:00
Ken 1fb3064f30 chore: sync refactor skill 2026-07-10 18:55:44 +02:00
Ken 9506e93017 refactor(core): removed legacy docgen implementation 2026-07-10 13:10:22 +02:00
KenandCopilot Autofix powered by AI bdaeac40e7 refactor(website): Clean up legacy props (#5056)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-10 12:21:00 +02:00
Ken 5fcbe84cb9 feat(website): theme-component examples 2026-07-10 11:29:46 +02:00
Ken 52536bea07 feat(website): added utilities as a category for komponenter 2026-07-10 09:43:52 +02:00
KenandCopilot Autofix powered by AI f23a389861 feat(website): Use new metadata link to render prop (#5055)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 19:04:09 +02:00
github-actions[bot] dcc69da50b Version Packages (#5038)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-09 09:27:24 +02:00
KenandCopilot Autofix powered by AI c61bd21b84 feat: New props/metadata link between react -> website (#5052)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 09:26:51 +02:00
Ken f08b1d7efe feat(website): add umami tracking to new contact cards 2026-07-08 15:22:03 +02:00
Ken a78e161a99 chore(core): small text-updates to ds-docs update skill 2026-07-07 09:17:10 +02:00
Ken 34ecb612dd fix(website): sanity cleanup 2026-07-07 08:57:34 +02:00
KenandCopilot Autofix powered by AI eaa280b995 [Website] Updated header and footer section for ds-pages (#5051)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-06 16:36:32 +02:00
Ken 2cf8586fc4 fix(core): fix broken type import 2026-07-06 14:16:05 +02:00
Ken 9a660af27c fix(core): fix broken react-build after tsconfig update 2026-07-06 14:14:20 +02:00
Ken f7361957b6 feat(website): added dates to articles on produktbloggen frontpage after feedback 2026-07-06 14:10:42 +02:00
dependabot[bot] cc0572d9ff ⬆️ Bump @vitejs/plugin-react from 6.0.1 to 6.0.3 (#5046)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 11:41:19 +02:00
dependabot[bot] d92b812604 ⬆️ Bump browserslist from 4.28.2 to 4.28.4 (#5045)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 11:40:55 +02:00
dependabot[bot] bb768c7c40 ⬆️ Bump globals from 17.6.0 to 17.7.0 (#5043)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 11:40:36 +02:00
KenandCopilot Autofix powered by AI cce5ac1631 feat(website): added args linter for examples (#5042)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-06 11:40:01 +02:00
Ken c53714148f perf(i18n): optimize useI18n hook (#5037) 2026-07-06 11:17:00 +02:00
Ken 26c1a5635f Summer cleanup (#5049) 2026-07-06 11:13:07 +02:00
Ken 662e4fef66 feat(core): new icons timeline (#5050) 2026-07-06 11:10:53 +02:00
82f086423a [Datagrid] Preferences component (#5030)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-07-03 10:27:24 +02:00
dependabot[bot] f2180d4265 ⬆️ Bump json-2-csv from 5.5.10 to 5.5.11 (#5040)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-03 10:02:07 +02:00
Ken e3c6ebb0d7 feat(workspace): Removed example-dir, added web-component decorator to SB (#5041) 2026-07-03 09:59:39 +02:00
Halvor Haugan 5156c07140 chore(biome): activate rule useImportType (#5039) 2026-07-02 12:00:29 +02:00
KenandHalvor Haugan e6dd922d8c feat(tailwind): Tw v4 config (#5036)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-07-01 13:43:13 +02:00
Ken 69f137e01e chore(core): clean up legacy _mappings implementation (#5035) 2026-07-01 12:33:10 +02:00
016cfd8e2b personvernerklaering: bytt Umami med Innblikk (#4965)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-30 14:19:16 +02:00
dependabot[bot]andKen bf22d22271 ⬆️ Bump the vitest group across 1 directory with 4 updates (#5027)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-30 14:10:55 +02:00
dependabot[bot] 2be954d706 ⬆️ Bump the storybook group with 9 updates (#5026)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 14:08:47 +02:00
dependabot[bot] e657befeba ⬆️ Bump http-proxy-middleware from 2.0.9 to 2.0.10 (#5034)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 14:07:01 +02:00
dependabot[bot]andKen 73bb87ce03 ⬆️ Bump @sanity/client from 7.22.1 to 7.23.0 in the sanity group (#5025)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-30 13:57:07 +02:00
Ken 7f1111d110 [Lookup] Focus border, text-decoration and example update (#5032) 2026-06-30 13:14:37 +02:00
Ken 8e00ffab9d fix: 4px gap for small inlinemessage (#5031) 2026-06-30 13:11:46 +02:00
dependabot[bot] de4b01bd80 ⬆️ Bump tsdown from 0.22.0 to 0.22.3 (#5028)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 12:46:59 +02:00
Halvor Haugan 88ca846ebd feat(Listbox): put id and setter in context + remove id prop (#5033) 2026-06-30 12:44:58 +02:00
Halvor Haugan fb4b5b5dde feat(Listbox): unique aria-activedescendant + remove memoization + require aria-selected (#5019) 2026-06-30 09:19:58 +02:00
Halvor Haugan c7e9d9e3b8 fix(website): fix typo in Lookup examples 2026-06-26 16:57:40 +02:00
Anniken Granberg e8198281a5 fix(website) Add minHeight and static to Lookup examples (#5024) 2026-06-26 14:53:24 +02:00
dependabot[bot] a60c9537de ⬆️ Bump dompurify from 3.4.10 to 3.4.11 (#5022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-26 14:13:17 +02:00
Anniken Granberg eb3c8c92c0 fix(Website): Description not showing for examples (#5023) 2026-06-26 13:39:15 +02:00
github-actions[bot] a9a5d63fa3 Version Packages (#5020)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-26 12:03:29 +02:00
3997c043a4 New component: Lookup (#4998)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-26 11:51:00 +02:00
Halvor Haugan 0bee88138e feat(Intro template): remove list item, adjust description, adjust example (#4988) 2026-06-25 16:02:16 +02:00
Ken 7f5b4da4c9 fix: sanity dataset export is now less strict when verifying assets 2026-06-25 10:44:22 +02:00
KenandCopilot Autofix powered by AI 0f7292b02c fix(Table): Content now shows when using ExpandableRows with React.Activity + React.Strictmode (#5018)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-25 10:41:13 +02:00
dependabot[bot]andKen d216f0264f ⬆️ Bump react-dom from 19.2.6 to 19.2.7 in the react group across 1 directory (#5002)
* ⬆️ Bump react-dom in the react group across 1 directory

Bumps the react group with 1 update in the / directory: [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>

* update(deps): react and react-dom 19.2.7

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-23 13:09:50 +02:00
Halvor Haugan d39fa801f8 [website] Migrate to useVisualEditingEnvironment() (#5009) 2026-06-23 12:11:28 +02:00
dependabot[bot] 6fc03c2b28 ⬆️ Bump launch-editor from 2.13.2 to 2.14.1 (#5015)
Bumps [launch-editor](https://github.com/vitejs/launch-editor) from 2.13.2 to 2.14.1.
- [Commits](https://github.com/vitejs/launch-editor/compare/v2.13.2...v2.14.1)

---
updated-dependencies:
- dependency-name: launch-editor
  dependency-version: 2.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 11:44:48 +02:00
Halvor Haugan 00a4ade5f3 ds-css: refactor how unwated lightningcss-variables are removed (#5012) 2026-06-23 10:32:34 +02:00
Halvor Haugan ddc0537ba2 Merge branch 'main' into copybutton-title 2026-06-23 10:21:41 +02:00
Halvor Haugan b40a1e11c4 Storybook: Use resolve.tsconfigPaths built into Vite instead of plugin (#5013) 2026-06-23 10:16:28 +02:00
Ken fda3acb0bc fix(website): No longer scroll to examples when they are allready in view 2026-06-22 15:02:24 +02:00
Halvor Haugan cbcea0a102 Upgrade to Sanity 6 (#5000) 2026-06-22 14:50:19 +02:00
Halvor Haugan d808fb23d2 ⬆️ Bump tar to 7.5.16 2026-06-22 12:59:19 +02:00
dependabot[bot]andHalvor Haugan 43ed97f15d ⬆️ Bump form-data from 4.0.5 to 4.0.6 (#5008)
* ⬆️ Bump form-data from 4.0.5 to 4.0.6

Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6.
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* sync versions of hasown

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-06-22 12:56:24 +02:00
Halvor Haugan 9e2f4c5477 ⬆️ Bump undici to 6.27.0/7.28.0 2026-06-22 12:51:40 +02:00
dependabot[bot] 4cf9343a39 ⬆️ Bump actions/checkout from 6 to 7 in the actions group (#5005)
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 12:16:23 +02:00
github-actions[bot] b475b34aa1 Version Packages (#5010)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-22 12:15:33 +02:00
Ken 619ed83c8c 🔥 removed legacy pkg types package 2026-06-22 11:53:01 +02:00
KenandHalvor Haugan 97b3ee0399 [Combobox] 🎉 Selected options are now persistent in dropdown for single-select (#4970)
* 🎉 Selected options are now persistent in dropdown for single-select combobox

* 🎉 Persistent rendering of selected single item when possible

* Update @navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-22 11:50:45 +02:00
dependabot[bot] 3a99edbf7c ⬆️ Bump dompurify from 3.4.9 to 3.4.10 (#5007)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.9 to 3.4.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.9...3.4.10)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 10:02:20 +02:00
dependabot[bot] 18c1ad1b5f ⬆️ Bump webpack-dev-server from 5.2.4 to 5.2.5 (#5006)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.4 to 5.2.5.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.4...v5.2.5)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 10:02:02 +02:00
dependabot[bot] e8dc35385a ⬆️ Bump @vitest/eslint-plugin in the vitest group (#5003)
Bumps the vitest group with 1 update: [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest).


Updates `@vitest/eslint-plugin` from 1.6.19 to 1.6.20
- [Release notes](https://github.com/vitest-dev/eslint-plugin-vitest/releases)
- [Commits](https://github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.19...v1.6.20)

---
updated-dependencies:
- dependency-name: "@vitest/eslint-plugin"
  dependency-version: 1.6.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 09:30:02 +02:00
dependabot[bot] 8640bc23cb ⬆️ Bump dompurify from 3.4.1 to 3.4.9 (#4993)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.1 to 3.4.9.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.1...3.4.9)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 13:22:44 +02:00
dependabot[bot]andHalvor Haugan 2dfbf56f41 ⬆️ Bump @babel/core from 7.29.0 to 7.29.7 (#4990)
* ⬆️ Bump @babel/core from 7.29.0 to 7.29.7

Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.29.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* sync versions

* sync more versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-06-19 13:21:28 +02:00
dependabot[bot] 7b641c78f5 ⬆️ Bump ws from 7.5.10 to 7.5.11 (#4992)
Bumps [ws](https://github.com/websockets/ws) from 7.5.10 to 7.5.11.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.10...7.5.11)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 13:20:41 +02:00
dependabot[bot] 0a15f2f548 ⬆️ Bump undici from 6.25.0 to 6.26.0 (#4999)
Bumps [undici](https://github.com/nodejs/undici) from 6.25.0 to 6.26.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.25.0...v6.26.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.26.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 13:17:21 +02:00
dependabot[bot] 2ad432017c ⬆️ Bump hono from 4.12.23 to 4.12.25 (#4991)
Bumps [hono](https://github.com/honojs/hono) from 4.12.23 to 4.12.25.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.23...v4.12.25)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 13:15:37 +02:00
dependabot[bot] 931c15b674 ⬆️ Bump markdown-it from 14.1.1 to 14.2.0 (#4989)
Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.1 to 14.2.0.
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-version: 14.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 13:03:42 +02:00
Ken cd1c63c5da 🎉 Added dekoratøren to sanity komponent grouping (#4994) 2026-06-19 12:39:57 +02:00
Ken d04e2d1674 [MCP] Search improvements (#4996)
* 🎉 Use fuzzy search for migrations and tokens

* ⚡ Use fuzzy token-search

* 📝 Docs improvements

* 📝 Update comments

* ⚡ Fuzzy icon-seach

* ⚡ Stricter icon-filter

* 🐛 Add limit to migrations
2026-06-19 09:01:06 +02:00
0694c147c4 [MCP] Implement fuse search functionality for aksel_find_docs (#4967)
* 📝 Add review

* 📝 update review

* 📝 Add deps

* 🎉 Set up fuse search

* 🎉 working fuse-search

* 📝 Update description

* 📝 Update zod params

* 📝 Update review

* Add secret

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 🐛 Fix caching, revert contract

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 🐛 Add outbound for sanity api

* 🐛 Handle no text case

* Update mcp-server/.nais/app.yaml

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update mcp-server/src/helpers/fetch-ds-docs.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 📝 Update docs based on review

* 📝 Add different answer based on if search fails or returns empty

* 🐛 Sync fusejs versions

* 📝 Norwegian categories

* ⚡ Use minMatchCharLength for schema, rely on treshold

* Update mcp-server/src/helpers/fuse-search.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-17 12:52:37 +02:00
dependabot[bot]andKen f73f93e9fb ⬆️ Bump @babel/core from 7.29.0 to 7.29.7 (#4980)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.29.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:12:40 +02:00
dependabot[bot]andKen e9ed1a37a6 ⬆️ Bump tar from 7.5.13 to 7.5.16 (#4981)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.13 to 7.5.16.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.13...v7.5.16)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:11:31 +02:00
dependabot[bot]andKen b453907cb7 ⬆️ Bump vite from 7.3.3 to 7.3.5 (#4982)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.3 to 7.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:11:08 +02:00
dependabot[bot]andKen 9e2180f91e ⬆️ Bump markdown-it from 14.1.1 to 14.2.0 (#4983)
Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.1 to 14.2.0.
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-version: 14.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:10:18 +02:00
dependabot[bot] cbfb17aae8 ⬆️ Bump dompurify from 3.4.1 to 3.4.8 (#4984)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.1 to 3.4.8.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.1...3.4.8)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 15:09:44 +02:00
dependabot[bot]andKen 1f676f87c9 ⬆️ Bump react-router from 7.15.0 to 7.15.1 (#4985)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.15.0 to 7.15.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.15.1/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.15.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:09:14 +02:00
dependabot[bot]andKen 3e17ef1fd5 ⬆️ Bump ws from 7.5.10 to 7.5.11 (#4986)
Bumps [ws](https://github.com/websockets/ws) from 7.5.10 to 7.5.11.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.10...7.5.11)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-16 15:08:39 +02:00
Ken d6e1150cbf 🐛 Hide storybook performance panel in chromatic 2026-06-16 15:02:20 +02:00
Ken 7ff390722f 🐛 Default checkboxes in publishingflow to checked for gp-articles 2026-06-16 14:50:41 +02:00
Ken 40e6652d08 ⚰️ Removed legacy meta.json implementation for changelogs 2026-06-16 14:49:13 +02:00
Ken 6f7f8148cb [website] 🐛 Add margin between exampletext-block 2026-06-16 10:33:14 +02:00
Ken afbf6c8367 🧪 Skip regression-tests for dependabot to avoid error 2026-06-16 09:43:20 +02:00
KenandHalvor Haugan 487ec580af [Endringslogg] Render on DS-pages (#4964)
* 🎉 Initial changelog-render

* 🎉 Render changelogs inline for ds-pages

* 🐛 Remove drafts from ids

* 🐛 Change back to prod

* 🐛 Use umami-links, correct prefix

* 🎉 Review and UmamiLink update

* Update aksel.nav.no/website/sanity/config.ts

* 🐛 Fixed type-error

* ⚡ Remove redundant optimization

* 💄 Adjustments to table layout

* 💄 10 -> 7rem th width for date

* Render endringslogg gp (#4966)

* 📝 gp-changelogs

* 📝 Text adjustments

* 🐛 Use correct type

* 🎉 Changelog article

* 🎉 Added intercepting routes

* 🎉 Update layout

* 🎉 Extract changelog utils

* todos

* 🚚 Move intercepted route

* 🚚 Moved intercepting routes

* 🐛 Fix intercepting routes

* 🎉 Added link when changelog exists

* 🐛 Change back to prod

* Update aksel.nav.no/website/app/(routes)/(designsystemet)/@endringslogg/(.)grunnleggende/endringslogg/[slug]/page.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🔥 removed old query-fields

* Update aksel.nav.no/website/app/(routes)/(designsystemet)/grunnleggende/endringslogg/[slug]/page.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🐛 Handle back/routing better

* 🏷️ Type update

* 🐛 Fix syntax bug

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-16 09:35:09 +02:00
github-actions[bot] 26ea51253d Version Packages (#4969)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-16 09:31:51 +02:00
a153926a2e [AnimateHeight] 🐛 Table and AnimateHeight now works with React.Activity (#4972)
* 🐛 Table and AnimateHeight now works with React.Activity

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update .changeset/empty-doodles-hunt.md

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🚚 Moved activity test story

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-16 09:29:33 +02:00
Halvor Haugan 5493a3155a [website] Fix error messages on missing tokens 2026-06-15 15:27:52 +02:00
Halvor Haugan 687bc8f5b8 Sanity: Add description for "Preview" status (#4973) 2026-06-15 15:04:42 +02:00
Ken f3b7857df4 [Icons] 🎉 Added new icons for eiendom (#4979)
* 🎉 Added new icons for eiendom

* 🐛 Fix width
2026-06-15 14:52:02 +02:00
dependabot[bot]andKen 7c0724ecaf ⬆️ Bump the vitest group with 4 updates (#4976)
* ⬆️ Bump the vitest group with 4 updates

Bumps the vitest group with 4 updates: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser), [@vitest/browser-playwright](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser-playwright), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@vitest/browser` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/browser)

Updates `@vitest/browser-playwright` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/browser-playwright)

Updates `@vitest/coverage-v8` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/coverage-v8)

Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🐛 Sync vitest versions across workspace

* 📝 Sync yarn

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-15 14:50:35 +02:00
dependabot[bot] c6d0557ff4 ⬆️ Bump @figma/plugin-typings from 1.125.0 to 1.128.0 (#4978)
Bumps [@figma/plugin-typings](https://github.com/figma/plugin-typings) from 1.125.0 to 1.128.0.
- [Commits](https://github.com/figma/plugin-typings/compare/v1.125.0...v1.128.0)

---
updated-dependencies:
- dependency-name: "@figma/plugin-typings"
  dependency-version: 1.128.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 14:50:11 +02:00
dependabot[bot] bf142f2c25 ⬆️ Bump clipboardy from 5.0.1 to 5.3.1 (#4977)
Bumps [clipboardy](https://github.com/sindresorhus/clipboardy) from 5.0.1 to 5.3.1.
- [Release notes](https://github.com/sindresorhus/clipboardy/releases)
- [Commits](https://github.com/sindresorhus/clipboardy/compare/v5.0.1...v5.3.1)

---
updated-dependencies:
- dependency-name: clipboardy
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 12:32:55 +02:00
Ken 59a6e9eb32 🐛 Add waitFor allowing flaky dismissablelayer tests to pass 2026-06-15 10:39:46 +02:00
Ken 7acd2a4416 [Timeline] Fix Axislabel calculation (#4968)
* 🐛 Handle overflow more gracefully

* ⚡ Optimize re-renders

* ⚡ Optimize timeline axis calculation

* 🐛 Fix overflow and sorting

* 🐛 Handle numer infinity issues

* 🐛 Fix case where left:0 returned null for period
2026-06-12 12:51:33 +02:00
Ken 386cbcbd1d [Datagrid] Column divider settings (#4963)
* 💄 Added columnDividers prop

* 📝 Changeset

* 📝 Update example

* 🎉 Added columnDividers to kitchensink demo
2026-06-12 09:12:31 +02:00
Ken d30f4c8077 🐛 Fix flaky storybook story by disabling performance panel in chromatic 2026-06-12 08:19:47 +02:00
KenandHalvor Haugan e6fdbdfcd9 [Endringslogg] Clean up legacy code, fix draft + published view (#4961)
* 🐛 Only render changelogs that are published

* 🎉 Clean up fremhevet code

* Update aksel.nav.no/website/app/(routes)/(designsystemet)/grunnleggende/endringslogg/_ui/LogEntry.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🔥 removed legacy spacing

* 🐛 Fix spacing

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-11 08:36:31 +02:00
github-actions[bot] e1654c08da Version Packages (#4962)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-09 20:21:05 +02:00
KenandCopilot Autofix powered by AI 7ede1a7a32 [Timeline] Axislabel overflow (#4959)
* 🐛 Axis-labels no longer render if width + left > 100%

* Clip overflow label

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 🐛 Add more affordance

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-09 18:39:01 +02:00
Ken a1b573a6f2 [Timeline] Axislabel story 2026-06-09 18:38:05 +02:00
Ken 1a1b7cf59e 🐛 Fix Chips docgen 2026-06-09 15:03:28 +02:00
Ken 0e9547ecab [Datagrid] Fixed shadow bug (#4960)
* 💄 Fix shadow/zebra issues

* 📝 changeset
2026-06-09 14:01:42 +02:00
Ken 5778d83765 📝 Update ds-component-restructure skill based on review 2026-06-09 13:08:38 +02:00
KenandHalvor Haugan 62a3be3a49 [LLM] Skill for re-structuring and refactoring components (#4934)
* 🎉 Chips refactor and skill

* 🔥 removed lowercase story

* Update .github/skills/ds-component-restructure/SKILL.md

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🎉 Review updates

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-09 13:04:07 +02:00
ba3ee03d58 [Endringslogg] Sanity-structure for new changelog on God praksis (#4945)
* 🎉 New document for gp endringslogg

* 🎉 Edit document

* 🎉 Add document resolver

* 📝 Updated schema

* 🎉 Added render of all changelogs for article

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 🎉 Review fixes

* 📝 Added changelog view to ds articles

* Update aksel.nav.no/website/sanity/schema/custom-components/gp/EndringsloggReferanser.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🔥 removed skrivehjelp

* 🐛 Update gp view filter based on use-context

* [Endringslogg] Publiseringsflyt og templates (#4949)

* 🎉 New flow for updating date and creating changelogs

* 🎉 Publication-flow with new document creation for changelog

* 🎉 Branched changelog based on doctype

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 📝 Updated blueprint

* Update aksel.nav.no/website/sanity/plugins/publication-flow/actions/lastVerified.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/sanity/plugins/publication-flow/actions/lastVerified.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 💄 default to not updating verifiedDate for ds-articles

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update mcp-server/.nais/app.yaml

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-09 13:03:20 +02:00
github-actions[bot] 59ebc666cf Version Packages (#4928)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-09 10:09:34 +02:00
Ken 992a955aa6 [Datepicker] Ranged logic refactor (#4896)
* 🎉 Set up needed data for from/to choices

* 🎉 Added reset-logic

* 📝 Changeset

* 🐛 Fix correct test-scenario

* 🚚 inputType -> callerName

* 🎉 Review changes

* 📝 Update changeset

* 🔥 Removed legacy comment

* 🔥 Reverted resetOnSelect prop
2026-06-09 09:54:05 +02:00
f79f054c9d [Website] Update stats cards on /designsystemet. Now fetches data from BQ on deploy (#4957)
* 🎉 Better statistics for major-version

* 🎉 Added upload to sanity

* 🎉 Stats-cards

* 🎉 Stats card

* 🎉 Added templates

* 🐛 Narrow time-check

* 🎉 Better validation of data

* 🎉 Add GCP creds to action

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 💄 Update number formatting

* Update aksel.nav.no/website/app/(routes)/(designsystemet)/designsystemet/_ui/stats/DesignsystemStats.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* ⚡ Use smaller table as timestamp reference

* 🚚 Query -> queries

* 📝 Move comments

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-09 09:53:53 +02:00
Halvor Haugan 6a6aab1104 [website] Fix issue with DS sidebar active marking (#4958) 2026-06-09 09:29:00 +02:00
Ken f6d965dfd0 [MCP] Update origins 2026-06-08 13:21:33 +02:00
Ken d967139925 [MCP] Fix nais-cinfig formatting 2026-06-08 12:56:12 +02:00
Ken c7b031cc65 [MCP] Update ingress to prod-gcp without ansatt 2026-06-08 12:45:18 +02:00
Ken 868901fc43 [MCP] Update readme 2026-06-08 11:22:40 +02:00
Ken 70cd115530 🐛 Sync updated package workspace 2026-06-08 11:20:15 +02:00
Ken 9057e5db03 [MCP] Rename from aksel-mcp-server -> aksel-mcp 2026-06-08 11:19:12 +02:00
Ken aac0bd903e [MCP-server] Bump version-number to 1.0 2026-06-08 11:17:03 +02:00
58b87ef6d2 [MCP] Rewrite from npm-package -> server (#4938)
* MCP buider skill

* 📝 Added plan and prompts

* 📝 Update plan

* 📝 MCP http server

* ⚡ Caching

* 🐛 Update config

* 🎉 Add metrics

* 🎉 Added logs

* 🔥 Removed time taken metric

* 🎉 Simplify setup

* ⚡ Re-add time taken metric

* 📝 Add more logging

* Dockerfile

* 🎉 Fix build

* unfo formatting

* 📝 Update yamls

* 🚚 Move config inline

* unfo formatting

* 🎉 Added mcp deploy action

* 🔥 Removed old server

* 🔥 removed skills and prompts

* 🐛 Re-added tests

* 📝 Added readme

* 🐛 Move server creating into request

* 📝 yarn sync

* 📝 Sync zod

* ⚡ Update origin check

* 🔒 Sec changes

* 🐛 Mcp intern ingress -> ansatt

* [MCP] Updates to remote MCP-server (#4946)

* 📝 Review One

* ⚡ Avoid schema bloat

* 🐛 Add timeout to fetch

* 🐛 Use zod-limit

* 🚚 Full renaming of APIs

* 🎉 Add future possibility for examples

* ⚡ Avoid pretty-printing

* ⚡ Reduce noise in doc return

* 🐛 Fix review findings

* 🔥 removed review files

* 📝 Update readme

* 📝 Comments

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ⚡ Simplify stringify

* 🎉 Added inspect script

* Apply suggestions from code review

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🚚 Rename category lookup

* 📝 Update example description

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-08 10:23:16 +02:00
dependabot[bot] 8bbf44bd87 ⬆️ Bump hono from 4.12.18 to 4.12.23 (#4947)
Bumps [hono](https://github.com/honojs/hono) from 4.12.18 to 4.12.23.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.18...v4.12.23)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 09:45:27 +02:00
dependabot[bot] 173baf612f ⬆️ Bump @vitest/eslint-plugin in the vitest group (#4954)
Bumps the vitest group with 1 update: [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest).


Updates `@vitest/eslint-plugin` from 1.6.17 to 1.6.19
- [Release notes](https://github.com/vitest-dev/eslint-plugin-vitest/releases)
- [Commits](https://github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.17...v1.6.19)

---
updated-dependencies:
- dependency-name: "@vitest/eslint-plugin"
  dependency-version: 1.6.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 09:44:27 +02:00
dependabot[bot] 4b2098e89a ⬆️ Bump vite-plugin-svgr from 4.5.0 to 5.2.0 (#4956)
Bumps [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr) from 4.5.0 to 5.2.0.
- [Release notes](https://github.com/pd4d10/vite-plugin-svgr/releases)
- [Commits](https://github.com/pd4d10/vite-plugin-svgr/compare/v4.5.0...v5.2.0)

---
updated-dependencies:
- dependency-name: vite-plugin-svgr
  dependency-version: 5.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 09:43:27 +02:00
Ken 3d5758b90e 🐛 Fix type-guarding causing sentry to give warnings (#4950) 2026-06-08 09:40:46 +02:00
dependabot[bot] f1e8f8c9a0 ⬆️ Bump prettier from 3.8.1 to 3.8.3 (#4955)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 09:39:49 +02:00
Halvor HauganandCopilot Autofix powered by AI 86f7b9e716 [website] Adjust descs on LinkCard examples (#4951)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 09:05:49 +02:00
Halvor Haugan 6bade9bf7d [website] Fix alignment of CopyButton in ExampleText (#4952) 2026-06-08 09:05:10 +02:00
Ken 8be81860b2 [DataGrid] Update detail-panel demo 2026-06-05 11:54:21 +02:00
Ken 9803cc2163 🎉 Print if article is published or updated 2026-06-05 08:49:44 +02:00
dependabot[bot] 3ecdce8a0c Bump tmp from 0.2.5 to 0.2.7 (#4942)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.5 to 0.2.7.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.5...v0.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 17:22:02 +02:00
Halvor Haugan 2eaf46c1bf [Storybook] Add performance addon (#4944) 2026-06-04 13:47:01 +02:00
Halvor Haugan 53da26defd Fix DataGrid resize story for real 2026-06-04 13:44:03 +02:00
Ken 5274ade01d 🐛 Fix flaky data-grid resize story 2026-06-04 12:52:03 +02:00
Ken dcd7b4053c 📝 Update review skill 2026-06-04 12:01:01 +02:00
dependabot[bot] df15b18766 Bump react-router from 7.14.2 to 7.15.0 (#4941)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.14.2 to 7.15.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.15.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.15.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 10:10:29 +02:00
Ken 31b44db871 🔥 Removed virtualfocus-code (#4940) 2026-06-04 08:08:08 +02:00
bae5fa9793 [Tabs] Refactored to not use descendants API (#4931)
* 🎉 Rewrite tabs to not use descendants

* 📝 changeset

* 🎉 Added tests

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 📝 Rename to rovingFocus

* 🔥 Removed fallback to activeElement, handle no current

* [ToggleGroup] Refactored to not use descendants API (#4932)

* 🎉 Updated ToggleGroup to not use descendants API

* ♻️ Update rovingFocus and fallback

* [ActionMenu] Refactored to not use descendants API  (#4933)

* 🎉 Updated ActionMenu to not use descendants API

* ♻️ Cleanup after roving rename

* 🐛 Update event-handler for close-submenu listener

* Update @navikt/core/react/src/utils/components/floating-menu/Menu.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-03 15:12:38 +02:00
Halvor Haugan 8573dcb594 [website] Update FileUpload example "too-many-files" according to Figma (#4939) 2026-06-03 14:08:30 +02:00
Anniken Granberg d3fedc54ff ActionMenu: Fix reference to wrong component in size example description (#4937)
* ActionMenu: Fix reference to wrong component in size example description

* ops wrong component
2026-06-03 11:15:31 +02:00
Halvor Haugan e2715ae976 Update console.log() in ActionMenu.stories.tsx 2026-06-02 12:03:11 +02:00
Ken 3dd63ddc8b 🔥 Removed readme skill 2026-06-02 11:45:33 +02:00
Ken 34ab9ab3cf 📝 Added README for mcp-server, fixed changeset config 2026-06-02 11:28:42 +02:00
KenandHalvor Haugan bcef7eae23 [MCP] Aksel mcp package (#4900)
* ✨ Boilerplate

* ✨ Initial test-script

* 📝 Add inspector

* 🎉 Working MCP-script

* 🎉 Refactor tool creation, added tests

* 🐛 Fix tsconfig include, update test script

* 🎉 Add genetic shutdown callback for telemetry

* 🎉 Added prompts handling

* 🎉 Added resouced

* 🎉 Added docs resource

* 🎉 Added working resource and tool to fetch aksel-docs

* 🏷️ Rename index files  -> dir names

* ♻️ Simplify return-type

* 📝 Rename mcp.json -> .mcp.json

* Sync packages

* 📝 Added spec

* 📝 Update spec

* 🎉 Added token-docs mcp resource

* 🎉 Added token resource and list

* 🎉 Added tool lookup for icons

* 📝 Update spec

* [CLI] Export parsed docs for migrations

* [LLM] Added migration guide tool

* 🔥 Remove prev migration implementation

* 🎉 Added migrations tool

* 🎉 Added props tool

* 📝 Lock sync

* 🔒 Improve tool input validation

* 🔒 Added enum validation for icon-search tool

* 🔒 Improved component-docs validation

* 📝 Yarn lock sync

* 📝 Update llm docs index

* 🐛 Resolve misc bugs

* 🐛 Better typing, comments

* 🎉 Ready MCP for npm deploy

* 🐛 Clean up unused config and files

* 🔥 Removed readme

* 🔥 Removed readme

* 📝 Sync vitest and ts version

* 🐛 Fix MIME-type

* 🐛 Match lovercase filtering

* 🐛 Fix tests, avoid adding test-files to build

* PR updates

* 🚚 Moved migrations to resources

* PR cleanup

* 📝 Mark package as private to allow manuall publishing

* 🐛 Sync versions

* ⚡ Use tsdown for package build

* Update @navikt/aksel-mcp/.mcp.json

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 📝 Sync rolldown-versions

* 🔥 Removed comment

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-02 11:20:57 +02:00
366d72feeb 🎨 ActionMenu: New medium content size, styling changes to group label and automatic indent in groups if one item has marker (#4911)
* Added small prop

* Added disableItemIndent prop

* Changes in group label styling, medium size styling and itenm indent

* Examples

* Changeset

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* make indent positive again

* remove hstack

* Move size to root so it can be added to context and applied to submenus too

* Add indent to SubMenu trigger too

* moved size prop

* Changed logic to indent being opt-in

* Correct story to new indent logic

* fix website examples based on changes

* Rename and fixed some spelling

* changeset

* add cromatic

* Update .changeset/great-kings-poke.md

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-06-02 09:51:32 +02:00
Halvor Haugan 34eb6fe660 [website] Adjust schema for props (#4936) 2026-06-02 09:11:06 +02:00
Ken 9976c5cbc8 ⬆️ Upgrade website docker-image (#4935) 2026-06-01 14:56:23 +02:00
Halvor Haugan 1ac816a1ae FileUpload: Warning about texts with aria-hidden (#4929) 2026-06-01 13:57:10 +02:00
dependabot[bot]andKen 226cca718d Bump jsdom from 27.1.0 to 29.1.1 (#4926)
* Bump jsdom from 27.1.0 to 29.1.1

Bumps [jsdom](https://github.com/jsdom/jsdom) from 27.1.0 to 29.1.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v27.1.0...v29.1.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* 📝 Sync jsdom-versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-06-01 13:18:14 +02:00
KenandCopilot Autofix powered by AI f455d01a58 [Instructions] Code review instructions (#4915)
* 🎉 Initial review checklist

* 📝 Changes to review checklist

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 13:08:49 +02:00
dependabot[bot] 1b4c489dc2 Bump the storybook group across 1 directory with 9 updates (#4924)
Bumps the storybook group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-a11y](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/a11y) | `10.3.6` | `10.4.1` |
| [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs) | `10.3.6` | `10.4.1` |
| [@storybook/addon-themes](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/themes) | `10.3.6` | `10.4.1` |
| [@storybook/addon-vitest](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/vitest) | `10.3.6` | `10.4.1` |
| [@storybook/nextjs-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/nextjs) | `10.3.6` | `10.4.1` |
| [@storybook/react-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/react-vite) | `10.3.6` | `10.4.1` |
| [eslint-plugin-storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/eslint-plugin) | `10.3.6` | `10.4.1` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) | `10.3.6` | `10.4.1` |
| [storybook-addon-pseudo-states](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/pseudo-states) | `10.3.6` | `10.4.1` |



Updates `@storybook/addon-a11y` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/addons/a11y)

Updates `@storybook/addon-docs` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/addons/docs)

Updates `@storybook/addon-themes` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/addons/themes)

Updates `@storybook/addon-vitest` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/addons/vitest)

Updates `@storybook/nextjs-vite` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/frameworks/nextjs)

Updates `@storybook/react-vite` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/frameworks/react-vite)

Updates `eslint-plugin-storybook` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/lib/eslint-plugin)

Updates `storybook` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/core)

Updates `storybook-addon-pseudo-states` from 10.3.6 to 10.4.1
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.4.1/code/addons/pseudo-states)

---
updated-dependencies:
- dependency-name: "@storybook/addon-a11y"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-themes"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-vitest"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/nextjs-vite"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/react-vite"
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: eslint-plugin-storybook
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: storybook-addon-pseudo-states
  dependency-version: 10.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 13:00:15 +02:00
dac40ad267 Bump the vitest group with 4 updates (#4925)
* Bump the vitest group with 4 updates

Bumps the vitest group with 4 updates: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser), [@vitest/browser-playwright](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser-playwright), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@vitest/browser` from 4.1.5 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/browser)

Updates `@vitest/browser-playwright` from 4.1.5 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/browser-playwright)

Updates `@vitest/coverage-v8` from 4.1.5 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/coverage-v8)

Updates `vitest` from 4.1.5 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: vitest
  dependency-version: 4.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>

* sync versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-06-01 12:57:21 +02:00
KenandCopilot Autofix powered by AI fdda91fe1e [logging] Use consoleWarning-utility across package (#4922)
* 📝 Improve error-logging

* 📝 Removed double prefix

* 📝 Changeset

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 📝 PR-review

* 🔥 Removed backticks

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 12:55:43 +02:00
Halvor Haugan fca2948494 ds-tokens: Fix token_docs types (#4930) 2026-06-01 12:46:30 +02:00
Ken 612a3250e9 🐛 Fix unstable inline event-handlers in useEventListener (#4923) 2026-06-01 11:29:29 +02:00
dependabot[bot] b81e697ff1 Bump globals from 17.5.0 to 17.6.0 (#4927)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.5.0 to 17.6.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.5.0...v17.6.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 11:28:39 +02:00
dependabot[bot] 5ca4091b16 Bump axios from 1.15.2 to 1.16.1 (#4918)
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 11:27:35 +02:00
Ken 0afa2574eb [Typos] Fix various typos (#4921)
* 📝 fix internal typo

* 📝 Fix comment typos

* 📝 reference correct props
2026-06-01 10:33:56 +02:00
Ken c1a274f347 [Stylelint] Misc fixes and cleanup (#4920)
* 🐛 Fix deprecated classes loop

* 🎉 Add word-highlighting

* ⚰️ Removed test-files from npm-bundle

* 📝 Changeset
2026-06-01 09:50:32 +02:00
Ken 64592729b8 [Clean] 🐛 Clean up buildinfo-files (#4919)
* 🐛 Clean up buildinfo-files

* 📝 Folder -> name
2026-06-01 09:50:14 +02:00
Ken 36a81bff7f [Icons] Update test-description 2026-05-30 15:16:26 +02:00
Ken 50a833f6c8 [Icons] 🐛 Fix metadata type definition 2026-05-30 15:10:05 +02:00
github-actions[bot] 9f0b850579 Version Packages (#4910)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-29 15:38:35 +02:00
Halvor Haugan cd4443e7cb [website] Adjust examples of Box and Bleed (#4914) 2026-05-29 14:09:07 +02:00
Halvor Haugan 23157ec0b5 [website] Add label and desc to FileUpload.Trigger example (#4913) 2026-05-29 14:06:54 +02:00
Ken 15d54f3f59 [Sanity] Use named exportDataset import for backup 2026-05-29 11:14:50 +02:00
Ken da9bab40b0 [Dismissablelayer] Focus edgecase (#4909)
* 🐛 Make sure Dismissablelayer handles focus-events when clicking inside container

* 📝 Changeset

* 🧪 Added tests
2026-05-29 10:31:43 +02:00
Ken c5e3f42c4f [LLM] Optimize instructions
[LLM] More optimizations
2026-05-29 09:48:38 +02:00
Halvor Haugan 54634ffed9 Tokens: Add types for token_docs (#4912) 2026-05-28 15:21:16 +02:00
df95714326 [DataGrid] Column display-prop (#4908)
* 🎉 Added columnDisplay setting

* 📝 Changeset

* 🐛 Handle silent error

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* 🐛 Re add closing tag

* Update @navikt/core/react/src/data/stories/Data.data-prop.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/data/stories/Data.data-prop.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-05-28 14:15:25 +02:00
Halvor Haugan 79444a7485 DataGrid.Table: Fix auto-width for col with "show nested rows" button (#4906) 2026-05-28 10:03:13 +02:00
Ken a375059cdb [DataGrid] 🐛 Loadingbar now follows sticky header properly (#4897)
* 🐛 Loadingbar now follows sticky header properly

* 💄 Fix loading-bar in safari, removed opacity transition

* ♻️ Removed unused data-sticky attrb on rows
2026-05-28 09:33:44 +02:00
Halvor Haugan 819c68e3a7 useControllableState: More precise type for setter (#4907) 2026-05-27 21:10:03 +02:00
dependabot[bot]andHalvor Haugan 354cc81223 Bump playwright from 1.59.1 to 1.60.0 (#4904)
* Bump playwright from 1.59.1 to 1.60.0

Bumps [playwright](https://github.com/microsoft/playwright) from 1.59.1 to 1.60.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* sync versions

* sync versions 2

* version sync 3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-05-26 21:41:12 +02:00
dependabot[bot]andHalvor Haugan 9f5c345386 Bump @biomejs/biome from 2.4.10 to 2.4.15 (#4905)
* Bump @biomejs/biome from 2.4.10 to 2.4.15

Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.10 to 2.4.15.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.15/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* migrate

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-05-26 21:00:05 +02:00
dependabot[bot] adad5b4214 Bump @changesets/cli from 2.29.7 to 2.31.0 (#4903)
Bumps [@changesets/cli](https://github.com/changesets/changesets) from 2.29.7 to 2.31.0.
- [Release notes](https://github.com/changesets/changesets/releases)
- [Commits](https://github.com/changesets/changesets/compare/@changesets/cli@2.29.7...@changesets/cli@2.31.0)

---
updated-dependencies:
- dependency-name: "@changesets/cli"
  dependency-version: 2.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 20:36:18 +02:00
Halvor Haugan 7ed5890dae [website] Make Stepper "wizard" example interactive 2026-05-26 15:48:28 +02:00
Halvor Haugan 0b31d893f6 [website] Adjust Select error example 2026-05-26 15:47:26 +02:00
c273939376 Bump react/react-dom from 19.2.5 to 19.2.6 (#4876)
* Bump react-dom in the react group across 1 directory

Bumps the react group with 1 update in the / directory: [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react-dom` from 19.2.5 to 19.2.6
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.6/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>

* 📝 Sync react-versions

* sync versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-05-26 12:17:05 +02:00
Anniken Granberg 92fa6410b7 DataTable: Small cleanup to drag and drop (#4899)
* DataTable: Small improvements to drag and drop

* changeset

* pr comments
2026-05-26 09:33:33 +02:00
Halvor Haugan c2f8cc28cd [website] Update descs for some primitives examples (#4898) 2026-05-22 18:29:57 +02:00
Ken d3620d4d15 [Website] DataGrid demo-update (#4895)
* [Website] Removed redundant column from DataGrid demo

* [Website] Demo-data refactor

* 🎉 Nested demo-data

* 🐛 Dynamic rng number

* 🔥 removed withNesting type

* 📝 Inline data-generation

* 📝 rename RNG -> randomnumbergenerator
2026-05-22 14:09:13 +02:00
Ken fd06b8caf6 [Analyzer] Bundle analyzer treeshake (#4894)
* 🐛 Better treeshaking of icon-package

* 📝 Lock rolldown to v1

* 📝 sync rolldown versions
2026-05-22 13:44:45 +02:00
Halvor Haugan f323321cb4 DataGridTable: remove todo 2026-05-22 09:16:50 +02:00
Ken 5a29f39e16 [Website] Re-order data-grid examples 2026-05-21 15:28:14 +02:00
github-actions[bot] 27e221d1be Version Packages (#4888)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-21 14:47:20 +02:00
Halvor Haugan b89dbce45b [DataGrid] Fix docgen (#4892) 2026-05-21 14:43:42 +02:00
Anniken Granberg 3d4f94d0fe DataTable: Change padding values and changed rowDensity variant naming 2026-05-21 14:01:22 +02:00
Halvor Haugan 0a1b110efc DataGridTable: Add todos 2026-05-21 13:45:18 +02:00
Halvor Haugan 6576b43734 [website] Use ShowMore in Preview note (#4891) 2026-05-21 13:43:30 +02:00
KenandHalvor Haugan a84c86e9a0 [DataGrid] Examples for website (#4889)
* 🎉 Added initial example for data-grid

* 🎉 Extract data-generation to separate util

* 🔥 removed old comment

* 🎉 Demo layout

* ♻️ Reduce height

* 📝 Re-order columns

* 🎉 Added more demos

* 🎉 Added loading and empty-state stories

* 🔥 Removed isloading from sorting

* 🎉 Fixed performance issues

* 🐛 Make sortable columnheader static when table is loading

* 🐛 Removed isLoading-prop from sorting-demo

* 🐛 Make onRowAction return rowData

* 📝 Updated examples with best practices

* 📝 Reduce line-count

* 📝 Use vstack

* 📝 Rename row-action

* Update aksel.nav.no/website/pages/eksempler/data-grid/row-click.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/data-grid/sorting.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🐛 Improve layout for nested example

* ♿ A11y improvements

* Update aksel.nav.no/website/components/website-modules/examples/__parts/DataGridDemoData.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-05-21 13:27:39 +02:00
KenandHalvor Haugan dc5299b1f9 [DataGrid] Release Preview 🎉 (#4885)
* 🎉 Ready component for release

* 🐛 Removed old import of table-css

* 📝 Rename to PREVIEW

---------

Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-05-20 13:13:11 +02:00
Ken 80c2458c66 [DataGrid] Added todo for columnheader sortable-area 2026-05-20 13:12:11 +02:00
Ken 96b6aed396 [DataGrid] Sync namespace types 2026-05-20 12:47:55 +02:00
Halvor Haugan baaf495ee6 [DataGrid] Fix typos 2026-05-20 12:46:15 +02:00
KenandCopilot Autofix powered by AI 76e04106f7 [Icons] New icon Newsletter (#4887)
* 🎉 Added new icon NewsLetter

* 📝 changeset

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-20 12:43:53 +02:00
Ken 67574283ca [DataGrid] Make keyboard-nav default enabled 2026-05-20 12:38:06 +02:00
Ken a74d2a75bb [DataGrid] Update namespace columns def 2026-05-20 12:23:27 +02:00
Ken ed0e6dbfd7 [DataGrid] Update Namespaces 2026-05-20 12:01:45 +02:00
Ken 4bb83d5e6f [DataGrid] Rename onRowClick -> onRowAction 2026-05-20 11:47:57 +02:00
Ken 5e43726f08 [DataGrid] Update selection API 2026-05-20 11:47:57 +02:00
Ken bde3d18341 [DataGrid] Rename sorting API 2026-05-20 11:47:57 +02:00
KenandHalvor Haugan 6700e5789a [Website] 🎉 Added custom preview-card block (#4886)
* 🎉 Added custom preview-card block

* 🐛 Fix unpacking of link-references

* fix type error

---------

Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2026-05-20 11:29:58 +02:00
Halvor Haugan dbeaddd186 [DataGrid] JSDoc adjustments and misc 2026-05-20 09:55:44 +02:00
Halvor Haugan c6d98539d9 DataGridTable: Fix sticky header 2026-05-19 20:01:49 +02:00
Halvor Haugan ebbff6520c [DataGrid] Rename DataTableProps -> DataGridTableProps 2026-05-19 19:59:24 +02:00
Ken ca7f40e1e5 [LLM] 🎉 Added docs-route for structured content (#4846)
* 🎉 Added docs-route for structured content

* ♻️ Extracted and re-used helpers

* ♻️ Refactor route handler
2026-05-19 15:40:27 +02:00
dependabot[bot] dbe6b4ba43 Bump postcss from 8.4.31 to 8.5.14 (#4880)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.31 to 8.5.14.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.5.14)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:34:26 +02:00
dependabot[bot] ae4f8c372b Bump webpack-dev-server from 5.2.3 to 5.2.4 (#4884)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.3 to 5.2.4.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.3...v5.2.4)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:33:26 +02:00
Ken 05a60ded83 [DataGrid] Bug: Make sure header cells have unique ids in case of multiple tables 2026-05-19 14:46:29 +02:00
Ken f8dc76d751 [DataGrid] Added namespace exports for props 2026-05-19 14:43:32 +02:00
Halvor Haugan a942e2585a [DataGrid] DataTableColumnHeader: SR improvements 2026-05-19 14:38:30 +02:00
Ken f98b44a037 [DataGrid] Fix props not giving error in stories 2026-05-19 13:54:05 +02:00
Ken 72b75c8edf [DataGrid] Optimize sort callback and resize-observing 2026-05-19 13:32:41 +02:00
Ken 831111bbd0 [DataGrid] Better types for stickyColumns 2026-05-19 13:32:41 +02:00
Ken bb5c02e07e [DataGrid] Optimized rendering 2026-05-19 13:32:41 +02:00
Ken fce759d17f [DataGrid] Refactor Settings name 2026-05-19 13:32:41 +02:00
Ken a005f4528b [DataGrid] Refactor Settings API 2026-05-19 13:32:40 +02:00
Ken 67d07f24a5 [DataGrid] Move settings 2026-05-19 13:32:40 +02:00
Ken cc38262dda [DataGrid] Move isLoading to root, rename loading -> loadingContent 2026-05-19 13:32:40 +02:00
Ken caf68104ee [DataGrid] Moved selectionTrigger prop to table itself 2026-05-19 13:32:40 +02:00
Ken 39c075be69 [DataGrid] Moved selection-prop to grid root 2026-05-19 13:32:40 +02:00
Halvor Haugan a6b434d46a DataGrid: Fix resize test 2026-05-19 12:21:14 +02:00
Halvor Haugan 2d77db8053 [DataGrid] Column width: Rename default -> defaultValue 2026-05-19 12:16:07 +02:00
Halvor Haugan 32a7038a10 DataGrid.Table: make sure all console warnings are prefixed 2026-05-19 11:57:45 +02:00
Halvor Haugan c0285b8950 DataTable: improved auto-width logic 2026-05-19 11:40:12 +02:00
Halvor Haugan 39106714d1 DataTable: ActionMenu in kitchen sink story 2026-05-19 11:29:27 +02:00
Halvor Haugan 3e789ce63d DataTable: overflow:hidden on cells 2026-05-19 11:25:44 +02:00
Halvor Haugan 1fe4d23504 Remove vitest.setup.ts (#4883) 2026-05-19 09:47:21 +02:00
Halvor Haugan 4fb8c14891 ⬆️ Upgrade next from 16.2.3 to 16.2.6 (#4882) 2026-05-19 09:47:16 +02:00
Halvor Haugan 61e1d1b012 Remove unused package cross-zip-cli 2026-05-19 09:32:38 +02:00
Halvor Haugan 15a96bb39b ⬆️ Bump uuid to latest patch versions 2026-05-18 19:42:30 +02:00
dependabot[bot] 566e1dcf3c Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7.29.4 (#4872)
Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.29.0 to 7.29.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-modules-systemjs"
  dependency-version: 7.29.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 19:37:46 +02:00
Halvor Haugan 84441246ac DataTable: remove TODOs out of JSDoc 2026-05-18 19:17:25 +02:00
Halvor Haugan 2986f265d8 DataTable: fix bug 2026-05-18 19:13:49 +02:00
Halvor Haugan e400caef2f [DataTable] Rename textAlign -> align 2026-05-18 19:03:49 +02:00
Halvor Haugan 73d4ed3104 DataTable: Rename header -> headerCell, label -> header 2026-05-18 18:57:18 +02:00
Halvor Haugan 2591ae4385 DataGridRoot.context.ts: RowDef -> RowT 2026-05-18 18:41:32 +02:00
Halvor Haugan 692340b9b2 Adjust workflow names 2026-05-18 16:51:45 +02:00
Halvor Haugan a37f514eac DataTable: Remove magic tag/button CSS 2026-05-18 16:23:44 +02:00
Halvor Haugan 815ff7c55f [website] Adjust desc on primitives examples (#4874) 2026-05-18 16:06:25 +02:00
dependabot[bot] 5dbf9d3f16 Bump fast-uri from 3.1.0 to 3.1.2 (#4873)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 16:01:12 +02:00
dependabot[bot] ffe22d2bf6 Bump @vitest/eslint-plugin from 1.6.16 to 1.6.17 in the vitest group (#4877)
Bumps the vitest group with 1 update: [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest).


Updates `@vitest/eslint-plugin` from 1.6.16 to 1.6.17
- [Release notes](https://github.com/vitest-dev/eslint-plugin-vitest/releases)
- [Commits](https://github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.16...v1.6.17)

---
updated-dependencies:
- dependency-name: "@vitest/eslint-plugin"
  dependency-version: 1.6.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 15:59:48 +02:00
dependabot[bot] a2912b928b Bump figlet from 1.10.0 to 1.11.0 (#4878)
Bumps [figlet](https://github.com/patorjk/figlet.js) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/patorjk/figlet.js/releases)
- [Commits](https://github.com/patorjk/figlet.js/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: figlet
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 15:59:01 +02:00
Ken 2a62921f7c [DataGrid] Rename Types and root context 2026-05-18 15:57:51 +02:00
Ken 0387e38e20 [DataGrid] Moved DataTable tests 2026-05-18 15:57:51 +02:00
Ken fc0fab5b60 [DataGrid] removed old legacy-implementation 2026-05-18 15:57:51 +02:00
Ken 329c59f4cd [DataGrid] Avoid extending div-element 2026-05-18 15:57:50 +02:00
Ken a9b7c16b89 [DataGrid] Rename table root -> DataGridTable 2026-05-18 15:57:50 +02:00
Ken 06178182e2 [DataGrid] Move data-grid dir 2026-05-18 15:57:50 +02:00
Ken f2bce6c3db [DataGrid] Add table as sub-component 2026-05-18 15:57:50 +02:00
github-actions[bot] ee976548c7 Version Packages (#4868)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-18 15:09:49 +02:00
KenandHalvor Haugan 34b28ca9cd [Website] 💄 Added preview-tag (#4869)
* 💄 Added preview-tag

* 🐛 Fix sidebar type error, change color to meta-purple

* Update aksel.nav.no/website/components/website-modules/StatusTag.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-05-18 15:09:36 +02:00
KenandHalvor Haugan 4fb29fa9a9 [DataTable] Table -> DataGrid wrapper (#4879)
* 🎉 Set up DataGrid root

* 🎉 Moved data props to grid-component

* Update @navikt/core/react/src/data/data-grid/root/DataGridRoot.context.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/data/data-grid/root/DataGridRoot.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 📝 Doc changes

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-05-18 14:45:53 +02:00
Halvor Haugan 30f687ba6c DataTable: Fix auto-resize issue 2026-05-18 13:24:09 +02:00
Ken 6ce7ce7d4b [DataTable] Fixed ref-issue for keyboard-nav 2026-05-18 12:11:41 +02:00
Halvor Haugan 154f12f84e DataTable: Type/jsdoc adjustments for detailsPanel and subRows 2026-05-18 11:20:42 +02:00
Anniken Granberg aea6430c9f DataTable: Fix row density for firefox 2026-05-18 11:06:35 +02:00
Halvor Haugan 2b9f450028 [DataTable] Root: Adjust JSDoc 2026-05-18 10:34:08 +02:00
Halvor Haugan b46949eb56 [DataTable] Root: fix docgen, adjust jsdoc 2026-05-18 09:03:05 +02:00
Halvor Haugan 137ddc1841 DataTable: Fix auto resize story test (#4875) 2026-05-15 16:41:15 +02:00
Halvor Haugan 19dc8cadb6 DataTable: fix auto resize test 2026-05-15 14:03:58 +02:00
Halvor Haugan 336503c5c1 DataTable: group width-props 2026-05-15 13:05:31 +02:00
Ken 4cdb7feb02 [DataTable] Moved fullWidthColSpan calculation to hook, renmaed const 2026-05-13 18:00:40 +02:00
Ken 7330c74c7b [DataTable] Moved TableElementWrapper out one level to avoid re-renders 2026-05-13 17:55:52 +02:00
Ken 604cd7b5f7 [DataTable] Moved sorting hook result into context 2026-05-13 17:51:56 +02:00
Halvor Haugan be7d524851 DataTable: fix test 2026-05-13 16:07:31 +02:00
Halvor Haugan b42b259a4f DataTable: some comments 2026-05-13 16:01:47 +02:00
Anniken Granberg def2ff5939 DataTable / dnd: Cleanup todos 2026-05-13 15:00:15 +02:00
Anniken Granberg bb8a9f96df DataTable: Remove large text size and change text size values 2026-05-13 15:00:15 +02:00
Ken e9d723a1ba [KI] Optimized instructions, added caveman 2026-05-13 11:57:54 +02:00
Ken 5fdca37439 [DataTable] Optimize context by moving item-context to root 2026-05-13 11:57:54 +02:00
Anniken GranbergandCopilot Autofix powered by AI d50aef6ced [Website] Added support for tabs in code block (#4870)
* [Website] Added support for tabs in code block

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-13 11:38:50 +02:00
Ken 8b0d4a5517 [DataTable] Update sorting state 2026-05-13 10:37:36 +02:00
Ken 79fd31c117 [DataTable] rename disableRowSelectionOnClick to selectionTrigger and disableRowSelection to enableRowSelection 2026-05-13 10:16:01 +02:00
Ken 92d599fa97 🔥 Removed old documentation 2026-05-13 10:16:01 +02:00
Halvor Haugan c34d9fc966 DataTable: contentMaxWidth can now be any string (or number) 2026-05-13 10:10:18 +02:00
Halvor Haugan f497787566 DataTable: truncateContent default false when layout=auto 2026-05-13 09:59:51 +02:00
Ken 2a6ec64316 [DataTable] removed DetailsT from columnDefinitions 2026-05-12 20:01:37 +02:00
Ken cdbda9c231 [DataTable] Added todos 2026-05-12 19:43:01 +02:00
Ken 6b0b2f9376 [DataTable] Remove legacy comment 2026-05-12 19:39:37 +02:00
Ken ead1139a09 [DataTable] Sticky-state now accounts for selection and expansion-state 2026-05-12 19:38:58 +02:00
Ken de438d487e [DataTable] update stickyColumns props 2026-05-12 19:13:26 +02:00
Ken 8028ff69e8 [DataTable] Refactor getRowId to only accept string, reference TableRowEntryId where possible 2026-05-12 16:20:16 +02:00
Ken 459ee4a66f [DataTable] Rename emptyState -> emptyContent 2026-05-12 15:53:48 +02:00
Ken 45c6635ec8 [DataTable] Refactor loading-api 2026-05-12 15:53:48 +02:00
Anniken Granberg ab3bfc8074 DataTable: remove dnd-kit package 2026-05-12 15:35:27 +02:00
Ken 4329775c9f [DataTable] Removed shouldBlockNavigation prop 2026-05-12 15:23:15 +02:00
Ken 6d3271d946 [DataTable] Require selectionMode to be set when selection-prop is defined 2026-05-12 15:12:43 +02:00
Ken dd02a7d4a9 [DataTable] Replace UNSAFE_isSelection with 'cellType=action' prop 2026-05-12 15:10:22 +02:00
Ken b8738227b2 📝 Add todos with priorities 2026-05-12 14:46:47 +02:00
Ken c717b5a2d1 Add review 2026-05-12 14:46:47 +02:00
Ken e9dd76d7e0 📝 Add review 2026-05-12 14:46:47 +02:00
19ac81dbb4 [website] DescriptionList component (#4862)
Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-05-12 11:45:18 +02:00
Halvor Haugan 20e6935a31 DataTable: CSS adjustment I forgot to commit after editing DataTableColumnHeader 2026-05-12 11:41:17 +02:00
Halvor Haugan 807b2feef2 [website] Fix riktekst toolbar alignment issue 2026-05-12 11:38:48 +02:00
Ken a9a8bad7a4 🔥 Removed deprecated description-fields in sanity-workspace config 2026-05-11 18:47:51 +02:00
Ken 412a00cdd2 🎉 Developer workspace in sanity is now hidden for non admins/devs 2026-05-11 18:23:20 +02:00
7727c3249a [DataTable] Selection api refactor (#4860)
* :WIP: Refactor selection with rowdata

* ♻️ Implement multiple-select handler

* ♻️ Fix bugs and performance

* 🔥 Removed unused code

* 🔥 removed old comments

* 🔥 removed old comments

* ♻️ Resolve function or boolean

* 🐛 Use console util for printing to dev

* 🔥 removed legacy-tests

* 🐛 Preserve previous pages selection

* ⚡ Use set directly

* avoid any

* comment

* rename useTableItemsReturn -> UseTableItemsReturn

* fix type of disableRowSelection

* 🐛 Handle selection of child-rows when parent-row is selected

* 🏷️ Types refactor

* 🐛 Update stories to reflect updated state

* Update @navikt/core/react/src/data/table/hooks/useTableSort.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 📝 Updated comments

* 🔥 Comment out non-working kitchen-sink demo

* forward T in canSelectTableRow types

* single select stories

* 🏷️ Removed unused type

---------

Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-05-11 18:05:41 +02:00
Halvor Haugan c40ecab7dd DataTable: Adjustments & comments in DataTableColumnHeader 2026-05-11 17:46:09 +02:00
Halvor Haugan ed2eb5885f DataTable: Storybook test for autoWidth 2026-05-11 17:28:31 +02:00
dependabot[bot] d9ed2ab6eb Bump esbuild from 0.27.7 to 0.28.0 (#4866)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.7 to 0.28.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 15:50:33 +02:00
dependabot[bot] 1338d97853 Bump ts-morph from 27.0.2 to 28.0.0 (#4865)
Bumps [ts-morph](https://github.com/dsherret/ts-morph) from 27.0.2 to 28.0.0.
- [Release notes](https://github.com/dsherret/ts-morph/releases)
- [Commits](https://github.com/dsherret/ts-morph/compare/27.0.2...28.0.0)

---
updated-dependencies:
- dependency-name: ts-morph
  dependency-version: 28.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 15:49:35 +02:00
Anniken Granberg fa6c935530 DataTable: remove old drag and drop 2026-05-11 14:17:25 +02:00
Anniken GranbergandKen 1f39de488f 🎨 InfoCard: Added bigger padding for medium sized InfoCard.Message (#4861)
* 🎨 InfoCard: Change padding for small and medium for InfoCard.Message

* changeset

* Update @navikt/core/css/src/alert.css

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-05-11 12:35:20 +02:00
Ken e29b94e4d2 [Website] Codeblock adjustments (#4859)
* 💄 Adjusted styles

* 📝 Adjust default language render

* 🐛 Sync codeblock-render with new tabs
2026-05-11 12:15:47 +02:00
dependabot[bot] cb2d048ed8 Bump the storybook group with 9 updates (#4864)
Bumps the storybook group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-a11y](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/a11y) | `10.3.5` | `10.3.6` |
| [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs) | `10.3.5` | `10.3.6` |
| [@storybook/addon-themes](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/themes) | `10.3.5` | `10.3.6` |
| [@storybook/addon-vitest](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/vitest) | `10.3.5` | `10.3.6` |
| [@storybook/nextjs-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/nextjs) | `10.3.5` | `10.3.6` |
| [@storybook/react-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/react-vite) | `10.3.5` | `10.3.6` |
| [eslint-plugin-storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/eslint-plugin) | `10.3.5` | `10.3.6` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) | `10.3.5` | `10.3.6` |
| [storybook-addon-pseudo-states](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/pseudo-states) | `10.3.5` | `10.3.6` |


Updates `@storybook/addon-a11y` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/a11y)

Updates `@storybook/addon-docs` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/docs)

Updates `@storybook/addon-themes` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/themes)

Updates `@storybook/addon-vitest` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/vitest)

Updates `@storybook/nextjs-vite` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/frameworks/nextjs)

Updates `@storybook/react-vite` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/frameworks/react-vite)

Updates `eslint-plugin-storybook` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/lib/eslint-plugin)

Updates `storybook` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/core)

Updates `storybook-addon-pseudo-states` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/pseudo-states)

---
updated-dependencies:
- dependency-name: "@storybook/addon-a11y"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-themes"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-vitest"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/nextjs-vite"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/react-vite"
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: eslint-plugin-storybook
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook-addon-pseudo-states
  dependency-version: 10.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:54:38 +02:00
Anniken Granberg 11d8874fba DataTable: Add textSize and connect row density to selected text size 2026-05-08 19:19:25 +02:00
Halvor Haugan 7d85a0c69e DataTable: BaseCell type improvements + comments 2026-05-08 16:32:39 +02:00
Halvor Haugan e537e040fe DataTable: Built-in spacing for for tags and buttons 2026-05-08 11:48:32 +02:00
github-actions[bot] ed6c7e999d Version Packages (#4853)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-07 13:23:04 +02:00
Anniken Granberg cf9992ca3e DataTable / dnd: fix types 2026-05-07 12:57:24 +02:00
Anniken Granberg 4db5cbfe54 DataTable: failed if i didnt remove comment??? 2026-05-07 12:20:39 +02:00
Anniken Granberg 54a077fbb7 DataTable / dnd: Fix type and other small stuff 2026-05-07 12:16:04 +02:00
Anniken Granberg a6e2a2f4d4 DataTable / dnd: Background color only on overlay, removed default padding and margin on ul 2026-05-07 12:16:04 +02:00
Anniken GranbergandCopilot 7e31ee6822 DataTable / dnd: Rename old to legacy
Co-authored-by: Copilot <copilot@github.com>
2026-05-07 12:16:04 +02:00
Halvor Haugan 35295e7c01 Pagination: Fix selected-styling in forced-colors mode (#4858) 2026-05-07 11:31:22 +02:00
Halvor Haugan 0522dfae59 Button: Fix issue where secondary and tertiary variants did not have correct text color is certain cases (#4857) 2026-05-07 08:31:50 +02:00
Halvor Haugan 53d40657bb [Sanity] Add component titles to preview of Props section (#4856) 2026-05-06 12:32:32 +02:00
Halvor Haugan 334770de25 Fix running tests in react package on Windows (#4855) 2026-05-06 09:57:10 +02:00
Anniken Granberg faec264163 DataTable / DND : Improved announcer 2026-05-05 22:31:32 +02:00
Anniken Granberg fbfdeba70b DataTable / DND: Better label, handle reset on esc 2026-05-05 21:14:17 +02:00
Halvor Haugan 76c3143d68 ⬆️ Bump typescript to 6.0.3 to fix build issue 2026-05-05 14:55:47 +02:00
Ken bec61c3743 [DataTable] Update feature gap md 2026-05-05 14:45:24 +02:00
Ken 3fbba87f06 [DataTable] Update kitchensink demo 2026-05-05 14:45:24 +02:00
Ken 62197a107b [DataTable] Refactor scroll-calculation 2026-05-05 14:45:24 +02:00
Ken 4f55967cf8 [DataTable] Dynamically add scroll-margin to focused cell 2026-05-05 14:45:23 +02:00
Halvor Haugan 335b7b7af2 [website] Add field for heading-level in PropsSeksjon (#4854) 2026-05-05 13:57:26 +02:00
Ken 1073f5d502 [DataTable] Keyboard-nav now acts like rowing tabindex pattern 2026-05-05 10:28:25 +02:00
Ken ffab045fcb [DataTable] Fixed issue with keyboard-nav not updating cache when expanding/collapsing rows 2026-05-05 10:08:47 +02:00
Ken 8bb8083be3 [DataTable] Remove index from getRowId callbacks 2026-05-05 10:00:10 +02:00
Ken ba7d2be630 [DataTable] Refactor object reference to unique ID 2026-05-05 09:41:24 +02:00
Ken 4d3839e9b5 [DataTable] TODO 2026-05-04 16:35:17 +02:00
Ken 3ff4d17fa0 [DataTable] Remove legacy todo 2026-05-04 16:32:37 +02:00
Ken 53497224fa [DataTable] Type button for buttons in columnHeader 2026-05-04 16:31:07 +02:00
Ken 1284fefa7b [DataTable] Named useEffect 2026-05-04 16:13:07 +02:00
Ken 2f264b7cb7 [DataTable] getRowId is now optional, falling back to unique ids, even for nested items 2026-05-04 15:23:42 +02:00
dependabot[bot]andKen 4359fb3aba Bump @floating-ui/react from 0.27.8 to 0.27.19 (#4851)
* Bump @floating-ui/react from 0.27.8 to 0.27.19

Bumps [@floating-ui/react](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/react) from 0.27.8 to 0.27.19.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/react/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/react@0.27.19/packages/react)

---
updated-dependencies:
- dependency-name: "@floating-ui/react"
  dependency-version: 0.27.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* 📝 Remove floating ui from website

* 📝 Changeset

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-05-04 14:41:18 +02:00
dependabot[bot]andKen 632fcff376 Bump the vitest group with 4 updates (#4847)
* Bump the vitest group with 4 updates

Bumps the vitest group with 4 updates: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser), [@vitest/browser-playwright](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser-playwright), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@vitest/browser` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/browser)

Updates `@vitest/browser-playwright` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/browser-playwright)

Updates `@vitest/coverage-v8` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/coverage-v8)

Updates `vitest` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>

* 📝 Sync versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-05-04 14:29:34 +02:00
dependabot[bot]andKen 08b1ef6d72 Bump fuse.js from 7.1.0 to 7.3.0 (#4849)
* Bump fuse.js from 7.1.0 to 7.3.0

Bumps [fuse.js](https://github.com/krisk/Fuse) from 7.1.0 to 7.3.0.
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v7.1.0...v7.3.0)

---
updated-dependencies:
- dependency-name: fuse.js
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* 🎉 Improve fuse-search

* 📝 yarn lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2026-05-04 14:29:14 +02:00
github-actions[bot] f7c9bde6d2 Version Packages (#4852)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-04 14:06:08 +02:00
Halvor Haugan 66d7a204cd ⬆️ Upgrade some dependencies (#4845) 2026-05-04 12:48:20 +02:00
Ken 6d56ec8f51 [ActionMenu] Now biases towards top/bottom placement on all screens (#4843)
* 🐛 Bias menus using floating util to stay locked to top/bottom side

* 📝 Changeset

* 📝 Changeset update

* ♻️ Improved collisionpadding syntax

* 📝 Updated changeset
2026-05-04 12:40:15 +02:00
dependabot[bot] 2ecf0ee03c Bump @dnd-kit/react from 0.3.2 to 0.4.0 (#4848)
Bumps [@dnd-kit/react](https://github.com/clauderic/dnd-kit) from 0.3.2 to 0.4.0.
- [Release notes](https://github.com/clauderic/dnd-kit/releases)
- [Commits](https://github.com/clauderic/dnd-kit/compare/@dnd-kit/react@0.3.2...@dnd-kit/react@0.4.0)

---
updated-dependencies:
- dependency-name: "@dnd-kit/react"
  dependency-version: 0.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 12:19:54 +02:00
Halvor Haugan ee8a3be487 DataTable: Story: KitchenSinkAdv: label as label on column switches 2026-05-04 09:41:59 +02:00
Halvor Haugan 4a4100d0b2 Exclude some more files from css package (#4844) 2026-05-04 08:45:12 +02:00
Halvor Haugan 1da8a06783 DataTable: useTableColumnResize: Rearrange props 2026-04-30 15:18:05 +02:00
Halvor HauganandCopilot 88f96906c7 DataTable: label prop in DataTableColumnHeader
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 14:52:42 +02:00
Ken a8ae8d2c54 [DataTable] Fixed bugged story 2026-04-30 14:18:54 +02:00
Ken 045dbda89a [DataTable] Added scroll shadows with scroll-timeline 2026-04-30 13:10:59 +02:00
Ken 193b23151e [DataTable] Add resize-observer to sticky shadow 2026-04-30 13:10:58 +02:00
Halvor Haugan b54380304d DataTable: Test data: Remove redundant date formattting 2026-04-30 12:44:42 +02:00
Ken 5706c959b2 [DataTable] Update kitchen-sink demo 2026-04-30 11:50:13 +02:00
Ken 9442ac558b [DataTable] Dynamically show/hide shadow for sticky columns 2026-04-30 11:06:57 +02:00
Ken e8080c269b [DataTable] Added more auto-sizing to kitchen-sink demo 2026-04-30 10:01:43 +02:00
Ken c35a9cd6a2 [DataTable] Added shadow to sticky columns 2026-04-30 10:00:10 +02:00
Halvor Haugan 0d0beae02d DataTable: fix issue with autoResize 2026-04-30 09:40:32 +02:00
Ken 2eacd71805 [DataTable] Added demo-stories for presentation, stickyheader now default 2026-04-30 09:35:49 +02:00
Halvor Haugan be22673419 DataTable: CSS fixes 2026-04-29 19:23:51 +02:00
Ken b2f7dd4e52 [DataTable] Temp optimization for auto-version 2026-04-29 15:13:40 +02:00
Ken 3284cee8a9 [DataTable] Memorize columnoptions 2026-04-29 15:07:46 +02:00
Ken 34fbdc9cd3 [DataTable] Fixed selection state for sticky columns 2026-04-29 14:46:30 +02:00
Ken 2f1aa0988c [DataTable] Details panel now has border 2026-04-29 14:43:50 +02:00
Ken 7bfacc698d [DataTable] Updated advanced kitchen-sink demo 2026-04-29 14:41:11 +02:00
Ken d02df8313c [DataTable] Create updated test-data for kitchen sink setup 2026-04-29 12:29:44 +02:00
Ken 5c94d6ec7f [DataTable] Fix type import/export 2026-04-29 11:20:22 +02:00
KenandHalvor Haugan 3c9d24ba87 [Website] 🎉 Added API-route for fetching component props based on route (#4833)
* 🎉 Added API-route for fetching component props based on route

* 📝 Added docs

* ⚡ Update caching strategy

* ⚡ Update caching strategy

* 🎉 Added example, params and return fields to props

* 🔒 Better slug parsing

* Update aksel.nav.no/website/app/api/component-props/route.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* ⚡ Remove redundant string check

* 🔒 Guard against long paths

* ⚡ Validate segments before parsing

* ⚡ better regex for exaxt path segments

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2026-04-29 10:22:21 +02:00
Ken 5e426e9d64 [DataTable] Added non-working tests for cache 2026-04-28 18:06:27 +02:00
Ken 7a85f0d27f [DataTable] Refactor keyboard-nav stories to use new API 2026-04-28 17:58:25 +02:00
Ken 8f75f4426a [DataTable] Replace root component with auto version, stories not synced 2026-04-28 15:56:12 +02:00
Ken 71c16eb6d8 [DataTable] Exctact util components from auto render 2026-04-28 15:46:00 +02:00
Ken 776aa84fa8 [DataTable] Default to keyboard nav true 2026-04-28 15:31:26 +02:00
Ken f985e1e83f [DataTable] Refactor prop-names for subRow and detailsPanel props 2026-04-28 14:50:43 +02:00
Halvor Haugan 48c7794cac useControllableState: The setter can have narrower type than the state (#4842) 2026-04-28 14:45:43 +02:00
dependabot[bot] 10b1892f54 Bump @vitejs/plugin-react from 5.1.4 to 6.0.1 (#4757)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.1.4 to 6.0.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 14:16:47 +02:00
Halvor HauganandCopilot 193200ec9c DataTable: resize adjustments
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 11:56:13 +02:00
Ken 01fc444219 [DataTable] Updated play-tests for auto 2026-04-28 11:41:07 +02:00
Ken cb5b73c1d5 📝 Re-order hooks 2026-04-28 11:23:51 +02:00
Ken 077eae7365 [DataTable]🏷️ Rename expansioncontext -> detailspanelcontext 2026-04-28 11:23:51 +02:00
Ken aa3f58c00d [DataTable] Simplify prop-drilling into hooks and providers 2026-04-28 11:23:51 +02:00
Ken 4368a14896 [DataTable] 🏷️ Grouped related props 2026-04-28 11:23:51 +02:00
dependabot[bot] 45b0939c52 Bump dompurify from 3.3.2 to 3.4.0 (#4829)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.2 to 3.4.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.2...3.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 11:20:13 +02:00
dependabot[bot] d2fb1d8bc1 Bump follow-redirects from 1.15.11 to 1.16.0 (#4830)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 11:19:08 +02:00
dependabot[bot]andKen 363ee8e483 Bump @vitest/eslint-plugin from 1.6.14 to 1.6.16 in the vitest group (#4835)
Bumps the vitest group with 1 update: [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest).


Updates `@vitest/eslint-plugin` from 1.6.14 to 1.6.16
- [Release notes](https://github.com/vitest-dev/eslint-plugin-vitest/releases)
- [Commits](https://github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.14...v1.6.16)

---
updated-dependencies:
- dependency-name: "@vitest/eslint-plugin"
  dependency-version: 1.6.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2026-04-28 11:18:37 +02:00
github-actions[bot] ff454dbc06 Version Packages (#4840)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-28 11:07:06 +02:00
Halvor Haugan 92f6d9e93b Add module decleration for ds-css (#4838) 2026-04-27 18:42:22 +02:00
Halvor HauganandCopilot 516634b852 [website] Improvements in props-section (#4839)
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 18:12:34 +02:00
Halvor Haugan 48939bcee0 Merge branch 'main' of github.com:navikt/aksel 2026-04-27 18:11:27 +02:00
KenandCopilot 5ea6069010 🐛 Stop className from being fowarded to input-element in radio/checkbox (#4837)
* 🐛 Stop className from being fowarded to input-element in radio/checkbox

* Update .changeset/cuddly-queens-roll.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-27 18:10:05 +02:00
Halvor Haugan 5eee3136a0 Adjust JSDoc in ModalProps 2026-04-27 18:10:04 +02:00
Ken 21521b892e [DataTable] Default to sticky header always 2026-04-27 14:05:06 +02:00
Ken f563c18a59 [DataTable] Move selection-props to single prop 2026-04-27 13:32:26 +02:00
Ken ca1bd3f5c0 [DataTable] Added 'align'-prop to columnDef 2026-04-27 13:20:54 +02:00
Ken a71f326426 [DataTable] Removed untrue comment 2026-04-27 13:01:23 +02:00
Ken fb79fd775c [DataTable] Fix scroll when defining detailsPanel height 2026-04-27 13:01:23 +02:00
Ken 8a0afad10c [DataTable] Only render loading-label once 2026-04-27 13:01:22 +02:00
Halvor HauganandCopilot 92e6128cc9 DataTable: Add resizable prop + fix resizing issue
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 10:04:30 +02:00
Ken ff0221736c Squashed commit of the following:
commit 82656da435
Author: Ken <ken.aleksander@gmail.com>
Date:   Mon Apr 27 09:57:48 2026 +0200

    📝 Added comments

commit ab9932ed8f
Author: Ken <ken.aleksander@gmail.com>
Date:   Mon Apr 27 09:55:47 2026 +0200

    📝 Added comments

commit 180ca25861
Author: Ken <ken.aleksander@gmail.com>
Date:   Fri Apr 24 13:38:51 2026 +0200

    ⚡ Add cached parser for optimization

commit cbac535fcb
Author: Ken <ken.aleksander@gmail.com>
Date:   Fri Apr 24 13:10:18 2026 +0200

    📝 Add docs

commit 296049b8c7
Author: Ken <ken.aleksander@gmail.com>
Date:   Fri Apr 24 13:00:21 2026 +0200

    [DataTable] Optimize recursive parsing

commit 073d811b3f
Author: Ken <ken.aleksander@gmail.com>
Date:   Fri Apr 24 12:45:15 2026 +0200

    [DataTable] Now supports branch selection

commit da1374e833
Author: Ken <ken.aleksander@gmail.com>
Date:   Fri Apr 24 12:22:41 2026 +0200

    [DataTable] Updated selection
2026-04-27 09:58:45 +02:00
Ken abe4a768e1 🐛 Revert nb locale update 2026-04-24 15:06:30 +02:00
Ken c94d801b45 [DataTable] removed unused tests 2026-04-24 13:42:08 +02:00
Halvor Haugan 8e01bbd81e CopyButton: Improve a11y by having title on button instead of icon 2026-04-14 13:26:27 +02:00
1840 changed files with 89412 additions and 36328 deletions

No files matched your search

+9 -4
View File
@@ -21,11 +21,16 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"aksel.nav.no",
"aksel-icons-figma-plugin",
"eslint-plugin-aksel-local",
"scripts",
"aksel-analyzer"
"aksel-changelog",
"aksel-analyzer",
"aksel-workspace-scripts",
"aksel.nav.no",
"website",
"aksel-sanity-studio",
"aksel-icons-figma-plugin",
"aksel-mcp",
"playroom"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
-1
View File
@@ -8,7 +8,6 @@
- [ ] Examples
- [ ] Documentation / Decision Records
- [ ] Storybook
- [ ] Style mappings (`@navikt/core/css/config/_mappings.js`)
- [ ] CSS class deprecations (`@navikt/aksel-stylelint/src/deprecations.ts`)
- [ ] Exports (`@navikt/core/react/src/index.ts` and `@navikt/core/react/package.json`)
- [ ] New component? CSS import (`@navikt/core/css/index.css`)
+1 -1
View File
@@ -9,7 +9,7 @@ runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
+47 -108
View File
@@ -1,43 +1,27 @@
# Aksel - Copilot Instructions
## Use this file first
## Approach
- Trust these instructions to narrow the search space, but still read the target code and nearest related files before editing.
- Optimize for local work. Prefer known paths, nearby patterns, and existing scripts over broad repo searches.
- Ignore release, deploy, changelog, CMS sync, and other remote/ops scripts unless the user asks.
- Narrow search space; don't replace reading code.
- Before editing: read target file + nearest related files (`index.ts`, story, test, exports).
- Prefer known paths/scripts over broad searches.
- Ignore release/deploy/changelog/ops scripts unless asked.
## How to use this file
## Behavior
- Use this file to narrow the search space, not to replace reading code.
- Before editing, read the target file and the nearest related files in the same folder such as `index.ts`, story, test, and package/root export files.
- Search wider only when nearby files do not answer the question.
## Agent mode
- No apologies, no “I think”, no change summaries.
- Don’t ask to confirm info that’s already in context.
- Favor performance and security; add robust error handling and safe logging.
- Default tone: short and answer-first. Aim for max 0–2 concise sentences where needed.
- Thinking and explanation process: Keep it short, omit whats not strictly necessary, and focus on the next edit.
- Avoid long explanations or multiple steps at once.
- Don't explain thinking process for each step, just do the next edit. Only add explanations when the user explicitly asks for it or when it's necessary to clarify a non-obvious change.
## Inline chat and editor completions
- Verify before you claim. Don’t speculate.
- Keep functions small and focused; remove duplication (rule of three).
- Show only what changes. Provide minimal diffs/snippets; don’t paste whole files or unrelated lines.
- Keep edits scoped to a single file unless explicitly asked to touch more.
- Extract requirements into a tiny checklist and proceed. Ask a question only when truly blocked.
- Avoid heavy formatting: bullets are OK; skip tables; use code fences only for code.
- For performance/security: avoid unnecessary re-renders, side effects at module top-level, unsafe HTML, and leaking env vars.
- No apologies, no "I think", no change summaries.
- Answer-first. 0-2 sentences max.
- No thinking aloud; do next edit. Explain only when asked or non-obvious.
- Show only what changes. Minimal diffs; don't paste whole files.
- Edits scoped to one file unless asked. Ask only when truly blocked.
- Favor perf/security; avoid re-renders, top-level side effects, unsafe HTML, env var leaks.
## Repo summary
- Large Yarn 4 workspaces monorepo for NAV's design system: public React/CSS/tokens/icons/stylelint/CLI packages plus docs site and playroom.
- Yarn 4 workspaces monorepo: public React/CSS/tokens/icons/stylelint/CLI packages + docs site + playroom.
- Stack: TypeScript 6, React 19, Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Biome 2, Prettier 3.
- Runtime: Node 22+ recommended, Yarn 4.12.0.
- Important constraint: code under `@navikt/*` must stay React 17 API compatible. Do not use React 18/19-only APIs there.
- Node 22+, Yarn 4.12.0.
- `@navikt/*` must stay React 17 compatible.
## Start and validate
@@ -45,101 +29,56 @@
2. `yarn install`
3. `yarn boot`
- Run `yarn boot` after first install and after `yarn clean`.
- Full install may need `NPM_AUTH_TOKEN` for GitHub Packages.
- `yarn boot` after first install + after `yarn clean`.
- Full install needs `NPM_AUTH_TOKEN` for GitHub Packages.
Public-package-only install:
```bash
yarn workspaces focus @navikt/aksel-icons @navikt/ds-tokens @navikt/ds-css @navikt/ds-react @navikt/ds-tailwind @navikt/aksel @navikt/aksel-stylelint
```
Public-package-only: `yarn workspaces focus @navikt/aksel-icons @navikt/ds-tokens @navikt/ds-css @navikt/ds-react @navikt/ds-tailwind @navikt/aksel @navikt/aksel-stylelint`
## Definition of done
- Behavior or visual change: update the implementation, closest story, and closest test.
- New public export: update the component `index.ts`, package `src/index.ts`, and `package.json` exports.
- User-facing package change: add a changeset unless the task says otherwise.
- Validate with the narrowest workspace command first; use root commands only for cross-workspace changes.
- Behavior/visual change: update impl + closest story + test.
- New public export: sync component `index.ts`, `src/index.ts`, `package.json` exports.
- User-facing change: add changeset unless told otherwise.
- Validate with narrowest workspace command first.
### Common local commands
### Commands
Always prefer using these commands that already exists in workspaces before running custom commands or doing manual steps. These are optimized for local development and validation. For example:
- `yarn boot` - build all workspaces
- `yarn test` - run all workspace tests
- `yarn lint` - eslint + stylelint + biome + cycle + version checks
- `yarn boot` - build all
- `yarn test` / `yarn lint` - all tests / all linting
- `yarn lint:eslint` / `yarn lint:css` / `yarn lint:biome`
- `yarn storybook` - root Storybook on `localhost:6006`
- `yarn storybook:aksel` - website/example Storybook on `localhost:6007`
- `yarn storybook:test` - Storybook browser tests, slow, uses Playwright/Firefox
- `yarn dev` - website dev server on `localhost:3000`
- `yarn storybook` - root `:6006`
- `yarn storybook:aksel` - website examples `:6007`
- `yarn storybook:test` - browser tests (Playwright/Firefox)
- `yarn dev` - website `:3000`
- `yarn clean` - remove build artifacts
- `yarn workspace @navikt/ds-react build`
- `yarn workspace @navikt/ds-react test`
- `yarn workspace @navikt/ds-react build|test`
- `yarn workspace website test`
- `yarn workspace playroom sync-imports`
### Validated locally
- Works: `yarn boot`, `yarn test`, `yarn storybook --ci --smoke-test`, `yarn storybook:aksel --ci --smoke-test`
- Caveat: `yarn lint` and `yarn lint:eslint` can fail if playroom imports are stale. Fix with `yarn workspace playroom sync-imports`.
`yarn lint` fails if playroom imports stale → `yarn workspace playroom sync-imports`.
## Where to edit
- `@navikt/core/react/` - `@navikt/ds-react`
- `src/<component>/<Component>.tsx`
- `src/<component>/index.ts`
- `src/<component>/*.stories.tsx`
- `src/index.ts`
- `package.json` exports map for new public components
- `@navikt/core/react/src/<component>/` - component source, index, stories
- `@navikt/core/react/src/index.ts` + `package.json` exports - public API surface
- `@navikt/core/css/src/` - component CSS
- `@navikt/core/tokens/src/` - design token source
- `@navikt/core/tokens/src/` - design tokens
- `@navikt/core/tailwind/` - Tailwind preset
- `@navikt/aksel-icons/icons/` - source SVGs; `src/` is generated
- `@navikt/aksel-stylelint/src/` - Stylelint rules/config
- `@navikt/aksel-icons/icons/` - source SVGs (`src/` generated)
- `@navikt/aksel-stylelint/src/` - Stylelint rules
- `@navikt/aksel/src/` - CLI and codemods
- `aksel.nav.no/website/` - Next.js docs site
- `aksel.nav.no/playroom/` - playroom and generated imports
- `examples/` - example apps
- `scripts/` - shared repo scripts
- Root references: `README.md`, `CONTRIBUTING.md`, `aksel.nav.no/README.md`, `package.json`, `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `tsconfig.json`, `tsconfig.build.json`, `.storybook/`, `.github/workflows/build-validation.yml`
- `apps/playroom/` - playroom
- `apps/mcp-server/` - MCP server
- `apps/figma-icon-plugin/` - Figma plugin
- Root: `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `tsconfig.json`, `.storybook/`
## Coding defaults
- Keep diffs small. No drive-by refactors, formatting churn, or new dependencies unless asked.
- Read the full file and copy the nearest existing pattern before changing code.
- Keep public APIs stable. Prefer additive props and exports.
- In `@navikt/*`, import React in TSX files; root JSX runtime is classic.
- Use named exports with explicit type exports.
- Keep `forwardRef`, `className`, `...rest`, and `as`/`OverridableComponent` patterns when the surrounding component uses them.
- Reuse internal helpers instead of re-implementing them: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
- Use `node:` imports for Node builtins.
- Never import from `esm/` or `cjs/` build output.
- Keep import order consistent: `@navikt/*`, `@/*`, then relative. Let Prettier handle sorting.
- Preserve or add JSDoc for public props/components when changing public APIs.
## React and ds-react specifics
- Component `index.ts` files in `@navikt/core/react/src/**` start with `"use client"`.
- When adding or exposing a public component, keep these in sync:
- component `index.ts`
- `@navikt/core/react/src/index.ts`
- `@navikt/core/react/package.json` exports
- Do not use React 18/19-only APIs under `@navikt/*`. Use internal helpers like `useId`.
## CSS and tokens
- Never hardcode colors, spacing, radius, or shadows. Use tokens only.
- Public tokens use `--ax-*`. Internal component variables use `--__axc-*`.
- Follow existing CSS nesting, `data-*` selectors, `focus-visible`, `forced-colors`, and reduced-motion patterns.
- Prettier formats; Biome is lint-only.
## Stories, tests, website
- Update the closest story and test whenever behavior or visuals change.
- Use Testing Library queries by role and name.
- Stories live next to components and use `@storybook/react-vite`.
- For component work, prefer Storybook over `yarn dev`.
- `aksel.nav.no/website/pages/eksempler/**` and `pages/templates/**` feed the website and Storybook. In these files, only import from `@navikt/*` and `react`.
- The website uses both `app/` and `pages/`; stay inside the router already used nearby.
- `yarn storybook:aksel` works for website examples/templates without Sanity secrets. `yarn dev` may need website env values such as `SANITY_READ_NO_DRAFTS`.
- Small diffs. No drive-by refactors or new deps unless asked.
- Copy nearest existing pattern before writing new code.
- Stable public APIs. Prefer additive props/exports.
- `node:` imports for Node builtins. Never import from `esm/`/`cjs/` output.
- Import order: `@navikt/*`, `@/*`, relative. (Prettier sorts)
- Preserve JSDoc on public props/components when changing public APIs.
- Reuse helpers: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
+4 -4
View File
@@ -3,13 +3,11 @@ registries:
npm-github:
type: npm-registry
url: https://npm.pkg.github.com
scope: "@navikt"
token: ${{ secrets.READER_TOKEN }}
updates:
- package-ecosystem: npm
directory: /
exclude-paths:
- "examples/astro/package.json"
- "examples/shadow-dom/package.json"
schedule:
interval: weekly
day: monday
@@ -54,7 +52,9 @@ updates:
- "*"
- package-ecosystem: docker
directory: "/"
directories:
- "/aksel.nav.no"
- "/apps/*"
schedule:
interval: weekly
day: monday
@@ -0,0 +1,78 @@
---
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra.
Always use caveman for code-related questions unless spesificed otherwise. For non-code topics, use caveman if user seems to want quick, concise answers.
applyTo: "**"
---
# caveman
ACTIVE EVERY RESPONSE.
default: full
switch mode:
- "/caveman lite" → lite
- "/caveman full" → full
- "/caveman ultra" → ultra
- "stop caveman" | "normal mode" → off
persist: keep last state
## intensity
| Level | What change |
| --------- | ------------------------------------------------------------------------------------------------------------- |
| **lite** | short sentences, no filler, keep grammar |
| **full** | fragments ok, drop articles, short words. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), fragments, abbrev (db/api/req/res/fn), arrows (→), minimal words |
Example - "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
## global rules
- no filler (just/really/basically/etc)
- no pleasantries
- no hedging
- keep tech exact
- no long sentences
- prefer symbols (→, =)
## patterns
Patterns: `[thing] [action] [reason]. [next step].`
## examples
lite:
"Component re-renders because you create a new object each render. Use useMemo."
full:
"New object each render → new ref → re-render. useMemo."
ultra:
"inline obj → new ref → re-render. fix: useMemo."
## auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
Example - destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
>
> ```sql
> DROP TABLE users;
> ```
>
> Caveman resume. Verify backup exist first.
## boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end
@@ -0,0 +1,42 @@
---
description: "Code review instructions used when reviewing pull requests."
applyTo: "**"
excludeAgent: ["coding-agent"]
---
# Code review focus
Review for issues that could block release, surprise users, or create maintenance risk. Prefer high-signal findings over style-only comments.
## What to check
### 1. Public API and behavior
- New or changed APIs should match existing naming, params, defaults, and behavior patterns.
- Watch for breaking changes, regressions, and accidental behavior changes in public components, hooks, CSS, tokens, and CLI output.
- Avoid introducing new patterns unless the change clearly justifies them.
### 2. Documentation and examples
- Public components and exported APIs should have accurate JSDoc, including `@default` where it adds clarity.
- Check whether Storybook stories, website examples, and decision records need updates to match the new behavior.
### 3. Tests
- Flag missing or insufficient tests for changed behavior, edge cases, and regressions.
- Prefer tests that prove the contract users rely on, not just implementation details.
### 4. CSS and styling
- If CSS class names are removed, verify deprecation handling in `@navikt/aksel-stylelint/src/deprecations.ts`.
- If new CSS files or selectors are added, confirm they follow the repo's import and naming conventions.
### 5. Exports and packaging
- New public exports must be wired through the correct `index.ts` files and `package.json` exports when relevant.
- Check that release-facing entry points stay consistent across packages.
### 6. Changesets and migrations
- User-facing changes should include a changeset with a clear summary and impact.
- Breaking changes should also include a codemod when needed.
@@ -1,11 +1,11 @@
description: "Use for JS/TS code in this repo."
applyTo: "**/\*.tsx", "**/_.jsx", "\*\*/_.ts", "\*_/_.js"
---
description: "JS/TS code conventions."
applyTo: "**/*.tsx", "**/*.jsx", "**/*.ts", "**/*.js"
---
# JS/TS instructions
- Prefer `for...of` over `Array.forEach`.
- Prefer named exports; keep type exports separate from value exports.
- Group exports at the bottom: values first, then types.
- In `*.stories.*`, keep the default export at the top.
- Named exports; type exports separate from value exports.
- Group exports at bottom: values first, then types.
- In `*.stories.*`, keep default export at top.
@@ -1,13 +1,17 @@
description: "Use for ds-react components and component CSS."
---
description: "ds-react components and component CSS."
applyTo: "@navikt/core/react/src/**", "@navikt/core/css/src/**"
---
# ds-react and component instructions
# ds-react component instructions
- Read the full component folder before editing.
- Preserve the surrounding `forwardRef`, `className`, `...rest`, and `as`/`OverridableComponent` patterns.
- Keep `@navikt/*` code React 17 API compatible.
- Use tokens, not hardcoded component values.
- If behavior or visuals change, update the nearest story and test.
- If a public component changes, keep component exports, package exports, and root exports in sync.
- Read full component folder before editing.
- Preserve `forwardRef`, `className`, `...rest`, `as`/`OverridableComponent` patterns.
- `@navikt/*` must stay React 17 compatible. No React 18/19-only APIs; use internal `useId` etc.
- `@navikt/*` TSX: import React explicitly (classic JSX).
- Component `index.ts` starts with `"use client"`.
- Use tokens, not hardcoded values. Token names: `--ax-*` (public), `--__axc-*` (internal).
- Follow CSS nesting, `data-*` selectors, `focus-visible`, `forced-colors`, reduced-motion.
- Prettier formats CSS; Biome lint-only.
- Behavior/visual change: update nearest story + test.
- Public component change: sync component `index.ts`, `src/index.ts`, `package.json` exports.
+10 -7
View File
@@ -1,11 +1,14 @@
description: "Use for tests and stories."
applyTo: "**/_.test._", "**/\*.stories.tsx"
---
description: "Tests and stories."
applyTo: "**/*.test.*", "**/*.stories.tsx"
---
# Testing and Storybook instructions
- Prefer story `play` functions and `yarn storybook:test` for real user flows; use Vitest for logic and non-browser cases.
- Prefer focused assertions over snapshots unless nearby tests already use snapshots.
- Cover the main path and one or two meaningful edge cases.
- If UI behavior changes, update the nearest story and test in the same change.
- Prefer story `play` functions + `yarn storybook:test` for user flows; Vitest for logic/non-browser.
- Prefer focused assertions over snapshots unless nearby tests use them.
- Cover main path + 1-2 meaningful edge cases.
- UI behavior change: update nearest story + test in same change.
- Testing Library: query by role and name.
- Stories use `@storybook/react-vite`, live next to components.
- Prefer `yarn storybook` for component work over `yarn dev`.
+8 -10
View File
@@ -1,14 +1,12 @@
description: "Use for website and playroom work."
applyTo:
- "aksel.nav.no/website/\*\*"
- "aksel.nav.no/playroom/\*\*"
---
description: "Website and playroom."
applyTo: "aksel.nav.no/website/**, aksel.nav.no/playroom/**"
---
# Website and playroom instructions
- Stay inside the router already used nearby; do not mix `app/` and `pages/` patterns in the same area.
- In `aksel.nav.no/website/pages/eksempler/**` and `aksel.nav.no/website/pages/templates/**`, only import from `@navikt/*` and `react`.
- Reuse nearby website utilities and patterns before adding abstractions.
- Prefer `yarn storybook:aksel` for examples/templates; use `yarn dev` only for app-only behavior.
- Stay in router already used nearby; don't mix `app/`/`pages/`.
- `pages/eksempler/**` and `pages/templates/**`: only import from `@navikt/*` and `react`.
- Reuse nearby utils/patterns before adding abstractions.
- Prefer `yarn storybook:aksel` for examples/templates; `yarn dev` only for app-specific behavior.
- `yarn storybook:aksel` needs no secrets. `yarn dev` needs `SANITY_READ_NO_DRAFTS`.
@@ -0,0 +1,34 @@
---
name: content-modeling-best-practices
description: Structured content modeling guidance for schema design, content architecture, content reuse, references versus embedded objects, separation of concerns, and taxonomies across Sanity and other headless CMSes. Use this skill when designing or refactoring content types, deciding field shapes, debating reusable versus nested content, planning omnichannel content models, or reviewing whether a schema is too page-shaped or presentation-driven.
---
# Content Modeling Best Practices
Principles for designing structured content that's flexible, reusable, and maintainable. These concepts apply to any headless CMS but include Sanity-specific implementation notes.
## When to Apply
Reference these guidelines when:
- Starting a new project and designing the content model
- Evaluating whether content should be structured or free-form
- Deciding between references and embedded content
- Planning for multi-channel content delivery
- Refactoring existing content structures
## Core Principles
1. **Content is data, not pages** — Structure content for meaning, not presentation
2. **Single source of truth** — Avoid content duplication
3. **Future-proof** — Design for channels that don't exist yet
4. **Editor-centric** — Optimize for the people creating content
## References
Start with the reference that matches the modeling decision in front of you, instead of loading every topic at once. See `references/` for detailed guidance on specific topics:
- `references/separation-of-concerns.md` — Separating content from presentation
- `references/reference-vs-embedding.md` — When to use references vs embedded objects
- `references/content-reuse.md` — Content reuse patterns and the reuse spectrum
- `references/taxonomy-classification.md` — Flat, hierarchical, and faceted classification
@@ -0,0 +1,127 @@
# Content Reuse Patterns
Effective content models maximize reuse while minimizing duplication. Here are patterns for achieving both.
## The Content Reuse Spectrum
```
Full Duplication ←————————————————→ Full Reference
(Copy everything) (Link to one source)
```
Most real-world content sits somewhere in between.
## Pattern 1: Shared Components
Create reusable content blocks that can be embedded anywhere.
**Use case:** Testimonials, FAQs, CTAs that appear on multiple pages.
```typescript
// Standalone testimonial documents
defineType({
name: "testimonial",
type: "document",
fields: [
defineField({ name: "quote", type: "text" }),
defineField({ name: "author", type: "string" }),
defineField({ name: "company", type: "string" }),
],
});
// Reference in page builders
defineField({
name: "pageBuilder",
type: "array",
of: [{ type: "reference", to: [{ type: "testimonial" }] }],
});
```
## Pattern 2: Shared Field Sets
Extract common fields into reusable definitions.
**Use case:** SEO fields, social metadata, common dates.
```typescript
// Shared field definition
export const seoFields = [
defineField({ name: "seoTitle", type: "string" }),
defineField({ name: "seoDescription", type: "text" }),
defineField({ name: "ogImage", type: "image" }),
];
// Spread into multiple types
defineType({
name: "page",
fields: [defineField({ name: "title", type: "string" }), ...seoFields],
});
defineType({
name: "post",
fields: [defineField({ name: "title", type: "string" }), ...seoFields],
});
```
## Pattern 3: Taxonomy References
Centralize classification for consistent tagging.
**Use case:** Categories, tags, topics that span content types.
```typescript
// Central taxonomy
defineType({
name: "category",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug" }),
],
});
// Used across content types
defineField({
name: "categories",
type: "array",
of: [{ type: "reference", to: [{ type: "category" }] }],
});
```
## Pattern 4: Content Fragments
Small, reusable pieces that combine into larger content.
**Use case:** Bios, addresses, contact info.
```typescript
// Fragment type
defineType({
name: "contactInfo",
type: "object",
fields: [
defineField({ name: "email", type: "email" }),
defineField({ name: "phone", type: "string" }),
defineField({ name: "address", type: "text" }),
],
});
// Reused across types
defineType({
name: "office",
fields: [
defineField({ name: "name", type: "string" }),
defineField({ name: "contact", type: "contactInfo" }),
],
});
```
## Anti-Pattern: Over-Abstraction
Not everything needs to be reusable. If content is only used in one place, embedding is simpler.
**Signs of over-abstraction:**
- References that are only used once
- Editors navigating multiple documents for one page
- Complex queries joining rarely-shared content
@@ -0,0 +1,93 @@
# Reference vs Embedding Content
When should content be linked (referenced) vs copied (embedded)? This decision affects reusability, query complexity, and editing workflows.
## The Trade-offs
| Aspect | Reference | Embedded Object |
| ---------------- | ----------------------------------- | ------------------------- |
| Reusability | ✅ Shared across documents | ❌ Copied per document |
| Single source | ✅ Update once, reflects everywhere | ❌ Must update each copy |
| Query complexity | Requires joins/expansion | Inline, simpler queries |
| Editing UX | Separate editing interface | All fields in one place |
| Independence | Can exist on its own | Only exists within parent |
## When to Reference
Use references when content:
- **Is reusable** — Same author across many articles
- **Needs central management** — Update product info once
- **Has its own lifecycle** — Published/draft independent of parent
- **Should stay in sync** — Price changes reflect everywhere
**Examples:**
- Author profiles
- Product catalog items
- Shared testimonials
- Category taxonomy
- Reusable CTAs
## When to Embed
Use embedded objects when content:
- **Is unique to this document** — Page-specific hero
- **Doesn't make sense alone** — SEO metadata
- **Should be copied, not linked** — Historical snapshot
- **Simplifies editing** — All fields in one form
**Examples:**
- SEO metadata
- Page-specific sections
- Address information
- Social links
- Configuration options
## Sanity Implementation
```typescript
// Reference: Author is reusable
defineField({
name: "author",
type: "reference",
to: [{ type: "author" }],
});
// Embedded: SEO is page-specific
defineField({
name: "seo",
type: "object",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "description", type: "text" }),
],
});
```
## The Hybrid Approach
Sometimes you want both: a reference for the canonical data, plus embedded overrides.
```typescript
defineField({
name: "featuredProduct",
type: "object",
fields: [
defineField({
name: "product",
type: "reference",
to: [{ type: "product" }],
}),
defineField({
name: "overrideTitle",
type: "string",
description: "Optional: Override the product title for this context",
}),
],
});
```
Query uses `coalesce(overrideTitle, product->title)`.
@@ -0,0 +1,69 @@
# Separation of Content and Presentation
The most important principle in structured content: **separate what content IS from how it LOOKS**.
## The Problem
When content is tied to presentation:
- Redesigns require content migration
- Content can't be reused across channels (web, mobile, voice)
- Editors make design decisions instead of content decisions
- A/B testing requires duplicate content
## The Principle
Model content based on **meaning and purpose**, not visual appearance.
### Bad: Presentation-Focused
```
BigHeroText → What if we want small heroes?
RedButton → What if brand colors change?
ThreeColumnLayout → What if mobile needs one column?
LeftSidebar → Position is a frontend concern
MobileImage → Device-specific content is fragile
```
### Good: Meaning-Focused
```
Headline → The main message (render however)
CallToAction → An action we want users to take
Features → A list of things (columns decided by frontend)
RelatedContent → Content relationships (position by context)
Image → One image with responsive crops
```
## Testing Your Model
Ask: "If we completely redesigned the site, would these field names still make sense?"
- `threeColumnFeatures` → ❌ Fails (what if 2 columns?)
- `features` → ✅ Works (describes the content's purpose: a list of product features)
- `blueHighlightBox` → ❌ Fails (what if we go purple?)
- `callout` → ✅ Works (describes the content's role: an attention-grabbing aside)
## Sanity Implementation
```typescript
// ❌ Avoid presentation-focused names
defineField({ name: "bigHeroText", type: "string" });
defineField({ name: "fontSize", type: "number" });
defineField({ name: "backgroundColor", type: "color" });
// ✅ Use meaning-focused names
defineField({ name: "headline", type: "string" });
defineField({
name: "emphasis",
type: "string",
options: { list: ["standard", "prominent"] },
});
defineField({
name: "tone",
type: "string",
options: { list: ["neutral", "warning", "success"] },
});
```
The frontend translates `tone: 'warning'` to visual styles. Content stays semantic.
@@ -0,0 +1,150 @@
# Taxonomy and Classification
Organizing content with taxonomies enables filtering, navigation, and content relationships. Well-designed taxonomies scale; poorly designed ones become maintenance nightmares.
## Types of Classification
### Flat Taxonomy
Simple list of terms with no hierarchy.
**Use for:** Tags, simple categories
**Example:** Blog tags: "javascript", "react", "tutorial"
```typescript
defineType({
name: "tag",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug" }),
],
});
```
### Hierarchical Taxonomy
Terms with parent-child relationships.
**Use for:** Product categories, content sections
**Example:** Electronics > Phones > Smartphones
```typescript
defineType({
name: "category",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug" }),
defineField({
name: "parent",
type: "reference",
to: [{ type: "category" }],
description: "Parent category (leave empty for top-level)",
}),
],
});
```
### Faceted Classification
Multiple independent dimensions.
**Use for:** Complex filtering (e-commerce)
**Example:** Filter by color AND size AND price range
```typescript
// Multiple taxonomy types
defineField({ name: "color", type: "reference", to: [{ type: "color" }] });
defineField({ name: "size", type: "reference", to: [{ type: "size" }] });
defineField({
name: "material",
type: "reference",
to: [{ type: "material" }],
});
```
## Design Principles
### 1. Mutual Exclusivity (When Appropriate)
Categories should be distinct. If items frequently belong to multiple categories, consider tags instead.
**Categories:** One primary classification
**Tags:** Many optional classifications
### 2. User-Centric Naming
Use terms your audience uses, not internal jargon.
**Bad:** "Content Assets" (internal term)
**Good:** "Resources" or "Downloads" (user term)
### 3. Balanced Depth
Too shallow: Everything lumped together
Too deep: Users can't find anything
**Rule of thumb:** 3-4 levels max for hierarchies
### 4. Scalable Structure
Design for 10x growth. Will your structure work with 10,000 items?
## Querying Taxonomies
### Get all items in a category
```groq
*[_type == "product" && category._ref == $categoryId]
```
### Get items in category OR children
```groq
// First get all descendant category IDs
*[_type == "product" && category._ref in
*[_type == "category" && (
_id == $categoryId ||
parent._ref == $categoryId ||
parent->parent._ref == $categoryId
)]._id
]
```
### Get category tree
```groq
*[_type == "category" && !defined(parent)]{
title,
slug,
"children": *[_type == "category" && parent._ref == ^._id]{
title,
slug,
"children": *[_type == "category" && parent._ref == ^._id]{
title,
slug
}
}
}
```
## Common Mistakes
### Over-categorization
Creating a category for everything results in mostly-empty categories.
**Fix:** Start minimal, add categories as content grows.
### Inconsistent Granularity
Some categories broad ("Technology"), others narrow ("React 18 Server Components").
**Fix:** Define clear criteria for category creation.
### No Governance
Anyone can create taxonomy terms, leading to duplicates and inconsistency.
**Fix:** Limit who can create/edit taxonomy documents. Use validation.
@@ -0,0 +1,297 @@
---
name: ds-component-restructure
description: "Restructure a ds-react component's files into canonical folder/naming conventions. Use when reorganizing a component's file layout, splitting a monolithic component file, or renaming files to match domain conventions."
argument-hint: "Path to component folder, e.g. @navikt/core/react/src/accordion"
---
# ds-component-restructure
Restructure a `ds-react` component folder to follow canonical conventions — without breaking the public API.
## Canonical Structure
```
<component>/
├── index.ts # "use client" + re-exports from root
├── <Component>.stories.tsx # Storybook stories
├── <Component>.tests.stories.tsx # Storybook play-tests (only if play tests exist)
├── <Component>.meta.ts # Meta info for component. Used for documentation generation.
├── root/
│ ├── <Component>Root.tsx # Main compound component + sub-component namespace
│ ├── <Component>Root.context.ts # Context (if shared across sub-components)
│ ├── <Component>Root.test.ts # Unit tests for root (if needed)
│ └── use<Hook>.ts # Files for root-level hooks (if needed)
├── <subcomponent>/ # One dir per sub-component
│ ├── <Component><Subcomponent>.tsx
│ ├── <Component><Subcomponent>.test.ts # Unit tests for sub-component (if needed)
│ └── use<Hook>.ts # Files for sub-component-level hooks (if needed)
├── helpers/
│ ├── <helper-name>.ts
│ └── <helper-name>.test.ts # Unit tests co-located with helper
└── <Component>.types.ts # Shared types (if needed across multiple files)
```
### Naming Rules
| What | Pattern | Example |
| ---------------------- | --------------------------------------- | ----------------------------- |
| Root component file | `<Component>Root.tsx` | `AccordionRoot.tsx` |
| Sub-component file | `<Component><Subcomponent>.tsx` | `AccordionItem.tsx` |
| Internal sub-component | `<Component><Subcomponent>Internal.tsx` | `DialogBackdropInternal.tsx` |
| Sub-component dir | kebab-case sub-component name | `item/` |
| Context file | `<Component>Root.context.ts` | `AccordionRoot.context.ts` |
| Shared types file | `<Component>.types.ts` | `Accordion.types.ts` |
| Stories | `<Component>.stories.tsx` | `Accordion.stories.tsx` |
| Play-test stories | `<Component>.tests.stories.tsx` | `Accordion.tests.stories.tsx` |
| Unit test file | `<filename>.test.ts(x)` | `useAccordion.test.ts` |
| Meta file | `<Component>.meta.ts` | `Accordion.meta.ts` |
| `index.ts` | always `index.ts` | `index.ts` |
## Procedure
### 1. Read the Component
Read every file in the component folder — `index.ts`, all `.tsx`/`.ts` source files, meta file, stories, and test stories. Ignore inconsistencies in file naming and structure; the goal is to fix those, not preserve them. Use the `component`-field in the meta file to determine the proper naming if unsure.
**Done when:** every file's role (component, hook, type, context, test, metadata) is known, and every symbol currently exported from `index.ts` is recorded.
### 2. Map Files to Target Structure
Build this table before touching any files:
| Old path | New path | Renamed? | Publicly exported? | Importers to update |
| ------------------- | --------------------------- | -------- | ------------------ | --------------------- |
| `DialogTrigger.tsx` | `trigger/DialogTrigger.tsx` | No | Yes | `root/DialogRoot.tsx` |
| ... | | | | |
For each file:
- Determine new path and whether it moves/renames
- Check whether content needs splitting (e.g. types → `<Component>.types.ts`)
- List every file that imports it (grep for the filename or exported symbol)
- Flag circular dep risks: context importing sub-components, helpers importing component files, types importing non-types
Resolve circular deps before proceeding: move shared pieces to a lower-level file (types → `.types.ts`, utilities → `helpers/`).
**Done when:** every file in the component folder has a row in the table, and every circular-dependency risk is resolved or explicitly noted.
### 3. Create New Files
For each row in the mapping table where the path changes:
1. Create the file at the new path with all import paths updated to reflect new locations
2. Update every file in the "Importers to update" column — use the mapping table, don't rely on memory
3. Do NOT delete old files yet
> **Case-only renames** (path differs only in letter casing, e.g. `heading.stories.tsx` → `Heading.stories.tsx`) must NOT use the create-new + delete-old flow. On case-insensitive filesystems (macOS and Windows default) the two paths resolve to the **same file**, so creating the new file overwrites the old and the later "delete old" step destroys your content — and git records no rename. Handle these with the two-step `git mv` in the [Case-only renames](#case-only-renames) edge case instead.
### 4. Update Stories
Move stories to component root dir. If stories import from internal paths, update those paths.
**Exception:** A `stories/` subdirectory is acceptable if it already exists — do not flatten it.
**Casing:** Story filenames follow `<Component>.stories.tsx` (PascalCase). When the existing file is lowercase (e.g. `heading.stories.tsx`), this is a **case-only rename** — follow the [Case-only renames](#case-only-renames) procedure, do not create + delete.
### 5. Update `index.ts`
All exports are **named** — no `default` exports anywhere. The component `index.ts` re-exports the compound component, its sub-components, and their types from the root file:
```ts
"use client";
export {
<Component>, // compound root (named)
<Component>Trigger, // sub-components (named)
// ...
} from "./root/<Component>Root";
export type {
<Component>Props,
<Component>TriggerProps,
// ...
} from "./root/<Component>Root";
```
Group values first, then types. If types live in `<Component>.types.ts`, re-export them from there.
#### Compound root file export pattern
In `<Component>Root.tsx`, assemble the compound component with `Object.assign` — **do not** use an `as <Component>Component` cast or a dedicated `<Component>Component` interface:
```tsx
const <Component>Root = forwardRef<HTMLDivElement, <Component>Props>((props, ref) => {
// ...
});
/**
* Component-level JSDoc (description, `@see`, `@example`) goes directly
* above the `Object.assign` — this is the exported `<Component>` value.
*/
const <Component> = Object.assign(<Component>Root, {
/**
* @see 🏷️ {@link <Component>TriggerProps}
*/
Trigger: <Component>Trigger,
// ...
});
export { <Component>, <Component>Trigger };
export type { <Component>Props, <Component>TriggerProps };
```
Why `Object.assign` instead of `as`: the cast types the const as an interface that has no runtime `valueDeclaration`, so `react-docgen-typescript` (used by `yarn docgen:meta`) fails to extract props and can't document the component when it is exported via a grouped `export { }` statement. `Object.assign` lets TypeScript infer the intersection type from the value, so metadata generation works with named, bottom-of-file exports.
Sub-component files follow the same rule — named value export plus named type export at the bottom, no inline `export interface` and no default:
```tsx
interface <Component>TriggerProps extends React.HTMLAttributes<HTMLDivElement> {
// ...
}
const <Component>Trigger = forwardRef<HTMLDivElement, <Component>TriggerProps>(/* ... */);
export { <Component>Trigger };
export type { <Component>TriggerProps };
```
### 6. Update Meta File
Update imports in `<Component>.meta.ts` to reflect new paths. If the meta file imports from the component's `index.ts`, no changes are needed.
If component has no meta file, create one in the component root with the following content based on existing patterns.
Note that each standalone component should have a meta file. But a "sub-component" (like `AccordionItem`) does not need a meta file.
### 7. Validate No Breaking Changes
**Export diff.** Before deleting old files, compare the exported names recorded during Step 1 against the new `index.ts`. Every name must be present:
- Exports in new index.ts that are missing from original → regression
- Exports in original that are missing from new index.ts → missing export (breaking change)
Concretely: grep for `export` lines in both versions and diff them. All component names and type names must match exactly.
**Build.** Run `yarn workspace @navikt/ds-react build` — must succeed with no TypeScript errors.
**Package root.** Verify `@navikt/core/react/src/index.ts` still re-exports the component. No changes needed there unless the component's `index.ts` path changed (it shouldn't).
**No case-duplicate tracked paths.** This is the most common restructure bug. On case-insensitive filesystems (macOS, Windows) git can end up tracking **two** entries that differ only in casing (e.g. `accordion.stories.tsx` **and** `Accordion.stories.tsx`) while the working tree shows only one file. The stale entry then breaks tests and checkout on other machines. `ls` / file search will NOT reveal it — you must ask git. Run:
```sh
git ls-files | awk '{l=tolower($0); if(seen[l]++) print "CASE-DUP:", $0}'
```
Any output is a leftover. Remove the stale entry from the index by its **exact old path** (this does not touch the correctly-cased file on disk):
```sh
git rm --cached '<exact old lowercase path>'
```
Then confirm only the intended casing remains, e.g. `git ls-files '*<component>*stories*'`.
**Done when:** the export diff is empty, the build succeeds, the package-root re-export is verified, and `git ls-files` reports no case-duplicate paths.
### 8. Remove Old Files
Only after verifying new files are correct and `index.ts` exports match the original, delete the old files.
Skip any rows already moved with `git mv` (case-only renames and other `git mv` moves) — there is no separate old file to delete for those.
### 9. Unit Tests
Unit tests live next to the file they test — same directory, same base name with `.test.ts` / `.test.tsx` suffix.
| What to test | File location |
| ---------------------- | --------------------------------------------------- |
| Helper utility | `helpers/<helper-name>.test.ts` |
| Sub-component logic | `<subcomponent>/<Component><Subcomponent>.test.tsx` |
| Root component / hooks | `root/<Component>Root.test.tsx` |
| Context helpers | `root/<Component>Root.context.test.ts` |
Do NOT move existing test files unless their source file moves. When moving a source file, move its test file with it and update import paths.
## Edge Cases
### Case-only renames
A rename that changes **only letter casing** (e.g. `heading.stories.tsx` → `Heading.stories.tsx`, or `accordion.tsx` → `Accordion.tsx`) is a trap on case-insensitive filesystems (macOS and Windows defaults):
- Both paths point to the **same** file, so `create_file` at the new path overwrites the old content, and the Step 8 "delete old" then removes it entirely.
- git does not see a rename (`core.ignorecase=true`), so history is lost and other machines get checkout/case-collision conflicts.
Do the rename in **two `git mv` steps through a temporary distinct name** so git records it and it works on every filesystem:
```sh
git mv heading.stories.tsx heading.stories.tmp.tsx
git mv heading.stories.tmp.tsx Heading.stories.tsx
```
Then edit the file's contents/imports in place. Skip the create-new + delete-old flow for these rows entirely.
Guidelines:
- Prefer `git mv` for **all** moves/renames (not just case-only) so history is preserved; only fall back to create + delete when the file content is being split or substantially rewritten.
- Detect case-only renames while building the Step 2 mapping table: compare old vs new path lowercased — if they're equal but the raw paths differ, mark the row as case-only.
- The temporary name must differ by more than case (add `.tmp`), otherwise the same collision reoccurs.
### Flat components without sub-components
Leave flat components flat - do NOT introduce subdirs unless the component contains internal utility-components (files that render JSX but are not public sub-components). If such a utility-component exists, move it to its own subdir following the canonical naming.
Example: `button/Button.tsx` has no other sub-components, so should stay in root.
### `parts/` directory
If the component uses a `parts/` dir, break it up into separate sub-component dirs.
### Internal sub-components
Sub-components that are implementation details (not exported publicly) use the same dir/file naming as public ones (see Naming Rules) with an `Internal` suffix, but must NOT be exported from `index.ts`:
```
backdrop/
└── DialogBackdropInternal.tsx ← internal, NOT exported from index.ts
```
### Nested subcomponents
Do NOT nest subcomponent dirs inside other subcomponent dirs. All sub-component dirs must be direct children of the component root:
```
# Wrong
dialog/
└── header/
├── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
# Correct
dialog/
├── header/
│ └── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
```
## Constraints
- **No breaking changes.** Public export names and prop shapes must be identical after restructuring.
- **Named exports only.** No `default` exports; group named exports at the bottom of each file (values first, then types).
- **Compound roots use `Object.assign`.** No `as <Component>Component` cast or `<Component>Component` interface — it breaks `yarn docgen:meta` prop extraction.
- **React 17 compatible.** No React 18/19-only APIs. Import React explicitly in `.tsx` files.
- `index.ts` must start with `"use client"`.
- Use tokens, not hardcoded values.
- Preserve `forwardRef`, `className`, `...rest`, `as`/`OverridableComponent` patterns.
- Keep JSDoc on public props and components. The component-level JSDoc block sits directly above the `Object.assign` call; sub-component `@see` tags go on the keys inside the `Object.assign` object literal.
## Reference Files
- Canonical example: `@navikt/core/react/src/dialog/`
- Package public API: `@navikt/core/react/src/index.ts`
- Component `index.ts` pattern: `@navikt/core/react/src/dialog/index.ts`
- Context pattern: `@navikt/core/react/src/dialog/root/DialogRoot.context.ts`
- Meta: `@navikt/core/react/src/accordion/Accordion.meta.ts`
@@ -0,0 +1,234 @@
---
name: next-cache-components-adoption
description: >
Turn on Cache Components in a Next.js app and resolve the blocking routes it
surfaces. Use when the user wants to enable, adopt, or migrate to Cache
Components, flip the `cacheComponents` flag, work through a flood of
blocking-prerender / instant validation errors, run the
`cache-components-instant-false` codemod, or decide between opting routes out
with `export const instant = false` and fixing them in place.
---
# next-cache-components-adoption
Enable Cache Components on an app and walk it to a passing build. This skill sequences the work; per-error recipes live in the dev overlay fix cards and the build's terminal output. The [migrating to Cache Components guide](https://nextjs.org/docs/app/guides/migrating-to-cache-components) is the canonical reference for the concepts and per-API recipes this skill applies — consult it whenever the skill steps reference a pattern (`"use cache"`, `cacheLife`, `<Suspense>` placement, etc.) and you want the full explanation.
## requires
- **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs.
- **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first:
- `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these.
- **`experimental.dynamicIO` is fatal.** It was renamed to top-level `cacheComponents` and the old key now aborts before any build can run — remove it (or replace with `cacheComponents: true`) first. `experimental.useCache` is still accepted as a deprecated alias; redundant once `cacheComponents: true` is set, so remove it for clarity.
### notes
- **No passing baseline before the flag.** If the app already uses `"use cache"`, the pre-flag build errors with `please enable the feature flag cacheComponents`. Enabling the flag is the first thing you do (in Incremental, before the codemod; in Direct, before fixing routes) — not a thing to do _after_ getting a passing build. Note this in your starting summary so it doesn't read as a regression.
- **Offline docs.** Guide links have offline copies under `node_modules/next/dist/docs/` (bundled since Next.js 16.2), with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled.
- **Older versions without bundled docs.** Suggest `npx @next/codemod@latest agents-md` to the user before starting: it downloads a version-matched copy to `.next-docs/` and writes an index into `AGENTS.md` / `CLAUDE.md`. It touches files in their repo, so ask first and run it only if they want it.
## the shape of the work
There's one loop: walk the route tree top-down, one feature at a time, adopting each route against `next dev` + a browser. The build is a final check for each feature, not the working surface.
The choice in step 1 is whether to opt every route out of validation first or fix routes as you go. Either way the loop is the same:
- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs.
- **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete.
In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature. Expect to spend most of the time in the loop, not in the pre-step.
## background
`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and fails the build. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree during the build, but client navigations still validate each descendant segment on its own. Reads wrapped in a [`"use cache"`](https://nextjs.org/docs/app/api-reference/directives/use-cache) function count as cache boundaries, not blocking reads.
Three classes of blocker come up, usually in this order:
1. **Request-time reads** (`cookies()`, `headers()`, `await params`, `await searchParams`). All four block when awaited at the top of a page or layout. `params` and `searchParams` often get missed because they're not framed as "request data" the way cookies and headers are. The fix is to push the read into a `<Suspense>`-wrapped child — and for `params`/`searchParams`, forward the promise into the child and await it there; don't `await` at the page top.
2. **Sync-IO at module/render time** (`new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()`). These fail the build even with `instant = false` — the opt-out doesn't suppress them. If they're in a shared layout, they block every route under it. The codemod can't fix them; you have to translate each one by hand before the build can pass (see the [incremental pre-step](#incremental)). Grep the whole repo for these calls before running anything else.
3. **`"use cache"` files that read request data.** A file with a top-level `"use cache"` directive can't export `instant`; combining the two errors with `Only async functions are allowed to be exported in a "use cache" file.`, which means the directive was wrong for that route. Remove it before running the codemod.
## working surfaces
### finding blocking routes
Prefer `next dev` over `next build` while you work.
- **`next dev`** — the working surface. Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so read the overlay (or `.next-dev.log`), not status codes. A cleared overlay is one half of calling a route clean — the other half is browser verification (see [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time)) and a passing build for that route.
- **`next build`** — detection only. The build is `next dev`'s authoritative check, not its replacement. Use it as the last gate on each feature in the loop (a passing build is part of the per-route success bar) and as the final verification across the whole app. In Incremental, the build also confirms the pre-step (codemod opted every route out, no shared layout still has a sync-IO blocker) before you ship that PR. Don't reach for the build instead of the dev loop while you're working a route — a passing compile doesn't tell you what ended up in the static shell and what streamed. By default the build stops at the first blocking route, so it's also poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of file paths relative to the project root, e.g. `--debug-build-paths="app/admin/**/page.tsx"` — not URL paths; `--debug-build-paths="app/(marketing)/about/page.tsx"` — not `/about`; `--debug-build-paths="app/admin"` matches nothing and silently builds zero routes), and `--debug-prerender` disables the early exit so the build continues past the first prerender failure, reports every blocking route, and prints a fuller stack trace that names the originating file and line.
Every blocking error has a docs page — open it. Both the dev overlay and the build terminal print a `https://nextjs.org/docs/messages/<slug>` link with each error. That page is the canonical recipe for the fix; the inline message is a summary. Fetch the link for every distinct error you encounter, even if you think you know the pattern — the recipes evolve, and the same error class can have different correct fixes depending on what the route reads. Don't improvise from the inline message alone. (`/docs/messages/*` pages aren't bundled offline; if you have no network, fall back to the per-API guides under `node_modules/next/dist/docs/` and note the limitation when you report back.)
### verifying each fix at runtime
A passing build or a cleared overlay isn't proof the route actually behaves — Cache Components is a runtime concern (a static shell with streamed data). Verify after every fix, not only at the end.
In preference order:
1. **[`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) — strongly preferred.** Cross-checks `/_next/mcp` against the live browser via `agent-browser` and surfaces both compile and runtime issues in one pass. The diagnostics (React tree, suspense boundaries, console + network) are richer than poking at `next dev` by hand.
Install it before starting the loop. Don't wait until you hit something `next dev` alone can't explain. Run:
```bash
npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop
```
The skill requires `agent-browser >= 0.27.0` and walks you through it.
**Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead.
You don't need permission to install `next-dev-loop` itself. It's a tool, like installing a dev dependency. If a user is present, briefly tell them you're installing it for verification. In a non-interactive run (CI, dashboard, sandbox), install it without asking — "can't prompt the user" is not a reason to skip. The only legitimate skip is a real technical blocker: no network, no npm, read-only filesystem, a stated no-new-deps policy, or a webpack-only dev script. If you skip, name the specific blocker in your final report.
2. **A browser you can drive yourself.** Playwright, `agent-browser` directly, any browser-automation tool. Use only when `next-dev-loop` is genuinely blocked. You'll miss the framework-side checks (`/_next/mcp`), so DOM assertions alone don't catch every regression — be more cautious about what you call "verified."
3. **Build-only.** If you can't run a dev server at all, the build is your only signal. `○ (Static)` routes with no `<Suspense>` are fully verified by the build (nothing streamed to test). `◐ (Partial Prerender)` routes are only shell-verified — flag them when you report back.
4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or commit the milestone you've reached and hand off.
## step 1: choose a strategy
Ask the user, in terms of the PRs they want, not the size of the job. Never use the internal labels (Incremental, Direct) when talking to the user — those are your own scaffolding. Ask in terms of PRs and features, e.g.: _"Do you want me to first open a PR that turns on Cache Components and opts every route out of validation, then handle the actual route adoptions feature-by-feature in follow-up PRs? Or do everything on one branch?"_ Even on a tiny app, the incremental path still has value (review-sized PR, revertible, the `// TODO: Cache Components adoption` markers double as your work queue for next session). Don't pick on their behalf.
If there's no user to ask, default to **Incremental** and document the choice.
- **Incremental** — quiet pre-step + the loop. Run the codemod to opt every page and layout out of validation, get the build passing, stop and check in with the user (see [end of the pre-step](#end-of-the-pre-step-check-in)), then enter [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time) and ship each feature as a follow-up PR.
- **Direct** — skip the pre-step. Enable `cacheComponents` and go straight to [step 2's loop](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time); the build's blocking routes are the work queue.
### incremental
Before invoking the codemod, fix the two classes of blocker it can't.
1. **Sync-IO at module/render time.** Grep the whole repo for `new Date()`, `Date.now()`, `Math.random()`, and `crypto.randomUUID()` (not only `app/**/layout.{js,jsx,ts,tsx}` — the read might live in any component imported by a layout). Unblock each match with the `await connection()` + `<Suspense>` fix from its `blocking-prerender-*` error card: it defers the value to request time, exactly as it behaved before the migration, so it needs no product decision. Add this exact comment on the line above the `await connection()`:
```tsx
// TODO: Cache Components adoption. Added to unblock the build: remove this connection() to re-trigger the error and review the fix options.
```
It shares the `TODO: Cache Components adoption` prefix with the comments the codemod writes, so the check-in grep finds both. Removing the `await connection()` makes the error fire again with its fix cards — the same motion as removing an opt-out in the loop.
2. **Incompatible segment configs.** Grep for `^export const (revalidate|dynamic|fetchCache)` across `app/` and translate per the `requires` note above. The codemod does not touch them; leaving them in place fails the build after the codemod.
The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. Common false positive: if you recently upgraded Next.js, `package.json` and the lockfile will already be dirty — commit those first.
Use the `@canary` channel, not `@latest`. The `cache-components-instant-false` transform isn't in the stable `@next/codemod` release; `@next/codemod@latest` errors with `Invalid transform choice`.
```bash
npx @next/codemod@canary cache-components-instant-false ./app
```
Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `app/**/{page,layout,default}` file, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for the loop.
If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand: for every `app/**/{page,layout,default}.{js,jsx,ts,tsx}` that isn't `"use client"` or `"use server"` and doesn't already declare `instant`, insert this after the imports:
```ts
// TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.
// See: https://nextjs.org/docs/app/guides/migrating-to-cache-components
export const instant = false;
```
The codemod opts every segment out, not only the root, on purpose. Resolution is top-down, first-explicit-config-wins: the highest `instant = false` decides the whole subtree. With an opt-out on every segment, removing one segment's opt-out validates only that segment; descendants keep their own opt-outs and stay passing. If only the root were opted out, removing it would re-arm validation for the entire app at once.
Because the highest opt-out wins, remove them top-down (root layout first, then descend). Removing a leaf's opt-out does nothing while an ancestor still holds one.
Confirm the pre-step with `next build`. The build is the proof, not the codemod run — a shared layout that calls `new Date()` / `Math.random()` directly still fails regardless of the opt-out (see [background](#background)).
After the build passes, confirm the root layout got an opt-out (`grep -n "export const instant" app/layout.*`). The root layout renders every route, including framework routes like `/_not-found`, so if it was missed, add `export const instant = false` to it by hand.
Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them) and rarely block on their own; when a client route blocks, fix the server-side data in its ancestor layout.
### end of the pre-step: check in
Incremental only. Stop here before starting step 2 — the pre-step is the shippable PR. Talk to the user in their language; don't say "Incremental" or other internal labels; talk about adoption, PRs, and what the app does now. Tell them:
- What you did: turned on Cache Components, ran the codemod that opts every page and layout out of the new validation (or did it by hand), fixed any blockers the codemod can't (list them), confirmed the build passes.
- What changed: every page and layout in `app/` now exports `instant = false` with a `// TODO: Cache Components adoption` comment, except client components and any that already had an `instant` export.
- What to sanity-check: the diff is mostly mechanical (new exports + comments). The build passes. Routes still behave exactly as they did before — the opt-outs preserve current behavior; no rendering changes yet.
- The question: "Want to open this as its own PR before we start adopting Cache Components route by route? Or keep going on this branch?" Wait for the answer.
Moving to step 2 without checking in defeats the point of taking the incremental path.
### direct
Set `cacheComponents: true` and move to [step 2](#step-2-the-inner-loop-remove-opt-outs-one-feature-at-a-time). The build's blocking routes are the work queue.
## step 2: the inner loop, remove opt-outs one feature at a time
A "feature" is a single product surface — `app/settings/profile/**`, `app/posts/[slug]/**` — not a whole top-level app like `app/dashboard/**`. Finish one end-to-end before starting the next.
Within a feature, walk top-down (layouts before pages, root layout first). Removing a layout's opt-out before its descendants exposes the layout's own blocking reads. (Direct: there are no opt-outs to remove — fix each failing route; if a hand-written opt-out on an ancestor shadows it, remove that first.)
A passing build mid-walk doesn't mean the layout is clean. Removing a layout's opt-out while its descendant pages still have theirs keeps the build passing — each page shadows the inherited validation. The layout's actual blocking reads only surface once nothing below it shadows them. Don't call a feature done at the layout boundary.
Use the **with-a-browser** loop unless a browser is genuinely unreachable. The [`next-dev-loop`](#verifying-each-fix-at-runtime) skill is the source of truth for what counts as "browser available" and how to install it.
### the loop, with a browser (preferred)
Per route:
- Remove the opt-out (Incremental) or target the failing route (Direct).
- Reload in dev. Overlay clean? Skip to verify. Overlay still red? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there. The inline overlay text is a summary; the docs page is the source of truth.
- Verify in the browser. Confirm the visible content on first paint is what you intended in the shell — not stuck on a fallback, not silently streaming everything out of an empty shell.
- Re-check siblings if the fix touched shared code (a layout, a sidebar component). A shared-shell change can fix the route you're on and break a sibling.
### the loop, build-only (fallback)
Used when there's no way to drive a browser — CI, sandbox, the user has no `next dev` running and you can't start one. Weaker signal: confirms the build passes and the route prerenders, but not what ended up in the static shell vs streamed.
Per route:
- Remove the opt-out (Incremental) or target the failing route (Direct).
- Rebuild with `--debug-build-paths app/<route>/**` (only that route) or `--debug-prerender` (full build, but past the first failure). Route passing? Move on. Still blocking? Fix.
- Fix — fetch the docs page linked from the error (`https://nextjs.org/docs/messages/<slug>`), apply the recipe from there.
- Re-check siblings if the fix touched shared code.
- Flag the route as build-only-verified when you hand the feature off. Each `◐` route still needs a browser pass before the feature is done.
### loop notes
- The [three blocker classes from background](#background) often get missed when fixing in place. Caching a downstream fetch (`getThing(id)`) doesn't clear an `await params` at the top of the page body — push the param promise into the `<Suspense>`-wrapped child.
- Ambiguous calls are user check-ins, not agent judgment. When you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read [references/per-page-decisions.md](./references/per-page-decisions.md) before editing. Show the route while you ask: the `next-dev-loop` session runs the browser headed, so drive to the page and leave it on screen so the user is looking at the thing they're deciding about, with a screenshot as the fallback when a headed browser isn't possible. "Should this stay blocking?" is much easier to answer while looking at the page than at a file path.
- Don't narrate the refactor with comments. The only comment the codemod (or you) should leave is `// TODO: Cache Components adoption` on opt-outs, and the user's existing comments. Don't annotate every `<Suspense>` boundary or `"use cache"` call with what it does — the code says that. Drop a comment only when the _why_ isn't clear from the code (e.g. a deliberate Block with a reason).
Keep a todo list of the feature's routes. When every route in the feature is clean, move to step 3.
## step 3: verify the feature
Checklist before checking in with the user:
- `next build` completes without blocking-route errors.
- No bare TODOs in the feature: `grep -rn "TODO: Cache Components adoption"` finds both the codemod's opt-out comments and the sync-IO unblocks from the pre-step. Any `instant = false` left behind is a deliberate, documented Block — comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place"). Any `await connection()` left behind has been reviewed and kept on purpose, not left over from the pre-step.
- Each route visited in the browser: confirm the static shell renders first and every `<Suspense>` fallback resolves to its real content. Capture both states if you can — the fallback (mid-stream) and the final paint — so you have a streaming-experience demo to show the user. Throttle the network in the browser if streaming is too fast to observe.
Then check in with the user. Same rule as the pre-step: speak their language. Don't say "feature-by-feature loop" or other internal labels; talk about the feature you adopted and what the user will see.
- What you did: which routes you touched, and the user-visible result per route (e.g. "the post page now streams the article body behind a skeleton while the layout stays static").
- What changed: opt-outs removed, fallbacks added, caching boundaries introduced.
- Show, don't tell. The `next-dev-loop` session runs the browser headed, so drive the route live for the user so they see the static shell → fallback → final content sequence in real time. If you can't drive a live browser, attach the before/after screenshots you captured instead.
- Give them the click-through: a short table of the feature's routes — the URL to open and what to look for (what renders instantly, which fallbacks appear, what streams in) — so they can verify each one themselves.
- The question: "Want to open this feature as a PR and move on to the next, or stop here?" Wait for the answer.
**Trivial features can skip the check-in.** If adopting a feature only meant removing its `// TODO: Cache Components adoption` opt-out (no `<Suspense>` added, no `'use cache'` introduced, no render order change), the user sees nothing different. Move on to the next feature without stopping; mention it in passing the next time you do check in.
When the loop has run on every feature — every remaining `instant = false` sits under a reason comment, `grep -rln "TODO: Cache Components adoption" app` returns nothing — point the user at [further reading](#further-reading) if they want to push the experience further, or stop and ship.
### route table glyphs
`ƒ` → `◐` is where adoption usually lands. `◐ (Partial Prerender)` means a static shell prerenders and the request-time content streams in — the goal state for any route that reads `cookies()`, `headers()`, `params`, or `searchParams`. Some routes legitimately stay `ƒ` when they do request-time work through a documented escape hatch (e.g. a layout that uses `await connection()`); the page is no longer _opted out_, it's genuinely dynamic. Don't remove the escape hatch only to chase a `◐`. The inverse holds: `instant = false` does not force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.
`◐` tells you a shell exists, not what's in it. A `<Suspense>` boundary placed too high (e.g. wrapping the entire page body, or `<Suspense fallback={null}>` around the article content) pushes the visible content out of the static shell into the streamed payload; the build still reports `◐` because _some_ shell prerendered (often only `<html><body>` with framework markup). The route table can't tell you what's in the shell; a browser can. If the shell is empty and everything streams, pull the `<Suspense>` boundary down closer to the actual dynamic read.
## further reading
The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.
- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a passing build doesn't guarantee that — a `<Suspense>` above the shared layout caught the page-load case but doesn't cover client navigation).
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it audits `<Link prefetch={true}>` calls (driven by the dev overlay's `link-prefetch-partial` warning) with the flag off first, then flips the `partialPrefetching` config. It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prefetching](https://nextjs.org/docs/app/guides/prefetching) and [Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap.
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.
@@ -0,0 +1,25 @@
# per-page decisions: removing `instant = false`
Read this when a route still blocks after you remove its `instant = false` and the dev overlay's fix card isn't enough on its own. Each section here covers a judgment call the agent shouldn't make alone.
## deciding what to do with a blocking read
Read the full linked page behind the fix card — not only the inline snippet — before editing. The card unblocks the build, but the page covers the details that make the route's navigation actually instant (e.g. where to place a `<Suspense>` boundary). Don't improvise.
If you're unsure which fix fits, the right call usually depends on what this part of the page is _for_, which the code doesn't capture. Ask the user about their goal for it rather than guessing. Frame it as a product/UX question: should this content be there instantly on load, or is it fine for it to stream in a moment later? Should everyone see the same thing (cacheable) or is it per-user / per-request? Tie the technical fix to that answer (cache it, wrap it in `<Suspense>`, or keep it request-time), so they're deciding the experience, not the API.
## security gates and other code you can't infer
If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `<Suspense>` would change what the code guarantees — stop and ask the user before refactoring. The build error wants `<Suspense>`, but wrapping a gate in `<Suspense>` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, or move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy).
If _every_ route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on.
For the broader picture, read the [Authentication guide](https://nextjs.org/docs/app/guides/authentication) (where auth checks belong: Proxy for routing, Data Access Layer for data) and the [Data Access Layer section of Data Security](https://nextjs.org/docs/app/guides/data-security#data-access-layer) (centralized auth checks that compose with `'use cache'`).
If you don't know how to make a piece of code Cache Components–correct without changing what it does, ask.
## when to leave a Block in place
If a route is genuinely meant to block — it's inherently per-request with no useful static shell — or the refactor would be large and the user would rather not take it on now, that's a legitimate outcome. Keep `instant = false`, but confirm it with the user first and turn its `// TODO: Cache Components adoption` comment into a reason, e.g. `// instant = false: kept on purpose — fully request-time dashboard` or `// instant = false: deferred, refactor too large for now`.
A documented, deliberate Block is fine to leave after the migration; an undocumented leftover opt-out is not.
@@ -0,0 +1,453 @@
---
name: next-cache-components-optimizer
description: >
Drive a Next.js route to instant navigation by setting up an agentic loop,
under Cache Components / PPR, on initial load (hard navigation) and
client-side navigation (soft navigation). Encode the goal as a failing
@next/playwright instant() e2e and work it to green, one verified route at a
time; the shipped test then guards against regression. Use when asked to make
a route's navigation instant (its static shell commits immediately), fix a
route whose static shell isn't prerendered/served/prefetched, grow a route's
static shell or fix its slow first paint, diagnose which Suspense boundary
keeps a route out of its static shell, or write the instant() e2e guard for
one. Requires Next.js 16.3+ with cacheComponents; directs an upgrade if older.
---
# next-cache-components-optimizer
Set up an agentic optimization loop that drives a Next.js route from "not
instant" to "instant" and keeps it there. The loop is test-driven: encode the
goal as a failing `@next/playwright` `instant()` test, work it to green, and
ship the test as the regression guard. Run it once per target route. Work the
phases P → G in order; each ends in a gate. Fix recipes live in two lazily-read
references — `reference/patterns.md` (before→after for each blocker type) and
`reference/real-app-patterns.md` (parallel routes, auth gates, the empty-shell
and responsive-skeleton failure modes). Read one only when its phase points
there.
## What is invariant, and what is yours
One thing here is fixed. The rest is yours. Read this before treating any
command, platform, or env var below as a requirement.
- **Invariant: the verification loop.** Maximizing the shell is worthless
unless you can prove it. The proof is an automated check: under a lock that
gates dynamic data, the static shell still commits. RED shows the gap, GREEN
shows it closed, the test ships as the regression guard. It must run on a
production-like build and must not be able to pass vacuously. Stand the loop
up once; every later optimization is then verifiable by construction. The
loop is the deliverable, not any one route.
- **The mechanism: `@next/playwright` `instant()`.** This skill locks with
[`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation):
a ruler, not a stopwatch (phase A). It comes from
`@next/playwright` (installed alongside `@playwright/test`, on the same
release line as `next`), so it isn't tied to any host. Keep it. Timing a
navigation by hand is too flaky to trust, and is the failure mode this skill
exists to prevent.
- **Yours: the rig.** How you build, deploy, authenticate, configure
Playwright, and loop belongs to your stack, not to this skill. A local
`next build && next start`, a CI/staging container, and a per-push preview
deploy are equally valid rigs; the verdict comes from the build, never the
platform. Phase 0 maps the invariant onto your repo. Read every platform
name, env-var spelling, and command below as an example to translate, not a
requirement.
## Two navigations, two loading states
A route reaches the user two ways, and both must be instant:
- **Initial load (hard navigation)** commits the route's prerendered static
shell; deferred parts stream in behind their loading skeletons (Suspense
fallbacks, `loading.tsx`).
- **Client-side navigation (soft navigation)** commits the destination's
prefetched App Shell — the `<Link>` default under Partial Prefetching —
re-rendering only the segments that change.
The fix patterns are identical for both; the test differs only in how the
navigation is driven ("Driving the navigation in tests" below). The two shells
can differ; guard the one you ship, both when both matter
(`reference/real-app-patterns.md`).
## Goal
Maximizing the static shell is the optimization objective: the most meaningful
prerendered content commits immediately, and only genuinely per-request data
streams in afterward. The shipped test deterministically encodes **present ∧
instant**; **non-blank** is the additional bar the workflow enforces by
judgment (D1/D2/E), because an `instant()` pass alone is satisfied by a blank
`fallback={null}` shell (the empty-shell failure mode,
`reference/real-app-patterns.md`).
`instant()` is a ruler, not a stopwatch: assert that the shell appears under
the lock; do not time it. A trustworthy verdict requires a production build
(phase A).
The GREEN under the lock is the deterministic verdict; each gate keeps it
trustworthy.
## Reporting to the user
This loop is meant to run unattended — ideally across many navigations in one
pass — so it doesn't stop to ask after each route. What matters is how you word
and present the results, not how often you interrupt. The mechanics below — the
rig, RED, GREEN, the gates — are your scaffolding; the user never needs to hear
those words.
- **Speak their language.** Describe the gap and the result in terms of what the
user sees: "navigating to the dashboard waited on the charts query before
anything painted; now the layout and skeletons paint instantly and the charts
stream in" — not RED/GREEN, the lock, or the phase letters.
- **Show, don't tell.** When you report a route, drive the browser (or attach
before/after screenshots) so the user watches the shell commit immediately and
the data stream in, rather than reading a claim. Identical before and after
means the fix did nothing — roll it back.
- **Present a run as a list of results,** one line per navigation — which route,
what's now instant, what streams — not a transcript of the loop.
- **Only surface a question for a genuine fork:** a fix that would change
behavior, a security-sensitive read, or a route that's dynamic by design (a
runtime-prefetch candidate, not a shell to grow). A clean instant fix is not a
fork — keep going. With no one to ask (an unattended run), don't block: take
the safe default and note the assumption — for a cache-freshness choice,
defer the read behind `<Suspense>` (always fresh, still instant) rather than
guess a `cacheLife`.
## The workflow
```
- [ ] P PREREQS Next.js 16.3+ with cacheComponents: true; upgrade first → below
- [ ] 0 SETUP once per repo: discover + write instant-nav.rig.md → rig-template.md
- [ ] A RIG production build with the testing API exposed → below
- [ ] B BASELINE unlocked: the marker renders for the test user → test-template.md
- [ ] C RED locked instant(): the shell does not commit → test-template.md
- [ ] C-gate VERIFY-RED: stop until the RED is trustworthy → reference/red-test-robustness.md
- [ ] D FIX push each Suspense boundary down to the data it guards → reference/patterns.md
- [ ] D1 reuse the route's existing loading UI; do not hand-build skeletons
- [ ] D2 the shell matches the real render at every breakpoint → reference/real-app-patterns.md
- [ ] E PARITY the refactor changed only whether the route is instant
- [ ] F DIFFERENTIAL revert only the fix → RED; re-apply → GREEN → reference/red-test-robustness.md
- [ ] G REVIEW PR checklist (below)
```
Phases B and C build the test; only the locked test from C ships.
---
## P. PREREQUISITES: current Next.js with Cache Components
The workflow depends on framework capabilities that ship with current Next.js:
- **Next.js 16.3+ with `cacheComponents: true`** in `next.config.ts`. Without
Cache Components there is no static shell to optimize.
- **`@next/playwright`** on the same release line as the project's `next`; it
provides `instant()`. Verify with `npm ls next @next/playwright` (or the
project's package manager) and align them if they differ. The matching
testing API is in the `next` runtime, gated by the
`experimental.exposeTestingApiInProductionBuild` config flag (phase A).
If the project does not meet these, upgrade first (`npx @next/codemod upgrade`
automates most of it), then enable Cache Components in `next.config.ts`:
```ts
export default { cacheComponents: true };
```
Enabling the flag surfaces the blocking routes to resolve first; the
[`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption)
skill drives that adoption. Reach for this optimizer once the app builds under
Cache Components.
This gate is deliberate: the skill targets current Next.js, and none of the
verdicts below are meaningful on older versions.
## 0. SETUP: discover this project's rig, once per repo
The principles in this skill are fixed; the infrastructure they run on is
yours. On first use in a repository, discover how the project builds, deploys,
authenticates, and tests (inspect the repository first, and ask the user only
what it cannot answer), then write the answers to a committed
`instant-nav.rig.md`. Every later run reads that file instead of
rediscovering. The six questions (BUILD / EXPOSE / RUN / TEST USER / DRIFT /
LOOP), the file template, and filled examples (local-only, generic CI +
container, preview deploy) are in **`rig-template.md`**.
If the repo has no Playwright e2e harness yet, standing up a minimal one
(`@next/playwright`, a config with `baseURL`, one authenticated path) is part
of this step; the loop does not assume a pre-existing suite.
## A. RIG: a production build with the testing API exposed
Stand up the rig described by `instant-nav.rig.md`. Two invariants hold on
every platform:
1. **Never measure on `next dev`.** It does not prefetch, and its lock is
unreliable for blocking routes, so a dev `instant()` result is not a valid
RED or GREEN.
2. **The measured build must expose the testing API.** Otherwise `instant()`
silently no-ops and the test passes vacuously (see
`reference/red-test-robustness.md`). The lock-engagement proof is the phase-C
RED itself: the unfixed target route is the known-blocking route, and its
RED under the lock shows the lock engages on this build (C-gate); the
self-validating variant in `test-template.md` is the in-band guarantee. Wire
`experimental.exposeTestingApiInProductionBuild` to a condition that is
true for every build you measure and never true in production:
```ts
experimental: {
// Use the condition your platform provides, and record it in the rig file:
// local: an explicit opt-in, as below
// generic CI: process.env.DEPLOY_ENV === 'staging'
// Vercel: process.env.VERCEL_ENV === 'preview'
exposeTestingApiInProductionBuild:
process.env.EXPOSE_TESTING_API === '1',
}
```
The rig is any production-like build that exposes the testing API: a local
`next build && next start`, a CI/staging container, and a preview deploy are
all equally valid; the verdict comes from the build, not the platform. See
`rig-template.md` for filled examples.
For any deployed or remote build, poll the rig's LIVENESS probe to confirm the
artifact contains `HEAD` before trusting a verdict (a stale deploy reads as a
false RED or GREEN); a local `next build && next start` needs none. The probe
mechanism is in `rig-template.md` (question 6).
## B. BASELINE (unlocked): development scaffold, do not ship
Drive the real navigation with no `instant()` lock and assert that the
destination's `SHELL_MARKER` renders **as the test user**: the account the
e2e suite authenticates as (in CI, the CI account; locally, your e2e login
fixture), with its flags, plan, role, and data. This establishes that the
marker is real and reachable: not flag-gated, not redirected away, not a
guessed selector. The suite runs as the test account, not the author's session;
that environment drift (the rig DRIFT list) is a common source of
untrustworthy REDs. Scaffold and run command: **`test-template.md`**.
**Delete this baseline before the PR.**
## C. RED (locked) + the VERIFY-RED gate
Wrap the same navigation in `instant()`; assert the shell commits under the
lock. A RED here is the gap. **This is the test that ships**
(`test-template.md`).
> **C-gate: do not start optimizing until the RED is verified trustworthy.** A
> RED that is red for the wrong reason sends you optimizing a route that was
> never broken.
The question that settles it: **does `SHELL_MARKER` render without the lock,
as the test user?** Answer it by re-running phase B as the test user, not by
adding assertions to the shipped test. The two-branch resolution (No → marker
or environment bug; Yes → genuine gap, proceed to D), the full taxonomy of
untrustworthy REDs, the checklist, and worked cases are in
**`reference/red-test-robustness.md`**. Read it now.
---
## D. FIX: push each boundary down to the data it guards
**The anti-pattern: one coarse boundary.** A single `<Suspense>` high in the
tree with a page-level fallback has three costs:
- The layout UI stays out of the static shell: only a throwaway copy of it is
prerendered.
- The entire subtree is replaced when the boundary resolves, which discards
client state and shifts layout.
- The hand-built fallback drifts out of sync as the UI changes, because it
duplicates structure that also exists in the resolved tree.
**The fix: hoist the static, push the Suspense down.** Render the layout UI
once, synchronously, in the shell, and wrap each await in a boundary scoped to
the single read it guards. Only that leaf streams; the stable ancestors are
reused as-is.
**Rule:** if an element renders in both the fallback and the resolved tree,
hoist it above the boundary.
### The most common blocker: a top-level `await` in a layout on a fallback route
```
app/[locale]/(app)/[tenant]/dashboard/...
│ generateStaticParams ✅ │ no generateStaticParams → fallback route
```
When any dynamic segment in the route lacks `generateStaticParams`, the route
is a fallback route, and **all** params defer to request time, including the
enumerated ones. A top-level `await` in a layout (`await params`, a
request-time session read, an auth gate) then blocks the whole subtree out of
the static shell, even when it reads a statically known param. Minimal shape: a
dynamic-segment route with one segment lacking `generateStaticParams`, plus a
top-level `await` in the layout above it.
### The fix: defer the gate, render children
Render `children` unconditionally; move the top-level `await` into a
`<Suspense fallback={null}>`-wrapped child. Mechanism and before→after:
`reference/real-app-patterns.md`, "Deferring an auth gate".
**Fix the page below the shell too, not only the layout.** A page-level
top-level `await` (commonly `await params`) blocks the same way the layout's
does, so make the page sync and push its dynamic reads into a
`<Suspense>`-wrapped leaf as well. `fallback={null}` is correct only when a gate renders nothing on
success; for data, the fallback must be a real loading skeleton (see D1).
Every other blocker shape — `cookies()`/`headers()`, uncached fetch or database
reads, `searchParams`, metadata, viewport, non-deterministic values (`Date.now()`,
`Math.random()`, `crypto.randomUUID()`) — surfaces its own insight when you hit
it: the build prints a `https://nextjs.org/docs/messages/<slug>` link. The
default build output is often abbreviated and may carry no usable stack trace;
add `--debug-prerender` for the full failing frame and to report every blocker
past the first. Scope the build to the route you're on with
`next build --debug-build-paths "app/<route>/**"` rather than rebuilding the app.
Open that page and apply its recipe; don't improvise from the inline message.
The before→after recipe for each shape is in `reference/patterns.md`, which maps it to the insight
that explains it.
A few things those per-error pages don't stress for the instant-navigation goal:
- **A boundary in the root layout isn't enough for client navigations.** It
passes a page-load check but leaves sibling client navigations blocking; put
the boundary below the lowest layout the source and destination routes share.
- **Keep the LCP element** (usually the main heading) out of any boundary, so it
paints in the shell instead of waiting on a stream.
- **A green check isn't always instant.** `export const instant = false` opts
the segment out of validation while the navigation still blocks, and a
`<Suspense>` above the document `<body>` prerenders an empty shell — neither
makes the route instant.
### D1: reuse the route's existing loading UI; do not hand-build skeletons
Before writing any skeleton, search the repository for the loading UI that
already exists for this route, in order:
1. the route's `loading.tsx`;
2. an exported `*Skeleton` colocated with the component;
3. the fallback already inside the component's own `<Suspense>`.
The **divergence point** is the lowest layout shared by the source and
destination routes: a soft navigation re-renders only the segments below it,
while an initial load re-runs every layout from the root. (Also called the
shared boundary.) A `loading.tsx` above the divergence point fills only
the initial-load shell; it sits above the soft-nav re-render scope. A
`loading.tsx` at the destination segment is itself the in-tree boundary for a
soft navigation into that segment and serves both. Reuse whichever boundary
actually covers the navigation you are shipping; below the divergence point,
`loading.tsx` and colocated skeletons are interchangeable for that purpose.
If a component has no skeleton, extract its loading markup into a colocated
skeleton beside it. Do not author a fresh skeleton that mirrors the page
layout: it duplicates structure, drifts as the page changes, and pulls the
design back toward a single coarse boundary. Reusing the component's own
skeleton also keeps the prefetched shell consistent with the loaded UI.
Exception: if the deferred component renders `null` for some users (for
example, a flag-gated control), `fallback={null}` is correct, since a skeleton
would flash and then collapse.
### D2: the shell must match the real render at every breakpoint
A skeleton frozen to one breakpoint misaligns on the others. Fix it the same
way: one responsive component renders both the live UI and the shell (D1
skeleton in its data slots), so the breakpoint switch happens once. Verify by
re-asserting the shell marker at two widths
(`await page.setViewportSize({ width: 1280, height: 800 })`, then
`{ width: 390, height: 844 }`), or by adding a mobile Playwright project, so
this gate is as machine-checkable as the others. Detail:
`reference/real-app-patterns.md`.
> **D-gate: phase D is complete when the locked test from phase C passes GREEN
> under the lock on the production-build rig**, not when the code compiles. That
> GREEN is the deterministic stop for the fix loop; proceed to E.
**When the read can't be pushed down** (an ID minted per request, an
all-dynamic page, a per-request auth/scope read the whole subtree needs), there
is no shell to grow. Don't force one: opt the route into **runtime prefetching**
so the prefetch runs the dynamic render ahead of the click and the soft nav
commits the real content. See [Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching)
for the mechanism (`prefetch = 'allow-runtime'` on the route plus a full
`<Link prefetch={true}>`) and the [dynamic-data-during-prefetching insight](https://nextjs.org/docs/messages/instant-link-prefetch-partial)
for adoption. The `instant()`-specific gotchas the docs don't cover:
- **The full prefetch is mandatory.** An auto/PPR prefetch bails before the
runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` /
`kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's
still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but
nothing on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry
incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects can't be
prefetched — the prefetch receives the redirect, not the tree. Point the link
and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic
data; issuing it on hover for every link is wasteful. Scope `kind: 'full'` to
the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a
client component, so its text isn't in the prefetch response. Assert a
`data-testid` that renders when the client subtree commits.
## E. PARITY: the refactor changed only whether the route is instant
The push-down is a mechanical transform, not a redesign. Afterward the route
must render the same tree, data, ordering, empty and error states, redirects,
and interactions as before; the only observable difference is that the shell
now commits instantly. Verify:
- **Same render output.** The moved `await`s compute and return the same
values; after the stream, the route shows the same content as the base
branch for the test user.
- **Side effects still fire.** A deferred `redirect()` or `notFound()` still
happens, at request time rather than during prerender. Confirm an
unauthorized user is still redirected and a missing record still returns 404.
- **Both viewports reach the real UI** after the stream (D2).
- **Client state survives.** Because the layout UI is hoisted into the stable
shell rather than swapped on resolve, open menus, scroll position, focus,
and input state persist across the stream.
If anything other than whether the route is instant changed, reduce the refactor.
## F. DIFFERENTIAL
Revert only the fix → RED; re-apply → GREEN; link both runs
(`reference/red-test-robustness.md`). On a deployed rig, confirm each run is live
(LIVENESS, phase A) before trusting its color.
## G. REVIEW (PR checklist)
A green final state means nothing if the RED was never trustworthy. The
test-trustworthiness items are the robustness checklist
(`reference/red-test-robustness.md`); confirm them, then require these
PR-specific items:
- [ ] **Differential shown**: RED without the fix, GREEN with it, runs linked.
- [ ] **Parity confirmed (E)**: same content, redirects, and state.
- [ ] **Existing loading UI reused (D1)**: no new page-mirroring skeleton.
- [ ] **Shell matches the real render at desktop and mobile widths (D2)**.
**Stop condition for the whole workflow:** the locked test from C is GREEN on
the rig, the differential (F) holds, and every item above is checked. Until all
three hold, you are not done.
## Driving the navigation in tests
- **Soft navigation** → drive a real `<Link>` click. **Initial load** → use
`page.goto()` inside `instant()` with the `baseURL` option. Do not substitute
`goto` for a soft-nav verdict; the two shells can differ
(`test-template.md`, `reference/real-app-patterns.md`).
- With parallel routes, only the slots that change re-render on a soft
navigation; client-rendered navigation UI does not re-render at all. Do not
chase a slot the navigation never touches
(`reference/real-app-patterns.md`).
## Files
- `rig-template.md`: phase 0, the six-question rig discovery, the
`instant-nav.rig.md` template, and filled examples (local-only, generic CI,
preview deploy).
- `test-template.md`: the shipped `instant()` specs for both navigation
types (phase C), and the delete-before-PR baseline scaffold (phase B).
- `reference/red-test-robustness.md`: the C-gate and phase F. The taxonomy of
untrustworthy REDs, the checklist, the differential recipe, the vacuous-pass
failure mode, and worked cases.
- `reference/real-app-patterns.md`: parallel routes, deferring an auth gate,
initial-load vs soft-navigation shells, the empty-shell failure mode, the
responsive-skeleton mismatch, edge cases.
@@ -0,0 +1,355 @@
# Refactor patterns — push dynamic down into the shell
Each pattern is **before → after**: keep as much as possible in the prerendered shell, and wrap only genuinely per-request work in a tight `<Suspense>` (or hoist it into `use cache`). Production shapes — parallel-route slots, deferring an auth gate, client slot-routers — are in `real-app-patterns.md`.
---
## 1. Awaiting at the top → move the await into a Suspense child
The most common blocking shape. Awaiting request-time data at the top of a page/layout makes **everything below it** dynamic.
```tsx
// ❌ before — top-level await of a non-static param + uncached data
export default async function Page(props: PageProps<"/store/[slug]">) {
const { slug } = await props.params;
const product = await db.products.findBySlug(slug);
return (
<article>
<h1>{product.name}</h1>
</article>
);
}
```
```tsx
// ✅ after — pass the params promise down; await inside a Suspense-wrapped child
import { Suspense } from "react";
export default function Page(props: PageProps<"/store/[slug]">) {
return (
<Suspense fallback={<p>Loading product…</p>}>
<Product params={props.params} />
</Suspense>
);
}
async function Product({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const product = await db.products.findBySlug(slug);
return (
<article>
<h1>{product.name}</h1>
</article>
);
}
```
Inline variant when you don't want a separate component — unwrap the promise without awaiting at the top:
```tsx
export default function Page(props: PageProps<"/store/[category]">) {
return (
<Suspense fallback={<Grid.Skeleton />}>
{props.params.then(({ category }) => (
<ProductGrid category={category} />
))}
</Suspense>
);
}
```
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 2. `cookies()` / `headers()` in a layout → start, don't await; pass down
A layout that awaits request data blocks the layout **and every page under it**.
```tsx
// ❌ before — whole layout (and all children) becomes dynamic
export default async function Layout({ children }) {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value;
return <body data-theme={theme}>{children}</body>;
}
```
```tsx
// ✅ after — start the read without awaiting, pass the promise to a Suspense child
import { cookies } from "next/headers";
import { Suspense } from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = cookies(); // not awaited → does not block the shell
return (
<body>
<nav>
<Suspense fallback={<UserMenu.Skeleton />}>
<UserMenu cookiePromise={cookieStore} />
</Suspense>
</nav>
{children}
</body>
);
}
async function UserMenu({
cookiePromise,
}: {
cookiePromise: ReturnType<typeof cookies>;
}) {
const theme = (await cookiePromise).get("theme")?.value;
return <div data-theme={theme}>…</div>;
}
```
`{children}` and `<nav>` stay in the shell; only `<UserMenu>` streams.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 3. Uncached fetch / DB read → choose `use cache` _or_ `<Suspense>`
Decide per data source. Same-for-everyone & rarely-changing → cache it (it joins the shell). Per-request & must-be-fresh → leave it uncached behind a boundary.
```tsx
// ❌ before — both block the shell
const product = await db.products.findBySlug(slug); // rarely changes
const inventory = await db.inventory.findBySlug(slug); // must be fresh
```
```tsx
// ✅ after — cache the stable one (shell), defer the fresh one (streams)
async function getProduct(slug: string) {
"use cache"; // → resolved at prerender, lands in the shell
return db.products.findBySlug(slug);
}
<Suspense fallback={<p>Checking availability…</p>}>
<Inventory params={params} /> {/* uncached read stays here, streams in */}
</Suspense>;
```
> A bare `'use cache'` applies the `default` `cacheLife` profile. Choose freshness explicitly with `cacheLife('<profile>')` (`default` / `seconds` / `minutes` / `hours` / `days` / `weeks` / `max`) rather than shipping the default lifetime by omission.
>
> Serverless note: `use cache` is in-memory and does not persist across instances — use [`use cache: remote`](https://nextjs.org/docs/app/api-reference/directives/use-cache-remote) for a durable shell.
**Insight:** [uncached data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-dynamic).
---
## 4. Dynamic params → `generateStaticParams` (shell) or `<Suspense>` (stream)
If the set of params is enumerable, prerender them so `await params` resolves into the shell. Otherwise treat params as request-time and wrap consumers in `<Suspense>`.
```tsx
// ✅ option A — enumerate → params resolve into the shell, no Suspense needed for params
export function generateStaticParams() {
return [{ slug: "shoes" }, { slug: "hats" }];
}
export default async function Page({ params }: PageProps<"/store/[slug]">) {
const { slug } = await params; // known at build → shell-safe
// ...
}
```
```tsx
// ✅ option B — not enumerable → params is request-time; await it inside a boundary (pattern #1)
```
Root params (the dynamic segments the root layout sits inside, e.g. `app/[lang]/layout.tsx`) are readable from any Server Component via `next/root-params` without prop-drilling — but under Cache Components they must still be enumerated by `generateStaticParams` (at least one value per root param) to land in the shell, the same as any other dynamic param.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 5. `searchParams` → always behind `<Suspense>` (on page load)
Search params are never known at build, so awaiting them (or `useSearchParams()`) suspends on a page load. Keep the rest of the page in the shell by isolating the consumer.
```tsx
// ✅ static content stays in the shell; the search-dependent part streams
export default function Page(props: PageProps<"/search">) {
return (
<>
<h1>Search</h1> {/* shell */}
<Suspense fallback={<Results.Skeleton />}>
<Results searchParams={props.searchParams} />
</Suspense>
</>
);
}
async function Results({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <ResultList query={q} />;
}
```
On a **client navigation** the router already has the URL, so a `useSearchParams()` consumer resolves synchronously and can appear in the prefetched shell — but you still need the boundary for the page-load path.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime) (or, via `useSearchParams` in a Client Component, [URL data in a Client Component](https://nextjs.org/docs/messages/blocking-prerender-client-hook)).
---
## 6. Non-deterministic values → `connection()` + `<Suspense>`, or cache
`Math.random()`, `Date.now()`, `crypto.randomUUID()` produce different output each run, so Cache Components makes you choose: per-request (defer) or fixed (cache).
```tsx
// ✅ per-request value: gate on connection() and wrap in Suspense
import { connection } from "next/server";
async function RequestId() {
await connection();
return <span>{crypto.randomUUID()}</span>;
}
// <Suspense fallback={null}><RequestId /></Suspense>
```
```tsx
// ✅ same value for everyone: cache it so it joins the shell
async function buildId() {
"use cache";
return Date.now();
}
```
**Insight:** [`Date.now()`](https://nextjs.org/docs/messages/blocking-prerender-current-time), [`Math.random()`](https://nextjs.org/docs/messages/blocking-prerender-random), or [`crypto`](https://nextjs.org/docs/messages/blocking-prerender-crypto) while prerendering.
---
## 7. Dynamic `generateMetadata` → static export, `use cache`, or a dynamic-marker for runtime data
```tsx
// ❌ before — reading request data blocks the route's metadata
export async function generateMetadata() {
const c = await cookies();
return { title: c.get("title")?.value };
}
```
```tsx
// ✅ option A — static
export const metadata = { title: "Store" };
// ✅ option B — cache the metadata (depends on external data, not runtime data)
export async function generateMetadata() {
"use cache";
return { title: await getTitle() };
}
```
```tsx
// ✅ option C — metadata genuinely needs runtime data (cookies/headers):
// keep generateMetadata dynamic, and add a dynamic-marker component to the
// page so the rest of the page still prerenders into the shell.
import { cookies } from "next/headers";
import { connection } from "next/server";
import { Suspense } from "react";
export async function generateMetadata() {
const token = (await cookies()).get("token")?.value;
return { title: token ? "Personalized" : "Store" };
}
async function DynamicMarker() {
await connection(); // signals intentional dynamic content
return null;
}
export default function Page() {
return (
<>
<article>{/* static content — stays in the shell */}</article>
<Suspense>
<DynamicMarker />
</Suspense>
</>
);
}
```
`generateViewport` is the same, except dynamic viewport blocks the **whole page**. Genuine instant fixes: a static `viewport` export, or `use cache`. The other two are dynamic-acceptance opt-outs, not instant fixes — do not treat them as a way to reach GREEN: `export const instant = false` opts the segment out of validation while the navigation still blocks, and a `<Suspense>` above the document `<body>` makes the whole route dynamic.
**Insight:** [runtime data in `generateMetadata()`](https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime).
---
## 8. Keep the LCP element in the shell
Don't bury the main heading (the LCP element) inside a boundary — it can't paint until the boundary resolves.
```tsx
// ✅ LCP outside the boundary → paints in the shell
<h1>{product.name}</h1> {/* shell (cache the name if needed) */}
<Suspense fallback={<Reviews.Skeleton />}>
<Reviews productId={id} /> {/* streams */}
</Suspense>
```
---
## 9. Granularity below shared layouts (client-nav correctness)
A single boundary in the **root** layout passes a page-load check but leaves sibling client navigations blocking. Put a boundary **below the shared layout**.
```tsx
// app/store/layout.tsx — boundary below the /store shared layout covers
// client navs like /store/shoes → /store/hats (the root boundary does not)
export default function StoreLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<StoreNav /> {/* shell */}
<Suspense fallback={<Page.Skeleton />}>{children}</Suspense>
</section>
);
}
```
Prefer per-component boundaries inside the page (patterns #1–#5) over one big layout boundary — they keep more real content in the shell and stream independently.
**Insight:** the read's own insight surfaces on the client navigation when the boundary is too high — see [where to place the boundary](https://nextjs.org/docs/messages/blocking-prerender-dynamic#choosing-where-to-place-the-boundary).
## 10. Can't push the read down? Runtime-prefetch the whole route
Patterns 1–9 grow a **static shell** by moving dynamic reads behind boundaries. Some routes resist that: an ID minted per request (`createId()`), an auth/scope resolution the whole subtree needs, a page that is _all_ dynamic by nature. The read can't move, so there is no meaningful shell to commit and the soft nav stays RED. The escape hatch is **runtime prefetching** — don't prerender a shell, run the dynamic render _in the prefetch_ so the whole route is warm before the click and the soft nav commits the real content instantly.
It has **two halves — both required**, and route config alone is RED:
```tsx
// 1. The route opts in — on EVERY leaf (page/default) segment, parallel @slots
// included, or instant validation re-triggers on the segments that lack it
// (the same all-or-nothing coupling as an `instant = false` opt-out).
export const prefetch = 'allow-runtime'
// 2. The <Link> asks for a FULL prefetch — that is what spawns the runtime
// request. A default/auto prefetch only warms the static shell.
<Link href={href} prefetch={true}>…</Link>
// <Link prefetch> already issues the full prefetch on hover and on viewport
// entry, so prefer it. An imperative full prefetch via router.prefetch needs
// the non-exported PrefetchKind enum, so it has no clean public form.
```
Under `instant()` the runtime entry is what commits, so the real content (not a skeleton) shows under the lock — that is the GREEN.
Gotchas (each cost real debugging time):
- **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` / `kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but `instant = false` (or nothing) on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects (a `/foo` that canonicalizes to `/`) can't be prefetched — the prefetch receives the redirect, not the tree. Point the link and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; issuing it on hover for every link (recents that point at whole chats) is wasteful. Scope `kind: 'full'` to the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a client component, so its text isn't in the prefetch response — assert a `data-testid` that renders when the client subtree commits, not a substring of the stream.
Prefer a static shell (patterns 1–9) whenever the read can move: it's cheaper than a runtime prefetch and also covers hard load. Reach for runtime prefetching when the read genuinely can't move, or for a route that's all-dynamic by design.
**Insight:** [dynamic data during prefetching](https://nextjs.org/docs/messages/instant-link-prefetch-partial).
@@ -0,0 +1,88 @@
# Real-app patterns
The rest of this skill models a single linear `layout → page` tree. Production App Router routes add **parallel routes, shared layout UI, and auth gates**, which is where most of the real static-shell work happens. These patterns bridge that gap. Read the skill's `SKILL.md` first.
## Parallel routes: each slot is its own boundary
Instant validation treats every parallel-route slot below the shared layout as an **independent** navigation boundary. Consequences:
- **Each `@slot` needs its own `<Suspense>`** around its dynamic reads; a boundary in one slot does not cover another.
- **An uncovered dynamic read in any slot blocks the whole navigation.** A perfect `@content` does not help if `@sidebar` awaits a session at the top.
- **A slot that renders `null` (e.g. `default.tsx`) is shell-safe**: it is static and performs no reads. Slots that do not re-render for this navigation cost nothing.
```
[tenant]/layout.tsx (shared: already mounted on a soft navigation; not re-rendered)
├ @content → settings/layout → billing/page ← guard each slot's dynamic reads…
├ @sidebar → side nav ← …here too (independent boundary)
└ @header → default.tsx → null ← free
```
## Client-rendered slot routing is not part of the soft-navigation re-render
A common pattern: a stable shared layout renders `@header`/`@sidebar` through a **client** component that swaps slot content based on `usePathname()`. On a soft navigation, Next.js only re-renders the **server** segments that changed below the shared layout; a client-component subtree is not part of that re-render. So that navigation UI neither blocks the navigation nor needs a server `<Suspense>` for it; only the server segments that actually change (e.g. `@content`) matter. It does participate in an initial load (see the caveat below).
## "Instant" is not "useful shell": the empty-shell failure mode
Validation checks that a dynamic read is **guarded by a boundary**, not that the fallback is non-empty. A `<Suspense>` with no `fallback` (or `fallback={null}`) passes validation and commits instantly, but renders a **blank** shell. If a layout and its page both `await getSession()` (your auth library's request-time read) at the top under one empty-fallback boundary, the whole frame collapses to nothing while the user waits. "Validates as instant" and "good loading experience" are different goals.
> Give every boundary a real loading skeleton, and place it low so the most real content stays in the shell. A `fallback={null}` directly above `<body>` is a deliberate empty-shell opt-out; an empty fallback lower in the tree is almost always a bug.
## The responsive-skeleton mismatch: the shell must match every breakpoint
A loading skeleton that misaligns with the loaded UI is its own bug, and it usually appears on mobile. A hand-built skeleton encodes one layout; the real component is responsive and changes shape at breakpoints, so a desktop-shaped skeleton no longer lines up once the viewport is small.
A concrete shape: a list-detail view renders a list or tree in a side panel on desktop, but collapses that panel into a single dropdown or drawer on mobile (with its own loading state). A row skeleton built for the desktop panel has nothing to align with on mobile.
The fix is the same push-down as everywhere else: **share the real responsive layout between the live render and the shell render.** One responsive component renders both (its data slots show the reused `*Skeleton` in the shell and real data after the stream), so the breakpoint switch happens once, for both renders, and there is no second desktop-only skeleton to drift.
(Same hoist rule, responsive layout included.) Verify the shell at both desktop and mobile widths against the real render at the same width.
## Deferring an auth gate / top-level `await` in a layout
A top-level `await` in a layout blocks everything below it (the most common blocker; see [Runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime)). Auth gates are the most common real instance:
```tsx
// ❌ Before: the await + redirect at the top blocks the whole settings frame
export default async function SettingsLayout({ children }) {
const session = await getSession(); // your auth library's request-time read; suspends during prerender → frame can't build
if (!session?.user) redirect(getLoginUrl());
return <Shell>{children}</Shell>;
}
```
```tsx
// ✅ After: render children unconditionally; move the gate into a Suspense child
import { Suspense } from "react";
export default function SettingsLayout({ children }) {
return (
<Shell>
<Suspense fallback={null}>
<AuthGate />
</Suspense>
{children}
</Shell>
);
}
async function AuthGate() {
const session = await getSession(); // the session read suspends during prerender…
if (!session?.user) redirect(getLoginUrl()); // …so redirect() never runs at build time
return null;
}
```
The shell prerenders as if authorized (the session read suspends before `redirect()` is reached, so the redirect only happens at request time), and `{children}` is now in the shell instead of behind the gate. (`fallback={null}` is correct here: `AuthGate` renders nothing on success.)
## Initial-load shell vs soft-navigation shell
The `test-template.md` specs drive a `<Link>` click for soft navigations and `page.goto()` for initial loads. The two shells can differ for the same route:
> **The initial-load shell can show less than the soft-navigation shell when a layout above the shared boundary awaits un-enumerated `params`/`searchParams`.** An initial load re-runs every layout from the root; if a parent layout does `await props.params` and that segment has no `generateStaticParams`, the param suspends on the initial load and its whole subtree drops out of the shell. A soft navigation does not re-render that parent and already has the params. Symptom: an element present after a `<Link>` click is missing after `goto`.
To assert the soft-navigation shell, drive a real `<Link>` click (through menus if necessary). Use `page.goto()` inside `instant()` to assert the initial-load shell, or when no parent above the shared boundary awaits un-enumerated params, in which case the two coincide.
## Edge cases
- **A `React.cache` (or custom memoization) wrapper around `cookies()`/`headers()` still suspends.** Memoizing the call does not make it shell-safe: the underlying request read still returns a pending promise during prerender. Only the **`use cache`** directive, keyed on static or param inputs, puts data in the shell.
- **Playwright cannot see a `display: contents` or fragment fallback.** Such a fallback reads as hidden, so `instant()` assertions cannot `toBeVisible()` it. Give fallbacks a real wrapper element with a `data-testid`.
@@ -0,0 +1,139 @@
# RED-test robustness: verify the RED before optimizing
The C-gate of the workflow. A RED that is red for the wrong reason sends you optimizing a route that
was never broken: the route becomes instant, the test stays red (or the code is contorted to
satisfy a broken assertion), and the effort lands on the wrong problem. The prevention is cheap:
spend a few minutes verifying the RED is trustworthy first.
## The deciding question
> **Does the marker render WITHOUT the lock, as the test user?**
- **No**: the test is red because the marker or page is not there for that user or environment. A
marker bug, not an instant-navigation bug. Fix the marker. (This is the most common case.)
- **Yes**: the marker exists and is reachable; a red under the lock is a genuine "not instant".
Optimize the route.
Everything below serves answering that question honestly.
## The robustness checklist (all must hold)
1. **Red on baseline**: fails on the unfixed route.
2. **Right reason**: without the lock, the marker is visible on the production-build rig (never
`next dev`), running as the test user, not only in the author's own logged-in session.
3. **Differential**: reverting only the fix → RED; re-applying → GREEN; nothing else moves it.
4. **Non-gameable marker**: a sync element of the static shell, never streamed data.
5. **Deterministic on a production build**: stable across N runs; never `next dev`.
6. **Discriminating both ways**: present under the lock on an instant route, absent under the
lock on a blocking one.
7. **Renders for the test user**: under that user's flags, plan, role, and data.
8. **Conditional redirects accounted for**: assert at the route's real destination for that user.
9. **Real selector**: a `data-testid` on a known static-shell node, not a guessed `role`/`name`.
10. **Visible marker**: not `display:none`, off-screen, or inside a hover overlay; for lists,
target `.filter({ visible: true }).first()`.
11. **Fresh build under test**: the deployment being measured contains the latest commit, not a
build URL still serving the previous deploy.
## Taxonomy: red for the wrong reason
Any of these makes a RED untrustworthy. None of them is "the navigation isn't instant."
| Wrong reason | How it occurs | How to rule it out |
| ---------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| **Selector matches nothing** | a guessed `getByRole('button', { name: 'Folder' })` | grep the component for the real accessible name; add a `data-testid` |
| **Conditional redirect** | the route `redirect()`s for the test user (flag/role), so the marker page is never reached | check the page's top-level branches; assert at the real destination, or pin the flag |
| **Flag / plan / role gate** | the author has the flag or plan; the test user does not | run the unlocked baseline as the test user; pin flags via the project's override mechanism |
| **Empty state** | the marker only exists when data does; the CI account is empty | pick a marker present in the empty state (a layout element such as the page header), or seed data |
| **Timeout / flake** | a slow API or transient infrastructure error | re-run; separate infrastructure flake from a real signal |
| **Streamed marker** | the marker is behind `<Suspense>`, so it is never in the shell | choose a sync shell element; verify it sits outside every `<Suspense>` |
| **Auth redirect** | unauthenticated → `/login` | confirm login succeeded before the navigation |
| **Stale deployment** | the test ran against the previous build (the URL under test still serves the prior deploy) | poll the deployment for a marker from the latest commit before trusting any verdict |
| **Hidden / off-screen** | the testid is on a hover-overlay or off-screen list item | put the marker on an always-visible node; `.filter({ visible: true }).first()` for lists |
## Worked cases
These are illustrative failures from real optimization runs; each was red for a wrong reason, and
none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and
plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong
reason; the rig file's DRIFT list says which rows apply to your app.
- **Guessed selector + empty state**: the marker was a button picked by a guessed accessible name
that no element actually had, on a list page whose CI account had no rows. → checks 7, 9. Fix: a
`data-testid` on a real static-shell node.
- **Hidden marker**: the testid sat first on a `hidden sm:block` hover-overlay link, then on an
off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10.
Fix: an always-visible node; for lists, `.filter({ visible: true }).first()`.
## Differential check (capture in the PR)
The strongest evidence that the RED measured the property:
```
1. on the fixed branch → GREEN
2. revert ONLY the fix (the <Suspense> push-down) → RED
3. re-apply → GREEN
4. confirm no other change moves it
```
Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who
sees the differential knows the test measures the property.
## `instant()` is not a stopwatch
The test does not measure how fast a navigation is. `instant()` gates dynamic data so the content
of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant
route's shell is present, and a blocking route's content never commits, regardless of wait time.
Therefore:
- The shipped assertion is `await expect(SHELL_MARKER).toBeVisible()` under the lock. Do not add a
custom timeout or a `painted` boolean.
- A custom short timeout (e.g. `3000`) implies a race against a clock that does not exist. It adds
nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask
late.
- Do not use `locator.isVisible({ timeout })` as a soft wait: Playwright deprecated and ignores
that timeout; the call returns immediately.
- "Renders for the test user" (checks 7-9) is established at authoring time with the unlocked
baseline scaffold, not by a timed assertion in the shipped test.
## `instant()` guards need no retries and no prefetch warming
An `instant()` guard is deterministic. Do not configure retries on one, and do not hover-warm to
help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it
before committing (even for a `prefetch={false}` link, even for a route already in the prefetch
cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch.
A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a
flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or
the marker; a retry masks the regression the guard exists to catch. The only legitimate
`.hover()`/menu-open is when the trigger element itself is not in the DOM until hovered or opened.
## Silent no-op: the testing API must be exposed in the measured build
`instant()` works by setting a cookie (`next-instant-navigation-testing`) that lock code inside the
build reads. It does not throw when that lock code is absent; it only throws on nested calls or an
unknown base URL. If the build was produced without the testing API
(`experimental.exposeTestingApiInProductionBuild`), the cookie is ignored, the navigation runs
normally, and the `instant()` test passes vacuously. A green `instant()` test is only meaningful if
the lock engaged.
Two defenses; use both:
1. **Confirm the API is exposed on the target.** Wire the flag to the platform's preview/staging
condition or an explicit environment variable; the rig file records the project's spelling
(SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set.
2. **Make the test self-validating**: for any route with deferred content, also assert that the
deferred content is gated under the lock, not only that the shell is present
(`test-template.md`, self-validating variant). If the lock did not engage, the content is
already present and `toHaveCount(0)` fails.
The gated half holds under the lock for both navigation types regardless of warm state: the
soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the
cookie on the document request and suspends dynamic data. A vacuous pass is only possible with
a build produced WITHOUT the testing API, which defense #1 above covers.
## Determinism and the rig
- Always measure on a production build, never `next dev`; SKILL.md phase A owns this invariant and
its rationale.
- Run the RED several times; an intermittently red gate is not a gate. If it flakes, determine
whether the cause is infrastructure (transient errors) or a real race before trusting either
color.
@@ -0,0 +1,103 @@
# Rig discovery: generate this project's `instant-nav.rig.md`
The skill's principles are environment-independent. Your build, deploy, auth,
and test infrastructure are not. This phase converts the principles into THIS
project's concrete workflow: run discovery once per repo, write the answers to
a committed `instant-nav.rig.md` (repo root, or next to your e2e config), and
every later run reads that file instead of rediscovering.
The skill is deliberately opinionated about **what** the rig must provide, and
deliberately unopinionated about **how** your stack provides it.
## How to discover
Inspect before asking. Most answers are already in the repo:
- `package.json` scripts (`build`, `start`, `test:e2e`)
- the e2e config (`playwright.config.*`: `baseURL`, `webServer`, projects)
- CI config (`.github/workflows/`, `vercel.json`, GitLab/Circle files,
Dockerfiles)
- `next.config.*` (existing `experimental` flags)
- existing e2e auth helpers (grep for `login`, `storageState`, `session`)
Ask the user only what the repo can't answer. Typically that means: which
deploy target counts as "preview", which account the suite runs as in CI, and
whether an agent is allowed to push and wait on CI unattended.
## The six questions (all must have answers), plus two derived fields
The six questions below must all have answers. The rig file template adds two
more fields the discovery feeds rather than asks directly: **LIVENESS** (the
SHA-echoing probe, derived from the LOOP answer) and **WALLS** (project-specific
build/run obstacles, accumulated as you first hit them).
1. **BUILD**: how is a production build of this app produced and served?
A per-push preview deploy, a staging container, or bare
`next build && next start`. Anything but `next dev`.
2. **EXPOSE**: what condition turns on
`experimental.exposeTestingApiInProductionBuild` for every measured build,
and never for real production? Spellings: an explicit
`EXPOSE_TESTING_API=1` for local production builds; `process.env.DEPLOY_ENV
=== 'staging'` for a generic CI/staging env var; `process.env.VERCEL_ENV ===
'preview'` on Vercel.
3. **RUN**: how is the Playwright suite invoked, and against which
`BASE_URL`?
4. **TEST USER**: which account does the suite run as, and how does login
happen (helper, `storageState`, API token)? What flags / plan / role / data
does that account have?
5. **DRIFT**: enumerate everything that can differ between the author's own
session and the test user's environment (feature flags, plans and
entitlements, roles, seeded vs empty data, locale, A/B buckets). Every item
is a way a RED can become untrustworthy; this list feeds the C-gate
(`reference/red-test-robustness.md`).
6. **LOOP**: the unattended iteration for your rig. Push → build → e2e
against the artifact → read the failure → fix → push (CI), or build → start
→ e2e (local). Note anything an agent cannot do alone (deploy approvals,
secrets, protected branches). Include the **liveness probe**: the endpoint
or response header that echoes the deployed commit SHA (e.g. a `/healthz`
route or an `x-deployed-sha` header), so a CI run can confirm the build
under test matches `HEAD` before trusting a verdict (SKILL.md phase A). If
the platform exposes no SHA-echoing endpoint or header, add one: surface a
build-time commit var (`VERCEL_GIT_COMMIT_SHA`, a CI commit variable) on a
`/healthz` route or a response header, or fall back to polling the deploy
platform's API for the deployment whose `commitSha === HEAD`. Record the
chosen mechanism. For a local `build && start` rig the artifact is the one
freshly built, so no probe is needed.
## The file: copy, fill, commit as `instant-nav.rig.md`
```md
# instant-nav rig: <project>
- BUILD: <command / platform that produces the measured production build>
- EXPOSE: <the condition wired to exposeTestingApiInProductionBuild>
- RUN: <e2e command> against <how BASE_URL is obtained>
- TEST USER: <account> via <login mechanism>; flags/plan/role/data: <...>
- DRIFT: <the enumerated drift surface>
- LOOP: <push → CI → e2e, or local build → start → test>; agent limits: <...>
- LIVENESS: <endpoint/header echoing the deployed SHA; n/a for local build && start>
- WALLS: <project-specific build/run obstacles + their workarounds>
```
Real apps rarely build for production cleanly on the first attempt: missing
secrets, server-only imports that fail prerender, ports held by respawning
servers. Record each wall and its workaround the first time you hit it. `WALLS`
accumulates the project-specific build/run obstacles that the other fields
cannot capture.
## Filled examples
**No CI / local-only.** BUILD: `EXPOSE_TESTING_API=1 next build && next
start`. EXPOSE: that env var. RUN: `BASE_URL=http://localhost:3000 playwright
test`. LOOP: build → start → test on one machine; fully agent-drivable, with
nothing to push, no secrets, and no deploy wait.
**Generic CI + container.** BUILD: the pipeline builds an image and deploys it
to a staging namespace. EXPOSE: `process.env.DEPLOY_ENV === 'staging'`. RUN: a
CI job runs Playwright against the staging URL. LOOP: push → pipeline → e2e;
fully agent-drivable once the pipeline is wired.
**Vercel preview deploys.** BUILD: every push builds a preview. EXPOSE:
`process.env.VERCEL_ENV === 'preview'`. RUN: `playwright test` with
`BASE_URL=<preview URL>`. LOOP: push → preview → e2e; fully agent-drivable
once the preview deploy and `VERCEL_ENV` gating are in place.
@@ -0,0 +1,165 @@
# Test template: the instant() guard
Ship one test per navigation type you are guarding: under `instant()`, assert that the
destination's static shell appears. `instant()` gates dynamic data, so a correctly instant route
commits its shell under the lock and a blocking route does not. `instant()` is a ruler, not a
stopwatch: do not add custom timeouts or timing races (see `reference/red-test-robustness.md`).
Whether the marker is the right one (rendering for the test user, not flag-gated, not redirected
away, not guessed) is established at authoring time with the unlocked baseline scaffold below
(phase B, the C-gate), not by additional assertions in the shipped test.
All identifiers in angle brackets (`<b>`, `<Trigger>`) and the `../helpers` import are placeholders;
substitute your project's e2e auth helper, URL helper, and real testids/trigger before running.
## Soft navigation (client-side navigation)
Drive a real `<Link>` click. The committed shell is the destination's prefetched App Shell.
Under the lock the router initiates and awaits the route prefetch itself, so no manual warming is
needed; if the shell is intermittently absent, treat it as a real blocker or marker bug (C-gate),
never as a warming race. Do not add waits or hovers.
```ts
import { instant } from "@next/playwright";
import { expect, test } from "@playwright/test";
// Use the auth/setup helpers your e2e suite already has. Run as the test user
// (defined in SKILL.md phase B).
import { logIntoTestAccount, testUrl } from "../helpers";
// A SYNC element of the destination's static shell (header, action button,
// column header), not data that streams in, and one that renders for the
// test user (not gated by a flag, plan, role, or empty state). Prefer a
// data-testid on a known static node over a guessed role/name.
const SHELL_MARKER = '[data-testid="<b>-shell-marker"]';
test.describe("instant nav: A -> B", () => {
test.beforeEach(async ({ page, browser }) => {
await logIntoTestAccount(page, browser);
});
test("B shell commits under instant()", async ({ page }) => {
await page.goto(testUrl("/"));
const trigger = page.getByRole("link", { name: "<Trigger>", exact: true });
await expect(trigger).toBeVisible({ timeout: 20000 });
await instant(page, async () => {
await trigger.click();
// static shell asserted under the lock; no timeout
await expect(page.locator(SHELL_MARKER)).toBeVisible();
});
});
});
```
The trigger selector follows the same rule as `SHELL_MARKER`: prefer a `data-testid` on the real
`<Link>` (`page.getByTestId('<trigger>-link')`) over a guessed accessible name. `getByRole({ name })`
is shown only for brevity; like the marker, the trigger must reliably resolve for the test user.
## Initial load (hard navigation)
Drive `page.goto()` inside `instant()` with the `baseURL` option. The served document is the
route's prerendered static shell. `baseURL` is required because `page` is still `about:blank` when
`instant()` runs (`resolveURL` falls back to `page.url()` only when no `baseURL` is passed).
Establish the session WITHOUT navigating `page` (inject `storageState`, or log in on a separate
context/page). A login helper that navigates `page` itself defeats the measurement for a different
reason: that navigation completes before `instant()` acquires the lock, so it runs unmeasured. The
session must be pre-established either way; otherwise an authenticated route redirects to login
and the RED is false.
If the project's only login helper navigates `page`, the agent must use a
storageState/separate-context path here instead, a session-injection call that
does NOT call `page.goto`:
```ts
test.describe("instant initial load: B", () => {
test.beforeEach(async ({ page }) => {
await injectTestUserSession(page); // storageState only; must NOT call page.goto
});
test("B shell is served", async ({ page }) => {
const url = testUrl("/<b>");
await instant(
page,
async () => {
await page.goto(url);
await expect(page.locator(SHELL_MARKER)).toBeVisible();
},
{ baseURL: new URL(url).origin },
);
});
});
```
## Self-validating variant (recommended for routes with deferred content)
Also assert that the deferred content is gated under the lock and streams after release. This
makes a vacuous pass impossible: if the lock did not engage (testing API missing from the build),
the content is already present and `toHaveCount(0)` fails (see `reference/red-test-robustness.md`).
`SHELL_MARKER` is the shell node; `[data-testid="<b>-content"]` is the deferred data it guards.
```ts
// soft navigation
await instant(page, async () => {
await trigger.click();
await expect(page.locator(SHELL_MARKER)).toBeVisible(); // shell present
await expect(page.getByTestId("<b>-content")).toHaveCount(0); // deferred data gated
});
await expect(page.getByTestId("<b>-content")).toBeVisible(); // streams after release
```
The two **gated-half** assertions (shell visible, deferred content `toHaveCount(0)`) apply to the
initial-load `page.goto()` form too. The cookie gates the deferred content identically for both:
on a soft navigation the client lock gates dynamic-data writes; on an initial load the server
honors the cookie on the document request (set via `addCookies()` before navigation, scoped by
`baseURL`) and suspends dynamic data, independent of whether the route was previously rendered or
cached. So the initial-load `toHaveCount(0)` gated half is as valid as the soft-nav one; it needs
no fresh browser context and no cache-busting query param.
The **post-release** assertion (`getByTestId('<b>-content').toBeVisible()` after the `instant()`
block) is soft-nav only. On an initial load the document was already emitted under the lock, so
nothing streams in after release; drop that assertion from the initial-load test, or
`page.reload()` first to fetch an unlocked document. The mechanism is in
`reference/red-test-robustness.md`.
## Baseline scaffold: do not ship
Before optimizing, confirm the target exists with an unlocked check (no `instant()`). It
disambiguates "not instant" from "marker absent for this user or environment". Run it as the test
user; mismatch against the rig DRIFT list is what the C-gate catches
(`reference/red-test-robustness.md`). Confirm the marker is real and reachable, then delete the
scaffold before the PR.
**The baseline must mirror the navigation type of the test you are shipping.** Drive a `<Link>`
click when guarding the soft-nav shell; drive `page.goto()` when guarding the initial-load shell.
The two shells can differ (`reference/real-app-patterns.md`): a click-driven baseline run against a
shipped `goto` test would confirm a marker that the `goto` path never shows, which produces exactly
the false RED the C-gate exists to prevent.
```ts
// soft-nav baseline: mirror the soft-nav instant() test
test("dev-only: navigating to <b> renders its shell (no lock)", async ({
page,
}) => {
await page.goto(testUrl("/"));
const trigger = page.getByRole("link", { name: "<Trigger>", exact: true });
await expect(trigger).toBeVisible({ timeout: 20000 });
await trigger.click();
await expect(page).toHaveURL(/\/<b>(\?|$)/); // confirm the real destination (no redirect away)
await expect(page.locator(SHELL_MARKER)).toBeVisible({ timeout: 15000 });
});
```
```ts
// initial-load baseline: mirror the initial-load instant() test (session pre-established)
test("dev-only: <b> shell is served (no lock)", async ({ page }) => {
await page.goto(testUrl("/<b>"));
await expect(page.locator(SHELL_MARKER)).toBeVisible({ timeout: 15000 });
});
```
Notes:
- Pick `SHELL_MARKER` as a sync element of the destination's static shell, never streamed data.
Use a `data-testid` on a known static node rather than a guessed role/name.
- Do not put a custom timeout, a `painted` boolean, or `isVisible({ timeout })` in the shipped
assertion, and do not add retries or hover-warming; see `reference/red-test-robustness.md`.
@@ -0,0 +1,81 @@
---
name: sanity-best-practices
description: Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, Sanity Functions, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.
---
# Sanity Best Practices
Comprehensive best practices and integration guides for Sanity development, maintained by Sanity. Use the quick reference below to load only the one or two topic files that match the task.
## When to Apply
Reference these guidelines when:
- Setting up a new Sanity project or onboarding
- Integrating Sanity with a frontend framework (Next.js, Nuxt, Astro, Remix, SvelteKit, Hydrogen)
- Writing GROQ queries or optimizing performance
- Designing content schemas
- Implementing Visual Editing and live preview
- Working with images, Portable Text, or page builders
- Configuring Sanity Studio structure
- Setting up TypeGen for type safety
- Implementing localization
- Migrating content from other systems
- Building custom apps with the Sanity App SDK
- Managing infrastructure with Blueprints
- Automating content workflows with Sanity Functions
## Global Rules
- Let Sanity generate `_id` values for ordinary documents. Do not create deterministic UUIDs, slug-derived IDs, or legacy-system IDs when creating documents.
- Model relationships with `reference` fields, then resolve related documents with GROQ lookups, source-key fields, or returned `_id` values from created documents.
- Use explicit document IDs mainly for singleton documents controlled by Studio Structure, including localized singletons such as `homePage-en`.
## Quick Reference
### Integration Guides
- `get-started` - Interactive onboarding for new Sanity projects
- `nextjs` - Next.js App Router, Live Content API, standalone Studio
- `nuxt` - Nuxt integration with @nuxtjs/sanity
- `angular` - Angular integration with @sanity/client, signals, resource API
- `astro` - Astro integration with @sanity/astro
- `remix` - React Router / Remix integration
- `svelte` - SvelteKit integration with @sanity/svelte-loader
- `hydrogen` - Shopify Hydrogen with Sanity
- `project-structure` - Standalone Studio and monorepo patterns
- `app-sdk` - Custom applications with Sanity App SDK
- `blueprints` - Infrastructure as Code: blueprint files, stacks, plan/deploy workflow, error recovery, CI deploys
- `functions` - Automating content workflows with Sanity Functions
### Topic Guides
- `groq` - GROQ query patterns, type safety, performance optimization
- `schema` - Schema design, field definitions, validation, deprecation patterns
- `visual-editing` - Presentation Tool, Stega, overlays, live preview
- `page-builder` - Page Builder arrays, block components, live editing
- `portable-text` - Rich text rendering and custom components
- `image` - Image schema, URL builder, hotspots, LQIP, Next.js Image
- `studio-structure` - Desk structure, singletons, navigation
- `typegen` - TypeGen configuration, workflow, type utilities
- `seo` - Metadata, sitemaps, Open Graph, JSON-LD
- `localization` - i18n patterns, document vs field-level, locale management
- `migration` - Content import overview (see also `migration-html-import`)
- `migration-html-import` - HTML to Portable Text with @portabletext/block-tools
## How to Use
Start with the single framework or topic guide that best matches the request, then read additional references only when the task crosses concerns. Use these reference files for detailed explanations and code examples:
```
references/groq.md
references/schema.md
references/nextjs.md
```
Each reference file contains:
- Comprehensive topic or integration coverage
- Incorrect and correct code examples
- Decision matrices and workflow guidance
- Framework-specific patterns where applicable
@@ -0,0 +1,576 @@
---
title: Angular & Sanity Integration Rules
description: Integration guide for Angular, including @sanity/client setup, data fetching with signals and resource API, Portable Text rendering, and image optimization.
---
# Angular & Sanity Integration Rules
Jump to the section that matches your Angular version or integration task instead of reading this guide straight through.
## Table of Contents
- Setup and configuration
- Client setup (service pattern)
- Data fetching patterns
- Routing
- Portable Text rendering
- Image optimization
- Modern Angular features
- SSR and prerendering
- Visual Editing
- Error handling
## 1. Setup & Configuration
Use the official template `sanity-template-angular-clean` as a starting point. It provides a monorepo structure:
```
project/
├── angular-app/ # Angular 19+ frontend
└── studio/ # Sanity Studio
```
Install dependencies in the Angular app:
```bash
npm install @sanity/client @sanity/image-url @portabletext/to-html
```
Configure environment files for Sanity credentials:
```typescript
// environments/environment.ts
export const environment = {
production: false,
sanity: {
projectId: "your-project-id",
dataset: "production",
apiVersion: "2025-05-01",
},
};
```
```typescript
// environments/environment.production.ts
export const environment = {
production: true,
sanity: {
projectId: "your-project-id",
dataset: "production",
apiVersion: "2025-05-01",
},
};
```
> There is no Angular-specific Sanity SDK. Use `@sanity/client` directly, wrapped in an Angular service.
### TypeGen in a Monorepo
Sanity TypeGen generates TypeScript types from your schema and GROQ queries. In the Angular monorepo template, TypeGen runs from the Studio side but scans your Angular app's source files. Ensure `studio/sanity.cli.ts` points at the Angular app:
```typescript
// studio/sanity.cli.ts
import { defineCliConfig } from "sanity/cli";
export default defineCliConfig({
typegen: {
enabled: true,
path: "../angular-app/src/**/*.ts",
generates: "../angular-app/sanity.types.ts",
},
});
```
The remaining defaults (`overloadClientMethods: true`, `schema: "schema.json"`) work as-is. Include the generated types file in `angular-app/tsconfig.json` (usually covered by `"include": ["src/**/*.ts", "sanity.types.ts"]`). See `typegen.md` for the full TypeGen workflow, git strategy, and configuration options.
## 2. Client Setup (Service Pattern)
Create an injectable service wrapping `@sanity/client` and `@sanity/image-url`:
```typescript
import { Injectable } from '@angular/core'
import { createClient, type ClientReturn, type QueryParams, type SanityClient } from '@sanity/client'
import imageUrlBuilder, { type ImageUrlBuilder } from '@sanity/image-url'
import type { SanityImageSource } from '@sanity/image-url/lib/types/types'
import { environment } from '../environments/environment'
@Injectable({ providedIn: 'root' })
export class SanityService {
private client: SanityClient
private builder: ImageUrlBuilder
constructor() {
this.client = createClient({
projectId: environment.sanity.projectId,
dataset: environment.sanity.dataset,
apiVersion: environment.sanity.apiVersion,
useCdn: true,
})
this.builder = imageUrlBuilder(this.client)
}
// ClientReturn resolves TypeGen's declaration-merged overloads for defineQuery strings
fetch<Query extends string>(query: Query, params?: QueryParams): Promise<ClientReturn<Query>> {
return this.client.fetch(query, params)
}
getImageUrlBuilder(source: SanityImageSource) {
return this.builder.image(source)
}
}
```
For preview/draft content, create a second client instance with a token and `useCdn: false`. Never expose tokens in client-side bundles — use server-side rendering or a proxy endpoint for authenticated requests.
## 3. Data Fetching Patterns
### A. `resource` API (Angular 19+, Recommended)
The `resource` API works natively with promises and integrates with Angular signals:
```typescript
import { Component, input, resource, inject } from '@angular/core'
import { defineQuery } from 'groq'
import { SanityService } from '../sanity.service'
const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{
title, body, mainImage, publishedAt
}`)
@Component({
selector: 'app-post',
standalone: true,
template: `
@if (post.value(); as p) {
<h1>{{ p.title }}</h1>
<time>{{ p.publishedAt | date }}</time>
} @else if (post.isLoading()) {
<p>Loading…</p>
} @else if (post.error()) {
<p>Error loading post</p>
}
`,
})
export default class PostComponent {
slug = input.required<string>()
private sanity = inject(SanityService)
post = resource({
params: () => ({ slug: this.slug() }),
loader: ({ params }) => this.sanity.fetch(POST_QUERY, params),
})
}
```
The `resource` automatically re-fetches when `slug` changes and exposes `value()`, `isLoading()`, and `error()` signals.
> **TypeGen:** Wrapping queries in `defineQuery` enables Sanity TypeGen to infer return types automatically — no manual type imports needed. See `typegen.md` for the full workflow.
### B. `rxResource` (Observable-based)
For teams using RxJS patterns or needing operators like `retry` and `debounceTime`:
```typescript
import { Component, input, inject } from '@angular/core'
import { rxResource } from '@angular/core/rxjs-interop'
import { defineQuery } from 'groq'
import { from } from 'rxjs'
import { SanityService } from '../sanity.service'
const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`)
@Component({ /* ... */ })
export default class PostComponent {
slug = input.required<string>()
private sanity = inject(SanityService)
post = rxResource({
params: () => ({ slug: this.slug() }),
loader: ({ params }) => from(this.sanity.fetch(POST_QUERY, params)),
})
}
```
### C. `toSignal` (Angular 17–18)
For apps not yet on Angular 19, convert observables to signals:
```typescript
import { Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { defineQuery } from 'groq'
import { from } from 'rxjs'
import { SanityService } from '../sanity.service'
const POSTS_QUERY = defineQuery(`*[_type == "post"] | order(publishedAt desc)`)
@Component({ /* ... */ })
export class HomeComponent {
private sanity = inject(SanityService)
posts = toSignal(from(this.sanity.fetch(POSTS_QUERY)), { initialValue: [] })
}
```
> **Note:** `toSignal` does not re-fetch on parameter changes. For dynamic queries, use `resource` or `rxResource`.
### Choosing a pattern
| Pattern | Angular Version | Reactivity | Best For |
| ------------ | --------------- | --------------------------- | ----------------------------- |
| `resource` | 19+ | Signal-based, auto re-fetch | New projects, dynamic queries |
| `rxResource` | 19+ | RxJS + signals | Teams using RxJS operators |
| `toSignal` | 17+ | One-shot conversion | Static queries, legacy apps |
## 4. Routing
Use lazy-loaded routes with `withComponentInputBinding()` so route params bind directly to component inputs:
```typescript
// app.config.ts
import { provideRouter, withComponentInputBinding } from "@angular/router";
import { routes } from "./app.routes";
export const appConfig = {
providers: [provideRouter(routes, withComponentInputBinding())],
};
```
```typescript
// app.routes.ts
import { Routes } from "@angular/router";
export const routes: Routes = [
{
path: "",
loadComponent: () => import("./home/home.component"),
pathMatch: "full",
},
{
path: "post/:slug",
loadComponent: () => import("./post/post.component"),
},
];
```
With `withComponentInputBinding()`, the `:slug` route param is automatically bound to `slug = input.required<string>()` on the component — no need to inject `ActivatedRoute`.
## 5. Portable Text Rendering
### A. `@portabletext/to-html` with Angular Pipe (Recommended)
```typescript
import { Pipe, PipeTransform, inject } from '@angular/core'
import { toHTML, type PortableTextComponents } from '@portabletext/to-html'
import type { PortableTextBlock } from '@portabletext/types'
import { SanityService } from '../sanity.service'
@Pipe({ name: 'portableTextToHTML', standalone: true })
export class PortableTextToHTMLPipe implements PipeTransform {
private sanity = inject(SanityService)
private components: PortableTextComponents = {
types: {
image: ({ value }) => {
const url = this.sanity.getImageUrlBuilder(value).width(800).auto('format').url()
return `<img src="${url}" alt="${value.alt || ''}" loading="lazy" />`
},
},
marks: {
link: ({ children, value }) =>
`<a href="${value.href}" rel="noopener noreferrer">${children}</a>`,
},
}
transform(value: PortableTextBlock[] | undefined): string {
if (!value) return ''
return toHTML(value, { components: this.components })
}
}
```
Usage in templates:
```html
<div [innerHTML]="post.body | portableTextToHTML"></div>
```
### B. `@limitless-angular/sanity` (Community, Component-based)
For full Angular component control over each block type, the community library `@limitless-angular/sanity` provides a component-based Portable Text renderer. This is useful when you need Angular-specific interactivity within rich text blocks.
See `portable-text.md` for Portable Text schema design and serialization rules.
## 6. Image Optimization
Create a pipe wrapping `@sanity/image-url`:
```typescript
import { Pipe, PipeTransform, inject } from '@angular/core'
import type { SanityImageSource } from '@sanity/image-url/lib/types/types'
import { SanityService } from '../sanity.service'
@Pipe({ name: 'sanityImage', standalone: true })
export class SanityImagePipe implements PipeTransform {
private sanity = inject(SanityService)
transform(value: SanityImageSource | undefined, width?: number): string | null {
if (!value) return null
const builder = this.sanity.getImageUrlBuilder(value)
if (width) return builder.width(width).auto('format').url()
return builder.auto('format').url()
}
}
```
Combine with Angular's `NgOptimizedImage` for LCP images:
```html
<!-- Priority image with NgOptimizedImage -->
<img
[ngSrc]="post.mainImage | sanityImage: 1200"
width="1200"
height="630"
priority
/>
<!-- Lazy-loaded image -->
<img
[src]="post.mainImage | sanityImage: 600"
[alt]="post.mainImage.alt"
loading="lazy"
/>
```
❌ **Bad:** Fetching full-size images without width constraints.
```html
<img [src]="post.mainImage | sanityImage" />
```
✅ **Good:** Specifying width and using `auto('format')` for WebP/AVIF delivery.
```html
<img [src]="post.mainImage | sanityImage: 800" loading="lazy" />
```
### LQIP with `NgOptimizedImage`
Sanity provides a base64 LQIP (Low Quality Image Placeholder) per image asset — but you must query it explicitly:
```groq
mainImage {
// @sanity/image-url needs these to build URLs with hotspot/crop support
asset,
hotspot,
crop,
alt,
// NgOptimizedImage needs these for placeholder and layout
"lqip": asset->metadata.lqip,
"width": asset->metadata.dimensions.width,
"height": asset->metadata.dimensions.height
}
```
Feed the LQIP directly into `NgOptimizedImage`'s `placeholder` attribute:
```html
<img
[ngSrc]="post.mainImage | sanityImage: 1200"
[width]="post.mainImage.width"
[height]="post.mainImage.height"
[placeholder]="post.mainImage.lqip"
[alt]="post.mainImage.alt"
priority
/>
```
Angular applies a CSS blur to the LQIP and crossfades to the full image on load. No extra libraries needed.
> **Note:** LQIP strings are small (~200 bytes) so they're safe to inline in SSR HTML and `TransferState`. See `image.md` for the full image query patterns.
See `image.md` for image field schema patterns and hotspot/crop configuration.
## 7. Modern Angular Features
When building with Sanity, leverage these Angular 19+ features:
- **Standalone components** — Default in Angular 19. No `NgModule` boilerplate needed.
- **Signals and `resource`** — Preferred over RxJS for data fetching. Simpler, less boilerplate.
- **New control flow** — Use `@if`, `@for`, `@switch` with `@empty` for cleaner templates:
```html
@for (post of posts.value(); track post._id) {
<app-post-card [post]="post" />
} @empty {
<p>No posts found.</p>
}
```
- **`@defer` blocks** — Lazy-load below-fold content:
```html
@defer (on viewport) {
<app-comments [postId]="post._id" />
} @placeholder {
<p>Scroll to see comments…</p>
}
```
- **`inject()` function** — Preferred over constructor injection for cleaner code.
- **Zoneless change detection** — Experimental in Angular 19. Works well with signals-based data fetching since signals automatically notify the framework of changes.
## 8. SSR & Prerendering
Angular 17+ includes built-in SSR support (replacing Angular Universal):
```typescript
// app.config.server.ts
import { provideClientHydration } from "@angular/platform-browser";
import { provideServerRendering } from "@angular/platform-server";
export const serverConfig = {
providers: [provideServerRendering(), provideClientHydration()],
};
```
Key considerations for Sanity + Angular SSR:
| Feature | Details |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Hydration** | `provideClientHydration()` preserves server-rendered DOM. The client reuses it instead of re-rendering. |
| **HTTP Transfer Cache** | Only works with Angular's `HttpClient`. Since `@sanity/client` uses its own HTTP transport, use `TransferState` manually (see below). |
| **Prerendering** | Use `getPrerenderParams` in route config to generate static pages at build time. |
### Transfer State for `@sanity/client`
Angular's built-in HTTP Transfer Cache does not cover `@sanity/client` requests. Without manual transfer, the client re-fetches every query during hydration. Add `TransferState` to the service from Section 2:
```typescript
+ async function hashQuery(query: string, params?: QueryParams): Promise<string> {
+ const input = query + JSON.stringify(params ?? {})
+ const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input))
+ return Array.from(new Uint8Array(buffer), b => b.toString(16).padStart(2, '0')).join('')
+ }
import { Injectable, inject } from '@angular/core'
+ import { isPlatformBrowser, isPlatformServer } from '@angular/common'
+ import { PLATFORM_ID, makeStateKey, TransferState } from '@angular/core'
import { createClient, type ClientReturn, type QueryParams, type SanityClient } from '@sanity/client'
export class SanityService {
private client: SanityClient
+ private transferState = inject(TransferState)
+ private platformId = inject(PLATFORM_ID)
async fetch<Query extends string>(query: Query, params?: QueryParams): Promise<ClientReturn<Query>> {
+ const key = makeStateKey<ClientReturn<Query>>(await hashQuery(query, params))
+
+ if (isPlatformBrowser(this.platformId)) {
+ const cached = this.transferState.get(key, null)
+ if (cached !== null) {
+ this.transferState.remove(key)
+ return cached
+ }
+ }
+
const result = await this.client.fetch(query, params)
+
+ if (isPlatformServer(this.platformId)) {
+ this.transferState.set(key, result)
+ }
+
return result
}
}
```
The `hashQuery` helper keeps `TransferState` keys short (SHA-256 hex) instead of embedding raw GROQ strings in the serialized HTML.
Prerendering dynamic routes:
```typescript
// app.routes.server.ts
import { RenderMode, ServerRoute } from "@angular/ssr";
export const serverRoutes: ServerRoute[] = [
{
path: "post/:slug",
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
// Fetch all slugs from Sanity at build time
const client = createClient({
projectId: "...",
dataset: "...",
apiVersion: "...",
useCdn: true,
});
const slugs = await client.fetch<string[]>(
`*[_type == "post"].slug.current`,
);
return slugs.map((slug) => ({ slug }));
},
},
{ path: "**", renderMode: RenderMode.Server },
];
```
❌ **Bad:** Using `isPlatformBrowser()` in templates to conditionally render content — causes hydration mismatch.
✅ **Good:** Using `@defer` or `afterNextRender()` for browser-only code.
## 9. Visual Editing
> **Important:** Angular does not have official Sanity Visual Editing support. There is no `@sanity/visual-editing` integration, no Stega encoding, and no click-to-edit overlay for Angular applications. This is unlike Next.js, Nuxt, and SvelteKit which have first-party support.
### Preview Mode (Basic)
For draft content preview, create a separate preview client with an API token:
```typescript
@Injectable({ providedIn: 'root' })
export class SanityService {
private client: SanityClient
private previewClient: SanityClient
constructor() {
this.client = createClient({
projectId: environment.sanity.projectId,
dataset: environment.sanity.dataset,
apiVersion: environment.sanity.apiVersion,
useCdn: true,
})
this.previewClient = this.client.withConfig({
useCdn: false,
token: environment.sanity.previewToken, // Server-side only!
perspective: 'drafts',
})
}
fetch<Query extends string>(query: Query, params?: QueryParams, preview = false): Promise<ClientReturn<Query>> {
const client = preview ? this.previewClient : this.client
return client.fetch(query, params)
}
}
```
> **Security:** Never expose the preview token in client-side bundles. Use this pattern only with SSR where the token stays on the server, or proxy preview requests through a backend API.
### Community Visual Editing
The community library `@limitless-angular/sanity` provides experimental Visual Editing support for Angular, including overlay click-to-edit functionality. Check its documentation for current status and limitations.
## 10. Error Handling
Common errors when integrating Angular with Sanity:
| Error | Cause | Solution |
| ------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | Invalid or missing API token | Verify token in [Sanity Manage](https://www.sanity.io/manage). Ensure it has correct permissions. |
| `403 Forbidden` | CORS origin not allowed | Add your Angular dev/production URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage). |
| `422 Invalid query` | GROQ syntax error | Test queries in Vision plugin or Sanity's GROQ playground. See `groq.md`. |
| Hydration mismatch | Conditional rendering based on platform | Use `@defer` or `afterNextRender()` instead of `isPlatformBrowser()` checks. |
| Empty response | Missing dataset or wrong `apiVersion` | Verify environment config. Use a date-based `apiVersion` (e.g., `'2025-05-01'`). |
| Images not loading | Missing `@sanity/image-url` setup | Ensure `getImageUrlBuilder` is called with a valid image reference. See `image.md`. |
For GROQ query patterns and best practices, see `groq.md`. For schema design, see `schema.md`.
@@ -0,0 +1,462 @@
---
title: Sanity App SDK
description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices.
---
# Sanity App SDK
Build custom React applications that interact with Sanity content in real-time.
## Tech Stack
- **Framework:** React 19+, TypeScript
- **Packages:** `@sanity/sdk`, `@sanity/sdk-react`
- **Optional UI:** `@sanity/ui`, `styled-components`
- **Runtime:** Node.js 20+
## Commands
```bash
# Basic quickstart
npx sanity@latest init --template app-quickstart --organization <your-org-id> --output-path . --typescript --skip-mcp
# With Sanity UI components
npx sanity@latest init --template app-sanity-ui --organization <your-org-id> --output-path . --typescript --skip-mcp
# Start development server
npm run dev
# Deploy to Sanity
npx sanity@latest deploy
# Install Sanity UI
npm install @sanity/ui styled-components
```
## Project Structure
```
my-app/
├── sanity.cli.ts # CLI config (org ID, entry point)
├── src/
│ ├── App.tsx # Root component with SanityApp provider
│ ├── App.css # Global styles
│ └── components/ # Your components
├── package.json
└── tsconfig.json
```
## Boundaries
- **Always:** Wrap data-fetching components in `<Suspense>`, use `documentId` as React `key`, read/write directly to Content Lake (not local state)
- **Always:** Use `useDocuments` for lists, `useDocumentProjection` for display, `useDocument` + `useEditDocument` for editing
- **Ask first:** Before using `useQuery` with raw GROQ (prefer `useDocuments` + `useDocumentProjection`)
- **Ask first:** Before adding multiple data-fetching hooks in a single component
- **Never:** Use `useState` for form values that should sync with Content Lake
- **Never:** Use array index as React `key` for document lists (breaks real-time updates)
- **Never:** Forget the `fallback` prop on `<SanityApp>` and `<Suspense>` boundaries
---
## Configuration
### CLI Config (`sanity.cli.ts`)
```typescript
import { defineCliConfig } from "sanity/cli";
export default defineCliConfig({
app: {
organizationId: "your-org-id",
entry: "./src/App.tsx",
},
});
```
### App Root (`src/App.tsx`)
```typescript
import { SanityApp, type SanityConfig } from '@sanity/sdk-react'
export default function App() {
const config: SanityConfig[] = [
{
projectId: 'your-project-id',
dataset: 'production',
},
]
return (
<SanityApp config={config} fallback={<div>Loading...</div>}>
<YourComponents />
</SanityApp>
)
}
```
### With Sanity UI
```typescript
import { SanityApp, type SanityConfig } from '@sanity/sdk-react'
import { ThemeProvider } from '@sanity/ui'
import { buildTheme } from '@sanity/ui/theme'
const theme = buildTheme()
export default function App() {
const config: SanityConfig[] = [
{ projectId: 'your-project-id', dataset: 'production' },
]
return (
<ThemeProvider theme={theme}>
<SanityApp config={config} fallback={<div>Loading...</div>}>
<YourComponents />
</SanityApp>
</ThemeProvider>
)
}
```
### Environment Variables
Prefix with `SANITY_APP_` for automatic bundling:
```bash
SANITY_APP_PROJECT_ID=abc123
SANITY_APP_DATASET=production
```
Access: `process.env.SANITY_APP_PROJECT_ID`
---
## Document Handles
Lightweight references to documents. Fetch handles first, then load content as needed.
```typescript
interface DocumentHandle {
documentId: string;
documentType: string;
projectId?: string;
dataset?: string;
}
```
### Creating Handles
```typescript
// Best: From useDocuments hook
const { data: handles } = useDocuments({ documentType: 'article' })
// Good: With helper (preserves literal types for TypeGen)
import { createDocumentHandle } from '@sanity/sdk'
const handle = createDocumentHandle({
documentId: 'my-doc-id',
documentType: 'article',
})
// Good: With as const (preserves literal types)
const handle = {
documentId: 'my-doc-id',
documentType: 'article',
} as const
```
---
## Hook Selection
| Hook | Use Case | Returns |
| ----------------------- | ------------------------------------ | ---------------------- |
| `useDocuments` | List of documents (infinite scroll) | Document handles |
| `usePaginatedDocuments` | Paginated lists with page controls | Document handles |
| `useDocument` | Single document, real-time editing | Full document or field |
| `useDocumentProjection` | Specific fields, display only | Projected data |
| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results |
---
## Code Patterns
### Fetching a Document List
```typescript
// Good: Fetch handles, render items with Suspense
import { Suspense } from 'react'
import { useDocuments } from '@sanity/sdk-react'
function ArticleList() {
const { data, hasMore, loadMore, isPending } = useDocuments({
documentType: 'article',
batchSize: 10,
orderings: [{ field: '_updatedAt', direction: 'desc' }],
})
return (
<>
<ul>
{data.map((handle) => (
<Suspense key={handle.documentId} fallback={<li>Loading...</li>}>
<ArticleItem {...handle} />
</Suspense>
))}
</ul>
{hasMore && (
<button onClick={loadMore} disabled={isPending}>
Load More
</button>
)}
</>
)
}
```
```typescript
// Bad: Over-fetching with raw GROQ, no pagination
function BadArticleList() {
const { data } = useQuery(`*[_type == "article"]`)
return data?.map((doc, i) => <li key={i}>{doc.title}</li>)
}
```
### Projecting Content from a Handle
```typescript
// Good: Project only needed fields
import { useDocumentProjection, type DocumentHandle } from '@sanity/sdk-react'
function ArticleItem(handle: DocumentHandle) {
const { data } = useDocumentProjection({
...handle,
projection: `{
title,
"authorName": author->name,
"imageUrl": image.asset->url
}`,
})
if (!data) return null
return (
<li>
<h2>{data.title}</h2>
<p>By {data.authorName}</p>
</li>
)
}
```
### Real-time Editing
```typescript
// Good: Read and write directly to Content Lake
import { useDocument, useEditDocument, type DocumentHandle } from '@sanity/sdk-react'
function TitleInput(handle: DocumentHandle) {
const { data: title } = useDocument({ ...handle, path: 'title' })
const editTitle = useEditDocument({ ...handle, path: 'title' })
return (
<input
type="text"
value={title ?? ''}
onChange={(e) => editTitle(e.currentTarget.value)}
/>
)
}
```
```typescript
// Bad: Local state with submit button - causes stale data
function BadTitleForm(handle: DocumentHandle) {
const [value, setValue] = useState('')
const editTitle = useEditDocument({ ...handle, path: 'title' })
function handleSubmit(e: FormEvent) {
e.preventDefault()
editTitle(value) // Only writes on submit!
}
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button type="submit">Save</button>
</form>
)
}
```
### Document Actions
```typescript
import {
useApplyDocumentActions,
publishDocument,
unpublishDocument,
deleteDocument,
} from '@sanity/sdk-react'
function DocumentActions({ handle }: { handle: DocumentHandle }) {
const apply = useApplyDocumentActions()
return (
<div>
<button onClick={() => apply(publishDocument(handle))}>Publish</button>
<button onClick={() => apply(unpublishDocument(handle))}>Unpublish</button>
<button onClick={() => apply(deleteDocument(handle))}>Delete</button>
</div>
)
}
```
---
## Suspense Patterns
The App SDK uses React Suspense. Every data-fetching component must be wrapped.
### One Hook Per Component
```typescript
// Good: Separate fetchers into separate components
function EventsAndVenues() {
return (
<>
<Suspense fallback="Loading events...">
<EventsList />
</Suspense>
<Suspense fallback="Loading venues...">
<VenuesList />
</Suspense>
</>
)
}
function EventsList() {
const { data } = useDocuments({ documentType: 'event' })
return <List items={data} />
}
function VenuesList() {
const { data } = useDocuments({ documentType: 'venue' })
return <List items={data} />
}
```
```typescript
// Bad: Multiple fetchers in one component
function BadComponent() {
const { data: events } = useDocuments({ documentType: "event" });
const { data: venues } = useDocuments({ documentType: "venue" });
// Both trigger Suspense together, causing unnecessary re-renders
}
```
### Prevent Layout Shift
```typescript
// Good: Fallback matches final component dimensions
const BUTTON_TEXT = 'Open in Studio'
export function OpenInStudio({ handle }: { handle: DocumentHandle }) {
return (
<Suspense fallback={<Button text={BUTTON_TEXT} disabled />}>
<OpenInStudioButton handle={handle} />
</Suspense>
)
}
function OpenInStudioButton({ handle }: { handle: DocumentHandle }) {
const { navigateToStudioDocument } = useNavigateToStudioDocument(handle)
return <Button onClick={navigateToStudioDocument} text={BUTTON_TEXT} />
}
```
---
## Event Handling
```typescript
import { DocumentEvent, useDocumentEvent } from "@sanity/sdk-react";
function DocumentWatcher(handle: DocumentHandle) {
useDocumentEvent({
...handle,
onEvent: (event) => {
switch (event.type) {
case "edited":
console.log("Edited:", event.documentId);
break;
case "published":
console.log("Published:", event.documentId);
break;
case "deleted":
console.log("Deleted:", event.documentId);
break;
}
},
});
return null;
}
```
---
## Multi-Project Apps
```typescript
const config: SanityConfig[] = [
{ projectId: "project-1", dataset: "production" },
{ projectId: "project-2", dataset: "staging" },
];
// Handles include project/dataset info
const handle: DocumentHandle = {
documentId: "doc-123",
documentType: "article",
projectId: "project-1",
dataset: "production",
};
```
---
## Lazy Loading with Refs
```typescript
function LazyContent(handle: DocumentHandle) {
const ref = useRef(null)
const { data } = useDocumentProjection({
...handle,
ref, // Only loads when element enters viewport
projection: '{ title, body }',
})
return <div ref={ref}>{data?.title}</div>
}
```
---
## What's NOT Included
The App SDK provides hooks and data stores. You bring:
- UI components (use Sanity UI or your own)
- Router
- Form validation
- Schema validation
---
## Troubleshooting
| Issue | Solution |
| ----------------- | ----------------------------------------------------- |
| Safari dev issues | Use Chrome or Firefox during development |
| Port 3333 in use | `npm run dev -- --port 3334` |
| Auth errors | `npx sanity@latest logout && npx sanity@latest login` |
@@ -0,0 +1,174 @@
---
title: Astro & Sanity Integration Rules
description: Integration guide for Astro, including @sanity/astro, visual editing, and data fetching.
---
# Astro & Sanity Integration Rules
## 1. Setup & Configuration
### Scaffold a new Astro app
```bash
npm create astro@latest my-app -- --template with-tailwindcss --install --git --yes
cd my-app
```
`--yes` accepts defaults non-interactively. `--install` runs `npm install` for you, `--git` initializes a repo.
### Installation
Add the `@sanity/astro` integration and the renderer/helper packages used by the examples below.
```bash
npx astro add @sanity/astro
npm install astro-portabletext @sanity/image-url groq
```
`@sanity/astro` provides the `sanity:client` virtual module. `astro-portabletext` renders Portable Text. `@sanity/image-url` builds image URLs. `groq` exports `defineQuery` for typed queries.
### Configuration (`astro.config.mjs`)
Use the official `@sanity/astro` integration. `astro.config.mjs` runs at config time before Astro's env loading, so `import.meta.env.PUBLIC_*` is not available there — use Vite's `loadEnv` to read the same `PUBLIC_` variables your pages will use.
```javascript
import sanity from "@sanity/astro";
import { defineConfig } from "astro/config";
import { loadEnv } from "vite";
const { PUBLIC_SANITY_PROJECT_ID, PUBLIC_SANITY_DATASET } = loadEnv(
process.env.NODE_ENV ?? "development",
process.cwd(),
"",
);
export default defineConfig({
integrations: [
sanity({
projectId: PUBLIC_SANITY_PROJECT_ID,
dataset: PUBLIC_SANITY_DATASET,
useCdn: false, // False for static builds
studioBasePath: "/admin", // Optional — only if embedding the Studio
}),
],
});
```
Inside `.astro` files and components you can keep using `import.meta.env.PUBLIC_SANITY_*` directly; the `loadEnv` shim above is config-only.
### Client Type Safety
Enable types in `tsconfig.json`.
```json
{
"compilerOptions": {
"types": ["@sanity/astro/module"]
}
}
```
## 2. Data Fetching
### Basic Fetching
Use `sanityClient` from `sanity:client` in the frontmatter of your `.astro` files.
```astro
---
import { sanityClient } from "sanity:client";
import { defineQuery } from "groq";
const POSTS_QUERY = defineQuery(`*[_type == "post"]{title, slug}`);
const posts = await sanityClient.fetch(POSTS_QUERY);
---
<ul>
{posts.map(post => <li>{post.title}</li>)}
</ul>
```
### Helper Functions
It's best practice to abstract queries into a utility file (e.g., `src/utils/sanity.ts`).
```typescript
import { defineQuery } from "groq";
import { sanityClient } from "sanity:client";
const POSTS_QUERY = defineQuery(`*[_type == "post" && defined(slug.current)]`);
export async function getPosts() {
return await sanityClient.fetch(POSTS_QUERY);
}
```
### Dynamic Routes (`[slug].astro`)
Astro hoists `getStaticPaths()` into a separate module context. Module-scope `const` declarations in the frontmatter are NOT accessible inside it — referencing them throws `ReferenceError: <NAME> is not defined` at request time. Define queries used by `getStaticPaths` inside the function, or import them from a utility module.
```astro
---
import { sanityClient } from "sanity:client";
import { defineQuery } from "groq";
import { PortableText } from "astro-portabletext";
// Module-scope queries are fine for module-scope code…
const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{ title, body }`);
// …but anything used inside getStaticPaths must live inside it.
export async function getStaticPaths() {
const SLUGS_QUERY = defineQuery(
`*[_type == "post" && defined(slug.current)]{ "params": { "slug": slug.current } }`
);
return await sanityClient.fetch(SLUGS_QUERY);
}
const { slug } = Astro.params;
const post = await sanityClient.fetch(POST_QUERY, { slug });
---
<article>
<h1>{post?.title}</h1>
{post?.body && <PortableText value={post.body} />}
</article>
```
## 3. Portable Text
Use `astro-portabletext` for rendering rich text.
```astro
---
import { PortableText } from "astro-portabletext";
const { body } = Astro.props;
---
<div class="prose">
<PortableText value={body} />
</div>
```
## 4. Image Handling
Use `@sanity/image-url` to generate optimized image URLs.
```typescript
import imageUrlBuilder from "@sanity/image-url";
import { sanityClient } from "sanity:client";
const builder = imageUrlBuilder(sanityClient);
export function urlFor(source) {
return builder.image(source);
}
```
## 5. Visual Editing (Live Preview)
Astro handles visual editing slightly differently depending on if you are using Hybrid or Static mode.
### Setup
Ensure `stega` is enabled in your client configuration if you want clickable overlays.
For real-time updates in the presentation tool, you typically need a React component wrapper (since Astro components don't re-render on the client) or use the View Transitions API with a loader.
_Note: The `@sanity/astro` integration is evolving. Check the latest docs for "Visual Editing" support._
@@ -0,0 +1,280 @@
---
title: Sanity Blueprints
description: Rules for Sanity Blueprints, the Infrastructure as Code solution for managing Sanity resources declaratively. Covers blueprint files, stacks and scope, the plan/deploy workflow, deletion policies, error recovery, and CI deployment.
---
# Sanity Blueprints
Sanity's Infrastructure as Code (IaC) solution. Define resources declaratively in `sanity.blueprint.ts`, track the file in version control, preview changes with `plan`, apply them with `deploy`. Blueprints is the only way to deploy Sanity Functions.
The CLI is built for unattended use: every command accepts `--json`, all IDs can be supplied via flags or environment variables, and `--help` on any command is the authoritative reference. Always run via `npx sanity@latest blueprints <command>`.
## Mental Model
```
Blueprint file (code, intent) → Stack (deployed state) → Resources (real infrastructure)
```
| Concept | What it is |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Blueprint file** | `sanity.blueprint.ts`, the declarative manifest of desired resources. The source of truth: if it's not in the file, it's not deployed |
| **Resource** | One managed thing (function, CORS origin, webhook, role, robot token, dataset). Identified by a unique `name` |
| **Definer** | Typed helper from `@sanity/blueprints` (e.g. `defineCorsOrigin`) that declares a resource and validates input as you write |
| **Stack** | The deployed counterpart of the file: the live resources on Sanity's side. One file can deploy to many Stacks (e.g. `staging`, `production`) |
| **Config file** | `.sanity/blueprint.config.json`, links the local file to a Stack and records scope. Gitignored by `init` automatically; not secret |
| **Scope** | `project` or `organization`. Determines which resource types a Stack can manage |
### Key behaviors
- Resources are matched **by `name`** between file and Stack. `plan` buckets every resource as create, update, no-op, or destroy.
- **There is no rename.** Changing a resource's `name` destroys the old resource and creates a new one under the new name.
- **Removal is destruction.** Deleting a resource from the file destroys it on the next deploy (subject to its deletion policy). `blueprints destroy` is equivalent to deploying an empty file.
- Deploys apply in dependency order. On partial failure, completed changes are rolled back best-effort in reverse order. Rollback is **not atomic**; some actions may be irreversible.
## Three different "deploy" commands
Do not confuse these:
| Command | What it deploys |
| -------------------------- | --------------------------------------------------------- |
| `sanity blueprints deploy` | Infrastructure resources defined in `sanity.blueprint.ts` |
| `sanity schema deploy` | Studio schema to Content Lake (for MCP and editor access) |
| `sanity deploy` | The Studio application to Sanity hosting |
## The Blueprint File
`sanity.blueprint.ts` default-exports a `defineBlueprint` call. TypeScript is idiomatic (definers validate inline and provide editor help); `.js` and hand-written `.json` also work but new files should use TypeScript.
```typescript
import {
defineBlueprint,
defineCorsOrigin,
defineDocumentFunction,
} from "@sanity/blueprints";
export default defineBlueprint({
values: {
corsOrigin: "https://studio.example.com",
},
resources: [
defineCorsOrigin({
name: "studio-cors",
origin: "$.values.corsOrigin",
}),
defineDocumentFunction({
name: "first-published",
event: {
on: ["create", "update"],
filter: "_type == 'post' && !defined(firstPublished)",
},
}),
],
});
```
- `resources` is an array of definer outputs. Every resource requires a unique `name`.
- `values` holds reusable file-level constants (always strings), referenced as `$.values.<key>`.
- Resource-type-specific fields are documented in the typed reference: https://reference.sanity.io/_sanity/blueprints/
### Discovering available resources
The definers exported by `@sanity/blueprints` are the canonical list of what Blueprints can manage. Discover them by inspecting the package's exports (every `define*` function is a resource definer) and reading their TSDoc for fields, defaults, and examples, or browse the typed reference at https://reference.sanity.io/_sanity/blueprints/. Do not rely on a memorized list; new definers ship additively.
When the Stack is organization-scoped, project-contained resources (CORS origins, webhooks, datasets, functions) must set their `project` field explicitly.
For the semantics of an individual resource type (what a CORS origin, webhook, or role does and which values make sense), consult that feature's own Sanity documentation. This reference covers how Blueprints manages resources, not the resources themselves.
## References and the Resource Graph
Resources reference values and each other with `$` string paths:
| Syntax | Resolves | When |
| ----------------------- | ---------------------------------------------- | ----------------------------------------- |
| `$.values.<key>` | A constant from the `values` block | At file evaluation |
| `$.resources.<name>` | Another resource in the file | At deploy time; creates a dependency edge |
| `$.resources.<name>.id` | The generated ID of that resource, as a string | At deploy time |
Blueprints builds the dependency graph from these references and orders work so dependencies are created first. Unresolved references and cycles fail validation before anything is deployed. To force ordering without a data reference, use `lifecycle: {dependsOn: '$.resources.<name>'}`.
### Referencing an existing project
The recommended pattern is an environment variable surfaced through `values`:
```typescript
import "dotenv/config";
// the CLI does not read .env files itself
export default defineBlueprint({
values: {
projectId: process.env.SANITY_PROJECT_ID ?? "",
},
resources: [
defineCorsOrigin({
name: "app-cors",
project: "$.values.projectId",
origin: "https://www.example.com",
}),
],
});
```
## Deletion Policies
Set `lifecycle.deletionPolicy` on a resource to control what removal means:
| Policy | Removed from file | Stack destroyed | Notes |
| --------- | -------------------- | ----------------------------- | -------------------------------------------- |
| `allow` | Resource destroyed | Resource destroyed | Default for stateless resources |
| `retain` | **Deployment fails** | Resource detached, kept alive | Default for stateful resources like datasets |
| `replace` | Resource destroyed | Resource destroyed | Updates become destroy + recreate |
| `protect` | **Deployment fails** | **Deployment fails** | Resource is never updated or destroyed |
Use `protect` for production-critical stateful resources:
```typescript
defineDataset({
name: "production",
lifecycle: { deletionPolicy: "protect" },
});
```
A deletion-policy violation is a pre-deploy validation failure, not a partial deploy: fix the file (or consciously change the policy) and re-run `plan`.
## Stacks, Environments, and Scope
One blueprint file deploys to many Stacks. The `--stack <name-or-id>` flag (on `deploy`, `plan`, `info`, `logs`) selects the target; without it, the Stack recorded in `.sanity/blueprint.config.json` is used.
- **Only `init` creates Stacks.** `--stack` never creates a Stack on a miss, so CI cannot accidentally provision infrastructure. Re-run `blueprints init .` to add another Stack for the same file.
- Accounts are currently limited to **three Stacks per project scope**.
- For per-environment differences, read an environment variable inside the file (e.g. `process.env.SANITY_ENV`) and deploy with the env var and `--stack` together. `SANITY_ENV` is a convention your file reads, not a CLI feature.
### Project vs organization scope
| | Project scope | Organization scope |
| ------------------------- | ------------- | ---------------------------------------------------------------- |
| Stored in config as | `projectId` | `organizationId` |
| Resources default to | The project | Nothing; each project-contained resource must name its `project` |
| Org-scoped resource types | Not available | Available |
Organization scope is recommended for anything beyond a single-project experiment. Convert an existing Stack with `blueprints promote` (safe, idempotent, additive, but **one-way**; requires `organization-update` admin permission).
## CLI Commands
```bash
npx sanity@latest blueprints <command>
```
| Command | Purpose |
| ------------------- | -------------------------------------------------------------------------------------------- |
| `init [dir]` | Create a blueprint file and provision a remote Stack. The only command that creates Stacks |
| `plan` | Preview the diff against the Stack. Read-only, always safe |
| `deploy` | Apply the file to the Stack (`-m` message, `--no-wait` to queue and return) |
| `info` | Show Stack status and deployed resources. Remote read; works without local files |
| `stacks` | List Stacks (`--include-projects` with `--organization-id` for org-wide audit) |
| `config` | View or edit `.sanity/blueprint.config.json` (`--edit` with ID flags for scripting) |
| `logs` | Deployment logs (`--watch`, `--limit 1-500`, `--since`/`--before` ISO timestamps) |
| `promote` | Convert a Stack from project to organization scope. One-way (`--force` to skip confirmation) |
| `destroy` | Destroy the Stack and its resources (`--force` to skip confirmation). Local files remain |
| `doctor` | Diagnose local/remote configuration issues (`--fix` to resolve interactively) |
| `mint-deploy-token` | Mint a long-lived robot token for CI deploys (`--print` for shell pipelines) |
Running `npx sanity@latest blueprints --help` will show the latest available commands.
Use `npx sanity@latest blueprints <command> --help` to see options and examples for a specific command.
Function scaffolding, local testing, secrets, and function logs live under `npx sanity@latest functions <command>` (`add`, `dev`, `test`, `env add|list|remove`, `logs`). See the `functions` reference.
### Standard workflow
```bash
npx sanity@latest blueprints init . # once: create file + Stack
# edit sanity.blueprint.ts
npx sanity@latest blueprints plan # preview the diff, catch validation errors safely
npx sanity@latest blueprints deploy # apply
npx sanity@latest blueprints info # verify deployed state
```
Always run `plan` before `deploy`. It is read-only and surfaces validation errors, reference problems, scope mismatches, and deletion-policy violations without touching anything.
### Exit codes
`deploy` and `destroy` return: **`0`** succeeded, **`2`** failed, **`75`** accepted but completion could not be confirmed.
**✅ Correct — treat exit 75 as unknown, then verify:**
```bash
npx sanity@latest blueprints deploy || status=$?
if [ "${status:-0}" -eq 75 ]; then
npx sanity@latest blueprints info --json # confirm actual Stack state
fi
```
**❌ Incorrect — treating any nonzero exit as failure and retrying.** Exit 75 means the deployment was accepted; blindly redeploying can queue duplicate operations (deploy also refuses to start while another operation is in progress).
### Scope resolution and environment variables
For `organizationId`, `projectId`, and `stackId`, the first source that resolves wins: CLI flags → environment variables → the blueprint file module → the local config file.
| Variable | Purpose |
| ---------------------------------------------- | ---------------------------------------------------------- |
| `SANITY_AUTH_TOKEN` | Auth token (in CI, the minted deploy token) |
| `SANITY_ORGANIZATION_ID` / `SANITY_PROJECT_ID` | Scope |
| `SANITY_BLUEPRINT_STACK_ID` | Target Stack |
| `SANITY_BLUEPRINT_PATH` | Path to the blueprint file or its directory |
| `SANITY_ASSET_TIMEOUT` | Seconds to wait for resource asset processing (default 60) |
The CLI does not load `.env` files. Export variables in the shell or import `dotenv/config` inside the blueprint file.
## Errors and Recovery
Errors include a name, a human-readable message, and the **resource path**, which maps to a specific resource in your file. Five failure kinds:
| Failure | Caught | What to do |
| --------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------- |
| Validation error (missing/invalid field, malformed file) | Before deploy | Fix the file at the reported resource path |
| Unresolved reference or dependency cycle | Before deploy | Fix the `$.resources.<name>` reference or break the cycle |
| Scope mismatch (org-scoped resource on project Stack, or no resolvable `project`) | Before deploy | Set the resource's `project` field, or `promote` the Stack |
| Deletion-policy violation (change would remove a `retain`/`protect` resource) | Before deploy | Restore the resource, or deliberately relax its policy |
| Execution failure (underlying service rejected the change) | During deploy | Read `blueprints logs`, fix the cause, redeploy |
Recovery procedure after any failure:
1. Read the error message and resource path (`blueprints logs` re-reads failure output).
2. Fix the blueprint file (or the external condition).
3. Run `blueprints plan` to confirm the corrected diff.
4. Run `blueprints deploy` again.
Retrying without changing anything only makes sense for transient execution failures; the first four failure kinds are deterministic and will fail identically until the file changes. If local and remote state seem inconsistent, run `blueprints doctor` (and `doctor --fix`) before attempting anything destructive. After a partial-deploy rollback, `blueprints info` shows what is actually live; resources owned by other Stacks are never touched by rollback.
## CI/CD
Recommended workflow: `plan` on pull requests, `deploy` on merge to main. Official GitHub Actions: https://github.com/sanity-io/blueprints-actions
```bash
# one-time, by a human with access:
npx sanity@latest blueprints mint-deploy-token --label "ci-deploy" --print
```
The minted robot token has exactly the role needed to plan, deploy, and destroy (`blueprints-deployer` for project scope, `blueprints-deployer-robot` for organization scope) and is revocable under Robots in Sanity Manage. Store it as a CI secret, then in the pipeline:
```bash
export SANITY_AUTH_TOKEN=<the minted token>
export SANITY_ORGANIZATION_ID=<orgId> # or SANITY_PROJECT_ID
export SANITY_BLUEPRINT_STACK_ID=<stackId>
npx sanity@latest blueprints deploy --json --message "CI deploy"
```
**✅ Correct — secrets via server-side function env vars:**
```bash
npx sanity@latest functions env add my-function API_KEY sk-...
```
**❌ Incorrect — secrets in the blueprint file's `env` block, which is committed to git:**
```typescript
defineDocumentFunction({
name: "my-function",
env: { API_KEY: "sk-..." }, // committed to version control!
});
```
@@ -0,0 +1,663 @@
---
title: Sanity Functions
description: Rules for Sanity Functions — serverless event handlers that react to content changes in Sanity's Content Lake. Covers blueprint configuration, handler patterns, testing, deployment, and recursion control.
---
# Sanity Functions
Serverless event handlers hosted on Sanity's infrastructure, configured via **Blueprints** and triggered by document lifecycle events.
> Always use `npx sanity@latest` so CLI and runtime versions stay current.
## When to use
- Set computed/derived fields (timestamps, slugs, summaries)
- Enrich or validate content on publish
- Trigger external services (CDN purge, deploy hooks, notifications)
- Automate workflows (translation, tagging, cross-posting)
- Sync content to external systems
- Invoke Agent Actions in response to content events
## When NOT to use
- Logic needs >900s execution or >200MB bundle — use an external worker
- High-throughput bulk operations that exceed rate limits (200/fn/30s, 4000/project/30s)
- A simple POST to an external URL on publish with no document data shaping — use a webhook
- Client-side or UI-driven logic (validation, conditional fields) — belongs in Studio schema config
## Requirements
| Dependency | Version |
| :------------------- | :--------------------------------------- |
| Node.js | v24.x (matches deployed runtime) |
| Sanity CLI | v4.12.0+ |
| `@sanity/blueprints` | Latest |
| `@sanity/functions` | Latest |
| `@sanity/client` | v7.12.0+ (includes recursion protection) |
## Project Structure
Organize functions alongside your Sanity project, one level above the Studio directory:
```
my-project/
├── studio/
├── next-app/
├── functions/
│ ├── my-function/
│ │ ├── index.ts # Handler code (entry point)
│ │ └── package.json # (optional) function-level dependencies
│ └── another-function/
│ └── index.ts
├── sanity.blueprint.ts # Blueprint configuration
├── package.json # Project-level dependencies
└── node_modules/
```
The function directory name must match the `name` in the blueprint config. Each function exports a `handler` from its `index.ts` (or `index.js`).
---
## Step-by-step: Creating a Function
### 1. Initialize a Blueprint
```bash
npx sanity@latest blueprints init . \
--type ts \
--stack-name production \
--project-id <your-project-id>
```
This creates `sanity.blueprint.ts` and `.sanity/blueprint.config.json` (gitignored automatically; it links your Blueprint to a Stack and is not secret).
### 2. Scaffold a Function
```bash
npx sanity@latest functions add \
--name my-function \
--type document-create --type document-update \
--installer npm
```
`--type` options: `document-create`, `document-update`, `document-delete`, `media-library-asset-create`, `media-library-asset-update`, `media-library-asset-delete`, `scheduled-function`, `sync-tag-invalidate`.
### 3. Configure the Blueprint
```typescript
// sanity.blueprint.ts
import { defineBlueprint, defineDocumentFunction } from "@sanity/blueprints";
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: "my-function",
event: {
on: ["create", "update"],
filter: '_type == "post"',
},
}),
],
});
```
### 4. Write the Handler
```typescript
// functions/my-function/index.ts
import { createClient } from "@sanity/client";
import { documentEventHandler } from "@sanity/functions";
interface PostData {
_id: string;
_type: string;
title: string;
}
export const handler = documentEventHandler<PostData>(
async ({ context, event }) => {
const { data } = event;
const client = createClient({
...context.clientOptions,
apiVersion: "2025-05-08",
});
try {
await client.patch(data._id, {
setIfMissing: { firstPublished: new Date().toISOString() },
});
console.log(`Set firstPublished on ${data._id}`);
} catch (error) {
console.error("Failed to patch document:", error);
}
},
);
```
### 5. Test Locally
```bash
# Visual dev playground
npx sanity@latest functions dev
# CLI testing
npx sanity@latest functions test my-function \
--dataset production \
--with-user-token
# With a specific document
npx sanity@latest functions test my-function \
--document-id abc123 \
--dataset production \
--with-user-token
```
### 6. Deploy
```bash
npx sanity@latest blueprints deploy
```
### 7. View Logs
```bash
npx sanity@latest functions logs my-function
npx sanity@latest functions logs my-function --watch
```
---
## Handler Reference
Every handler receives `{ context, event }`:
### `context`
| Property | Type | Description |
| :------------------------ | :--------------------- | :------------------------------- |
| `clientOptions.apiHost` | `string` | API host URL |
| `clientOptions.projectId` | `string` | Sanity project ID |
| `clientOptions.dataset` | `string` | Dataset name |
| `clientOptions.token` | `string` | Robot token (deployed only) |
| `local` | `boolean \| undefined` | `true` during local testing |
| `eventResourceType` | `string` | `'dataset'` or `'media-library'` |
| `eventResourceId` | `string` | e.g., `'projectId.datasetName'` |
### `event`
```typescript
{
data: {
_id: string;
_type: string;
// ... rest of document (shaped by projection if set)
}
}
```
When testing locally, `context.clientOptions` only has `projectId` and `apiHost`. Use `--dataset` and `--with-user-token` flags to supply the rest.
---
## Blueprint Configuration
### `defineDocumentFunction` Options
| Option | Type | Default | Description |
| :------------ | :----------------------- | :----------------- | :----------------------------------------------- |
| `name` | `string` | required | Must match the directory name under `functions/` |
| `displayName` | `string` | — | Human-readable display name |
| `src` | `string` | `functions/<name>` | Path to function source directory |
| `memory` | `number` | `1` | Memory in GB (max 10) |
| `timeout` | `number` | `10` | Timeout in seconds (max 900) |
| `runtime` | `string` | `'nodejs24.x'` | `'node'`, `'nodejs22.x'`, or `'nodejs24.x'` |
| `project` | `string` | — | Project ID. Required if blueprint is org-scoped. |
| `robotToken` | `string` | — | Custom robot token name for the function |
| `event` | `object` | required | Event configuration (see below) |
| `env` | `Record<string, string>` | — | Environment variables via `process.env` |
### `event` Options
| Option | Type | Default | Description |
| :------------------- | :--------- | :------- | :------------------------------------------------------------------- |
| `on` | `string[]` | required | `'create'`, `'update'`, `'delete'` |
| `filter` | `string` | — | GROQ filter body (no `*[...]` wrapper) |
| `projection` | `string` | — | GROQ projection to shape `event.data`. Wrap in `{}`. |
| `includeDrafts` | `boolean` | `false` | Trigger on draft changes |
| `includeAllVersions` | `boolean` | `false` | Trigger on all document versions |
| `resource` | `object` | — | Scope to dataset: `{ type: 'dataset', id: 'projectId.datasetName' }` |
### `defineMediaLibraryAssetFunction`
For Media Library asset events. Requires `@sanity/blueprints` v0.4.0+ and `@sanity/functions` v1.1.0+.
```typescript
import {
defineBlueprint,
defineMediaLibraryAssetFunction,
} from "@sanity/blueprints";
export default defineBlueprint({
resources: [
defineMediaLibraryAssetFunction({
name: "asset-handler",
event: {
on: ["delete"],
filter: "documents::incomingGlobalDocumentReferenceCount() > 0",
projection: "{_id, versions, title}",
resource: {
type: "media-library",
id: "mlYourLibraryId",
},
},
}),
],
});
```
---
## Event Types
| Event | Description |
| :------- | :--------------------------------------------------------------------------------------- |
| `create` | New document created |
| `update` | Existing document modified (for published docs, fires when a draft/version is published) |
| `delete` | Document deleted |
Often best to use `['create', 'update']` together for published document triggers.
---
## GROQ Filter Tips
- Only the filter body — `_type == 'post'`, not `*[_type == 'post']`
- `delta::changedAny(fieldName)` — trigger only when specific fields change
- `sanity::dataset() == 'production'` — scope to a dataset without `resource` config
- `_id in path('drafts.**')` with `includeDrafts: true` — draft-only triggers
- Combine conditions to prevent recursion: `_type == 'post' && !defined(processedAt)`
---
## Projections
- Shape the data passed to `event.data`
- Limited to the invoking document's scope (plus `→` for references)
- Nested filters in projections (like `*[references(^._id)]`) will fail silently — query inside the function instead
- Wrap in `{}`: `projection: '{title, _id, slug}'`
---
## Environment Variables
Three ways to set them:
1. Blueprint config: `env: { MY_VAR: 'value' }`
2. CLI: `npx sanity@latest functions env add my-function MY_VAR my-value`
3. Local testing: `MY_VAR=value npx sanity functions test my-function`
Access in handler code via `process.env.MY_VAR`.
---
## Critical Rules
### Preventing Recursion
If your function mutates the same document type it listens to, you **will** create an infinite loop.
**✅ Correct — use GROQ filters to exclude processed documents:**
```typescript
defineDocumentFunction({
name: "first-published",
event: {
on: ["create", "update"],
filter: "_type == 'post' && !defined(firstPublished)",
},
});
```
**✅ Correct — use `@sanity/client` v7.12.0+ for automatic lineage headers:**
```typescript
import { createClient } from "@sanity/client";
// Client automatically sets X-Sanity-Lineage header
// Recursive chains are limited to 16 invocations
const client = createClient({
...context.clientOptions,
apiVersion: "2025-05-08",
});
```
**❌ Incorrect — no recursion guard:**
```typescript
defineDocumentFunction({
name: "update-post",
event: {
on: ["create", "update"],
filter: "_type == 'post'", // Will re-trigger on its own writes!
},
});
```
### Local Testing Safety
Use `context.local` to prevent accidental mutations during testing:
```typescript
// Skip mutations entirely in test
if (!context.local) {
await client.createOrReplace(someDoc);
}
// Or use dryRun
await client
.patch(event.data._id, {
set: { processed: true },
})
.commit({ dryRun: context.local });
// Or use noWrite for Agent Actions
await client.agent.action.generate({
schemaId: "your-schema-id",
documentId: event.data._id,
instruction: "Summarize this document",
target: { path: ["summary"] },
noWrite: context.local,
});
```
### Limits
- Max bundle size: 200MB (including dependencies). Prefer slim, platform-agnostic packages.
- Rate limits: 200 invocations/fn/30s, 4000/project/30s
- Max timeout: 900s. Larger functions = slower cold starts.
### Cost
Cost = invocations × (memory GB × duration seconds). Default is 1GB memory. A function averaging 1GB and 40ms duration can run ~500k invocations within 20K GB-seconds. [Monitor usage at the organization level](https://www.sanity.io/manage).
---
## Common Patterns
### Deploy hook / CDN invalidation
**Blueprint:**
```typescript
defineDocumentFunction({
name: "deploy-hook",
event: {
on: ["create", "update"],
filter: '_type == "page"',
},
});
```
**Handler:**
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const URL = process.env.DEPLOY_HOOK_URL;
if (!URL) throw new Error("DEPLOY_HOOK_URL is not set");
await fetch(URL);
console.log("Deploy hook triggered");
});
```
Set the env var: `npx sanity@latest functions env add deploy-hook DEPLOY_HOOK_URL https://...`
### Set a timestamp on first publish
Uses the same pattern as the step-by-step example above. The key insight: the `!defined(firstPublished)` GROQ filter prevents re-triggering after the field is set. The `setIfMissing` patch is a redundant safety net.
```typescript
defineDocumentFunction({
name: "first-published",
event: {
on: ["create", "update"],
filter: '_type == "post" && !defined(firstPublished)',
},
});
```
### Auto-translate with Agent Actions
**Blueprint:**
```typescript
defineDocumentFunction({
name: "translate",
event: {
on: ["create", "update"],
filter: "_type == 'post' && language == 'en-US'",
projection: "{_id}",
},
});
```
**Handler:**
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const client = createClient({ ...context.clientOptions, apiVersion: "vX" });
await client.agent.action.translate({
schemaId: "your-schema-id",
async: true,
documentId: event.data._id,
languageFieldPath: "language",
targetDocument: {
operation: "create",
},
fromLanguage: { id: "en-US", title: "English" },
toLanguage: { id: "el-GR", title: "Greek" },
});
});
```
The GROQ filter ensures only English documents trigger the function. The translated document gets a different `language` value, preventing recursive triggers.
Let Sanity assign the translated document's `_id` for ordinary localized content. To find or update translations later, query by language, slug, or translation metadata instead of deriving IDs from the source document. Reserve explicit `targetDocument._id` values for singleton-style targets.
### Auto-tag with Agent Actions
**Blueprint:**
```typescript
defineDocumentFunction({
name: "auto-tag",
event: {
on: ["create", "update"],
filter: "_type == 'post'",
projection: "{_id, title, body}",
},
});
```
**Handler:**
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const client = createClient({ ...context.clientOptions, apiVersion: "vX" });
await client.agent.action.generate({
schemaId: "your-schema-id",
documentId: event.data._id,
instruction:
"Analyze the content and generate 3 relevant tags. Reuse existing tags when possible.",
target: { path: ["tags"] },
async: true,
});
});
```
### Slack notification on publish
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
if (!WEBHOOK_URL) throw new Error("SLACK_WEBHOOK_URL not set");
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `📝 New content published: *${event.data.title || event.data._id}* (${event.data._type})`,
}),
});
});
```
### Scope to a specific dataset
**Option A — `resource` config:**
```typescript
defineDocumentFunction({
name: "production-only",
event: {
on: ["update"],
filter: "_type == 'post'",
resource: { type: "dataset", id: "myProjectId.production" },
},
});
```
**Option B — GROQ filter:**
```typescript
defineDocumentFunction({
name: "production-only",
event: {
on: ["update"],
filter: "_type == 'post' && sanity::dataset() == 'production'",
},
});
```
### React to Media Library asset changes
Requires `@sanity/blueprints` v0.4.0+ and `@sanity/functions` v1.1.0+.
**Blueprint:**
```typescript
import {
defineBlueprint,
defineMediaLibraryAssetFunction,
} from "@sanity/blueprints";
export default defineBlueprint({
resources: [
defineMediaLibraryAssetFunction({
name: "asset-deleted",
event: {
on: ["delete"],
filter: "documents::incomingGlobalDocumentReferenceCount() > 0",
projection: "{_id, versions, title}",
resource: { type: "media-library", id: "mlYourLibraryId" },
},
}),
],
});
```
**Handler:**
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const { eventResourceId } = context; // Media Library ID
const client = createClient({
...context.clientOptions,
apiVersion: "2025-05-08",
});
const response = await client.request({
uri: `/media-libraries/${eventResourceId}/query`,
method: "POST",
body: { query: `*[_type == 'sanity.imageAsset']` },
});
console.log("Assets:", response);
});
```
### Recursion control with custom HTTP clients
If not using `@sanity/client`, implement lineage tracking manually:
```typescript
export const handler = documentEventHandler(async ({ context, event }) => {
const lineage = process.env.X_SANITY_LINEAGE;
await fetch(
`https://${context.clientOptions.projectId}.api.sanity.io/v2025-05-08/data/mutate/production`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${context.clientOptions.token}`,
...(lineage ? { "X-Sanity-Lineage": lineage } : {}),
},
body: JSON.stringify({
mutations: [
{ patch: { id: event.data._id, set: { processed: true } } },
],
}),
},
);
});
```
### Multiple functions in one blueprint
```typescript
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: "first-published",
event: {
on: ["create", "update"],
filter: "_type == 'post' && !defined(firstPublished)",
},
}),
defineDocumentFunction({
name: "notify-slack",
event: {
on: ["create", "update"],
filter: "_type == 'post'",
projection: "{title, _id}",
},
}),
defineDocumentFunction({
name: "sync-algolia",
timeout: 30,
event: {
on: ["create", "update", "delete"],
filter: "_type == 'product'",
},
}),
],
});
```
---
## CI/CD Deployment
Use the [Blueprints GitHub Action](https://github.com/sanity-io/blueprints-actions)
```yaml
- uses: sanity-io/blueprints-actions/deploy@deploy-v3
with:
sanity-token: ${{ secrets.SANITY_DEPLOY_TOKEN }}
```
Mint a long-lived deploy token with `npx sanity@latest blueprints mint-deploy-token` (creates a robot token with the role required to plan, deploy, and destroy) and store it as a CI secret. Recommended workflow: `blueprints plan` on pull requests, `blueprints deploy` on merge to main. See the `blueprints` reference for CI environment variables and exit codes.
@@ -0,0 +1,425 @@
---
title: Sanity Getting Started Guide
description: Use these rules when users ask to 'Get started with Sanity' or need help setting up a new Sanity project.
---
# Sanity Getting Started Guide
## Overview
Getting started with Sanity follows three phases:
1. **Studio & Schema** — Set up Sanity Studio and define your content model
2. **Content** — Import existing content or generate placeholder content via MCP
3. **Frontend** — Integrate with your application (framework-specific)
## Communication Style
**Keep responses succinct:**
- Tell the user what you did: "Created post schema with title, body, and slug"
- Ask direct questions: "What kind of content are you building?"
- Avoid verbose explanations of what you're about to do
- Don't explain every step unless the user asks
**Examples:**
- **Good**: "Schema deployed. Ready to add some content?"
- **Bad**: "I'm going to deploy your schema to the Content Lake so that the MCP server can recognize your new document types. This will allow..."
---
## Get Started with Sanity (Interactive Guide)
**TRIGGER PHRASE:** When the user says "Get started with Sanity" or similar, follow these steps.
**Before starting:** Let the user know they can pause and resume anytime by saying "Continue Sanity setup".
**RESUME TRIGGER:** If the user says "Continue Sanity setup", check what's already configured:
- Does `sanity.config.ts` exist (typically in a `studio/` folder)? → Studio is set up
- Are there files in `schemaTypes/`? → Schema exists
- Is there a frontend framework in `package.json`? → May need integration
Resume from where they left off.
---
## Phase 1: Studio & Schema
### Step 1: Check for Existing Studio
**Look for `sanity.config.ts` or `sanity.cli.ts` across the workspace** — in the recommended side-by-side layout the Studio lives in its own folder (`studio/`, or `studio-*` when created by the Sanity onboarding flow) next to the app folder:
**If NO Studio found:**
- Ask: "Want to create a new Sanity Studio?"
- If yes, run from the repo root — **not inside a Next.js app folder**, where the CLI would switch to its embedded flow (not recommended):
```bash
npm create sanity@latest -- --template clean --typescript --output-path studio
```
- This creates a standalone Studio in `studio/`, alongside your app folder (see `project-structure.md`)
**If Studio exists:**
- Read the config to get `projectId` and `dataset`
- Proceed to Step 2
### Step 2: Check for Existing Schema
**Look in `schemaTypes/`, `schemas/`, or `src/sanity/schemaTypes/`:**
**If NO schema found:**
- Ask: "What kind of content are you building? (e.g., Blog, E-commerce, Portfolio)"
- Create appropriate schema types based on their answer
- See `schema.md` for patterns
**If schema exists:**
- Show them what you found
- Ask: "Want to add more content types or modify existing ones?"
**If they want a quick example:**
Create a basic blog schema:
```typescript
// schemaTypes/post.ts
import { defineField, defineType } from "sanity";
export const post = defineType({
name: "post",
title: "Post",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
defineField({ name: "body", type: "array", of: [{ type: "block" }] }),
],
});
```
### Step 3: Deploy Schema
**Required before Phase 2:**
```bash
npx sanity schema deploy
```
This uploads your schema to the Content Lake so MCP tools can work with it.
---
## Phase 2: Content
### Step 1: Check for Existing Content
**Use MCP `query_documents` to check:**
```
*[_type == "post"][0...5]
```
**If content exists:**
- Show them a summary
- Ask: "Want to add more content or move to frontend integration?"
**If NO content:**
- Ask: "Do you want to:
1. Import existing content (from another CMS, markdown, etc.)
2. Generate sample content with AI
3. Skip this and add content manually in the Studio"
### Step 2a: Import Existing Content
If migrating from another CMS or files:
- See `migration.md` and the `sanity-migration` skill for guidance
- Use MCP content tools such as `create_documents` and `patch_documents` after converting content to structured Sanity documents
### Step 2b: Generate Sample Content (MCP)
Ask the agent to draft structured sample content, then create it with the Sanity MCP Server:
```
Tool: create_documents
Documents: [{ type: "post", content: { title: "Getting started with Sanity", body: [] } }]
```
**If MCP content tools cannot see new types or fields:** Remind them to run `npx sanity schema deploy` first.
### MCP Setup (If Not Configured)
**Quick start via Sanity CLI:**
```bash
npx sanity@latest mcp configure
```
**Cursor:** [One-click install →](cursor://anysphere.cursor-deeplink/mcp/install?name=Sanity&config=eyJ1cmwiOiJodHRwczovL21jcC5zYW5pdHkuaW8iLCJ0eXBlIjoiaHR0cCJ9Cg==)
Or add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"Sanity": {
"type": "http",
"url": "https://mcp.sanity.io"
}
}
}
```
**Claude Code:**
```bash
claude mcp add Sanity -t http https://mcp.sanity.io --scope user
```
**VS Code:** Command Palette → `MCP: Open User Configuration` → add:
```json
{
"servers": {
"Sanity": {
"type": "http",
"url": "https://mcp.sanity.io"
}
}
}
```
---
## Phase 3: Frontend Integration
### Client Bundle Warning (Vite-based frameworks)
React Router, SvelteKit, Astro, and Nuxt all run on Vite. **Any module imported by a client component will be bundled to the browser.** `process.env` doesn't exist there.
For publishable values (`projectId`, `dataset`, `apiVersion`, public studio URL), use the framework's client-safe env mechanism:
- React Router / Remix: `import.meta.env.VITE_*`
- SvelteKit: `$env/static/public`
- Astro: `import.meta.env.PUBLIC_*`
- Nuxt: `useRuntimeConfig().public`
For secrets (read tokens, webhook secrets), read `process.env.*` (or the server equivalent) **only from server-only modules** — `.server.ts`, route handlers, API endpoints. Don't centralize them in a shared `env.ts` that anything else imports.
This trap is invisible at SSR — the page renders fine on first load. It surfaces on client-side route transitions, when a lazy-loaded route chunk pulls a shared client/image module into the browser.
### Step 1: Find the App and Detect Framework
The working directory is often a parent folder with the Studio and the app side by side. Identify the app folder first: a sibling of the Studio folder with its own `package.json` (commonly `web/`). If several candidates exist, ask the user which app to integrate — never assume.
**Check the app's `package.json` dependencies:**
| Dependency | Framework | Rule File |
| ------------------------------------ | -------------------- | ----------- |
| `next` | Next.js | `nextjs.md` |
| `@remix-run/react` or `react-router` | React Router / Remix | `remix.md` |
| `svelte` or `@sveltejs/kit` | SvelteKit | `svelte.md` |
| `nuxt` | Nuxt | `nuxt.md` |
| `astro` | Astro | `astro.md` |
**If NO framework found:**
- Ask: "Which framework are you using, or would you like to create a new app?"
- Guide them to create one or specify their choice
### Step 2: Next.js Integration (Inline)
If Next.js is detected, follow these essential steps:
**Scaffold a new app (if you don't have one yet):**
Run from the repo root so the app sits alongside your `studio/` folder:
```bash
npx create-next-app@latest web --tailwind --ts --app --src-dir --eslint --import-alias "@/*" --turbopack
cd web
```
**Install dependencies:**
```bash
npm install next-sanity @sanity/image-url
```
`next-sanity` is the official Sanity toolkit for Next.js. It bundles `@sanity/client`, `groq` (with `defineQuery`), and `@portabletext/react`, plus dedicated subpath exports for Next.js-specific features:
- `next-sanity` — `createClient`, `defineQuery`, `PortableText`, `SanityDocument`, `stegaClean`
- `next-sanity/live` — `defineLive` for live content with Next.js cache integration
- `next-sanity/draft-mode` — Draft Mode endpoint helpers
- `next-sanity/visual-editing` — `<VisualEditing />` component for click-to-edit overlays
- `next-sanity/image` — Sanity-aware `<Image />` wrapping `next/image`
- `next-sanity/studio` — embed the Sanity Studio at a route (legacy setups only — keep the Studio standalone, see `nextjs.md`)
- `next-sanity/webhook` — webhook signature verification
Don't also install `@sanity/client`, `@portabletext/react`, or `groq` directly — import them from `next-sanity`. `@sanity/image-url` is not bundled (yet), so add it separately.
**Create the client (`src/sanity/client.ts`):**
```typescript
import { createClient } from "next-sanity";
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: "2026-05-15", // Use current date for new projects
useCdn: false, // Use API directly for server-side rendering; set true for client-side reads
});
```
**Fetch content in a Server Component:**
```typescript
// src/app/page.tsx
import { client } from "@/sanity/client";
import { defineQuery, type SanityDocument } from "next-sanity";
const POSTS_QUERY = defineQuery(
`*[_type == "post" && defined(slug.current)] | order(_createdAt desc){ _id, title, slug }`
);
const options = { next: { revalidate: 30 } };
export default async function PostsPage() {
const posts = await client.fetch<SanityDocument[]>(POSTS_QUERY, {}, options);
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={`/${(post.slug as { current?: string })?.current}`}>{post.title as string}</a>
</li>
))}
</ul>
);
}
```
`{ next: { revalidate: 30 } }` opts the fetch into Next.js' ISR cache with a 30-second revalidation window. Tune to taste; omit `options` to use defaults.
**Render an individual post (`src/app/[slug]/page.tsx`):**
```typescript
import { PortableText, defineQuery, type SanityDocument } from "next-sanity";
import { notFound } from "next/navigation";
import { client } from "@/sanity/client";
const POST_QUERY = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{ _id, title, body }`
);
const options = { next: { revalidate: 30 } };
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await client.fetch<SanityDocument | null>(POST_QUERY, { slug }, options);
if (!post) return notFound();
return (
<article>
<h1>{post.title as string}</h1>
{Array.isArray(post.body) && <PortableText value={post.body} />}
</article>
);
}
```
**Add environment variables (`.env.local`):**
```
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
```
For advanced patterns (TypeGen, Visual Editing with `next-sanity/visual-editing`, live content with `defineLive` from `next-sanity/live`, standalone Studio architecture), see `nextjs.md`.
### Step 3: Other Frameworks
For non-Next.js frameworks, read the corresponding rule file and follow its integration guide:
- **React Router / Remix:** `remix.md`
- **SvelteKit:** `svelte.md`
- **Nuxt:** `nuxt.md`
- **Astro:** `astro.md`
Each rule file contains framework-specific patterns for data fetching, Portable Text rendering, and Visual Editing.
### Step 4: Smoke Test
Before declaring integration done, exercise both render paths:
1. `npm run dev` (in the app folder)
2. Load the home page (lists posts).
3. **Click through to a detail page** via an in-app `<Link>` / `<a>` — do not paste the URL.
4. Open the browser console. It should be clean. No `ReferenceError: process is not defined`, no hard reload to `/`.
5. For good measure, reload the detail page directly (URL bar) — that exercises SSR.
Server-side rendering passing isn't enough. Client-side route transitions pull lazy chunks that exercise different code paths, and that's where env/bundling traps surface.
---
## What's Next
Once setup is complete, let the user know:
"You're all set! Here are some things I can help with:
- **Visual Editing** — Click-to-edit in the Presentation tool (`visual-editing.md`)
- **TypeGen** — Type-safe queries with generated types (`typegen.md`)
- **Studio Structure** — Customize the Studio sidebar (`studio-structure.md`)
- **SEO** — Metadata, sitemaps, and Open Graph (`seo.md`)
- **i18n** — Multi-language content (`localization.md`)
Just ask about any of these!"
---
## Environment Variables
### Framework-Specific Prefixes
| Framework | Client-Side Prefix | Example |
| -------------------- | ------------------ | ------------------------------- |
| Next.js | `NEXT_PUBLIC_` | `NEXT_PUBLIC_SANITY_PROJECT_ID` |
| React Router / Remix | `VITE_` | `VITE_SANITY_PROJECT_ID` |
| SvelteKit | `PUBLIC_` | `PUBLIC_SANITY_PROJECT_ID` |
| Nuxt | `NUXT_PUBLIC_` | `NUXT_PUBLIC_SANITY_PROJECT_ID` |
| Astro | `PUBLIC_` | `PUBLIC_SANITY_PROJECT_ID` |
**Secrets** (read tokens, webhook secrets) stay **unprefixed** and are read via `process.env` (or the framework's server-only equivalent) from server-only modules — `*.server.ts`, route handlers, API routes. Never re-export a secret from a module that a route component can import.
---
## Common Commands
```bash
npx sanity@latest mcp configure # Configure MCP for your editor
npx sanity dev # Start Studio locally
npx sanity schema deploy # Deploy schema for MCP/editor access
npx sanity deploy # Deploy Studio to Sanity hosting
npx sanity manage # Open project settings
npm run typegen # Generate TypeScript types
```
---
## Important Notes
- **Be succinct** — Guide step-by-step without over-explaining
- **Check context first** — Read existing files before suggesting changes
- **Don't give up** — If something fails, give the user a way to complete manually
- **Deploy schema early** — MCP content tools need deployed schemas to see new types and fields
- **One phase at a time** — Complete each phase before moving to the next
@@ -0,0 +1,373 @@
---
title: GROQ Query Maintenance & Best Practices
description: Guidelines for GROQ queries, type safety, performance optimization, and syntax highlighting.
---
# GROQ Query Maintenance & Best Practices
Use this contents list to jump to the query concern you need to solve.
## Table of Contents
- Query definition and imports
- Query fragments
- Expansion patterns
- Maintenance workflow
- Common patterns
- Performance rules
- API version best practices
## 1. Query Definition & Imports
### The `defineQuery` Function
**ALWAYS** wrap GROQ queries in `defineQuery` for TypeGen support. The import location depends on your framework:
```typescript
// Framework-agnostic (Angular, Remix, SvelteKit, Astro, vanilla)
import { defineQuery } from "groq";
// Next.js (re-exported for convenience)
import { defineQuery } from "next-sanity";
```
### Syntax Highlighting
For VS Code syntax highlighting, either:
1. Use the `groq` tagged template (recommended): `groq\`...\``
2. Or prefix with `/* groq */` comment when using `defineQuery`
```typescript
import { defineQuery } from "groq";
// ✅ Option A: groq tag (provides highlighting automatically)
import groq from "groq";
const QUERY = defineQuery(groq`*[_type == "post"]`);
// ✅ Option B: Comment prefix (for plain template literals)
const QUERY = defineQuery(/* groq */ `*[_type == "post"]`);
// ✅ Also valid: Just defineQuery (TypeGen works, but no editor highlighting)
const QUERY = defineQuery(`*[_type == "post"]`);
```
## 2. Query Fragments
Use string interpolation to reuse query logic and keep queries maintainable.
```typescript
// src/sanity/fragments/image.ts
export const imageFragment = /* groq */ `
asset->{
_id,
url,
metadata { lqip, dimensions }
},
alt
`;
// src/sanity/queries/post.ts
import { defineQuery } from "groq";
import { imageFragment } from "../fragments/image";
export const POST_QUERY = defineQuery(/* groq */ `
*[_type == "post"][0] {
title,
mainImage {
${imageFragment}
}
}
`);
```
## 3. Expansion Patterns (Page Builder)
When building a Page Builder query, expand all potential component types.
**Best Practice:** Use a `pageFields` fragment or similar strategy to keep the main query clean.
```typescript
const pageBuilderExpansion = /* groq */ `
pageBuilder[] {
...,
_type == "hero" => {
...,
cta[] { link, label }
},
_type == "gallery" => {
images[] { ${imageFragment} }
}
}
`;
```
## 4. Maintenance Workflow
When you add a new field or component to the Schema:
1. **Update the Query:** Add the new field/expansion to the relevant GROQ query immediately.
2. **Run TypeGen:** If you have `typegen.enabled: true` in `sanity.cli.ts`, types regenerate automatically during `sanity dev`/`sanity build`. Otherwise, run `npm run typegen` manually.
3. **Verify:** Ensure the new field is available in the generated types.
## 5. Common Patterns
### Ordering
```groq
// Single field
*[_type == "post"] | order(publishedAt desc)
// Multiple fields (tiebreaker)
*[_type == "post"] | order(featured desc, publishedAt desc)
// ⚠️ Order BEFORE slice, not after!
*[_type == "post"] | order(publishedAt desc)[0...10] // ✅ Correct
*[_type == "post"][0...10] | order(publishedAt desc) // ❌ Wrong order
```
### Slice Notation
```groq
*[_type == "post"][0] // Single document (object, not array)
*[_type == "post"][0...5] // First 5 (exclusive) ← Most common
*[_type == "post"][$start...$end] // Pagination with params
```
### Default Values with `coalesce()`
```groq
*[_type == "page"]{
"title": coalesce(seoTitle, title, "Untitled"),
"image": coalesce(ogImage, mainImage, defaultImage)
}
```
### Conditionals with `select()`
```groq
*[_type == "product"]{
title,
"badge": select(
stock == 0 => "Out of Stock",
stock < 5 => "Low Stock",
"In Stock"
)
}
```
### Aggregation with `count()`
```groq
// Total count
count(*[_type == "post" && defined(slug.current)])
// Count per document
*[_type == "category"]{
title,
"postCount": count(*[_type == "post" && references(^._id)])
}
```
### Reverse References
```groq
*[_type == "author"]{
name,
"posts": *[_type == "post" && references(^._id)]{ title, slug }
}
```
### Array Filtering
```groq
*[_type == "movie"]{
title,
"mainCast": castMembers[role == "lead"]->{name}
}
// Check if value exists in array
*[_type == "post" && "tech" in categories[]->slug.current]
```
### Special Variables
```groq
// ^ = parent document (in nested queries)
*[_type == "author"]{
name,
"posts": *[_type == "post" && author._ref == ^._id]
}
// @ = current item (in array operations)
*[_type == "post"]{
"tagCount": count(tags[@ != null])
}
```
## 6. Performance Rules
### Optimizable vs Non-Optimizable Filters
GROQ uses indexes for **optimizable** filters. Non-optimizable filters scan ALL documents.
| Pattern | Optimizable | Example |
| ----------------------- | ----------- | -------------------------------- |
| `_type == "x"` | ✅ Yes | `*[_type == "post"]` |
| `_id == "x"` | ✅ Yes | `*[_id == "abc123"]` |
| `slug.current == $slug` | ✅ Yes | `*[slug.current == "hello"]` |
| `defined(field)` | ✅ Yes | `*[defined(publishedAt)]` |
| `references($id)` | ✅ Yes | `*[references("author-123")]` |
| `field->attr == x` | ❌ No | Resolves reference for every doc |
| `fieldA < fieldB` | ❌ No | Compares two attributes |
**Fix non-optimizable filters by stacking:**
```groq
// Stack optimizable filters FIRST to reduce search space
*[_type == "product" && defined(salePrice) && salePrice < displayPrice]
```
### Avoid Joins in Filters
Reference resolution (`->`) in filters is expensive. Use `_ref` instead:
```groq
// ❌ Slow: Resolves reference for every document
*[_type == "post" && author->name == "Bob Woodward"]
// ✅ Fast: Direct _ref comparison
*[_type == "post" && author._ref == "author-bob-woodward-id"]
```
**When you need dynamic lookups** (don't know the ID upfront):
```groq
// Two-step approach:
// 1. Get the reference ID first
*[_type == "author" && name == "Bob Woodward"][0]._id
// 2. Use that ID in your main query
*[_type == "post" && author._ref == $authorId]
// Or use a subquery (still better than -> in filter):
*[_type == "post" && author._ref in *[_type == "author" && name == "Bob Woodward"]._id]
```
### Merge Repeated Reference Resolutions
Each `->` is a subquery. Don't repeat it:
```groq
// ❌ Slow: Two separate subqueries
*[_type == "category"]{
"parentTitle": parent->title,
"parentSlug": parent->slug.current
}
// ✅ Fast: Single subquery, merged
*[_type == "category"]{
...(parent->{ "parentTitle": title, "parentSlug": slug.current })
}
```
### Cursor-Based Pagination (Not Deep Slicing)
Deep slices are slow because all skipped docs must be sorted first.
```groq
// ❌ Slow: Must sort and skip 10,000 docs
*[_type == "article"] | order(_id)[10000...10020]
// ✅ Fast: Cursor-based, only fetches 20
*[_type == "article" && _id > $lastId] | order(_id)[0...20]
```
**For custom sort orders**, include the sort field in the cursor:
```groq
// Compound cursor: publishedAt + _id for deterministic pagination
*[_type == "article" && (
publishedAt < $lastDate ||
(publishedAt == $lastDate && _id > $lastId)
)] | order(publishedAt desc, _id)[0...20]
```
### Always Project Fields
Always use projections to return only the fields your application needs. Fetching entire documents wastes bandwidth and processing time.
```groq
// ❌ Returns ALL fields including unused ones, metadata, revisions
*[_type == "post"]
// ✅ Only fetch what the component needs
*[_type == "post"]{
_id,
title,
"slug": slug.current,
publishedAt,
excerpt
}
```
Apply projections at every level, including nested references:
```groq
*[_type == "post"]{
title,
author->{ name, "avatar": image.asset->url },
categories[]->{ title, "slug": slug.current }
}
```
Use conditional projections for different contexts:
```groq
*[_type == "post"]{
title,
slug,
// Only include body for single post view
$includeBody == true => { body }
}
```
### Don't Filter/Sort on Projected Values
Computed attributes can't use indexes:
```groq
// ❌ Not optimizable (computed attribute)
*[_type == "person"]{
"fullName": firstName + " " + lastName
} | order(fullName)
// ✅ Optimizable (original attribute)
*[_type == "person"] | order(firstName, lastName)
```
### Quick Checklist
| Rule | Why |
| ----------------------------------- | --------------------------------------------- |
| Always project `{ fields }` | Reduces data returned |
| Use `defined()` checks | Filters use indexes |
| Use `$params` not interpolation | Prevents query manipulation + enables caching |
| Order BEFORE slice | `order()[0...N]` not `[0...N] order()` |
| Use `_ref` not `->field` in filters | Avoids expensive joins |
| Merge repeated `->` calls | Single subquery vs many |
| Cursor pagination for deep pages | Avoids sorting entire dataset |
## 7. API Version Best Practices
Always use dated versions (`YYYY-MM-DD`) for consistent behavior:
```typescript
const client = createClient({
apiVersion: "2026-02-01", // Use current date for new projects
});
```
- **New projects:** Use current date (e.g., `2026-02-01`)
- **Existing projects:** Keep current version unless you need new features
- Dated versions lock behavior; `v1` or `vX` may change unexpectedly
@@ -0,0 +1,275 @@
---
title: "Sanity + Shopify + Hydrogen Rules"
description: Integration guide for Sanity with Shopify using the Hydrogen framework (React Router 7).
---
# Sanity + Shopify + Hydrogen Rules
**Package:** [`hydrogen-sanity`](https://github.com/sanity-io/hydrogen-sanity) — requires `@shopify/hydrogen >= 2025.5.0`
## 1. Architecture Overview
| Component | Purpose |
| ------------------ | ------------------------------------------------------------------- |
| **Shopify** | Product catalog, inventory, checkout (source of truth for commerce) |
| **Sanity Connect** | Syncs Shopify data to Sanity in real-time |
| **Sanity Studio** | Editorial content, rich descriptions, media (enhances Shopify data) |
| **Hydrogen** | React Router 7 front-end optimized for Shopify |
**Project Structure:**
```
./
├── /studio # Sanity Studio
└── /web # Hydrogen front-end
```
## 2. Environment Variables
```bash
# web/.env
PUBLIC_STOREFRONT_API_TOKEN="your-public-storefront-token"
PRIVATE_STOREFRONT_API_TOKEN="your-private-storefront-token"
PUBLIC_STORE_DOMAIN="your-store.myshopify.com"
SESSION_SECRET="your-random-session-secret"
# Sanity
SANITY_PROJECT_ID="your-project-id"
SANITY_DATASET="production"
SANITY_API_VERSION="2026-02-01"
SANITY_PREVIEW_TOKEN="your-sanity-viewer-token" # Viewer token for previews
```
## 3. Sanity Client Setup
### Vite Config
```typescript
// web/vite.config.ts
import { hydrogen } from "@shopify/hydrogen/vite";
import { sanity } from "hydrogen-sanity/vite";
export default defineConfig({
plugins: [hydrogen(), sanity()],
});
```
### Context Setup
```typescript
// web/app/lib/context.ts
import { type SanityContext, createSanityContext } from "hydrogen-sanity";
import { isPreviewEnabled } from "hydrogen-sanity/preview";
import { PreviewSession } from "hydrogen-sanity/preview/session";
const sanity = await createSanityContext({
request,
cache,
waitUntil,
client: {
projectId: env.SANITY_PROJECT_ID,
dataset: env.SANITY_DATASET,
apiVersion: env.SANITY_API_VERSION || "2026-02-01",
useCdn: true,
stega: {
enabled: isPreviewEnabled(env.SANITY_PROJECT_ID, previewSession),
studioUrl: "http://localhost:3333",
},
},
preview: {
token: env.SANITY_PREVIEW_TOKEN,
session: previewSession,
},
});
```
### Provider Setup (entry.server.tsx)
```typescript
const {SanityProvider} = context.sanity
const body = await renderToReadableStream(
<NonceProvider>
<SanityProvider>
<ServerRouter context={reactRouterContext} url={request.url} nonce={nonce} />
</SanityProvider>
</NonceProvider>,
)
```
### Root Layout (root.tsx)
```typescript
import {Sanity} from 'hydrogen-sanity'
export function Layout({children}) {
const nonce = useNonce()
return (
<html>
<body>
{children}
<Sanity nonce={nonce} /> {/* Required for client-side */}
<Scripts nonce={nonce} />
</body>
</html>
)
}
```
## 4. Data Fetching
Fetch from **both** Shopify (GraphQL) and Sanity (GROQ). Use `defineQuery` for TypeGen support.
### Recommended: `query` + `Query` component
```typescript
import {defineQuery} from 'groq'
import {Query} from 'hydrogen-sanity'
const PRODUCT_QUERY = defineQuery(`*[_type == "product" && store.slug.current == $handle][0]{ body }`)
// Loader
export async function loader({params, context: {sanity}}: LoaderFunctionArgs) {
const initial = await context.sanity.query(PRODUCT_QUERY, params)
return {initial}
}
// Component - auto-enables live preview when active
export default function ProductPage({loaderData}) {
return (
<Query query={PRODUCT_QUERY} options={{initial: loaderData.initial}}>
{(data) => <div>{data?.body}</div>}
</Query>
)
}
```
### Alternative methods
| Method | Use Case |
| -------------------------- | ------------------------------- |
| `sanity.query()` + `Query` | Recommended - auto preview mode |
| `sanity.loadQuery()` | Manual loader integration |
| `sanity.fetch()` | No preview needed, lightweight |
| `sanity.client` | Mutations in actions |
### Images
```typescript
import {useImageUrl} from 'hydrogen-sanity'
function Hero({image}) {
const imageUrl = useImageUrl(image)
return <img src={imageUrl.width(1200).height(600).url()} />
}
```
**Key Insight:** Shopify fields synced via Sanity Connect are `readOnly`. Use Sanity for editorial enhancements only.
## 5. Visual Editing Setup
### Root Layout
```typescript
// web/app/root.tsx
import {usePreviewMode} from 'hydrogen-sanity/preview'
import {VisualEditing} from 'hydrogen-sanity/visual-editing'
export function Layout({children}: {children?: React.ReactNode}) {
const previewMode = usePreviewMode()
return (
<html>
<body>
{children}
{previewMode ? <VisualEditing action="/api/preview" /> : null}
</body>
</html>
)
}
```
### Preview Route
```typescript
// web/app/routes/api.preview.ts
export { action, loader } from "hydrogen-sanity/preview/route";
```
### Content Security Policy
```typescript
// web/entry.server.tsx
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
frameAncestors: isPreviewEnabled ? [studioHostname] : [],
connectSrc: [
`https://${projectId}.api.sanity.io`,
`wss://${projectId}.api.sanity.io`,
],
});
```
## 6. Studio: Presentation Tool
```typescript
// studio/sanity.config.ts
import { presentationTool } from "sanity/presentation";
export default defineConfig({
plugins: [
presentationTool({
resolve: {
locations: {
product: defineLocations({
select: { title: "store.title", slug: "store.slug.current" },
resolve: (doc) => ({
locations: [
{
title: doc?.title || "Untitled",
href: `/products/${doc?.slug}`,
},
{ title: "Products", href: `/collections/all` },
],
}),
}),
},
},
previewUrl: {
origin: "http://localhost:3000",
previewMode: { enable: "/api/preview" },
},
}),
],
});
```
## 7. Commands
```bash
# Install dependencies
pnpm add hydrogen-sanity @sanity/client @portabletext/react
# Development (run in separate terminals)
cd studio && pnpm dev # Studio at localhost:3333
cd web && pnpm dev # Hydrogen at localhost:3000
# Sanity Manage (CORS, tokens): https://www.sanity.io/manage
pnpm dlx sanity manage
```
## 8. Boundaries
- Always:
- Query Shopify for commerce data (price, inventory, variants)
- Query Sanity for editorial content (rich text, custom fields)
- Use `hydrogen-sanity` package for Visual Editing
- Add Hydrogen URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage)
- Ask First:
- Before modifying Sanity Connect sync settings
- Before changing CSP configuration
- Never:
- Edit Shopify-synced fields in Sanity (they're `readOnly`)
- Expose `SANITY_API_TOKEN` to client-side code
- Query Sanity for commerce data that should come from Shopify
@@ -0,0 +1,133 @@
---
title: "Sanity Image Rules"
description: "Best practices for handling images in Sanity: Schema, URL generation, and Next.js Image integration."
---
# Sanity Image Rules
## 1. Schema Definition
**Always** enable `hotspot: true`. This allows editors to control cropping and the focal point.
```typescript
defineField({
name: "mainImage",
title: "Main Image",
type: "image",
options: {
hotspot: true, // CRITICAL
},
fields: [
defineField({
name: "alt",
type: "string",
title: "Alternative Text",
validation: (rule) =>
rule.required().warning("Alt text is important for SEO"),
}),
],
});
```
## 2. URL Builder (`urlFor`)
Use the Sanity Image URL Builder to generate optimized URLs (resize, crop, format).
**Setup (`sanity/lib/image.ts`):**
```typescript
import createImageUrlBuilder from "@sanity/image-url";
import { dataset, projectId } from "../env";
const builder = createImageUrlBuilder({ projectId, dataset });
export const urlFor = (source: any) => {
return builder.image(source);
};
```
**Usage:** The URL builder automatically uses hotspot/crop data when available:
```typescript
const imageUrl = urlFor(mainImage)
.width(800)
.height(600)
.fit("crop") // Respects hotspot when cropping
.url();
```
## 3. Next.js Image Component Pattern
Create a reusable `SanityImage` component that handles the `urlFor` logic and `next/image` props.
```typescript
import Image from 'next/image'
import { urlFor } from '@/sanity/lib/image'
interface SanityImageProps {
value: any // SanityImageSource
width?: number
height?: number
className?: string
priority?: boolean
}
export function SanityImage({ value, width = 800, height, className, priority }: SanityImageProps) {
if (!value?.asset) return null
return (
<Image
className={className}
src={urlFor(value)
.width(width)
.height(height || Math.round(width / 1.5)) // Default aspect ratio if no height
.url()}
alt={value.alt || ''}
width={width}
height={height || Math.round(width / 1.5)}
priority={priority}
// Optional: Use LQIP (Low Quality Image Placeholder)
placeholder={value.asset.metadata?.lqip ? 'blur' : 'empty'}
blurDataURL={value.asset.metadata?.lqip}
/>
)
}
```
## 4. Querying Images
**Critical:** LQIP (Low Quality Image Placeholder) is **not automatic**. You must explicitly query it via `asset->{ metadata { lqip } }`.
### Minimal Query (No LQIP)
```groq
mainImage {
asset->{ _id, url },
alt
}
```
### Full Query (With LQIP & Dimensions)
```groq
mainImage {
asset->{
_id,
url,
metadata {
lqip, // Base64 blur placeholder
dimensions { width, height } // For aspect ratio
}
},
alt,
hotspot, // Include if using hotspot cropping
crop // Include if using cropping
}
```
**Why this matters:** Without querying `metadata.lqip`, the `blurDataURL` in your component will be `undefined` and the blur effect won't work.
## 5. Performance Tips
- **Auto Format:** Sanity CDN automatically serves WebP/AVIF if the browser supports it (no need to specify `.format('webp')` manually in most cases, but `next/image` handles this too).
- **Sizing:** Always request the exact size you need using `.width()` and `.height()` in `urlFor`. Don't download a 4000px image for a thumbnail.
@@ -0,0 +1,458 @@
---
title: Sanity Localization Rules
description: Localization patterns for Sanity using official plugins and best practices.
---
# Sanity Localization Rules
Use the contents list to jump directly to the localization pattern you need.
## Table of Contents
- Guiding principles
- Terminology
- Locale content type
- Choosing document-level vs field-level localization
- Document-level localization
- Localized singletons
- Field-level localization
- AI-powered translation
- UI enhancement
- Frontend URL best practices
## 1. Guiding Principles
### Priority: Easy Authoring Experience
The structured nature of Sanity schemas and GROQ make it easy to parse localized content for your frontend. **Never** let frontend architecture dictate your localization approach — prioritize the editor experience.
### Avoid Content Duplication
Don't create nearly identical copies with slight differences (e.g., US vs British English). Use Portable Text marks and custom blocks to swap out words or sections as needed.
## 2. Terminology
| Term | Definition |
| ------------------------------- | --------------------------------------------------------- |
| **Internationalization (i18n)** | Designing your frontend to support multiple languages |
| **Localization** | Adapting content for a specific language/region |
| **Language Tag** | Code like `en`, `en-US`, `zh-Hant-TW` (per IETF RFC 5646) |
| **Locale** | A language tag with region info (e.g., `en-US`) |
## 3. Create a Locale Content Type
**Best Practice:** Store locales in Sanity, not just in code. This allows sharing between Studio and frontend.
```typescript
// schemaTypes/locale.ts
import { TranslateIcon } from "@sanity/icons";
import { defineField, defineType } from "sanity";
export const localeType = defineType({
name: "locale",
icon: TranslateIcon,
type: "document",
fields: [
defineField({
name: "name",
type: "string",
validation: (r) => r.required(),
}),
defineField({
name: "tag",
type: "string",
description: "IANA tag (en, en-US)",
validation: (r) => r.required(),
}),
defineField({
name: "fallback",
type: "reference",
to: [{ type: "locale" }],
}),
defineField({ name: "default", type: "boolean" }),
],
preview: { select: { title: "name", subtitle: "tag" } },
});
```
**Tip:** Restrict locale editing to admins via Structure by filtering `locale` from non-admin users.
## 4. Choose Your Localization Method
| Content Type | Examples | Recommended Method |
| ----------------------- | --------------------------------------- | ------------------ |
| **Structured** (things) | Products, People, Locations, Categories | Field-level |
| **Presentation** (UI) | Pages, Posts, Components | Document-level |
### Decision Questions
1. **Are fields shared across languages?** → Field-level
2. **Should changes be "global" for all locales?** (e.g., reordering components) → Field-level
3. **Is content mostly the same except regional differences?** → Field-level with PT marks
4. **Need to publish language versions independently?** → Document-level
## 5. Document-Level Localization
Use the **@sanity/document-internationalization** plugin.
```bash
npm install @sanity/document-internationalization
```
### Configuration
```typescript
// sanity.config.ts
import { documentInternationalization } from "@sanity/document-internationalization";
export default defineConfig({
plugins: [
documentInternationalization({
// Fetch from Content Lake
supportedLanguages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
// Document types to localize
schemaTypes: ["post", "page"],
}),
],
});
```
### Add Language Field to Schema
```typescript
// In each schema type listed in schemaTypes
defineField({
name: "language",
type: "string",
readOnly: true,
hidden: true,
});
```
### Initial Value Templates
Pre-set language when creating documents outside the translation UI:
```typescript
// sanity.config.ts
import { template } from "sanity";
export default defineConfig({
// ...
document: {
newDocumentOptions: (prev, { creationContext }) => {
// Filter to only show base language in "New document" menu
// The plugin handles creating translations from there
return prev.filter(
(item) =>
!["post", "page"].includes(item.templateId) ||
item.parameters?.language === "en",
);
},
},
// Initial value templates for each language
templates: (prev) => [
...prev,
template.initial({
id: "post-en",
title: "Post (English)",
schemaType: "post",
parameters: [{ name: "language", type: "string" }],
value: ({ language }) => ({ language }),
}),
],
});
```
### Querying Translated Documents
```groq
// Get document in specific language
*[_type == "post" && language == $locale && slug.current == $slug][0]
// Get all translations via metadata document
*[_type == "translation.metadata" && references($docId)][0] {
translations[] {
_key,
value-> { title, slug, language }
}
}
```
## 6. Localized Singletons (Homepage per Locale)
For singletons like homepages that need a separate document per locale, combine document-level localization with the singleton pattern.
### Schema Definition
```typescript
// schemaTypes/homePage.ts
import { HomeIcon } from "@sanity/icons";
import { defineField, defineType } from "sanity";
export const homePageType = defineType({
name: "homePage",
title: "Home Page",
type: "document",
icon: HomeIcon,
fields: [
defineField({
name: "language",
type: "string",
readOnly: true,
hidden: true,
}),
defineField({ name: "title", type: "string" }),
defineField({ name: "pageBuilder", type: "pageBuilder" }),
// ... other fields
],
preview: {
select: { language: "language" },
prepare({ language }) {
return {
title: "Home Page",
subtitle: language?.toUpperCase() || "No language",
};
},
},
});
```
### Initial Value Templates
Create templates that pre-set the language for each locale:
```typescript
// sanity.config.ts
import { Template, defineConfig } from "sanity";
// Define your supported locales
const LOCALES = [
{ id: "en", title: "English" },
{ id: "fr", title: "French" },
{ id: "de", title: "German" },
];
export default defineConfig({
// ...
templates: (prev) => {
// Create a template for each locale
const homePageTemplates: Template[] = LOCALES.map((locale) => ({
id: `homePage-${locale.id}`,
title: `Home Page (${locale.title})`,
schemaType: "homePage",
parameters: [{ name: "language", type: "string" }],
value: { language: locale.id },
}));
return [...prev, ...homePageTemplates];
},
});
```
### Structure: Localized Singleton Helper
Create a helper to show one singleton per locale in the Structure:
```typescript
// src/structure/index.ts
import { HomeIcon } from "@sanity/icons";
import { StructureBuilder, StructureResolver } from "sanity/structure";
const LOCALES = ["en", "fr", "de"];
function createLocalizedSingleton(
S: StructureBuilder,
typeName: string,
title: string,
icon?: React.ComponentType,
) {
return S.listItem()
.title(title)
.icon(icon)
.child(
S.list()
.title(title)
.items(
LOCALES.map((locale) =>
S.listItem()
.title(`${title} (${locale.toUpperCase()})`)
.icon(icon)
.child(
S.document()
.schemaType(typeName)
.documentId(`${typeName}-${locale}`) // Fixed ID per locale
.title(`${title} (${locale.toUpperCase()})`),
),
),
),
);
}
export const structure: StructureResolver = (S) =>
S.list()
.title("Content")
.items([
// Localized singletons
createLocalizedSingleton(S, "homePage", "Home Page", HomeIcon),
S.divider(),
// Filter localized singletons from default list
...S.documentTypeListItems().filter(
(item) => !["homePage"].includes(item.getId() as string),
),
]);
```
### Querying Localized Singletons
```groq
// Get homepage for specific locale
*[_type == "homePage" && language == $locale][0]{
title,
pageBuilder[]{...}
}
// Or by fixed document ID
*[_id == "homePage-" + $locale][0]{...}
```
### Key Points
- **Fixed IDs:** Use `${typeName}-${locale}` only for localized singletons; let Sanity generate IDs for ordinary localized content
- **Initial Value Templates:** Essential for the "New document" menu to work correctly
- **Structure:** Group all locale versions under one list item for cleaner navigation
- **See also:** `studio-structure.md` for more singleton patterns
## 7. Field-Level Localization
Use **sanity-plugin-internationalized-array** (NOT localized objects — they hit attribute limits).
```bash
npm install sanity-plugin-internationalized-array
```
### Configuration
```typescript
// sanity.config.ts
import { internationalizedArray } from "sanity-plugin-internationalized-array";
export default defineConfig({
plugins: [
internationalizedArray({
languages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
fieldTypes: ["string", "text", "simpleBlockContent"],
}),
],
});
```
### Usage in Schema
```typescript
// The plugin creates types like `internationalizedArrayString`
defineField({
name: "jobTitle",
type: "internationalizedArrayString", // Localized string field
});
```
### Portable Text Localization
Create a reusable block content type, then add it to `fieldTypes`:
```typescript
// schemaTypes/simpleBlockContent.ts
export default defineType({
name: "simpleBlockContent",
type: "array",
of: [
{
type: "block",
styles: [{ title: "Normal", value: "normal" }],
lists: [],
},
],
});
// sanity.config.ts
fieldTypes: ["string", "simpleBlockContent"];
// In your schema
defineField({
name: "bio",
type: "internationalizedArraySimpleBlockContent",
});
```
### Querying Internationalized Arrays
```groq
// Get specific locale value
*[_type == "author"][0] {
"jobTitle": jobTitle[_key == $locale][0].value
}
// With fallback
*[_type == "author"][0] {
"jobTitle": coalesce(
jobTitle[_key == $locale][0].value,
jobTitle[_key == "en"][0].value
)
}
```
## 8. AI-Powered Translation
Use **@sanity/assist** for automated translations.
```bash
npm install @sanity/assist
```
```typescript
// sanity.config.ts
import { assist } from "@sanity/assist";
export default defineConfig({
plugins: [
assist({
translate: {
// For document-level localization
document: {
languageField: "language",
},
// For field-level localization
field: {
languages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
documentTypes: ["author", "category"],
},
},
}),
],
});
```
## 9. UI Enhancement
Use **@sanity/language-filter** to let editors show/hide locales:
```bash
npm install @sanity/language-filter
```
## 10. Frontend URL Best Practices
**Always include locale in the URL** for SEO:
- `yoursite.com/en/my-page` → `yoursite.com/fr/my-page`
- `yoursite.com/my-page` → redirects to default locale
**Avoid:** Having the default locale at root without prefix — causes SEO edge cases.
Use Next.js middleware (or framework equivalent) to redirect paths missing a locale prefix to the default locale.
@@ -0,0 +1,143 @@
---
title: Import HTML to Portable Text
description: Use @portabletext/block-tools with JSDOM to convert HTML content
---
## Import HTML to Portable Text
Use `@portabletext/block-tools` with `JSDOM` to convert HTML from legacy CMSs to Portable Text.
### Setup
```bash
npm install @portabletext/block-tools jsdom
```
### Basic Conversion
```typescript
import { htmlToBlocks } from "@portabletext/block-tools";
import { JSDOM } from "jsdom";
// Get block content type from your schema
const blockContentType = schema.get("blockContent");
const blocks = htmlToBlocks(htmlString, blockContentType, {
parseHtml: (html) => new JSDOM(html).window.document,
});
```
### Custom Deserializers
Handle specific HTML patterns:
```javascript
const blocks = htmlToBlocks(htmlString, blockContentType, {
parseHtml: (html) => new JSDOM(html).window.document,
rules: [
{
deserialize(el, next, block) {
// Custom link handling
if (el.tagName.toLowerCase() === "a") {
return {
_type: "link",
href: el.getAttribute("href"),
blank: el.getAttribute("target") === "_blank",
};
}
// Custom image handling
if (el.tagName.toLowerCase() === "img") {
return {
_type: "image",
// Upload image separately, store reference
_sanityAsset: `image@${el.getAttribute("src")}`,
};
}
return undefined; // Fall through to default handling
},
},
],
});
```
### Pre-Processing HTML
Clean HTML before conversion:
```javascript
function cleanHtml(html) {
const dom = new JSDOM(html);
const doc = dom.window.document;
// Remove layout elements
doc
.querySelectorAll("header, footer, nav, .sidebar")
.forEach((el) => el.remove());
// Extract metadata before processing body
const title = doc.querySelector("title")?.textContent;
const description = doc.querySelector('meta[name="description"]')?.content;
return {
body: doc.body.innerHTML,
metadata: { title, description },
};
}
```
### Image Upload
Don't just link external images—upload them:
```javascript
async function uploadImage(client, imageUrl) {
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
const asset = await client.assets.upload("image", Buffer.from(buffer), {
filename: imageUrl.split("/").pop(),
});
return {
_type: "image",
asset: { _type: "reference", _ref: asset._id },
};
}
```
### Using in a Migration
Wrap this in `defineMigration` for controlled imports:
```typescript
// migrations/import-wordpress-posts/index.ts
import { htmlToBlocks } from "@portabletext/block-tools";
import { create, defineMigration } from "sanity/migrate";
export default defineMigration({
title: "Import WordPress posts",
async *migrate(documents, context) {
const posts = await fetchWordPressPosts(); // Your import source
for (const post of posts) {
const blocks = htmlToBlocks(post.content, blockContentType, {
parseHtml: (html) => new JSDOM(html).window.document,
});
yield create({
_type: "post",
title: post.title,
slug: { _type: "slug", current: post.slug },
legacyId: String(post.id),
body: blocks,
});
}
},
});
```
Let Sanity generate document IDs for ordinary imported content. Add schema fields for legacy identifiers or slugs, then use GROQ lookups against those fields when you need to rerun an import, patch existing documents, or create references between imported records. Set `_id` directly only for singleton documents.
Run with: `sanity migration run import-wordpress-posts --no-dry-run`
Reference: [Schema and Content Migrations](https://www.sanity.io/docs/content-lake/schema-and-content-migrations)
@@ -0,0 +1,59 @@
---
title: Sanity Content Migration Rules
description: Best practices for migrating content (HTML, Markdown) into Sanity Portable Text.
---
# Sanity Content Migration Rules
## Document Identity During Import (Critical)
Let Sanity generate `_id` values for imported documents unless you are intentionally creating a singleton. Do not derive deterministic UUIDs or document IDs from slugs, file paths, legacy IDs, or related document IDs.
- Store legacy identifiers in fields such as `legacyId`, `externalId`, or `slug`.
- Make imports idempotent by looking up existing documents with GROQ before creating or patching them.
- Create relationships by querying the target document and using its real `_id` in a `reference`; do not predict `_ref` values from naming conventions.
- Reserve explicit `_id` values for singleton documents such as `settings`, `homePage`, or localized singleton IDs like `homePage-en`.
## 1. HTML Import (Legacy CMS)
Use `@portabletext/block-tools` with `JSDOM` to convert HTML to Portable Text. This covers setup, custom deserializers, pre-processing, image uploads, and wrapping in `defineMigration`.
**See `migration-html-import.md` for the full guide with working examples.**
## 2. Markdown Import (Static Sites)
Use `@portabletext/markdown` for direct, schema-aware Markdown ↔ Portable Text conversion.
**Recommended: Direct Conversion with `@portabletext/markdown`**
```typescript
import { markdownToPortableText } from "@portabletext/markdown";
const blocks = markdownToPortableText(markdownString);
```
This handles headings, lists, bold, italic, code, links, images, and tables. Use `@portabletext/sanity-bridge` to pass your Sanity schema so only valid types are produced.
**Alternative: Markdown → HTML → Portable Text**
For complex Markdown with non-standard extensions, convert to HTML first, then use `htmlToBlocks` (see above).
1. **Parse:** `marked` or `remark` to convert MD to HTML.
2. **Convert:** Use `htmlToBlocks` from `@portabletext/block-tools`.
> **Note:** `@sanity/block-content-to-markdown` and `@sanity/block-tools` are deprecated. Use `@portabletext/markdown` and `@portabletext/block-tools` instead.
## 3. Image Handling (Universal)
Don't just link to external images. Download them and upload to Sanity Asset Pipeline.
1. **Extract:** Find `<img>` tags or Markdown image syntax.
2. **Download:** Fetch the image buffer.
3. **Upload:** `client.assets.upload('image', buffer)`
4. **Replace:** Return a Sanity Image block with the new asset reference.
## 4. Schema Validation
Ensure your destination schema allows the structures you are importing.
- **Tables:** Need a `table` type (HTML `<table>` or GFM tables).
- **Code:** Need a `code` type (HTML `<pre><code>` or MD code fences).
@@ -0,0 +1,563 @@
---
title: Next.js & Sanity Integration Rules
description: Integration guide for Next.js App Router, Live Content API, and a standalone Sanity Studio.
---
# Next.js & Sanity Integration Rules
Jump to the section that matches the task instead of reading this guide end-to-end.
## Table of Contents
- Architecture patterns
- Data fetching (Live Content API)
- Caching and revalidation
- Visual Editing and clean data
- Studio setup (standalone)
- Draft Mode setup
- Error handling
- Presentation queries
- Pagination pattern
## 1. Architecture Patterns
### Option A: Standalone Studio (Recommended)
**Best for:** All new Next.js projects.
The Studio is its own app, living alongside the Next.js app in the same repo:
```
your-project/
├── studio/ # Sanity Studio (standalone)
└── web/ # Next.js frontend
```
Why standalone instead of embedding the Studio in the Next.js app:
- **Faster dev and builds:** `sanity dev` and `sanity build` run on Vite and are dramatically faster (10-30x) than compiling the Studio through `next dev` / `next build`.
- **Auto-updates:** Standalone Studios receive bugfixes and new features automatically, with no dependency bump or redeploy. Embedded Studios can't auto-update (Next.js does not support ESM with import maps), so every update means bump + deploy.
- **TypeGen watch mode:** With `sanity dev`, TypeGen regenerates types as queries change. Embedded Studios can't hook into `next dev`, so you must re-run `sanity typegen generate` manually after every query edit.
- **Content model independence:** A separate Studio keeps the content model from becoming website-centric and makes collaboration easier.
**Setup:**
- Run both apps side by side in separate terminals: `next dev` (localhost:3000) and `sanity dev` (localhost:3333).
- Add your Next.js app URL to **CORS Origins**: `npx sanity cors add http://localhost:3000 --credentials` (repeat for your production URL), or via [Sanity Manage](https://www.sanity.io/manage).
- See `project-structure.md` rule for detailed structure.
### Option B: Embedded Studio (Not Recommended)
The Studio can be mounted inside the Next.js app at `/app/studio/[[...tool]]/page.tsx` via `next-sanity/studio`. Avoid this for new projects: it slows builds, ties every Studio update to an app deploy, and rules out auto-updates and TypeGen watch mode. For maintaining or migrating an existing embedded Studio, see section 5.
## 2. Data Fetching (Live Content API)
We use `defineLive` (next-sanity v11+) to enable real-time content updates and Visual Editing automatically.
### Setup (`src/sanity/lib/live.ts`)
```typescript
import { defineLive } from "next-sanity";
import { client } from "./client";
export const { sanityFetch, SanityLive } = defineLive({
client: client.withConfig({
apiVersion: "2026-02-01",
}),
serverToken: process.env.SANITY_API_READ_TOKEN,
browserToken: process.env.SANITY_API_READ_TOKEN,
});
```
### Rendering (`src/app/layout.tsx`)
You **must** render `<SanityLive />` in the root layout to enable real-time updates.
```typescript
import { SanityLive } from '@/sanity/lib/live'
import { VisualEditing } from 'next-sanity/visual-editing'
import { draftMode } from 'next/headers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<SanityLive />
{(await draftMode()).isEnabled && <VisualEditing />}
</body>
</html>
)
}
```
## 3. Caching & Revalidation
### Prefer Live Content API (Default)
**Use `defineLive` by default.** It handles fetching, caching, and invalidation automatically. Only implement manual caching when you need fine-grained control.
### When to Use Manual Caching
| Scenario | Approach |
| ---------------------------------------- | ------------------------------- |
| Real-time updates, Visual Editing | `defineLive` (default) |
| Static marketing pages, rarely updated | Time-based revalidation |
| Blog posts, products with frequent edits | Tag-based revalidation |
| Critical accuracy (stock levels, prices) | Path-based + short revalidation |
### Debugging: Enable Fetch Logging
See every fetch with cache HIT/MISS status:
```typescript
// next.config.ts
const nextConfig: NextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};
```
Console output shows cache status:
```text
GET /posts 200 in 39ms
│ GET https://...apicdn.sanity.io/... 200 in 5ms (cache hit)
```
### Sanity CDN vs API
| Setting | Speed | Freshness | Use When |
| --------------- | ------ | -------------------- | -------------------------------- |
| `useCdn: true` | Fast | May have brief delay | Default for all runtime fetches |
| `useCdn: false` | Slower | Guaranteed fresh | `generateStaticParams`, webhooks |
Override per-request:
```typescript
// For static generation, use API directly
export async function generateStaticParams() {
const slugs = await client.withConfig({ useCdn: false }).fetch(SLUGS_QUERY);
return slugs;
}
```
### Manual `sanityFetch` Helper (Advanced)
For manual caching control, create a wrapper:
```typescript
// src/sanity/lib/client.ts
export async function sanityFetch<const QueryString extends string>({
query,
params = {},
revalidate = 60,
tags = [],
}: {
query: QueryString;
params?: QueryParams;
revalidate?: number | false;
tags?: string[];
}) {
return client.fetch(query, params, {
next: {
revalidate: tags.length ? false : revalidate,
tags,
},
});
}
```
### Time-Based Revalidation
Simple and predictable. Good for content that changes infrequently.
```typescript
const posts = await sanityFetch({
query: POSTS_QUERY,
revalidate: 3600, // Revalidate every hour
});
```
**The "Typo Problem":** With time-based only, content authors may wait up to an hour to see changes. Use webhooks for instant updates.
### Path-Based Revalidation
Surgically revalidate specific routes when documents change.
**1. Create API Route:**
```typescript
// src/app/api/revalidate/path/route.ts
import { parseBody } from "next-sanity/webhook";
import { revalidatePath } from "next/cache";
import { type NextRequest, NextResponse } from "next/server";
type WebhookPayload = { path?: string };
export async function POST(req: NextRequest) {
try {
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true, // Add delay to allow CDN to update
);
if (!isValidSignature) {
return new Response("Invalid signature", { status: 401 });
}
if (!body?.path) {
return new Response("Missing path", { status: 400 });
}
revalidatePath(body.path);
return NextResponse.json({ revalidated: body.path });
} catch (err) {
return new Response((err as Error).message, { status: 500 });
}
}
```
**2. Create GROQ-Powered Webhook:**
- URL: `https://yoursite.com/api/revalidate/path`
- Filter: `_type in ["post"]`
- Projection: `{ "path": "/posts/" + slug.current }`
- Add `SANITY_REVALIDATE_SECRET` to webhook and `.env.local`
### Tag-Based Revalidation
"Update once, revalidate everywhere" — best for referenced content.
**1. Tag Your Queries:**
```typescript
// Posts index - revalidate when ANY post, author, or category changes
const posts = await sanityFetch({
query: POSTS_QUERY,
tags: ["post", "author", "category"],
});
// Individual post - more granular, includes slug-specific tag
const post = await sanityFetch({
query: POST_QUERY,
params,
tags: [`post:${params.slug}`, "author", "category"],
});
```
**2. Create API Route:**
```typescript
// src/app/api/revalidate/tag/route.ts
import { parseBody } from "next-sanity/webhook";
import { revalidateTag } from "next/cache";
import { type NextRequest, NextResponse } from "next/server";
type WebhookPayload = { tags: string[] };
export async function POST(req: NextRequest) {
try {
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true,
);
if (!isValidSignature) {
return new Response("Invalid signature", { status: 401 });
}
if (!Array.isArray(body?.tags) || !body.tags.length) {
return new Response("Missing tags", { status: 400 });
}
body.tags.forEach((tag) => revalidateTag(tag));
return NextResponse.json({ revalidated: body.tags });
} catch (err) {
return new Response((err as Error).message, { status: 500 });
}
}
```
**3. Create GROQ-Powered Webhook:**
- URL: `https://yoursite.com/api/revalidate/tag`
- Filter: `_type in ["post", "author", "category"]`
- Projection: `{ "tags": [_type, _type + ":" + slug.current] }`
### Stale Data After Webhook?
Webhooks fire _before_ Sanity CDN updates. If you see stale data:
1. **Add delay** — Pass `true` as third arg to `parseBody`
2. **Or bypass CDN** — Set `useCdn: false` in client config (use sparingly)
## 4. Visual Editing (Stega) & Clean Data
Visual Editing injects invisible characters into strings to enable click-to-edit.
### A. The Golden Rule of Stega
If a string field controls logic (alignment, colors, IDs), you **must** clean it before comparing.
```typescript
import { stegaClean } from "@sanity/client/stega";
export function Layout({ align }: { align: string }) {
// ❌ Bad: Will fail in Edit Mode due to invisible chars
// if (align === 'center') ...
// ✅ Good: Clean the value first
const cleanAlign = stegaClean(align);
return <div className={cleanAlign === 'center' ? 'mx-auto' : ''} />
}
```
### B. Metadata & SEO (Critical)
**Never** let Stega characters leak into `<head>` tags. Always set `stega: false` for metadata fetching.
```typescript
export async function generateMetadata({ params }) {
const { data } = await sanityFetch({
query: SEO_QUERY,
params: await params,
stega: false, // 👈 Critical for SEO
});
return { title: data?.title };
}
```
### C. Static Params
When generating static params, fetch only published content and disable stega.
```typescript
export async function generateStaticParams() {
const { data } = await sanityFetch({
query: SLUGS_QUERY,
perspective: "published", // 👈 No drafts
stega: false,
});
return data;
}
```
## 5. Setup: Studio (Standalone)
Create the Studio as its own app from the repo root — **not inside the Next.js app folder**, where the CLI would switch to its embedded flow:
```bash
npm create sanity@latest -- --project <projectId> --dataset production --template clean --typescript --output-path studio
```
Run it with `npm run dev` inside `studio/` (defaults to http://localhost:3333). For Visual Editing, point the Presentation Tool's `previewUrl.origin` at the Next.js app (see `visual-editing.md`).
### Migrating an Existing Embedded Studio
Embedded Studios (`<NextStudio />` mounted at a route like `/app/studio/[[...tool]]/page.tsx`) keep working, but migrating to a standalone Studio is recommended:
1. Create a standalone Studio folder as above, reusing your existing `projectId` and dataset.
2. Move `sanity.config.ts`, `sanity.cli.ts`, and your schema types into it.
3. Delete the `/app/studio/[[...tool]]/` route from the Next.js app. Keep `next-sanity` — the app still needs it for fetching, Live Content, and Visual Editing.
4. Add the app's URLs to CORS origins and set the Presentation Tool's `previewUrl.origin` to the app's URL.
## 6. Setup: Draft Mode
Enable Presentation Tool and Visual Editing by setting up a draft mode route.
**`src/app/api/draft-mode/enable/route.ts`:**
```typescript
import { defineEnableDraftMode } from "next-sanity/draft-mode";
import { client } from "@/sanity/lib/client";
import { token } from "@/sanity/lib/token";
// Helper to get token
export const { GET } = defineEnableDraftMode({
client: client.withConfig({ token }),
});
```
## 7. Error Handling
Use `notFound()` for missing documents. Common errors:
| Error | Cause | Solution |
| ------------------ | --------------------- | ---------------------------------- |
| 401 Unauthorized | Invalid/missing token | Check `SANITY_API_READ_TOKEN` |
| 403 Forbidden | CORS not configured | Add URL to CORS origins |
| Query syntax error | Invalid GROQ | Test in Vision plugin first |
| Empty result | Wrong filter/params | Log params, check `_type` spelling |
```typescript
import { notFound } from 'next/navigation'
export default async function PostPage({ params }: Props) {
const { data } = await sanityFetch({ query: POST_QUERY, params: await params })
if (!data) notFound()
return <Post data={data} />
}
```
## 8. Presentation Queries (`usePresentationQuery`)
For faster live editing in the Presentation Tool, use `usePresentationQuery` to fetch only the specific block being edited, rather than re-rendering the entire page.
### Why Use This
- **Without:** Editing a hero title re-fetches the whole page, re-renders all blocks
- **With:** Only the hero block re-fetches and re-renders
This is especially valuable for pages with many Page Builder blocks or complex Portable Text.
### Basic Pattern
```typescript
'use client'
import { usePresentationQuery } from 'next-sanity/hooks'
import { HERO_PRESENTATION_QUERY } from '@/sanity/lib/queries'
type HeroProps = {
_key: string
documentId: string
title: string
subtitle?: string
// ... other initial props from page query
}
export function Hero({ _key, documentId, title, subtitle, ...rest }: HeroProps) {
// Fetch block-specific data for faster updates in Presentation Tool
const { data } = usePresentationQuery({
query: HERO_PRESENTATION_QUERY,
params: { documentId, blockKey: _key },
})
// Use presentation data if available, fallback to initial server props
const blockData = data?.heroBlock || { title, subtitle, ...rest }
return (
<section>
<h1>{blockData.title}</h1>
{blockData.subtitle && <p>{blockData.subtitle}</p>}
</section>
)
}
```
### The Presentation Query
Create a query that targets the specific block by `_key`:
```typescript
// queries.ts
export const HERO_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
_id,
_type,
"heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
title,
subtitle,
image,
theme,
// Include all fields the component needs
}
}
`);
```
### Passing Document Context
Your PageBuilder component needs to pass `documentId` to each block:
```typescript
export function PageBuilder({ content, documentId }: { content: Block[]; documentId: string }) {
return (
<main>
{content.map((block) => {
switch (block._type) {
case "hero":
return <Hero key={block._key} documentId={documentId} {...block} />
// ... other blocks
}
})}
</main>
)
}
```
### For Portable Text Blocks
The same pattern works for custom blocks inside Portable Text:
```typescript
export const PTE_IMAGE_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
"pteImageBlock": body[_key == $blockKey && _type == "pteImage"][0]{
image,
caption,
alt
}
}
`);
```
**See also:** `visual-editing.md` for the conceptual overview and `page-builder.md` for full Page Builder patterns.
## 9. Pagination Pattern
For listing pages with many entries, use offset-based pagination with a count query.
### Queries
```typescript
// Paginated listing
export const ARTICLES_QUERY = defineQuery(`
*[_type == "article" && defined(slug.current)]
| order(date desc) [$start...$end] {
_id, title, "slug": slug.current, date
}
`);
// Total count for pagination UI
export const ARTICLES_COUNT_QUERY = defineQuery(`
count(*[_type == "article" && defined(slug.current)])
`);
```
### Listing Page
```typescript
const ENTRIES_PER_PAGE = 10;
export default async function BlogPage({
searchParams
}: {
searchParams: Promise<{ page?: string }>
}) {
const { page: pageParam } = await searchParams;
const page = parseInt(pageParam || "1");
const start = (page - 1) * ENTRIES_PER_PAGE;
const end = start + ENTRIES_PER_PAGE;
const [{ data: articles }, { data: total }] = await Promise.all([
sanityFetch({ query: ARTICLES_QUERY, params: { start, end } }),
sanityFetch({ query: ARTICLES_COUNT_QUERY })
]);
const totalPages = Math.ceil(total / ENTRIES_PER_PAGE);
return (
<main>
{articles.map(article => (
<ArticleCard key={article._id} article={article} />
))}
<Pagination current={page} total={totalPages} />
</main>
);
}
```
@@ -0,0 +1,171 @@
---
title: Nuxt & Sanity Integration Rules
description: Integration guide for Nuxt, including @nuxtjs/sanity, visual editing, and data fetching.
---
# Nuxt & Sanity Integration Rules
## 1. Setup & Configuration
### Scaffold a new Nuxt app
```bash
npm create nuxt@latest my-app -- -t ui -M "" --packageManager npm --no-gitInit
cd my-app
```
`-t ui` selects the Nuxt UI starter. `-M ""` skips the interactive module-selection prompt (empty string = no extra modules). `--packageManager npm` and `--no-gitInit` suppress the other two prompts so the scaffold runs end-to-end without input.
### Installation
```bash
npx nuxi@latest module add sanity
```
`nuxi module add sanity` resolves to the official `@nuxtjs/sanity` module and registers it in `nuxt.config.ts` automatically. The module bundles `@sanity/client`, `@sanity/visual-editing`, `@portabletext/vue`, and `groq` as direct dependencies — no separate installs needed.
`groq` and `defineQuery` are also **auto-imported** by the module, so you can use them in `.vue` files without an `import` statement.
For manual image-URL building (an alternative to the auto-registered `<SanityImage>` component), add `@sanity/image-url`:
```bash
npm install @sanity/image-url
```
### What the module auto-imports
**Composables** (use directly in `<script setup>`, no imports needed):
- `useSanity()` — get the client and its config
- `useSanityQuery()` / `useLazySanityQuery()` — reactive query helpers
- `useSanityConfig()` — read the resolved module config
- `useSanityPerspective()`, `useSanityPreviewPerspective()`, `useSanityPreviewEnvironment()` — perspective helpers for drafts/preview
- `useSanityVisualEditingState()`, `useIsSanityLivePreview()`, `useIsSanityPresentationTool()` — visual-editing state helpers
**GROQ helpers** (template tags): `groq`, `defineQuery`
**Components** (use directly in `<template>`):
- `<SanityContent>` — Portable Text renderer (uses `@portabletext/vue` internally; prop is `:value`)
- `<SanityImage>` — image renderer; takes an `assetId` (the image asset's `_ref`); upgrades to `<NuxtImg>` automatically when `@nuxt/image` is installed
- `<SanityFile>` — file renderer
### Configuration (`nuxt.config.ts`)
```typescript
export default defineNuxtConfig({
modules: ["@nuxtjs/sanity"],
sanity: {
projectId: process.env.NUXT_SANITY_PROJECT_ID,
dataset: process.env.NUXT_SANITY_DATASET,
apiVersion: "2026-05-15",
// Live Visual Editing Configuration
visualEditing: {
studioUrl: process.env.NUXT_SANITY_STUDIO_URL,
token: process.env.NUXT_SANITY_API_READ_TOKEN, // Required for fetching drafts
stega: true, // Enable stega for visual editing
mode: "live-visual-editing", // Default: enables live updates
},
},
});
```
**Important:** Don't enable the `minimal` client if you want the full feature set (composables, components, visual editing).
## 2. Data Fetching
### `useSanityQuery`
Use the composable for reactive fetching. It handles preview state automatically when `visualEditing` is configured. `groq` and `defineQuery` are auto-imported — use either.
```vue
<!-- app/pages/posts.vue -->
<script setup lang="ts">
const query = groq`*[_type == "post" && defined(slug.current)]{ _id, title, slug }`
const { data: posts } = await useSanityQuery<Array<{ _id: string; title?: string; slug?: { current?: string } }>>(query)
</script>
<template>
<ul>
<li v-for="post in posts || []" :key="post._id">
<NuxtLink :to="`/${post.slug?.current}`">{{ post.title }}</NuxtLink>
</li>
</ul>
</template>
```
### Dynamic Routes (`[slug].vue`)
Pull the slug off `useRoute()` and pass it as a query parameter. The `<SanityContent>` component renders Portable Text — note the prop is `value`, not `blocks` (renamed in v2).
```vue
<!-- app/pages/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const query = groq`*[_type == "post" && slug.current == $slug][0]{ _id, title, body }`
const { data: post } = await useSanityQuery<{ _id: string; title?: string; body?: unknown[] }>(
query,
{ slug: route.params.slug }
)
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<SanityContent v-if="post.body" :value="post.body" />
</article>
</template>
```
## 3. Visual Editing (Live Preview)
### Automatic Setup
When `visualEditing` is configured in `nuxt.config.ts`, the module handles:
1. Injecting the Visual Editing overlays.
2. Refreshing data when content changes in the Studio.
3. Enabling Stega encoding.
### Handling Stega in Logic
If you use stega-encoded strings in logic (e.g. `v-if="post.layout === 'full'"`), you must clean them. `stegaClean` is exported from `@sanity/client/stega` (a transitive of `@nuxtjs/sanity`, so no separate install).
```typescript
import { stegaClean } from "@sanity/client/stega";
const layout = computed(() => stegaClean(props.layout));
```
## 4. Components
### Portable Text — `<SanityContent>`
The module auto-registers `<SanityContent>`. Don't install `@portabletext/vue` separately; it's a direct dep of the module.
```vue
<SanityContent :value="post.body" />
```
For custom blocks/marks, pass `:components`:
```vue
<SanityContent :value="post.body" :components="{ block: { h2: MyH2 } }" />
```
### Images — two options
**Option A — `<SanityImage>` (recommended).** Auto-registered. Takes the asset's `_ref` (the `assetId`) and builds the URL via the module's resolved projectId/dataset. If `@nuxt/image` is installed, it transparently upgrades to `<NuxtImg>` for responsive sizing.
```vue
<SanityImage :asset-id="post.mainImage.asset._ref" width="800" />
```
**Option B — `@sanity/image-url` builder.** Install `@sanity/image-url` separately and build URLs manually. Useful when you need fine-grained control (hotspot/crop, format negotiation, srcset).
```typescript
import imageUrlBuilder from "@sanity/image-url";
const builder = imageUrlBuilder(useSanity().client);
// builder.image(source).width(800).url()
```
@@ -0,0 +1,329 @@
---
title: "Sanity Page Builder Patterns"
description: Patterns for Sanity Page Builder arrays, block components, and live editing.
---
# Sanity Page Builder Patterns
This guide covers **Page Builder** patterns—arrays of block objects that allow content teams to compose flexible page layouts. For Portable Text (rich text within documents), see `portable-text.md`.
## 1. What is a Page Builder?
A page builder is an **array of objects** (`pageBuilder[]`) that allows content teams to compose pages from reusable blocks without developer intervention.
**When to use:**
- Flexible layouts needed (marketing pages, landing pages)
- Content can be reordered
- Different components on different pages
**When NOT to use:**
- Rigid, formulaic content (blog posts, product pages)
- Highly structured data that doesn't change layout
- Rich text within a document body—use Portable Text instead
## 2. Schema Organization
### Directory Structure
```
schemaTypes/
├── blocks/ # Page builder blocks (objects)
│ ├── heroType.ts
│ ├── featuresType.ts
│ └── faqsType.ts
├── pageBuilderType.ts # The array definition
└── pageType.ts # Document using the page builder
```
### Objects vs References
| Use **Objects** | Use **References** |
| ------------------------------ | -------------------------------- |
| Content is unique to this page | Content reused across many pages |
| Simpler queries | Needs central management |
| Default choice | FAQs, CTAs, testimonials |
**Rule:** Use references sparingly. Most blocks should be objects.
### Page Builder Array
```typescript
// pageBuilderType.ts
import { defineArrayMember, defineType } from "sanity";
export const pageBuilderType = defineType({
name: "pageBuilder",
type: "array",
of: [
defineArrayMember({ type: "hero" }),
defineArrayMember({ type: "splitImage" }),
defineArrayMember({ type: "features" }),
defineArrayMember({ type: "faqs" }),
],
options: {
insertMenu: {
views: [
// Optional: Show visual thumbnails in the insert menu grid
{
name: "grid",
previewImageUrl: (type) => `/block-previews/${type}.png`,
},
],
},
},
});
```
### Block Preview Pattern
Every block should have consistent previews:
```typescript
import { BlockContentIcon } from "@sanity/icons";
import { defineType } from "sanity";
export const splitImageType = defineType({
name: "splitImage",
type: "object",
icon: BlockContentIcon,
fields: [
/* ... */
],
preview: {
select: { title: "title", media: "image" },
prepare({ title, media }) {
return {
title: title || "Untitled",
subtitle: "Split Image", // Block type name
media: media ?? BlockContentIcon, // Fallback to icon
};
},
},
});
```
## 3. Querying Page Builders
Expand references only for blocks that need them:
```groq
*[_type == "page" && slug.current == $slug][0]{
...,
content[]{
...,
_type == "faqs" => {
...,
faqs[]-> // Expand only FAQ references
}
}
}
```
## 4. Rendering Page Builders
### TypeScript Typing
Use `Extract` to type individual blocks from the query result:
```typescript
import { PAGE_QUERYResult } from "@/sanity/types";
type HeroProps = Extract<
NonNullable<NonNullable<PAGE_QUERYResult>["content"]>[number],
{ _type: "hero" }
>;
export function Hero({ title, image }: HeroProps) {
// Fully typed!
}
```
### Switch-Based Rendering
```typescript
export function PageBuilder({ content }: { content: Block[] }) {
if (!Array.isArray(content)) return null;
return (
<main>
{content.map((block) => {
switch (block._type) {
case "hero":
return <Hero key={block._key} {...block} />;
case "features":
return <Features key={block._key} {...block} />;
case "splitImage":
return <SplitImage key={block._key} {...block} />;
default:
return <div key={block._key}>Unknown: {block._type}</div>;
}
})}
</main>
);
}
```
**Always use `_key` for React keys:**
```typescript
// Breaks Visual Editing and causes hydration issues
{items.map((item, i) => <Component key={i} {...item} />)}
// Always use Sanity's _key
{items.map((item) => <Component key={item._key} {...item} />)}
```
### Cleaning Values for Logic
Use `stegaClean` when block fields control rendering logic:
```typescript
import { stegaClean } from "next-sanity";
function SplitImage({ orientation, title, image }) {
return (
<section data-orientation={stegaClean(orientation) || "imageLeft"}>
{/* ... */}
</section>
);
}
```
## 5. Presentation Queries for Live Editing (Next.js)
For faster live updates in the Presentation Tool, use **presentation queries** that fetch only the specific block being edited, rather than re-fetching the entire page.
> **Note:** This pattern uses `usePresentationQuery` from `next-sanity/hooks`. For other frameworks, check your loader package for equivalent functionality.
### The Pattern
1. **Create a block-specific presentation query:**
```typescript
// queries.ts
export const HERO_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
_id,
_type,
"heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
title,
subtitle,
image,
// ... all fields the component needs
}
}
`);
```
2. **Use `usePresentationQuery` in your component:**
```typescript
'use client'
import { usePresentationQuery } from 'next-sanity/hooks'
import { HERO_PRESENTATION_QUERY } from '@/sanity/lib/queries'
type HeroProps = {
_key: string
documentId: string
// ... initial props from page query
}
export function Hero({ _key, documentId, ...initialProps }: HeroProps) {
// Fetch block-specific data for faster updates
const { data } = usePresentationQuery({
query: HERO_PRESENTATION_QUERY,
params: { documentId, blockKey: _key },
})
// Use presentation data if available, fallback to initial props
const blockData = data?.heroBlock || initialProps
return (
<section>
<h1>{blockData.title}</h1>
{/* ... */}
</section>
)
}
```
### Why This Is Faster
- **Without:** Editing a field triggers a full page re-render with all blocks
- **With:** Only the specific block re-renders with its targeted query
This pattern is especially valuable for pages with many blocks or complex nested data.
**Note:** See `nextjs.md` for more details on `usePresentationQuery` and `visual-editing.md` for the conceptual overview.
## 6. Page Builder Pitfalls
| Pitfall | Solution |
| ------------------------- | ------------------------------------------------------------ |
| Too many block variations | Split into separate blocks if >2 variants |
| Paradox of choice | Limit blocks per document type |
| Overusing references | Default to objects; references only for truly shared content |
| Unused blocks accumulate | Prune regularly; see deprecation patterns |
| Inconsistent previews | Always set title, subtitle (block name), and media/icon |
## 7. Component Alignment Pattern
Map Sanity "alignment" fields (usually string/select) to CSS classes using utility functions.
**Schema:**
```typescript
defineField({
name: "align",
type: "string",
options: { list: ["left", "center", "right"], layout: "radio" },
});
```
**Implementation (Utility):**
```typescript
import { stegaClean } from "@sanity/client/stega";
export function getTextAlign(align?: string) {
// CLEAN the value before switching!
switch (stegaClean(align)) {
case "left":
return "text-left";
case "right":
return "text-right";
default:
return "text-center";
}
}
```
## 8. Semantic Heading Levels
**Rule:** Do NOT store heading levels (h1, h2) in Sanity schema options. Determine them dynamically in the frontend to ensure accessibility.
**Bad Schema:**
```typescript
// Don't do this
{ name: 'level', type: 'string', options: { list: ['h1', 'h2'] } }
```
**Good Component:**
Pass a `semanticLevel` prop based on the component's context/nesting.
```typescript
type Props = {
block: HeroBlock;
level?: 'h1' | 'h2' | 'h3'; // Default to h2 if undefined
}
export default function Section({ block, level = 'h2' }: Props) {
const Tag = level;
return <Tag>{block.title}</Tag>;
}
```
_Note: For Image patterns, see `image.md`. For Portable Text patterns, see `portable-text.md`._
@@ -0,0 +1,384 @@
---
title: "Sanity Portable Text Rules"
description: Portable Text (Rich Text) rendering and custom component creation for React/Next.js.
---
# Sanity Portable Text Rules
Portable Text is Sanity's rich text format, used for content like article bodies (`body[]`). This guide covers rendering and creating custom PTE components.
**Note:** For page-level layout blocks (`pageBuilder[]`), see `page-builder.md`.
## 1. The Component
Use the `PortableText` component from `next-sanity` (or `@portabletext/react`).
```typescript
import { PortableText } from "next-sanity";
// or import { PortableText } from "@portabletext/react";
export function Content({ value }: { value: any }) {
return <PortableText value={value} components={components} />;
}
```
## 2. Custom Components (`components` prop)
**Always** define a typed components object to handle custom blocks, marks, and list styles.
```typescript
import { PortableTextComponents } from "next-sanity";
const components: PortableTextComponents = {
// 1. Block styles (paragraphs, headings)
block: {
h1: ({ children }) => <h1 className="text-4xl font-bold">{children}</h1>,
h2: ({ children }) => <h2 className="text-3xl font-bold">{children}</h2>,
blockquote: ({ children }) => <blockquote className="border-l-4 pl-4">{children}</blockquote>,
},
// 2. Custom types (non-text blocks like images, videos)
types: {
image: ({ value }) => <SanityImage value={value} />,
callToAction: ({ value }) => <Button href={value.url}>{value.text}</Button>,
},
// 3. Marks (inline decorators and annotations)
marks: {
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
link: ({ children, value }) => {
const rel = !value.href.startsWith("/") ? "noreferrer noopener" : undefined;
return <a href={value.href} rel={rel} className="underline text-blue-600">{children}</a>;
},
},
// 4. Lists
list: {
bullet: ({ children }) => <ul className="list-disc ml-4">{children}</ul>,
number: ({ children }) => <ol className="list-decimal ml-4">{children}</ol>,
},
};
```
## 3. Component Categories
Portable Text has three types of custom components, each with different patterns:
| Type | Examples | Pattern |
| ---------------- | -------------------------- | --------------------------------- |
| **Block styles** | h1, h2, blockquote, normal | Text blocks with `children` prop |
| **Custom types** | image, video, callToAction | Non-text blocks with `value` prop |
| **Marks** | link, strong, productRef | Inline annotations wrapping text |
## 4. Creating Block Style Components
Block styles are text blocks like headings and paragraphs. For simple styling, inline components work fine:
```typescript
block: {
h2: ({ children }) => <h2 className="mt-8 mb-4 text-3xl font-bold">{children}</h2>,
normal: ({ children }) => <p className="mb-4 leading-relaxed">{children}</p>,
}
```
### With Visual Editing Support
For live editing in the Presentation Tool, block style components may need **both** a client and server version:
```typescript
// Heading2.tsx (Server - simple SSR for production)
export function Heading2({ children }: { children: React.ReactNode }) {
return <h2 className="mt-8 mb-4 text-3xl font-bold">{children}</h2>;
}
// Heading2Client.tsx (Client - for visual editing context)
'use client'
export function Heading2Client({ children, value }: { children: React.ReactNode; value: any }) {
// Can access block data via `value` for advanced patterns
return <h2 className="mt-8 mb-4 text-3xl font-bold">{children}</h2>;
}
```
Use `useIsPresentationTool` to conditionally render the client version:
```typescript
import { useIsPresentationTool } from 'next-sanity/hooks'
function Heading2Wrapper(props) {
const isPresentationTool = useIsPresentationTool()
if (isPresentationTool) {
return <Heading2Client {...props} />
}
return <Heading2 {...props} />
}
```
## 5. Creating Custom Type Components
Custom types are non-text blocks like images, videos, or CTAs embedded in rich text.
### Schema Definition
```typescript
// schemaTypes/blocks/pteImageBlock.ts
import { defineField, defineType } from "sanity";
export const pteImageBlock = defineType({
name: "pteImage",
title: "Image",
type: "object",
fields: [
defineField({ name: "image", type: "image", options: { hotspot: true } }),
defineField({ name: "caption", type: "string" }),
defineField({
name: "alt",
type: "string",
validation: (r) => r.required(),
}),
],
preview: {
select: { title: "caption", media: "image" },
},
});
```
### Register in Body Schema
```typescript
defineField({
name: "body",
type: "array",
of: [
{ type: "block" }, // Standard text
{ type: "pteImage" }, // Custom image block
{ type: "pteVideo" }, // Custom video block
],
});
```
### Frontend Component
```typescript
// PteImageComponent.tsx
'use client'
type PteImageProps = {
value: {
_key: string
image: any
caption?: string
alt: string
}
}
export function PteImageComponent({ value }: PteImageProps) {
if (!value.image) return null
return (
<figure className="my-8">
<SanityImage value={value.image} alt={value.alt} />
{value.caption && (
<figcaption className="text-sm text-gray-600 mt-2">{value.caption}</figcaption>
)}
</figure>
)
}
// Register in components
const components: PortableTextComponents = {
types: {
pteImage: PteImageComponent,
},
}
```
## 6. Creating Mark Components
Marks are inline annotations that wrap text—links, highlights, or custom references.
### Schema Definition (Annotation)
```typescript
// In your block configuration
defineField({
name: "body",
type: "array",
of: [
{
type: "block",
marks: {
decorators: [
{ title: "Strong", value: "strong" },
{ title: "Emphasis", value: "em" },
{ title: "Highlight", value: "highlight" },
],
annotations: [
{
name: "link",
type: "object",
title: "Link",
fields: [
{ name: "href", type: "url", title: "URL" },
{
name: "openInNewTab",
type: "boolean",
title: "Open in new tab",
},
],
},
{
name: "productRef",
type: "object",
title: "Product Reference",
fields: [
{ name: "product", type: "reference", to: [{ type: "product" }] },
],
},
],
},
},
],
});
```
### Frontend Component
```typescript
// LinkMark.tsx
type LinkMarkProps = {
children: React.ReactNode
value: {
href: string
openInNewTab?: boolean
}
}
export function LinkMark({ children, value }: LinkMarkProps) {
const { href, openInNewTab } = value
const target = openInNewTab ? '_blank' : undefined
const rel = openInNewTab ? 'noopener noreferrer' : undefined
return (
<a href={href} target={target} rel={rel} className="text-blue-600 underline">
{children}
</a>
)
}
// Register in components
const components: PortableTextComponents = {
marks: {
link: LinkMark,
highlight: ({ children }) => <mark className="bg-yellow-200">{children}</mark>,
},
}
```
## 7. Presentation Queries for PTE Blocks
For faster live editing of custom PTE blocks, use presentation queries that fetch only the specific block:
```typescript
// queries.ts
export const PTE_IMAGE_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
_id,
_type,
"pteImageBlock": body[_key == $blockKey && _type == "pteImage"][0]{
_key,
image,
caption,
alt
}
}
`);
```
Then in your component:
```typescript
"use client";
import { usePresentationQuery } from "next-sanity/hooks";
export function PteImageComponent({
value,
documentId,
}: {
value: any;
documentId?: string;
}) {
const { data } = usePresentationQuery({
query: PTE_IMAGE_PRESENTATION_QUERY,
params: { documentId, blockKey: value._key },
});
const blockData = data?.pteImageBlock || value;
// ... render with blockData
}
```
**Note:** You'll need to pass `documentId` through to your PTE components. See `visual-editing.md` for context patterns.
## 8. GROQ Fragment for PTE
When querying documents with Portable Text, expand custom blocks:
```groq
*[_type == "article" && slug.current == $slug][0]{
...,
body[]{
...,
_type == "pteImage" => {
...,
"imageUrl": image.asset->url
},
_type == "pteVideo" => {
...,
video->{ title, url }
}
}
}
```
## 9. Stega and Visual Editing
When Visual Editing is enabled, text content contains invisible stega characters for click-to-edit functionality.
**For text rendering:** Let stega characters pass through—they enable overlays:
```typescript
// Good - stega preserved for click-to-edit
<h2>{children}</h2>
```
**For logic/comparisons:** Clean the values first:
```typescript
import { stegaClean } from '@sanity/client/stega'
// Clean before using in logic
const cleanedStyle = stegaClean(block.style)
if (cleanedStyle === 'h2') { ... }
```
## 10. Type Safety
When using TypeGen, the Portable Text value usually has a complex generated type. You can often use `any` or `PortableTextBlock[]` for the _prop_, but cast specific blocks if needed.
```typescript
import { PortableTextBlock } from "next-sanity";
type Props = {
value: PortableTextBlock[];
};
```
## 11. Best Practices
- **Tailwind Typography:** For simple blogs, wrap `<PortableText />` in a `<div className="prose">` (from `@tailwindcss/typography`) instead of manually styling every block.
- **Handling Nulls:** Always check if `value` exists and is an array before rendering.
- **Keys:** The `PortableText` component handles React keys automatically using the `_key` from Sanity. Do not add keys manually.
- **Separate from Page Builder:** PTE blocks live in `body[]` (rich text fields), not `pageBuilder[]` (page layout). Keep these patterns separate.
@@ -0,0 +1,98 @@
---
title: Sanity Project Structure
description: Project structure patterns for Sanity projects including standalone Studio and monorepo setups.
---
# Sanity Project Structure
## Standalone Studio
Best for content-only projects, API-first architectures, or when frontend is managed separately.
```
your-project/
├── schemaTypes/
│ ├── index.ts
│ ├── documents/
│ ├── objects/
│ └── blocks/
├── sanity.config.ts
├── sanity.cli.ts
└── package.json
```
**Use cases:**
- Content modeling with MCP/AI tools (no frontend needed)
- Headless CMS with external consumers
- Prototyping and content design
## Monorepo (Recommended with a frontend)
Best for most projects pairing Sanity with a Next.js (or other framework) app. The Studio stays standalone — Vite-based dev/builds, auto-updates, TypeGen watch mode — while living in the same repo as the frontend.
```
your-project/
├── studio/ # Sanity Studio (standalone)
│ ├── schemaTypes/
│ │ ├── index.ts
│ │ ├── documents/
│ │ ├── objects/
│ │ └── blocks/
│ ├── sanity.config.ts
│ ├── sanity.cli.ts # CLI + TypeGen configuration
│ └── package.json
└── web/ # Next.js (or other framework)
├── src/
│ ├── app/
│ └── sanity/
│ ├── client.ts
│ ├── live.ts # defineLive setup
│ └── queries.ts
├── sanity.types.ts # Generated types (from TypeGen)
└── package.json
```
No workspace tooling is required — each app manages its own dependencies. For larger repos, the same shape works under `apps/` with npm or pnpm workspaces.
**Setup:**
1. Add the web app URL to CORS origins: `npx sanity cors add http://localhost:3000 --credentials` (or via [Sanity Manage](https://www.sanity.io/manage))
2. Configure `typegen` in `studio/sanity.cli.ts` to read queries from `../web` and output types to `../web/sanity.types.ts` (see `typegen.md`)
3. Optionally add a root `package.json` with scripts that run both dev servers
## Embedded Studio (Legacy — Not Recommended)
Older Next.js projects may mount the Studio inside the app at `src/app/studio/[[...tool]]/page.tsx`, with `sanity.config.ts` in the app root. This still works but is no longer recommended: it slows builds, ties Studio updates to app deploys, and rules out auto-updates and TypeGen watch mode. See `nextjs.md` for the rationale and migration steps.
## File Naming Conventions
- **kebab-case** for all files: `user-profile.ts`, `hero-block.ts`
- `.ts` for schemas/utilities, `.tsx` for React components
- Each schema exports a named const matching filename
## Schema Directory Structure
```
schemaTypes/
├── index.ts # Exports all types
├── documents/ # Standalone content types
│ ├── post.ts
│ └── author.ts
├── objects/ # Embeddable/reusable types
│ ├── seo.ts
│ └── link.ts
├── blocks/ # Portable Text blocks
│ ├── hero.ts
│ └── callout.ts
└── shared/ # Shared field definitions
└── seoFields.ts
```
## Key Files
| File | Purpose |
| ------------------ | ------------------------------------------------------- |
| `sanity.config.ts` | Studio configuration (plugins, schema, structure) |
| `sanity.cli.ts` | CLI configuration (project ID, dataset, TypeGen config) |
| `structure.ts` | Custom desk structure |
@@ -0,0 +1,276 @@
---
title: React Router (Remix) & Sanity Integration Rules
description: Integration guide for React Router v7 (and Remix v2) with Sanity, including loaders and visual editing.
---
# React Router (Remix) & Sanity Integration Rules
## Version Note
The primary examples below use **React Router v7** (the current shape — Remix v2 was renamed to React Router v7 starting with the v7 release). Import paths and the route-types file (`./+types/<route>`) come from the `react-router` package and the framework's typegen.
If you are on the older **Remix v2** stack, the integration shape is identical; only the import paths differ:
| React Router v7 | Remix v2 |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `react-router` | `@remix-run/node` / `@remix-run/react` |
| `import type { Route } from "./+types/<route>"` | `import type { LoaderFunctionArgs } from "@remix-run/node"` + `useLoaderData<typeof loader>()` |
| `react-router.config.ts` | `remix.config.js` |
## 1. Setup & Client Pattern
### Scaffold a new React Router v7 app
```bash
npx create-react-router@latest my-app -y
cd my-app
npm install @sanity/client @sanity/react-loader @sanity/visual-editing @portabletext/react groq
```
`-y` accepts defaults. The Sanity packages cover server loaders (`@sanity/react-loader`, `@sanity/client`), live preview (`@sanity/visual-editing`), Portable Text rendering (`@portabletext/react`), and typed queries (`groq`).
To support both server-side fetching and client-side live previews, use the **Split Loader Pattern**.
### A. Environment Variables
React Router runs on Vite. **Any module reachable from a route component gets bundled into the client** — `process.env` doesn't exist there and will throw `ReferenceError: process is not defined` on client-side route transitions (SSR will still work, which makes this trap easy to miss).
Split publishable values from secrets:
- **Publishable** (`projectId`, `dataset`, `apiVersion`, `studioUrl`): prefix with `VITE_` and read via `import.meta.env`. Safe to import from anywhere.
- **Secrets** (read tokens, webhook secrets): keep unprefixed and read via `process.env` **only inside `*.server.ts` files**. Never re-export them from a shared module.
`.env`:
```
VITE_SANITY_PROJECT_ID=your-project-id
VITE_SANITY_DATASET=production
VITE_SANITY_API_VERSION=2026-02-01
VITE_SANITY_STUDIO_URL=http://localhost:3333
SANITY_API_READ_TOKEN=your-read-token
```
`app/sanity/env.ts` — browser-safe, publishable values only:
```typescript
export const projectId = import.meta.env.VITE_SANITY_PROJECT_ID!;
export const dataset = import.meta.env.VITE_SANITY_DATASET!;
export const apiVersion =
import.meta.env.VITE_SANITY_API_VERSION ?? "2026-02-01";
export const studioUrl = import.meta.env.VITE_SANITY_STUDIO_URL;
```
### B. Shared Loader (`app/sanity/loader.ts`)
Defines the store config (SSR enabled, client deferred).
```typescript
import { createQueryStore } from "@sanity/react-loader";
export const { loadQuery, setServerClient, useQuery, useLiveMode } =
createQueryStore({ client: false, ssr: true });
```
### C. Server Loader (`app/sanity/loader.server.ts`)
Initializes the server client. Read the token directly from `process.env` here — do **not** import it from `env.ts`, or it will leak into the client bundle the moment any client-reachable module touches `env.ts`.
```typescript
import { createClient } from "@sanity/client";
import { apiVersion, dataset, projectId, studioUrl } from "./env";
import { loadQuery, setServerClient } from "./loader";
const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
token: process.env.SANITY_API_READ_TOKEN,
stega: {
// Stega encodes invisible markers into string fields for click-to-edit
// overlays in the Presentation tool. Those markers can leak into copy/paste,
// screen readers, and some downstream renderers, so only enable when actually
// previewing — gate on an env var that's only set in preview environments.
enabled: Boolean(studioUrl),
studioUrl,
},
});
setServerClient(client);
export { loadQuery };
```
### D. Browser-safe Client + Image URL Builder (`app/sanity/client.ts`, `app/sanity/image.ts`)
Anything used by a route component runs in the browser too. Build a separate publishable-only client for things like the image URL builder:
```typescript
// app/sanity/client.ts
import { createClient } from "@sanity/client";
import { apiVersion, dataset, projectId } from "./env";
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
});
```
```typescript
// app/sanity/image.ts
import imageUrlBuilder from "@sanity/image-url";
import { client } from "./client";
const builder = imageUrlBuilder(client);
export const urlFor = (source: Parameters<typeof builder.image>[0]) =>
builder.image(source);
```
Install `@sanity/image-url` if you'll render images:
```bash
npm install @sanity/image-url
```
### E. Queries (`app/sanity/queries.ts`)
Keep query definitions in one place so route loaders, components, and TypeGen all read the same source.
```typescript
import { defineQuery } from "groq";
export const POSTS_QUERY = defineQuery(
`*[_type == "post" && defined(slug.current)] | order(_createdAt desc){
_id, title, slug
}`,
);
export const POST_QUERY = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{
_id, title, body, image
}`,
);
```
## 2. Data Fetching (Loaders)
Use `loadQuery` from your **server** file in route loaders. Import the generated `Route` type from `./+types/<route>` — React Router writes one type module per route file.
```typescript
// app/routes/home.tsx
import { loadQuery } from "~/sanity/loader.server";
import { POSTS_QUERY } from "~/sanity/queries";
import type { Route } from "./+types/home";
export async function loader() {
const initial = await loadQuery(POSTS_QUERY, {});
return { initial, query: POSTS_QUERY, params: {} };
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { initial } = loaderData;
// …pass to component
}
```
For Remix v2: replace `Route.ComponentProps` / `Route.LoaderArgs` with `useLoaderData<typeof loader>()` and `LoaderFunctionArgs` from `@remix-run/node`.
## 3. Dynamic Routes (`:slug`)
Register the dynamic route in `app/routes.ts`:
```typescript
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route(":slug", "routes/post.tsx"),
] satisfies RouteConfig;
```
Then in `app/routes/post.tsx`:
```typescript
import type { Route } from "./+types/post";
import { PortableText } from "@portabletext/react";
import { loadQuery } from "~/sanity/loader.server";
import { useQuery } from "~/sanity/loader";
import { urlFor } from "~/sanity/image";
import { POST_QUERY } from "~/sanity/queries";
export async function loader({ params }: Route.LoaderArgs) {
const initial = await loadQuery(POST_QUERY, { slug: params.slug });
return { initial, query: POST_QUERY, params: { slug: params.slug } };
}
export default function Post({ loaderData }: Route.ComponentProps) {
const { initial, query, params } = loaderData;
const { data: post } = useQuery(query, params, { initial });
return (
<article>
<h1>{post?.title}</h1>
{post?.image && (
<img src={urlFor(post.image).width(1200).url()} alt={post.title ?? ""} />
)}
{post?.body && <PortableText value={post.body} />}
</article>
);
}
```
This route is the canonical shape that exposes the env trap: `urlFor` → `client.ts` → `env.ts`. If `env.ts` reads `process.env`, the route works under SSR (curl returns HTML) but the client-side `<Link>` navigation will throw `ReferenceError: process is not defined` in the browser console and React Router will hard-reload back to `/`.
## 4. Real-time Preview & Visual Editing
### A. Use `useQuery` in Components
Import `useQuery` from your **shared** loader file.
```typescript
import { useQuery } from "~/sanity/loader";
export default function Page({ loaderData }: Route.ComponentProps) {
const { initial, query, params } = loaderData;
const { data, encodeDataAttribute } = useQuery(query, params, { initial });
return (
<h1 data-sanity={encodeDataAttribute("title")}>
{data?.title}
</h1>
);
}
```
### B. Enable Live Mode (`VisualEditing.tsx`)
Create a component to handle the connection.
```typescript
import { enableVisualEditing } from "@sanity/visual-editing";
// Your browser-safe client
import { useEffect } from "react";
import { client } from "~/sanity/client";
import { useLiveMode } from "~/sanity/loader";
export default function VisualEditing() {
useEffect(() => enableVisualEditing(), []);
useLiveMode({ client });
return null;
}
```
Render this component in `root.tsx` only when valid (e.g., check env vars or user session).
## 5. Stega Cleaning
When using data for logic (routing, classNames), use `stegaClean`.
```typescript
import { stegaClean } from "@sanity/client/stega"
// ...
if (stegaClean(slug) === 'home') { ... }
```
@@ -0,0 +1,445 @@
---
title: Sanity Schema Best Practices
description: Rules for defining Sanity Content Models (Schemas), including field definitions, strict typing, and validation patterns.
---
# Sanity Schema Best Practices
Use this contents list to jump to the schema design decision you are making.
## Table of Contents
- Core philosophy: data over presentation
- Strict definition syntax
- Shared fields pattern
- Field patterns
- References vs nested objects
- Document creation and IDs
- Safe schema updates
- Validation patterns
## 1. Core Philosophy: Data > Presentation
Model **what things are**, not **what they look like**.
- ❌ **Bad:** `bigHeroText`, `redButton`, `threeColumnRow`, `color`, `fontSize`
- ✅ **Good:** `heroStatement`, `callToAction`, `featuresSection`, `status`, `role`
**The test:** "If we redesigned the site, would this field name still make sense?"
- `threeColumnLayout` → ❌ Fails (what if we go to 2 columns?)
- `features` → ✅ Passes (features are features regardless of layout)
## 2. Strict Definition Syntax
Always use the helper functions from `sanity` for type safety and autocompletion.
- **ALWAYS** use `defineType` for the root export.
- **ALWAYS** use `defineField` for fields.
- **ALWAYS** use `defineArrayMember` for items inside arrays.
```typescript
import { TagIcon } from "@sanity/icons";
import { defineArrayMember, defineField, defineType } from "sanity";
export const article = defineType({
name: "article",
title: "Article",
type: "document",
icon: TagIcon,
fields: [
defineField({
name: "title",
type: "string",
validation: (rule) => rule.required(),
}),
defineField({
name: "tags",
type: "array",
of: [
// ALWAYS use defineArrayMember for array items
defineArrayMember({ type: "reference", to: [{ type: "tag" }] }),
],
}),
],
});
```
## 3. Shared Fields Pattern
Export arrays of fields to reuse common patterns (e.g., SEO, standard page headers).
```typescript
// src/schemaTypes/shared/seoFields.ts
export const seoFields = [
defineField({ name: "seoTitle", type: "string", title: "SEO Title" }),
defineField({ name: "seoDesc", type: "text", title: "SEO Description" }),
];
// Usage
defineType({
name: "page",
fields: [
defineField({ name: "title", type: "string" }),
...seoFields, // Spread shared fields
],
});
```
## 4. Field Patterns
### A. Array Keys (`_key`)
Every item in a Sanity array automatically gets a `_key` property. This is **critical** for:
- React reconciliation (use as `key` prop)
- Visual Editing overlays (click-to-edit)
- Portable Text rendering
**Schema:** Sanity auto-generates `_key` for array items. You don't define it.
**Frontend:** Always use `_key` as React's `key`:
```typescript
// ✅ Correct
{items.map((item) => <Component key={item._key} {...item} />)}
// ❌ Wrong - index keys break Visual Editing
{items.map((item, i) => <Component key={i} {...item} />)}
```
**Querying:** Always include `_key` in array projections:
```groq
*[_type == "page"][0]{
pageBuilder[]{
_key, // Always include _key in queries
_type,
...
}
}
```
### B. Icons
Always assign an icon from `@sanity/icons` to documents and objects. This improves the Studio UX significantly. Browse all icons at [icons.sanity.build](https://icons.sanity.build/all).
| Content Type | Icon |
| -------------- | ------------------ |
| Article, Post | `DocumentTextIcon` |
| Author, Person | `UserIcon` |
| Category, Tag | `TagIcon` |
| Settings | `CogIcon` |
| Page | `DocumentIcon` |
| Image block | `ImageIcon` |
| Video block | `PlayIcon` |
| FAQ | `HelpCircleIcon` |
| Link | `LinkIcon` |
### C. Boolean vs. List
Avoid boolean fields for binary states that might expand later.
- **Prefer:** `options.list` with "radio" layout.
```typescript
defineField({
name: "status",
type: "string",
options: {
list: [
{ title: "Draft", value: "draft" },
{ title: "Published", value: "published" },
],
layout: "radio",
},
});
```
### D. The "Toggle" Pattern (Conditional Fields)
Use a radio/boolean field to toggle visibility of other fields (often grouped in fieldsets).
```typescript
(defineField({
name: "linkType",
type: "string",
options: { list: ["internal", "external"], layout: "radio" },
}),
defineField({
name: "internalLink",
type: "reference",
hidden: ({ parent }) => parent?.linkType !== "internal",
}),
defineField({
name: "externalUrl",
type: "url",
hidden: ({ parent }) => parent?.linkType !== "external",
}));
```
## 5. References vs Nested Objects
A **critical modeling decision**: when to use `reference` vs embedding an `object`.
### Use References When:
- Content is **reusable** across documents (authors, categories, products)
- Content needs its **own editing interface** in Studio
- You need to query/filter by the related content independently
- Multiple documents should share the **same instance** (update once, reflect everywhere)
```typescript
// ✅ Author is reusable and independently editable
defineField({
name: "author",
type: "reference",
to: [{ type: "author" }],
});
```
### Use Nested Objects When:
- Content is **specific to this document** (not shared)
- Content doesn't make sense on its own (address, SEO metadata)
- You want **simpler editing** (all fields in one place)
- You need the data to be **copied** not linked
```typescript
// ✅ SEO is document-specific, not shared
defineField({
name: "seo",
type: "object",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "description", type: "text" }),
],
});
```
### Quick Decision Matrix
| Scenario | Use |
| ------------------------- | ------------------------------------- |
| Blog post author | `reference` (reusable) |
| Product category | `reference` (shared taxonomy) |
| Page SEO fields | `object` (page-specific) |
| Hero section content | `object` (page-specific) |
| Team member on About page | `reference` (might be used elsewhere) |
| Call-to-action button | `object` (usually page-specific) |
### Querying Differences
```groq
// Reference requires expansion
*[_type == "post"]{ author->{ name, bio } }
// Object is already inline
*[_type == "post"]{ seo { title, description } }
```
## 6. Document Creation and IDs
Sanity document `_id` values are implementation identifiers, not a content modeling tool.
- **Prefer generated IDs:** Let Sanity assign `_id` values for ordinary content documents. Avoid deterministic UUIDs, slug-derived IDs, and IDs copied from legacy systems.
- **Use relationships, not ID conventions:** Connect documents with `reference` fields and set `_ref` from an actual lookup or from the `_id` returned after creating the related document.
- **Store source identity as content:** For imports, put legacy IDs, external IDs, or stable slugs in explicit fields such as `legacyId`, `externalId`, or `slug`, then query by those fields when you need to find or upsert content.
- **Keep explicit IDs rare:** Directly setting `_id` is mainly useful for singleton documents managed through Studio Structure, such as `settings` or localized singletons like `homePage-en`.
```typescript
// ✅ Correct - relationship comes from a lookup
import { defineQuery } from "groq";
const AUTHOR_BY_EXTERNAL_ID_QUERY = defineQuery(`
*[_type == "author" && externalId == $externalId][0]{_id}
`);
const author = await client.fetch(AUTHOR_BY_EXTERNAL_ID_QUERY, {
externalId: post.authorId,
});
if (!author?._id) throw new Error(`Missing author for ${post.authorId}`);
await client.create({
_type: "post",
title: post.title,
slug: { _type: "slug", current: post.slug },
legacyId: post.id,
author: { _type: "reference", _ref: author._id },
});
// ❌ Wrong - IDs encode relationships and source data
await client.createOrReplace({
_id: `post-${post.id}`,
_type: "post",
author: { _type: "reference", _ref: `author-${post.authorId}` },
});
```
## 7. Safe Schema Updates (The Deprecation Pattern)
**NEVER** delete a field that contains production data. It will cause data loss or Studio crashes. Instead, follow the **ReadOnly -> Hidden -> Deprecated** lifecycle.
### The Pattern
1. **`deprecated`**: Adds a visual warning and reason.
2. **`readOnly: true`**: Prevents new edits but keeps data visible.
3. **`hidden`**: Hides it from _new_ documents (where value is undefined).
4. **`initialValue: undefined`**: Ensures new documents don't get this field.
```typescript
defineField({
name: "oldTitle", // The field you want to remove
title: "Article Title (Deprecated)",
type: "string",
deprecated: {
reason: 'Use the new "seoTitle" field instead. This will be removed in v2.',
},
readOnly: true,
hidden: ({ value }) => value === undefined,
initialValue: undefined,
});
```
### Migration Workflow
**Phase 1: Deprecate** — Apply the deprecation pattern above. Deploy.
**Phase 2: Migrate** — Update frontend to use new fields (with `coalesce()` fallbacks). Create a migration:
```typescript
// migrations/rename-oldTitle-to-newTitle/index.ts
import { at, defineMigration, setIfMissing, unset } from "sanity/migrate";
export default defineMigration({
title: "Rename oldTitle to newTitle",
documentTypes: ["article"],
filter: "defined(oldTitle) && !defined(newTitle)",
migrate: {
document(doc) {
if (!doc.oldTitle || doc.newTitle) return;
return [
at("newTitle", setIfMissing(doc.oldTitle)),
at("oldTitle", unset()),
];
},
},
});
```
```bash
# Dry run first (default)
sanity migration run rename-oldTitle-to-newTitle
# Execute when ready
sanity migration run rename-oldTitle-to-newTitle --no-dry-run
```
**Phase 3: Remove** — Once `oldTitle` is undefined for all documents, delete the field definition.
## 8. Validation Patterns
Beyond `rule.required()`, Sanity offers powerful validation options.
### Common Patterns
```typescript
// Email validation
defineField({
name: "email",
type: "string",
validation: (rule) => rule.email().required(),
});
// URL validation (with custom message)
defineField({
name: "website",
type: "url",
validation: (rule) =>
rule
.uri({
scheme: ["http", "https"],
})
.error("Must be a valid URL starting with http:// or https://"),
});
// Length constraints
defineField({
name: "excerpt",
type: "text",
validation: (rule) =>
rule.max(200).warning("Keep it under 200 characters for best SEO"),
});
// Regex pattern
defineField({
name: "slug",
type: "slug",
validation: (rule) =>
rule.required().custom((slug) => {
if (!slug?.current) return "Required";
if (!/^[a-z0-9-]+$/.test(slug.current)) {
return "Slug must be lowercase with hyphens only";
}
return true;
}),
});
```
### Cross-Field Validation
```typescript
defineField({
name: "endDate",
type: "datetime",
validation: (rule) =>
rule.custom((endDate, context) => {
const startDate = context.document?.startDate;
if (startDate && endDate && new Date(endDate) < new Date(startDate)) {
return "End date must be after start date";
}
return true;
}),
});
```
### Array Validation
```typescript
defineField({
name: "tags",
type: "array",
of: [{ type: "string" }],
validation: (rule) =>
rule
.min(1)
.error("Add at least one tag")
.max(10)
.warning("Too many tags may hurt SEO")
.unique(),
});
```
### Async Validation (Uniqueness Check)
```typescript
defineField({
name: "slug",
type: "slug",
validation: (rule) =>
rule.required().custom(async (slug, context) => {
if (!slug?.current) return true;
const client = context.getClient({ apiVersion: "2026-02-01" });
const id = context.document?._id?.replace(/^drafts\./, "");
const existing = await client.fetch(
`count(*[_type == "post" && slug.current == $slug && _id != $id])`,
{ slug: slug.current, id },
);
return existing === 0 || "Slug already exists";
}),
});
```
@@ -0,0 +1,340 @@
---
title: Sanity SEO Best Practices
description: SEO best practices for Sanity with Next.js, including metadata, Open Graph, sitemaps, redirects, and JSON-LD structured data.
---
# Sanity SEO Best Practices
## 1. Core Philosophy
SEO doesn't require complex configurations. A few core principles, applied consistently:
- **Smart defaults with optional overrides** — Don't require SEO fields; use existing content as fallback
- **Use GROQ for fallback logic** — Move conditional logic into queries, not components
- **Leverage Next.js APIs** — Use `generateMetadata`, `sitemap.ts`, not manual `<meta>` tags
- **Structured content = structured data** — Your content model is already SEO-ready
## 2. SEO Schema Type (Reusable)
Create a reusable SEO object type for consistent metadata across document types.
```typescript
// schemaTypes/seoType.ts
import { defineField, defineType } from "sanity";
export const seoType = defineType({
name: "seo",
title: "SEO",
type: "object",
fields: [
defineField({
name: "title",
description: "Overrides the page title if provided",
type: "string",
}),
defineField({
name: "description",
type: "text",
rows: 3,
}),
defineField({
name: "image",
description: "Image for social sharing (1200x630 recommended)",
type: "image",
options: { hotspot: true },
}),
defineField({
name: "noIndex",
description: "Hide this page from search engines",
type: "boolean",
initialValue: false,
}),
],
});
```
**Usage in document types:**
```typescript
defineField({
name: "seo",
type: "seo",
});
```
## 3. GROQ Queries with Fallbacks
Use `coalesce()` to provide fallback values. This keeps frontend logic clean.
```groq
*[_type == "page" && slug.current == $slug][0]{
...,
"seo": {
// Use SEO field if provided, otherwise fall back to main title
"title": coalesce(seo.title, title, ""),
"description": coalesce(seo.description, ""),
"image": seo.image,
"noIndex": seo.noIndex == true
}
}
```
**Key principle:** `seo.title` will never be `null` — it contains either the SEO override, the page title, or empty string.
## 4. Next.js Metadata (The Right Way)
Use `generateMetadata` — never render `<title>` or `<meta>` tags directly in components.
```typescript
// app/(frontend)/[slug]/page.tsx
import type { Metadata } from "next";
import { urlFor } from "@/sanity/lib/image";
type RouteProps = {
params: Promise<{ slug: string }>;
};
// Extract fetch to reuse in both functions
const getPage = async (params: RouteProps["params"]) =>
sanityFetch({
query: PAGE_QUERY,
params: await params,
stega: false, // Critical for SEO!
});
export async function generateMetadata({
params,
}: RouteProps): Promise<Metadata> {
const { data: page } = await getPage(params);
if (!page) return {};
const metadata: Metadata = {
title: page.seo.title,
description: page.seo.description,
};
// Open Graph image
if (page.seo.image) {
metadata.openGraph = {
images: {
url: urlFor(page.seo.image).width(1200).height(630).url(),
width: 1200,
height: 630,
},
};
}
// noIndex robots directive
if (page.seo.noIndex) {
metadata.robots = "noindex";
}
return metadata;
}
export default async function Page({ params }: RouteProps) {
const { data: page } = await getPage(params);
// ... render page
}
```
**Critical:** Always set `stega: false` when fetching for metadata. Stega characters in `<title>` destroy SEO.
## 5. Dynamic Sitemap
Use Next.js `sitemap.ts` convention to auto-generate from Sanity content.
### GROQ Query
```groq
*[_type in ["page", "post"] && defined(slug.current) && seo.noIndex != true] {
"href": select(
_type == "page" => "/" + slug.current,
_type == "post" => "/posts/" + slug.current,
slug.current
),
_updatedAt
}
```
### Route Implementation
```typescript
// app/sitemap.ts
import { MetadataRoute } from "next";
import { client } from "@/sanity/lib/client";
import { SITEMAP_QUERY } from "@/sanity/lib/queries";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
try {
const paths = await client.fetch(SITEMAP_QUERY);
if (!paths) return [];
return paths.map((path) => ({
url: new URL(path.href!, baseUrl).toString(),
lastModified: new Date(path._updatedAt),
changeFrequency: "weekly",
priority: 1,
}));
} catch (error) {
console.error("Sitemap generation failed:", error);
return [];
}
}
```
**Note:** Sitemap limit is 50,000 URLs per file. For larger sites, use sitemap index.
## 6. Redirects (Managed in Sanity)
Create a redirect document type for content team management.
### Schema
```typescript
// schemaTypes/redirectType.ts
import { LinkIcon } from "@sanity/icons";
import { SanityDocumentLike, defineField, defineType } from "sanity";
function isValidPath(value: string | undefined) {
if (!value) return "Required";
if (!value.startsWith("/")) return "Must start with /";
if (/[^a-zA-Z0-9\-_/:]/.test(value)) return "Invalid characters";
return true;
}
export const redirectType = defineType({
name: "redirect",
title: "Redirect",
type: "document",
icon: LinkIcon,
validation: (Rule) =>
Rule.custom((doc: SanityDocumentLike | undefined) => {
if (doc?.source === doc?.destination) {
return "Source and destination cannot be the same";
}
return true;
}),
fields: [
defineField({
name: "source",
type: "string",
validation: (Rule) => Rule.required().custom(isValidPath),
}),
defineField({
name: "destination",
type: "string",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "permanent",
description: "301 (permanent) or 302 (temporary)",
type: "boolean",
initialValue: true,
}),
defineField({
name: "isEnabled",
type: "boolean",
initialValue: true,
}),
],
});
```
### Next.js Config
```typescript
// next.config.ts
import { fetchRedirects } from "@/sanity/lib/fetchRedirects";
const nextConfig: NextConfig = {
async redirects() {
return await fetchRedirects();
},
};
```
**Limits:** Vercel allows max 1,024 redirects in `next.config`. For more, use middleware.
## 7. Dynamic Open Graph Images
Generate OG images on-the-fly using Next.js Edge Runtime at `/api/og`.
```typescript
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export async function GET(request: Request) {
const id = new URL(request.url).searchParams.get("id");
if (!id) return new Response("Missing id", { status: 400 });
const data = await client.fetch(`*[_id == $id][0]{ title }`, { id });
return new ImageResponse(
<div tw="flex w-full h-full bg-blue-500 text-white p-10">
<h1 tw="text-6xl font-bold">{data?.title || "Untitled"}</h1>
</div>,
{ width: 1200, height: 630 }
);
}
```
Use as fallback in metadata: `url: page.seo.image ? urlFor(page.seo.image).url() : \`/api/og?id=\${page.\_id}\``
## 8. JSON-LD Structured Data
Use `schema-dts` for type-safe structured data.
```bash
npm install schema-dts
```
### FAQ Example
```typescript
import { FAQPage, WithContext } from "schema-dts";
const generateFaqData = (faqs: FAQ[]): WithContext<FAQPage> => ({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.title,
acceptedAnswer: {
"@type": "Answer",
text: faq.text, // Use pt::text() in GROQ to get plain text
},
})),
});
// In component
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateFaqData(faqs)) }}
/>
```
### GROQ for Plain Text
```groq
faqs[]->{
_id,
title,
body,
"text": pt::text(body) // Convert Portable Text to plain string
}
```
## 9. Testing Tools
- **Open Graph:** [opengraph.ing](https://opengraph.ing/)
- **Facebook:** [Sharing Debugger](https://developers.facebook.com/tools/debug/)
- **Twitter:** [Card Validator](https://cards-dev.twitter.com/validator)
- **LinkedIn:** [Post Inspector](https://www.linkedin.com/post-inspector/)
- **Sitemap:** [XML Sitemaps Validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html)
@@ -0,0 +1,146 @@
---
title: "Sanity Studio Structure Rules"
description: Rules for customizing the Sanity Studio Structure (S.structure).
---
# Sanity Studio Structure Rules
## 1. Setup
Custom structure is defined in `sanity.config.ts` using the `structureTool`.
```typescript
import { structureTool } from "sanity/structure";
import { structure } from "./src/structure";
export default defineConfig({
// ...
plugins: [structureTool({ structure })],
});
```
## 2. Structure Definition
**Location:** `src/structure/index.ts`
Use a function that receives `S` (StructureBuilder).
```typescript
import type { StructureResolver } from "sanity/structure";
export const structure: StructureResolver = (S) =>
S.list().title("Content").items([
// ... items
]);
```
## 3. Organization Principles
1. **Singletons First:** Place critical site-wide settings (Global Settings, Homepage) at the top.
2. **Dividers:** Use `S.divider()` to visually separate logical groups.
3. **Filtered Lists:** Always exclude Singleton documents from generic `documentTypeList` items to avoid duplication.
## 4. Singleton Pattern (Critical)
**Singletons are enforced via Structure, NOT schema options.** There is no `singleton: true` schema option.
This is the main case where explicit document IDs are appropriate. For ordinary content documents, let Sanity generate `_id` values and use references or GROQ lookups to connect records.
### How Singletons Work
1. Use `S.document().documentId('fixed-id')` to lock the document to a specific ID.
2. Filter the type from generic lists to prevent duplicate entries.
### Singleton Helper Function
```typescript
// Helper to create singleton list items
function createSingleton(
S: StructureBuilder,
typeName: string,
title: string,
icon?: ComponentType,
) {
return S.listItem().title(title).icon(icon).child(
S.document()
.schemaType(typeName)
.documentId(typeName) // Fixed ID = singleton
.title(title),
);
}
// Usage
createSingleton(S, "settings", "Site Settings", CogIcon);
```
### Querying Singletons
```groq
// By fixed ID (most efficient)
*[_id == "settings"][0]
// By type (works but slower)
*[_type == "settings"][0]
```
**For localized singletons** (e.g., homepage per language), see `localization.md` Section 6.
## 5. Implementation Pattern
```typescript
// Define singleton types to exclude from generic lists
const SINGLETONS = ["settings", "homePage"];
export const structure: StructureResolver = (S) =>
S.list()
.title("Website Content")
.items([
// 1. Singletons
S.listItem()
.title("Site Settings")
.icon(CogIcon)
.child(S.document().schemaType("settings").documentId("settings")),
S.divider(),
// 2. Content Verticals
S.listItem()
.title("Blog")
.child(
S.list()
.title("Blog Content")
.items([
S.documentTypeListItem("post").title("Posts"),
S.documentTypeListItem("author").title("Authors"),
]),
),
S.divider(),
// 3. Remaining Documents (Filtered)
...S.documentTypeListItems().filter(
(listItem) => !SINGLETONS.includes(listItem.getId() as string),
),
]);
```
## 6. Views (Split Pane)
Add "Web Preview" or other views to documents.
```typescript
export const defaultDocumentNode: DefaultDocumentNodeResolver = (
S,
{ schemaType },
) => {
switch (schemaType) {
case `post`:
return S.document().views([
S.view.form(), // Default form
S.view.component(PreviewComponent).title("Preview"), // Custom view
]);
default:
return S.document().views([S.view.form()]);
}
};
```
@@ -0,0 +1,340 @@
---
title: "SvelteKit & Sanity Integration Rules"
description: Integration guide for SvelteKit with Sanity using @sanity/sveltekit, including Visual Editing and Preview Mode.
---
# SvelteKit & Sanity Integration Rules
This guide uses the official **`@sanity/sveltekit`** package (Svelte 5 + SvelteKit 2). The older `@sanity/svelte-loader` does not work with Svelte 5 — its `useQuery` store returns empty on the client. Use `@sanity/sveltekit` instead.
## 1. Setup & Configuration
### Scaffold a new SvelteKit app
```bash
npx sv@latest create my-app --template minimal --types ts --no-add-ons --install npm
cd my-app
```
`--template minimal` is the bare app. `--types ts` enables TypeScript. `--no-add-ons` skips the add-on picker. `--install <pm>` chooses the package manager (`npm`, `pnpm`, `yarn`, or `bun`).
### Installation
```bash
npm install @sanity/sveltekit @sanity/image-url @portabletext/svelte
```
`@sanity/sveltekit` is the one-stop integration: it bundles `@sanity/client`, `@sanity/visual-editing`, `@sanity/core-loader`, `groq`, and friends, and re-exports `createClient`, `defineQuery`, `groq`, and `stegaClean`. **Do not** also install `@sanity/client`, `@sanity/visual-editing`, or `groq` directly — import them from `@sanity/sveltekit`. `@sanity/image-url` and `@portabletext/svelte` are not bundled, so add them separately.
### Environment variables (`.env.local`)
```bash
PUBLIC_SANITY_PROJECT_ID=your-project-id
PUBLIC_SANITY_DATASET=production
PUBLIC_SANITY_API_VERSION=2026-05-15
PUBLIC_SANITY_STUDIO_URL=http://localhost:3333
SANITY_API_READ_TOKEN=
```
SvelteKit's `$env/static/public` requires the `PUBLIC_` prefix for any var read on the client. `SANITY_API_READ_TOKEN` must be declared (even empty) if any file imports it from `$env/static/private`, otherwise Vite throws at build time.
## 2. Files
### `src/lib/sanity/api.ts` — env var resolution
```ts
import {
PUBLIC_SANITY_API_VERSION,
PUBLIC_SANITY_DATASET,
PUBLIC_SANITY_PROJECT_ID,
PUBLIC_SANITY_STUDIO_URL,
} from "$env/static/public";
function assertEnvVar<T>(value: T | undefined, name: string): T {
if (value === undefined || value === "") {
throw new Error(`Missing environment variable: ${name}`);
}
return value;
}
export const dataset = assertEnvVar(
PUBLIC_SANITY_DATASET,
"PUBLIC_SANITY_DATASET",
);
export const projectId = assertEnvVar(
PUBLIC_SANITY_PROJECT_ID,
"PUBLIC_SANITY_PROJECT_ID",
);
export const apiVersion = PUBLIC_SANITY_API_VERSION || "2026-05-15";
export const studioUrl = PUBLIC_SANITY_STUDIO_URL || "http://localhost:3333";
```
### `src/lib/sanity/client.ts` — public client
```ts
import { apiVersion, dataset, projectId, studioUrl } from "$lib/sanity/api";
import { createClient } from "@sanity/sveltekit";
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
stega: { studioUrl },
});
```
Import `createClient` from `@sanity/sveltekit`, not `@sanity/client`. `useCdn: true` is for production reads; the server (preview) client below overrides to `false`.
### `src/lib/sanity/client.server.ts` — server (preview) client
```ts
import { SANITY_API_READ_TOKEN } from "$env/static/private";
import { client } from "$lib/sanity/client";
export const serverClient = client.withConfig({
token: SANITY_API_READ_TOKEN,
useCdn: false,
stega: true,
});
```
### `src/lib/sanity/queries.ts` — queries + types
```ts
import { groq } from "@sanity/sveltekit";
export const postsQuery = groq`*[_type == "post" && defined(slug.current)] | order(_createdAt desc){
_id, _createdAt, title, slug, excerpt, mainImage, body
}`;
export const postQuery = groq`*[_type == "post" && slug.current == $slug][0]{
_id, _createdAt, title, slug, excerpt, mainImage, body
}`;
export interface Post {
_id: string;
_createdAt: string;
title?: string;
slug: { current: string };
excerpt?: string;
mainImage?: unknown;
body?: unknown[];
}
```
Use `defineQuery` instead of `groq` if you want TypeGen-friendly query definitions; both are re-exported from `@sanity/sveltekit`.
### `src/lib/sanity/image.ts` — image URL builder
```ts
import { createImageUrlBuilder } from "@sanity/image-url";
import { client } from "./client";
const builder = createImageUrlBuilder(client);
export function urlFor(source: unknown) {
return builder.image(source as never);
}
```
Use the named `createImageUrlBuilder` export; the default export logs a deprecation warning at runtime.
## 3. Hooks & Locals
### `src/hooks.server.ts` — wire preview + query loader
```ts
import { serverClient } from "$lib/sanity/client.server";
import {
handlePreviewMode,
handleQueryLoader,
setServerClient,
} from "@sanity/sveltekit";
import { redirect } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
setServerClient(serverClient);
export const handle = sequence(
handlePreviewMode({
client: serverClient,
preview: { redirect },
}),
handleQueryLoader(),
);
```
`handlePreviewMode` installs `/preview/enable` and `/preview/disable` endpoints, reads the preview cookie, and populates `locals.sanity` with `{client, fetch, loadQuery, previewEnabled, previewPerspective, browserToken}`. `handleQueryLoader` attaches `loadQuery` to `locals.sanity` for use in `+page.server.ts` / `+layout.server.ts`.
### `src/app.d.ts` — typed locals
```ts
import type { SanityLocals } from "@sanity/sveltekit";
declare global {
namespace App {
interface Locals extends SanityLocals {}
}
}
export {};
```
## 4. Layout: Preview + Visual Editing Providers
### `src/routes/+layout.server.ts` — propagate previewEnabled
```ts
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = (event) => {
const { previewEnabled } = event.locals.sanity;
return { previewEnabled };
};
```
### `src/routes/+layout.svelte` — wrap children in providers (Svelte 5)
```svelte
<script lang="ts">
import {PreviewMode, QueryLoader, VisualEditing} from '@sanity/sveltekit'
import type {LayoutProps} from './$types'
import {client} from '$lib/sanity/client'
const {children, data}: LayoutProps = $props()
// svelte-ignore state_referenced_locally
const {previewEnabled} = data
</script>
<PreviewMode enabled={previewEnabled}>
<VisualEditing enabled={previewEnabled}>
<QueryLoader enabled={previewEnabled} {client}>
{@render children()}
</QueryLoader>
</VisualEditing>
</PreviewMode>
```
Svelte 5 idioms here are mandatory:
- `const {children, data} = $props()` — not `export let data`.
- `{@render children()}` — not `<slot />`.
- The `svelte-ignore state_referenced_locally` comment silences a warning about destructuring reactive props at module scope.
`<VisualEditing>` dynamically imports its component only when `enabled === true`, so a preview-off app never loads the React-Compiler-runtime chunk.
## 5. Data Fetching (Loaders + `useQuery`)
### Posts list
`src/routes/+page.server.ts`:
```ts
import { type Post, postsQuery as query } from "$lib/sanity/queries";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals }) => {
const { loadQuery } = locals.sanity;
const initial = await loadQuery<Post[]>(query);
return { query, options: { initial } };
};
```
The return shape `{query, params?, options: {initial}}` is what `useQuery(data)` on the client expects — don't change the field names.
`src/routes/+page.svelte`:
```svelte
<script lang="ts">
import {useQuery} from '@sanity/sveltekit'
import type {Post} from '$lib/sanity/queries'
import type {PageProps} from './$types'
const {data}: PageProps = $props()
const query = $derived(useQuery<Post[]>(data))
const posts = $derived($query.data)
</script>
<h1>Posts</h1>
{#if posts?.length}
<ul>
{#each posts as post (post._id)}
<li><a href={`/post/${post.slug.current}`}>{post.title}</a></li>
{/each}
</ul>
{:else}
<p>No posts yet.</p>
{/if}
```
Critical Svelte 5 pattern:
- `useQuery` returns a Svelte Readable store. Wrap in `$derived(useQuery(data))` so the store reference stays current across reactive updates.
- Subscribe via `$query` (Svelte's auto-subscription) and read `.data`.
- Works on both SSR and client.
### Post detail (`[slug]`)
`src/routes/post/[slug]/+page.server.ts`:
```ts
import { type Post, postQuery as query } from "$lib/sanity/queries";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals, params }) => {
const { loadQuery } = locals.sanity;
const { slug } = params;
const initial = await loadQuery<Post>(query, { slug });
return { query, params: { slug }, options: { initial } };
};
```
`src/routes/post/[slug]/+page.svelte`:
```svelte
<script lang="ts">
import {useQuery} from '@sanity/sveltekit'
import {PortableText} from '@portabletext/svelte'
import {urlFor} from '$lib/sanity/image'
import type {Post} from '$lib/sanity/queries'
import type {PageProps} from './$types'
const {data}: PageProps = $props()
const query = $derived(useQuery<Post>(data))
const post = $derived($query.data)
</script>
{#if post}
<article>
<h1>{post.title}</h1>
{#if post.mainImage}
<img src={urlFor(post.mainImage).width(800).url()} alt={post.title ?? ''} />
{/if}
{#if post.body}
<PortableText value={post.body} />
{/if}
</article>
{:else}
<p>Post not found.</p>
{/if}
```
## 6. Stega Cleaning
When using fetched strings for logic (routing, classNames), strip the stega markers first.
```ts
import { stegaClean } from "@sanity/sveltekit";
// …
if (stegaClean(slug) === "home") {
/* … */
}
```
## 7. Caveats
- **Yarn classic + Visual Editing.** `@sanity/visual-editing` lazy-loads a chunk that imports `react/compiler-runtime`. Yarn classic doesn't auto-install peer deps, so users who flip preview mode on with yarn classic also need `yarn add react react-dom`. (Other package managers handle this automatically.) `<VisualEditing>` only loads this chunk when `enabled === true`, so a default preview-off app is unaffected.
- **No `<slot />`.** Svelte 5 layouts use `{@render children()}`.
- **No `export let`.** Pages and components use `const {data} = $props()`.
- **`@sanity/image-url` default export.** Use the named `createImageUrlBuilder`; the default export still works but logs a runtime deprecation warning.
@@ -0,0 +1,234 @@
---
title: Sanity TypeGen Rules
description: Workflow for generating TypeScript types from Sanity Schema and GROQ queries.
---
# Sanity TypeGen Rules
## 1. The Workflow
Sanity TypeGen generates TypeScript types from your schema and GROQ queries. Types can be generated automatically or manually.
### Automatic (Recommended)
Enable in `sanity.cli.ts` — types regenerate during `sanity dev` and `sanity build`:
```typescript
// sanity.cli.ts
import { defineCliConfig } from "sanity/cli";
export default defineCliConfig({
typegen: {
enabled: true,
},
});
```
### Manual
Run the extract + generate cycle whenever schema or queries change:
1. **Extract:** Converts your Schema (TS/JS) into a static JSON representation.
2. **Generate:** Scans your codebase for GROQ queries and generates TypeScript types.
```bash
npx sanity schema extract && npx sanity typegen generate
```
### Watch Mode (for separate frontends)
If your frontend is in a separate repo from the Studio, use watch mode:
```bash
npx sanity typegen generate --watch
```
## 2. The "Update Types" Pattern
For manual workflows, implement a single script:
**package.json:**
```json
"scripts": {
"typegen": "sanity schema extract && sanity typegen generate"
}
```
### Git Strategy for Generated Files
**Option A: Commit generated types (Recommended for most teams)**
- Types available immediately after `git pull`
- CI/CD doesn't need to run typegen
- Can cause merge conflicts
**Option B: Generate in CI (Recommended for larger teams)**
Add to `.gitignore`:
```gitignore
# Sanity TypeGen (generated)
sanity.types.ts
schema.json
```
Then ensure CI runs typegen before build:
```yaml
# Example GitHub Actions
- run: npm run typegen
- run: npm run build
```
## 3. Configuration (`sanity.cli.ts`)
> **Note:** `sanity-typegen.json` is deprecated. Move your configuration to `sanity.cli.ts`.
```typescript
// sanity.cli.ts
import { defineCliConfig } from "sanity/cli";
export default defineCliConfig({
typegen: {
enabled: true, // Auto-generate during sanity dev/build
path: "./src/**/*.{ts,tsx,js,jsx,astro,svelte,vue}", // Glob to find queries
schema: "schema.json", // Schema file from extract
generates: "./sanity.types.ts", // Output file
overloadClientMethods: true, // Auto-type client.fetch() calls
},
});
```
### Project Structure Examples
**Monorepo (recommended)** (Studio in `studio/`, Frontend in `web/` — same config works under `apps/`):
```typescript
export default defineCliConfig({
typegen: {
path: "../web/src/**/*.{ts,tsx,js,jsx}",
schema: "schema.json",
generates: "../web/sanity.types.ts",
},
});
```
**Single Repo / Embedded Studio (legacy):**
Use defaults — no extra config needed.
**Separate Repos:**
Use `--watch` mode in your frontend: `sanity typegen generate --watch`
## 4. Usage in Code
### Automatic Type Inference (Recommended)
With `overloadClientMethods: true` (default), `client.fetch()` automatically returns typed results when you use `defineQuery`:
```typescript
import { defineQuery } from "groq";
import { createClient } from "@sanity/client";
const client = createClient({...});
const POSTS_QUERY = defineQuery(`*[_type == "post"]{ title, slug }`);
// Return type is automatically inferred — no manual type import needed!
const posts = await client.fetch(POSTS_QUERY);
```
### Manual Type Import (Alternative)
You can also import generated types directly:
```typescript
import { defineQuery } from "groq";
// Next.js re-exports defineQuery for convenience:
// import { defineQuery } from "next-sanity";
const AUTHOR_QUERY = defineQuery(`*[_type == "author" && slug.current == $slug][0]{ name, bio }`);
import type { AUTHOR_QUERYResult } from "@/sanity.types";
export default function Author({ data }: { data: AUTHOR_QUERYResult }) {
return <h1>{data.name}</h1>
}
```
### Required Fields
Use `--enforce-required-fields` during extraction to translate `validation: rule => rule.required()` into non-optional types:
```bash
npx sanity schema extract --enforce-required-fields
npx sanity typegen generate
```
> **Warning:** If you use draft previews, fields may still be `undefined` even with required validation, since drafts can be in an invalid state.
### Type Utilities
TypeGen provides utilities for working with complex types:
```typescript
import type { FilterByType, Get } from "sanity";
import type { Page, PageBuilder } from "./sanity.types";
// Extract deeply nested type (up to 20 levels)
type HeroSection = Get<Page, "sections", number, "hero">;
// Filter specific types from unions using _type discriminator
type HeroBlock = FilterByType<PageBuilder, "hero">;
```
### Unique Query Names
All queries must have unique variable names. Duplicate names across files will cause TypeGen to silently overwrite types. Use descriptive, scoped names:
```typescript
// Unique names
const POSTS_INDEX_QUERY = defineQuery(`*[_type == "post"]{ title }`)
const POST_DETAIL_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`)
// Duplicate names will conflict
const QUERY = defineQuery(`*[_type == "post"]`) // file-a.ts
const QUERY = defineQuery(`*[_type == "author"]`) // file-b.ts — overwrites!
```
### Supported Query Formats
Queries must be assigned to a variable using `groq` or `defineQuery`:
```typescript
// Works — groq template tag
const query = groq`*[_type == "post"]`
// Works — defineQuery
const query = defineQuery(`*[_type == "post"]`)
// Won't work — inline query
await client.fetch(groq`*[_type == "post"]`)
```
### Supported File Types
TypeGen parses queries from: `.ts`, `.tsx`, `.js`, `.jsx`, `.astro`, `.svelte`, `.vue`
### tsconfig Requirements
Ensure `sanity.types.ts` is included in your `tsconfig.json`'s `include` array. If your config restricts includes (e.g., `["src/**/*"]`) and the types file is at the project root, TypeScript won't pick up the generated types:
```json
{
"include": ["src/**/*", "sanity.types.ts"]
}
```
### Skipping Individual Queries
Add `@sanity-typegen-ignore` in a comment before a query to skip type generation:
```typescript
// @sanity-typegen-ignore
const debugQuery = groq`*[_type == "debug"]`;
```
@@ -0,0 +1,279 @@
---
title: "Sanity Visual Editing Rules"
description: Comprehensive guide for Sanity Visual Editing, including Presentation Tool, Stega (Content Source Maps), and Overlays.
---
# Sanity Visual Editing Rules
## 1. Concepts
### Presentation Tool
The Studio plugin (`sanity/presentation`) that renders your front-end application inside an iframe in the Studio. It enables the "Edit" overlay and bidirectional navigation.
### Content Source Maps (Stega)
Invisible characters embedded in strings that tell the Presentation Tool which field in which document the content comes from.
- **Mechanism:** Sanity encodes document ID, field path, and dataset info into string values.
- **Result:** Click-to-edit functionality in the preview.
### Loaders
Framework-agnostic or specific libraries that handle:
1. Fetching data (production vs. preview).
2. Subscribing to real-time updates (Live Content API).
3. Encoding Stega strings (if not handled by the Content Lake automatically).
## 2. The Golden Rule of Stega (Clean Data)
When Visual Editing is enabled, string fields will contain invisible characters. You **MUST** clean them before using the value for logic.
| Scenario | Clean? | Why |
| ------------------------------------ | ------ | --------------------- |
| Comparing strings (`if (x === 'y')`) | ✅ Yes | Stega breaks equality |
| Using as object keys | ✅ Yes | Keys won't match |
| Using as HTML IDs | ✅ Yes | Invalid characters |
| Passing to third-party libraries | ✅ Yes | May validate input |
| Rendering text (`<h1>{title}</h1>`) | ❌ No | Breaks click-to-edit |
| Passing to `<PortableText />` | ❌ No | Handles internally |
| Passing to image helpers | ❌ No | Handles internally |
```typescript
import { stegaClean } from "@sanity/client/stega";
export function Layout({ align }: { align: string }) {
// Good: Clean before comparison
const cleanAlign = stegaClean(align);
return <div className={cleanAlign === 'center' ? 'mx-auto' : ''} />
}
```
## 3. Token Handling (Security)
Store your read token in a dedicated file that throws if missing:
```typescript
// src/sanity/lib/token.ts
export const token = process.env.SANITY_API_READ_TOKEN;
if (!token) {
throw new Error("Missing SANITY_API_READ_TOKEN");
}
```
**Never** expose tokens in client bundles. Pass to `defineLive` for server/browser use only when Draft Mode is enabled.
## 4. Setup: Presentation Tool
**File:** `sanity.config.ts`
```typescript
import { defineConfig } from "sanity";
import { presentationTool } from "sanity/presentation";
import { resolve } from "@/sanity/presentation/resolve";
export default defineConfig({
// ...
plugins: [
presentationTool({
resolve, // Document locations (see below)
previewUrl: {
// The front-end origin — required when the Studio runs standalone
origin:
process.env.SANITY_STUDIO_PREVIEW_ORIGIN || "http://localhost:3000",
previewMode: {
enable: "/api/draft-mode/enable",
},
},
}),
],
});
```
### Document Locations
Show where documents appear in the front-end — enables quick navigation between Structure and Presentation tools.
```typescript
// src/sanity/presentation/resolve.ts
import {
PresentationPluginOptions,
defineLocations,
} from "sanity/presentation";
export const resolve: PresentationPluginOptions["resolve"] = {
locations: {
post: defineLocations({
select: { title: "title", slug: "slug.current" },
resolve: (doc) => ({
locations: [
{ title: doc?.title || "Untitled", href: `/posts/${doc?.slug}` },
{ title: "Posts index", href: `/posts` },
],
}),
}),
// Add more document types as needed
},
};
```
## 5. Visual Editing Overlays
Render `<VisualEditing />` in Draft Mode for click-to-edit overlays.
**Next.js (App Router):**
```typescript
// layout.tsx
import { VisualEditing } from 'next-sanity/visual-editing'
import { draftMode } from 'next/headers'
import { DisableDraftMode } from '@/components/disable-draft-mode'
export default async function RootLayout({ children }) {
return (
<html>
<body>
{children}
{(await draftMode()).isEnabled && (
<>
<DisableDraftMode />
<VisualEditing />
</>
)}
</body>
</html>
)
}
```
### Disable Draft Mode Button
Useful for content authors to exit preview and see published content:
```typescript
// src/components/disable-draft-mode.tsx
'use client'
import { useDraftModeEnvironment } from 'next-sanity/hooks'
export function DisableDraftMode() {
const environment = useDraftModeEnvironment()
// Only show outside of Presentation Tool
if (environment !== 'live' && environment !== 'unknown') return null
return (
<a href="/api/draft-mode/disable" className="fixed bottom-4 right-4 bg-gray-50 px-4 py-2">
Disable Draft Mode
</a>
)
}
```
**Remix/Svelte:** See framework-specific rules for `useLiveMode` and `enableVisualEditing` patterns.
## 6. SEO & Metadata (Critical)
**NEVER** allow Stega strings in `<head>` tags (Title, Description, Canonical URLs). It destroys SEO rankings and looks broken in search results.
- **Next.js:** Set `stega: false` in `generateMetadata`.
- **General:** Explicitly clean fields used in `<title>` or `<meta>`.
```typescript
// Next.js Example — disable stega at fetch level
export async function generateMetadata({ params }) {
const { data } = await sanityFetch({
query: SEO_QUERY,
stega: false, // Critical
});
return { title: data.title };
}
```
**Alternative:** If you can't disable stega at the fetch level, clean explicitly:
```typescript
import { stegaClean } from "@sanity/client/stega";
export async function generateMetadata({ params }) {
const { data } = await sanityFetch({ query: PAGE_QUERY });
return {
title: stegaClean(data.title),
description: stegaClean(data.description),
openGraph: { url: stegaClean(data.canonicalUrl) },
};
}
```
## 7. Drag-and-Drop Reordering (Advanced)
For arrays (e.g., "Related Posts"), enable drag-and-drop in the preview using `data-sanity` attributes and `useOptimistic`:
```typescript
import { createDataAttribute } from 'next-sanity'
import { useOptimistic } from 'next-sanity/hooks'
// Add data-sanity to array container
<ul data-sanity={createDataAttribute({ id: documentId, type: 'post', path: 'relatedPosts' }).toString()}>
{items.map((item) => (
<li key={item._key} data-sanity={createDataAttribute({
id: documentId, type: 'post', path: `relatedPosts[_key=="${item._key}"]`
}).toString()}>
{item.title}
</li>
))}
</ul>
```
**Key requirements:**
- Query must include `_key` for array items
- Use `useOptimistic` hook for instant UI updates during mutations
## 8. Optimistic Updates for Faster Editing
By default, editing a field in the Presentation Tool triggers a full page re-render. For pages with many components, this can feel sluggish. **Presentation queries** solve this by fetching only the specific block being edited.
### The Concept
Instead of:
1. User edits a field -> Full page query re-runs -> All components re-render
You get:
1. User edits a field -> Block-specific query runs -> Only that component re-renders
### How It Works
1. **Create a targeted query** that fetches just the block data using `_key`:
```groq
*[_id == $documentId][0]{
"heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
title, subtitle, image
}
}
```
2. **Use a presentation query hook** in your component (e.g., `usePresentationQuery` in Next.js)
3. **Fall back to initial props** when not in presentation mode
This pattern works for both Page Builder blocks (`pageBuilder[]`) and Portable Text blocks (`body[]`).
**See framework-specific rules for implementation:**
- Next.js: `nextjs.md` (Section 9)
- Page Builder: `page-builder.md` (Section 5)
- Portable Text: `portable-text.md` (Section 7)
## 9. Framework Specifics
| Framework | Loader Package | Key Components |
| :---------- | :---------------------- | :----------------------------------------------------------- |
| **Next.js** | `next-sanity` | `<VisualEditing />`, `defineLive`, `usePresentationQuery` |
| **Remix** | `@sanity/react-loader` | `createQueryStore`, `useLiveMode`, `enableVisualEditing` |
| **Svelte** | `@sanity/svelte-loader` | `createRequestHandler`, `useLiveMode`, `enableVisualEditing` |
| **Nuxt** | `@nuxtjs/sanity` | Automatic via module config (`visualEditing: {}`) |
| **Astro** | `@sanity/astro` | `sanity({ useCdn: false, stega: true })` |
+5 -5
View File
@@ -14,24 +14,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: cd aksel.nav.no/website && yarn
run: cd aksel.nav.no/sanity-studio && yarn
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Export dataset
run: cd aksel.nav.no/website && yarn backup
run: cd aksel.nav.no/sanity-studio && yarn sanity:backup
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
@@ -43,6 +43,6 @@ jobs:
- name: Upload backup to GCS bucket
uses: "google-github-actions/upload-cloud-storage@v3"
with:
path: ./aksel.nav.no/website
path: ./aksel.nav.no/sanity-studio
destination: "aksel-website-prod/backups"
glob: "backup-*.tar.gz"
+5 -5
View File
@@ -1,4 +1,4 @@
name: E2E tests (Playwright)
name: E2E tests
on:
push:
branches: [main, next]
@@ -28,7 +28,7 @@ jobs:
REGISTRY: ghcr.io
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -62,7 +62,7 @@ jobs:
name: Playwright shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
image: mcr.microsoft.com/playwright:v1.61.1-noble
services:
testapp:
image: ${{ needs.prebuild-app-for-playwright.outputs.image }}
@@ -78,12 +78,12 @@ jobs:
shardTotal: [5]
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
+6 -6
View File
@@ -1,4 +1,4 @@
name: Validation (Lint, Tests)
name: Validation
on:
push:
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -72,10 +72,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
image: mcr.microsoft.com/playwright:v1.61.1-noble
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -1,4 +1,4 @@
name: Visual regression (Chromatic)
name: Visual regression
on:
push:
@@ -27,7 +27,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -37,6 +37,7 @@ jobs:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Chromatic
if: github.actor != 'dependabot[bot]'
uses: chromaui/action@latest
id: chromatic_tests
continue-on-error: false
@@ -54,14 +55,14 @@ jobs:
STORYBOOK_GITHUB_SHA: ${{ github.sha }}
- name: Get commit sha
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
id: vars
shell: bash
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Find Comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
uses: peter-evans/find-comment@v4
id: fc
with:
@@ -70,7 +71,7 @@ jobs:
body-includes: Storybook demo
- name: Create or update comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
+10 -10
View File
@@ -30,7 +30,7 @@ jobs:
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout main branch
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
ref: main
@@ -48,7 +48,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: trunk-results.json
path: scripts/analyzer
path: tooling/analyzer
retention-days: 1
analyze-local:
@@ -58,7 +58,7 @@ jobs:
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout PR branch
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -75,7 +75,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: branch-results.json
path: scripts/analyzer
path: tooling/analyzer
retention-days: 1
compare-against-trunk:
@@ -84,7 +84,7 @@ jobs:
needs: [analyze-trunk, analyze-local]
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -96,13 +96,13 @@ jobs:
- name: Download trunk json from GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: scripts/analyzer
path: tooling/analyzer
name: trunk-results.json
- name: Download branch json from GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: scripts/analyzer
path: tooling/analyzer
name: branch-results.json
- name: Analyze compare branches
@@ -113,7 +113,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: compare-markdown.md
path: scripts/analyzer
path: tooling/analyzer
retention-days: 1
update-comment:
@@ -127,7 +127,7 @@ jobs:
- name: Download comparison markdown to GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: scripts/analyzer
path: tooling/analyzer
name: compare-markdown.md
- name: Find Comment
@@ -143,5 +143,5 @@ jobs:
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: scripts/analyzer/compare-markdown.md
body-path: tooling/analyzer/compare-markdown.md
edit-mode: replace
+4 -4
View File
@@ -30,7 +30,7 @@ jobs:
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -50,7 +50,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: compare-markdown.md
path: scripts/analyzer
path: tooling/analyzer
retention-days: 1
update-comment:
@@ -64,7 +64,7 @@ jobs:
- name: Download comparison markdown to GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: scripts/analyzer
path: tooling/analyzer
name: compare-markdown.md
- name: Find Comment
@@ -80,5 +80,5 @@ jobs:
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: scripts/analyzer/compare-markdown.md
body-path: tooling/analyzer/compare-markdown.md
edit-mode: replace
+2 -2
View File
@@ -19,12 +19,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: "actions/checkout@v6"
uses: "actions/checkout@v7"
- name: Upload fonts to CDN
id: upload
+2 -3
View File
@@ -21,7 +21,7 @@ jobs:
deployments: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -46,9 +46,8 @@ jobs:
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to dev-gcp
uses: nais/deploy/actions/deploy@master
uses: nais/deploy/actions/deploy@v2
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-dev.yaml
CLUSTER: dev-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+55
View File
@@ -0,0 +1,55 @@
name: Deploy Aksel MCP server
on:
push:
branches:
- main
paths:
- "apps/mcp-server/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE: ghcr.io/navikt/aksel
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test mcp-server
run: cd apps/mcp-server && yarn test
- name: Build mcp-server
run: cd apps/mcp-server && yarn build
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
dockerfile: apps/mcp-server/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@v2
env:
RESOURCE: apps/mcp-server/.nais/app.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+8 -6
View File
@@ -18,21 +18,24 @@ jobs:
sync_data_with_sanity:
name: Sync data to Sanity
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Run ts-docgen
run: yarn docgen
- name: Run metadata generation
run: yarn docgen:meta
- name: Update Sanity
env:
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS_BQ_READ }}
run: yarn workspace aksel.nav.no sanity:update
build_and_deploy:
@@ -45,7 +48,7 @@ jobs:
deployments: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -73,9 +76,8 @@ jobs:
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
uses: nais/deploy/actions/deploy@v2
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+3 -4
View File
@@ -21,12 +21,12 @@ jobs:
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
@@ -54,9 +54,8 @@ jobs:
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
uses: nais/deploy/actions/deploy@v2
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: v7-aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+2 -7
View File
@@ -23,22 +23,17 @@ jobs:
pull-requests: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: Install dependencies
run: yarn
env:
@@ -1,34 +0,0 @@
name: Update article views (Naisjob)
on:
workflow_dispatch:
jobs:
execute:
name: Build and deploy article views job
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: scripts/deno/article-views/Dockerfile.update-article-views
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy Naisjob to prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: scripts/deno/article-views/nais-update-views-job.yml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+2 -9
View File
@@ -90,15 +90,10 @@ dist/
assets.json
lib/
lerna-debug.log
examples/legacy/node_modules
examples/legacy/out
examples/webpack/node_modules
examples/webpack/out
.gitattributes
.iml
.DS_Store
*.d.ts
*-exports.less
*/figma-api/icons
*/ds-icons/png
@@ -112,13 +107,12 @@ utilities/**/css
*/**/NAV-ikonpakke-png.zip
*/**/NAV-ikonpakke-svg.zip
react-docgen.json
react-docgen-docs.json
cjs
esm
@navikt/community/navno/css
.vercel
_docs.json
_metadata.json
**/component-names.generated.ts
.yarn/*
.yarn/cache
@@ -136,7 +130,6 @@ _docs.json
.next
!*/website/lib/
!*/website/public/
!aksel.nav.no/**/*.d.ts
!*/sanity/lib/
!*/sanity/static/
out
+7 -34
View File
@@ -1,36 +1,9 @@
echo "husky - DEPRECATED
Please remove the following two lines from $0:
#!/usr/bin/env sh
if [ -z "$husky_skip_init" ]; then
debug () {
if [ "$HUSKY_DEBUG" = "1" ]; then
echo "husky (debug) - $1"
fi
}
. \"\$(dirname -- \"\$0\")/_/husky.sh\"
readonly hook_name="$(basename -- "$0")"
debug "starting $hook_name..."
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
readonly husky_skip_init=1
export husky_skip_init
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
if [ $exitCode = 127 ]; then
echo "husky - command not found in PATH=$PATH"
fi
exit $exitCode
fi
They WILL FAIL in v10.0.0
"
-3
View File
@@ -1,6 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
CHANGED=`git diff $1 $2 --stat -- yarn.lock | wc -l`
if [ $CHANGED -gt 0 ];
then
-3
View File
@@ -1,6 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
CHANGED=`git diff HEAD@{1} --stat -- yarn.lock | wc -l`
if [ $CHANGED -gt 0 ];
then
-3
View File
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn lint-staged
-3
View File
@@ -1,6 +1,3 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
# Only run tests when pushing to main branch
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
-2
View File
@@ -1,5 +1,3 @@
**/nais*.yaml
**/nais*.yml
**/codemod/**/tests/**/*.js
**/@navikt/aksel/**/*.input.*
**/@navikt/aksel/**/*.output.*
+17 -14
View File
@@ -5,9 +5,7 @@ import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { loadCsf } from "storybook/internal/csf-tools";
import { InlineConfig } from "vite";
import turbosnap from "vite-plugin-turbosnap";
import TsconfigPathsPlugin from "vite-tsconfig-paths";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -17,6 +15,17 @@ const includeWebsiteStories = process.env.WITH_WEBSITE === "true";
const indexRegex = /export const args = {\s+index: (\d+),/;
const addons = [
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-themes"),
getAbsolutePath("@storybook/addon-docs"),
getAbsolutePath("@storybook/addon-vitest"),
];
if (process.env.NODE_ENV === "development") {
addons.push(getAbsolutePath("@github-ui/storybook-addon-performance-panel"));
}
export default defineMain({
experimental_indexers: (indexers) => {
// Changes here might need to be reflected in aksel.nav.no/website/.storybook/main.ts
@@ -79,12 +88,7 @@ export default defineMain({
stories: resolveStoriesPaths(),
addons: [
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-themes"),
getAbsolutePath("@storybook/addon-docs"),
getAbsolutePath("@storybook/addon-vitest"),
],
addons,
framework: {
name: getAbsolutePath("@storybook/react-vite"),
@@ -99,21 +103,20 @@ export default defineMain({
async viteFinal(config, { configType }) {
const { mergeConfig } = await import("vite");
// The TsconfigPathsPlugin is only used to silence errors when importing nextjs components, but the imports does not acutally work.
const tsConfigPathsPluginOpts = { root: "aksel.nav.no/website/" };
return mergeConfig(config, {
build: { cssMinify: "lightningcss" },
resolve: {
tsconfigPaths: true,
},
plugins:
configType === "PRODUCTION"
? [
TsconfigPathsPlugin(tsConfigPathsPluginOpts),
turbosnap({
rootDir: config.root ?? process.cwd(),
}),
]
: [TsconfigPathsPlugin(tsConfigPathsPluginOpts)],
} satisfies InlineConfig);
: [],
} satisfies typeof config);
},
/* Lets us preview Roboto-flex font in storybook. */
+106 -11
View File
@@ -1,39 +1,101 @@
import addonPerformancePanel from "@github-ui/storybook-addon-performance-panel";
import addonA11y from "@storybook/addon-a11y";
import addonDocs from "@storybook/addon-docs";
import addonThemes, { withThemeByClassName } from "@storybook/addon-themes";
import addonVitest from "@storybook/addon-vitest";
import { definePreview } from "@storybook/react-vite";
import React, { useEffect } from "react";
import "../@navikt/core/css/src/data-table.css";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import "../@navikt/core/css/src/data-token-filter.css";
import dataTokenFilterStyles from "../@navikt/core/css/src/data-token-filter.css?inline";
import "../@navikt/core/css/src/data-toolbar.css";
import dataToolbarStyles from "../@navikt/core/css/src/data-toolbar.css?inline";
import "../@navikt/core/css/src/form/combobox2.css";
import combobox2Styles from "../@navikt/core/css/src/form/combobox2.css?inline";
import "../@navikt/core/css/src/index.css";
import dsStyles from "../@navikt/core/css/src/index.css?inline";
import "../@navikt/core/css/src/listbox.css";
import listboxStyles from "../@navikt/core/css/src/listbox.css?inline";
import { Provider } from "../@navikt/core/react/src/provider";
import { Translations } from "../@navikt/core/react/src/utils/i18n/i18n.types";
import type { Translations } from "../@navikt/core/react/src/utils/i18n/i18n.types";
import { en, nb, nn } from "../@navikt/core/react/src/utils/i18n/locales";
import "./layout.css";
type Language = "nb" | "nn" | "en";
type WebComponentMode = "off" | "open" | "closed";
const locales: Record<Language, Translations> = {
nb,
nn,
en,
};
const LanguageDecorator = ({
const StoryProviderDecorator = ({
children,
lang,
rootElement,
}: {
children: React.ReactNode;
lang: Language | undefined;
rootElement?: HTMLElement;
}) => {
useEffect(() => {
document.documentElement.lang = lang || "nb";
}, [lang]);
return (
<Provider locale={lang ? locales[lang] : undefined}>{children}</Provider>
<Provider
rootElement={rootElement}
locale={lang ? locales[lang] : undefined}
>
{children}
</Provider>
);
};
const webComponentStyles = [
dataTokenFilterStyles,
dataToolbarStyles,
listboxStyles,
combobox2Styles,
dsStyles,
].join("\n");
const WebComponentDecorator = ({
children,
mode,
}: {
children: (rootElement: HTMLDivElement) => React.ReactNode;
mode: Exclude<WebComponentMode, "off">;
}) => {
const hostRef = useRef<HTMLDivElement>(null);
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const host = hostRef.current;
if (!host) {
return;
}
const shadowRoot = host.attachShadow({ mode });
const style = document.createElement("style");
style.textContent = webComponentStyles;
shadowRoot.appendChild(style);
const mount = document.createElement("div");
shadowRoot.appendChild(mount);
setRootElement(mount);
return () => {
setRootElement(null);
};
}, [mode]);
return (
<>
<div ref={hostRef} key={mode} />
{rootElement ? createPortal(children(rootElement), rootElement) : null}
</>
);
};
@@ -58,8 +120,17 @@ const TypoDecorator = ({
return <>{children}</>;
};
const addons = [addonA11y(), addonThemes(), addonDocs(), addonVitest()];
/**
* Fixes flaky interaction tests
*/
if (process.env.NODE_ENV === "development") {
addons.push(addonPerformancePanel());
}
export default definePreview({
addons: [addonA11y(), addonThemes(), addonDocs(), addonVitest()],
addons,
parameters: {
options: {
storySort: {
@@ -110,11 +181,23 @@ export default definePreview({
dynamicTitle: true,
},
},
webComponentWrapper: {
toolbar: {
icon: "component",
items: [
{ value: "off", title: "DOM: Default" },
{ value: "open", title: "Web component: Open shadow root" },
{ value: "closed", title: "Web component: Closed shadow root" },
],
dynamicTitle: true,
},
},
},
initialGlobals: {
mode: "default",
font: "Source Sans 3",
webComponentWrapper: "off",
},
decorators: [
@@ -123,11 +206,23 @@ export default definePreview({
<StoryFn />
</TypoDecorator>
),
(StoryFn, context) => (
<LanguageDecorator lang={context.globals.language}>
<StoryFn />
</LanguageDecorator>
),
(StoryFn, context) =>
context.globals.webComponentWrapper === "off" ? (
<StoryProviderDecorator lang={context.globals.language}>
<StoryFn />
</StoryProviderDecorator>
) : (
<WebComponentDecorator mode={context.globals.webComponentWrapper}>
{(rootElement) => (
<StoryProviderDecorator
lang={context.globals.language}
rootElement={rootElement}
>
<StoryFn />
</StoryProviderDecorator>
)}
</WebComponentDecorator>
),
withThemeByClassName({
themes: {
light: "light",
-7
View File
@@ -1,7 +0,0 @@
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import { setProjectAnnotations } from "@storybook/react-vite";
import * as projectAnnotations from "./preview";
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
+40
View File
@@ -1,5 +1,45 @@
# @navikt/aksel-icons
## 8.16.1
## 8.16.0
## 8.15.0
### Minor Changes
- New icons: Timeline and TimelineFill ([#5050](https://github.com/navikt/aksel/pull/5050))
## 8.14.0
## 8.13.1
## 8.13.0
### Minor Changes
- Icons: New icons Armchair, FocusRoom, PhoneRoom and Sleep ([#4979](https://github.com/navikt/aksel/pull/4979))
## 8.12.1
## 8.12.0
## 8.11.1
## 8.11.0
### Minor Changes
- Icons: New icon `Newsletter` and `NewsletterFill` ([#4887](https://github.com/navikt/aksel/pull/4887))
## 8.10.6
## 8.10.5
## 8.10.4
## 8.10.3
## 8.10.2
## 8.10.1
@@ -3,7 +3,7 @@ import { JSON_SCHEMA, load } from "js-yaml";
import fs from "node:fs";
import path from "node:path";
import { describe, expect, test } from "vitest";
import { IconYml } from "../config/figma/make-configs";
import type { IconYml } from "../config/figma/make-configs";
const basePath = path.resolve(__dirname, "../icons");
@@ -28,7 +28,7 @@ describe(`SVG should be correctly parsed`, () => {
</svg>
`;
test("a", () => {
test("should replace width and height with 1em, and fill with currentColor", () => {
expect(parseIcon(simpleSvg)).toEqual(simpleSvgResult);
expect(parseIcon(advancedSvg)).toEqual(advancedSvgResult);
});
+1 -1
View File
@@ -2,7 +2,7 @@
* Fikser https://github.com/navikt/aksel/issues/1758
*/
const fs = require("fs");
const fs = require("node:fs");
const basePath = "./dist/react/esm";
@@ -1,4 +1,4 @@
import {
import type {
BadRequestErrorResponseWithErrMessage,
ErrorResponsePayloadWithErrMessage,
GetFileComponentsResponse,
@@ -12,20 +12,17 @@ const FIGMA_FILE_KEY = "wEdyFjCQSBR3U7FvrMbPXa";
export const fetchIcons = async (): Promise<PublishedComponent[]> => {
console.group("Fetching list of published icons from Figma...");
const data:
| GetFileComponentsResponse
| BadRequestErrorResponseWithErrMessage = await fetch(
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`,
{
GetFileComponentsResponse | BadRequestErrorResponseWithErrMessage =
await fetch(`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`, {
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
},
},
)
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
})
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
if (data.status !== 200) {
throw new Error(data.err);
@@ -1,4 +1,4 @@
import { PublishedComponent } from "@figma/rest-api-spec";
import type { PublishedComponent } from "@figma/rest-api-spec";
import { camelCase, startCase } from "lodash";
export const resolveName = (icon: PublishedComponent) => {
@@ -1,4 +1,4 @@
import { PublishedComponent } from "@figma/rest-api-spec";
import type { PublishedComponent } from "@figma/rest-api-spec";
import { dump } from "js-yaml";
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
@@ -52,7 +52,7 @@ export const makeConfig = (
const yml = dump(config, {
noRefs: true,
skipInvalid: false,
quotingType: '"',
quoteStyle: "double",
});
writeFileSync(resolve(dirLocation, `${config.name}.yml`), yml, {
+1 -1
View File
@@ -1,4 +1,4 @@
const path = require("path");
const path = require("node:path");
const defaultIndexTemplate = (paths) => {
const exportEntries = paths.map((filePath) => {
+6 -6
View File
@@ -1,7 +1,7 @@
const fastglob = require("fast-glob");
const path = require("path");
const jsYaml = require("js-yaml");
const fs = require("fs");
const path = require("node:path");
const { load, JSON_SCHEMA } = require("js-yaml");
const fs = require("node:fs");
const basePath = path.resolve(__dirname, "../icons");
@@ -12,8 +12,8 @@ const ymlList = fastglob
const metadata = {};
ymlList.forEach((file) => {
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
const ymlData = load(fs.readFileSync(`${basePath}/${file}`), {
schema: JSON_SCHEMA,
});
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
@@ -42,7 +42,7 @@ fs.writeFileSync(
id: string;
name: string;
category: string;
sub_category: "Time",
sub_category: string;
keywords: string[];
variant: "stroke" | "fill";
created_at: Date;
+2 -2
View File
@@ -1,12 +1,12 @@
const fastglob = require("fast-glob");
const path = require("path");
const path = require("node:path");
const {
existsSync,
readFileSync,
mkdirSync,
rmSync,
writeFileSync,
} = require("fs");
} = require("node:fs");
function main() {
const basePath = path.resolve(__dirname, "../icons");
+5 -5
View File
@@ -5,10 +5,10 @@ const {
mkdirSync,
rmSync,
copyFileSync,
} = require("fs");
const path = require("path");
} = require("node:fs");
const path = require("node:path");
const fastGlob = require("fast-glob");
const jsYaml = require("js-yaml");
const { load, JSON_SCHEMA } = require("js-yaml");
const iconFolder = "./core-icons";
@@ -26,8 +26,8 @@ async function generateFile() {
const categories = new Map();
ymlList.forEach((file) => {
const ymlData = jsYaml.load(readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
const ymlData = load(readFileSync(`${basePath}/${file}`), {
schema: JSON_SCHEMA,
});
categories.set(
ymlData.category,
@@ -1,4 +0,0 @@
<svg width="32" height="32" viewBox="0 0 170 170" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.07318 54.5734C-0.692218 51.806 -0.690888 47.3204 2.07614 44.5546L38.6019 8.04512C38.7787 7.8684 38.9576 7.69433 39.1387 7.52295L39.1205 7.50476C42.7235 3.90124 47.1588 1.63753 51.8075 0.71364C53.8316 0.243049 55.9182 1.07479e-05 58.0297 1.01387e-05L142.517 0C157.695 5.55246e-06 170 12.3065 170 27.4874V111.958C170 113.916 169.791 115.852 169.386 117.737C168.522 122.523 166.241 127.103 162.542 130.803L162.533 130.794C162.342 130.997 162.148 131.197 161.95 131.395L125.425 167.925C122.659 170.692 118.174 170.692 115.408 167.925C112.642 165.158 112.642 160.673 115.408 157.906L151.933 121.376C153.698 119.61 154.917 117.4 155.48 115.008C155.999 111.779 155.014 108.355 152.525 105.866L64.1955 17.5236C61.632 14.9597 58.0865 13.9765 54.7698 14.574C52.4605 15.1572 50.3292 16.3547 48.6162 18.0669L12.0905 54.5764C9.32349 57.3422 4.83857 57.3409 2.07318 54.5734ZM80.8759 14.1688L155.833 89.1374V27.4874C155.833 20.1317 149.871 14.1688 142.517 14.1688L80.8759 14.1688Z" fill="#004367"/>
<path d="M2.07466 167.925C-0.691554 165.158 -0.691554 160.673 2.07466 157.906L65.8247 94.1468C68.5909 91.3801 73.0758 91.3801 75.842 94.1468C78.6082 96.9134 78.6082 101.399 75.842 104.166L12.092 167.925C9.32579 170.692 4.84087 170.692 2.07466 167.925Z" fill="#004367"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import React from "react";
import * as AkselIcons from "@navikt/aksel-icons";
import { StarFillIcon } from "@navikt/aksel-icons";
import type { StarFillIcon } from "@navikt/aksel-icons";
const meta: Meta<typeof StarFillIcon> = {
title: "aksel-icons/Icons",
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8398 3.25C16.7677 3.25 17.5195 4.0335 17.5195 5V8.25H18.7197C19.6476 8.25 20.4004 9.0335 20.4004 10V16C20.4004 16.9665 19.6476 17.75 18.7197 17.75H16.8398L17.4834 19.7627C17.6091 20.1557 17.4046 20.5809 17.0273 20.7119C16.6502 20.8426 16.2419 20.6301 16.1162 20.2373L15.3203 17.75H8.67969L7.88379 20.2373C7.75809 20.6301 7.34978 20.8426 6.97266 20.7119C6.59542 20.5809 6.39086 20.1557 6.5166 19.7627L7.16016 17.75H5.28027C4.35244 17.75 3.59961 16.9665 3.59961 16V10C3.59961 9.0335 4.35244 8.25 5.28027 8.25H6.48047V5C6.48047 4.0335 7.23232 3.25 8.16016 3.25H15.8398ZM5.28027 9.75C5.14773 9.75 5.04004 9.86193 5.04004 10V16C5.04004 16.1381 5.14773 16.25 5.28027 16.25H18.7197C18.8523 16.25 18.96 16.1381 18.96 16V10C18.96 9.86193 18.8523 9.75 18.7197 9.75H16.7998C16.6673 9.75011 16.5596 9.862 16.5596 10V13C16.5596 13.4142 16.2375 13.75 15.8398 13.75H8.16016C7.76251 13.75 7.44043 13.4142 7.44043 13V10C7.44043 9.862 7.33265 9.75011 7.2002 9.75H5.28027ZM8.16016 4.75C8.02761 4.75 7.91992 4.86193 7.91992 5V8.41992C8.48722 8.7009 8.87988 9.30212 8.87988 10V12.25H15.1201V10C15.1201 9.30212 15.5128 8.7009 16.0801 8.41992V5C16.0801 4.86193 15.9724 4.75 15.8398 4.75H8.16016Z" fill="#202733"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+7
View File
@@ -0,0 +1,7 @@
name: Armchair
category: Workplace
sub_category: Workplace
keywords:
- Armchair
variant: Stroke
created_at: 15.06.2026
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.95508 9.45605C7.22008 9.45617 7.43457 9.67999 7.43457 9.95605V12.6582C7.43457 13.1 7.77897 13.458 8.20312 13.458H15.7812C16.2054 13.458 16.5498 13.1 16.5498 12.6582V9.95605C16.5498 9.67996 16.7643 9.45613 17.0293 9.45605H19.9209C20.186 9.45605 20.4014 9.67991 20.4014 9.95605V16.0596C20.4014 16.9432 19.7135 17.66 18.8652 17.6602H16.8262L17.4707 19.6738C17.5964 20.0666 17.3926 20.4919 17.0156 20.623C16.6384 20.754 16.2293 20.5414 16.1035 20.1484L15.3076 17.6602H8.66699L7.87109 20.1484C7.74537 20.5413 7.33716 20.7539 6.95996 20.623C6.58272 20.4921 6.37816 20.0668 6.50391 19.6738L7.14844 17.6602H5.11719C4.26891 17.6601 3.59961 16.9432 3.59961 16.0596V9.95605C3.59961 9.67991 3.81498 9.45605 4.08008 9.45605H6.95508ZM15.8271 3.33984C16.3572 3.33984 16.7869 3.83751 16.7871 4.38965V7.61523C16.787 7.80915 16.6364 7.96679 16.4502 7.9668C15.7053 7.9668 15.1017 8.59523 15.1016 9.37109V11.4541C15.1014 11.7301 14.8861 11.9541 14.6211 11.9541H9.36816C9.1032 11.9541 8.88791 11.7301 8.8877 11.4541V9.38379C8.8877 8.60107 8.27875 7.9668 7.52734 7.9668C7.33967 7.96669 7.18765 7.80778 7.1875 7.6123V4.38965C7.18769 3.83751 7.61738 3.33984 8.14746 3.33984H15.8271Z" fill="#202733"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
name: ArmchairFill
category: Workplace
sub_category: Workplace
keywords:
- ArmchairFill
variant: Fill
created_at: 15.06.2026
+2 -1
View File
@@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.4695 11.0304C16.7624 11.3233 17.2371 11.3233 17.53 11.0304L21.03 7.53042C21.1707 7.38977 21.2498 7.19906 21.2498 7.00015C21.2498 6.80124 21.1707 6.61053 21.03 6.46988L17.53 2.96988C17.2371 2.67698 16.7624 2.67698 16.4695 2.96988C16.1766 3.26277 16.1766 3.73753 16.4695 4.03042L18.6892 6.25015L9.49976 6.25015C9.08555 6.25015 8.74976 6.58593 8.74976 7.00015C8.74976 7.41436 9.08555 7.75015 9.49976 7.75015L18.6892 7.75015L16.4695 9.96988C16.1766 10.2628 16.1766 10.7375 16.4695 11.0304ZM6.46949 21.0304C6.76238 21.3233 7.23714 21.3233 7.53003 21.0304C7.82293 20.7375 7.82293 20.2628 7.53003 19.9699L5.31031 17.7501H14.4998C14.914 17.7501 15.2498 17.4144 15.2498 17.0001C15.2498 16.5859 14.914 16.2501 14.4998 16.2501H5.31031L7.53003 14.0304C7.82293 13.7375 7.82293 13.2628 7.53003 12.9699C7.23714 12.677 6.76238 12.677 6.46949 12.9699L2.96949 16.4699C2.67659 16.7628 2.67659 17.2375 2.96949 17.5304L6.46949 21.0304Z" fill="#202733"/>
<path d="M16.4695 11.0302C16.7624 11.3231 17.2372 11.3231 17.5301 11.0302L21.0301 7.53018C21.1707 7.38953 21.2498 7.19882 21.2498 6.9999C21.2498 6.80099 21.1707 6.61028 21.0301 6.46963L17.5301 2.96963C17.2372 2.67674 16.7624 2.67674 16.4695 2.96963C16.1766 3.26252 16.1766 3.73728 16.4695 4.03018L18.6892 6.2499H9.49979C9.08558 6.2499 8.74979 6.58569 8.74979 6.9999C8.74979 7.41412 9.08558 7.7499 9.49979 7.7499H18.6892L16.4695 9.96963C16.1766 10.2625 16.1766 10.7373 16.4695 11.0302Z" fill="#202733"/>
<path d="M6.46952 21.0302C6.76241 21.3231 7.23717 21.3231 7.53006 21.0302C7.82296 20.7373 7.82296 20.2625 7.53006 19.9696L5.31034 17.7499H14.4998C14.914 17.7499 15.2498 17.4141 15.2498 16.9999C15.2498 16.5857 14.914 16.2499 14.4998 16.2499H5.31034L7.53006 14.0302C7.82296 13.7373 7.82296 13.2625 7.53006 12.9696C7.23717 12.6767 6.76241 12.6767 6.46952 12.9696L2.96952 16.4696C2.67662 16.7625 2.67662 17.2373 2.96952 17.5302L6.46952 21.0302Z" fill="#202733"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+2 -1
View File
@@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4697 21.0303C16.7626 21.3232 17.2374 21.3232 17.5303 21.0303L21.0303 17.5303C21.3232 17.2374 21.3232 16.7626 21.0303 16.4697C20.7374 16.1768 20.2626 16.1768 19.9697 16.4697L17.75 18.6893L17.75 9.5C17.75 9.08579 17.4142 8.75 17 8.75C16.5858 8.75 16.25 9.08579 16.25 9.5L16.25 18.6893L14.0303 16.4697C13.7374 16.1768 13.2626 16.1768 12.9697 16.4697C12.6768 16.7626 12.6768 17.2374 12.9697 17.5303L16.4697 21.0303ZM4.03033 7.53033L6.25 5.31066L6.25 14.5C6.25 14.9142 6.58579 15.25 7 15.25C7.41421 15.25 7.75 14.9142 7.75 14.5L7.75 5.31066L9.96967 7.53033C10.2626 7.82322 10.7374 7.82322 11.0303 7.53033C11.3232 7.23744 11.3232 6.76256 11.0303 6.46967L7.53033 2.96967C7.23744 2.67678 6.76256 2.67678 6.46967 2.96967L2.96967 6.46967C2.67678 6.76256 2.67678 7.23744 2.96967 7.53033C3.26256 7.82322 3.73744 7.82322 4.03033 7.53033Z" fill="#202733"/>
<path d="M7 15.25C7.41422 15.25 7.75 14.9142 7.75 14.5V5.31055L9.96973 7.53028C10.2626 7.82317 10.7374 7.82317 11.0303 7.53028C11.3232 7.23738 11.3232 6.76262 11.0303 6.46973L7.53028 2.96973C7.23738 2.67684 6.76262 2.67684 6.46973 2.96973L2.96973 6.46973C2.67684 6.76262 2.67684 7.23738 2.96973 7.53028C3.26262 7.82317 3.73738 7.82317 4.03028 7.53028L6.25 5.31055V14.5C6.25 14.9142 6.58579 15.25 7 15.25Z" fill="#202733"/>
<path d="M16.4697 21.0303C16.7626 21.3232 17.2374 21.3232 17.5303 21.0303L21.0303 17.5303C21.3232 17.2374 21.3232 16.7626 21.0303 16.4697C20.7374 16.1768 20.2626 16.1768 19.9697 16.4697L17.75 18.6895V9.5C17.75 9.08579 17.4142 8.75 17 8.75C16.5858 8.75 16.25 9.08579 16.25 9.5V18.6895L14.0303 16.4697C13.7374 16.1768 13.2626 16.1768 12.9697 16.4697C12.6768 16.7626 12.6768 17.2374 12.9697 17.5303L16.4697 21.0303Z" fill="#202733"/>
</svg>

Before

Width:  |  Height:  |  Size: 999 B

After

Width:  |  Height:  |  Size: 957 B

+2 -1
View File
@@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.25 19.5C11.25 19.9142 10.9143 20.25 10.5 20.25H4.50003C4.30112 20.25 4.11035 20.171 3.9697 20.0303C3.82905 19.8897 3.75003 19.6989 3.75003 19.5L3.75003 13.5C3.75003 13.0858 4.08582 12.75 4.50003 12.75C4.91424 12.75 5.25003 13.0858 5.25003 13.5L5.25003 17.6894L9.96971 12.9697C10.2626 12.6768 10.7375 12.6768 11.0304 12.9697C11.3233 13.2626 11.3233 13.7374 11.0304 14.0303L6.3107 18.75L10.5 18.75C10.9143 18.75 11.25 19.0858 11.25 19.5ZM19.5 11.25C19.0858 11.25 18.75 10.9142 18.75 10.5L18.75 6.31066L14.0304 11.0303C13.7375 11.3232 13.2626 11.3232 12.9697 11.0303C12.6768 10.7374 12.6768 10.2626 12.9697 9.96967L17.6894 5.24998L13.5 5.24998C13.0858 5.24998 12.75 4.9142 12.75 4.49998C12.75 4.08577 13.0858 3.74998 13.5 3.74998L19.5 3.74998C19.6989 3.74998 19.8897 3.829 20.0304 3.96965C20.171 4.11031 20.25 4.30107 20.25 4.49998L20.25 10.5C20.25 10.9142 19.9142 11.25 19.5 11.25Z" fill="#202733"/>
<path d="M19.5 11.25C19.9142 11.25 20.25 10.9142 20.25 10.5V4.5C20.25 4.30109 20.1709 4.11038 20.0303 3.96973C19.8896 3.82907 19.6989 3.75 19.5 3.75H13.5C13.0858 3.75002 12.75 4.0858 12.75 4.5C12.75 4.9142 13.0858 5.24998 13.5 5.25H17.6895L12.9697 9.96973C12.6769 10.2626 12.6769 10.7374 12.9697 11.0303C13.2626 11.3232 13.7374 11.3232 14.0303 11.0303L18.75 6.31055V10.5C18.75 10.9142 19.0858 11.25 19.5 11.25Z" fill="#202733"/>
<path d="M10.5 20.25C10.9142 20.25 11.25 19.9142 11.25 19.5C11.25 19.0858 10.9142 18.75 10.5 18.75H6.31055L11.0303 14.0303C11.3232 13.7374 11.3232 13.2626 11.0303 12.9697C10.7374 12.6768 10.2626 12.6768 9.96973 12.9697L5.25 17.6895V13.5C5.25 13.0858 4.91421 12.75 4.5 12.75C4.08579 12.75 3.75 13.0858 3.75 13.5V19.5C3.75 19.6989 3.82907 19.8896 3.96973 20.0303C4.11038 20.1709 4.30109 20.25 4.5 20.25H10.5Z" fill="#202733"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 957 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.71973 10.25C7.11737 10.25 7.44043 10.5858 7.44043 11V15C7.44043 15.138 7.54728 15.2498 7.67969 15.25H10.0801C11.0079 15.25 11.7598 16.0335 11.7598 17V21C11.7598 21.4142 11.4377 21.75 11.04 21.75C10.6424 21.75 10.3203 21.4142 10.3203 21V17C10.3203 16.862 10.2126 16.75 10.0801 16.75H7.67969C6.75199 16.7498 6 15.9664 6 15V11C6 10.5859 6.32221 10.2502 6.71973 10.25ZM18.7197 11.25C19.1174 11.25 19.4404 11.5858 19.4404 12C19.4404 12.4142 19.1174 12.75 18.7197 12.75H16.0801V21C16.0801 21.4141 15.7578 21.7498 15.3604 21.75C14.9627 21.75 14.6396 21.4142 14.6396 21V12.75H11.5195C11.1221 12.7497 10.7998 12.4141 10.7998 12C10.7998 11.5859 11.1221 11.2503 11.5195 11.25H18.7197ZM18.5742 5.62793C18.7715 5.26833 19.2124 5.14321 19.5576 5.34863C19.9026 5.55421 20.0223 6.01256 19.8252 6.37207L17.6982 10.25H13.9199C13.5223 10.25 13.2002 9.91419 13.2002 9.5C13.2002 9.08581 13.5223 8.75004 13.9199 8.75H16.8623L18.5742 5.62793ZM6.71973 3C8.31031 3 9.59961 4.34315 9.59961 6C9.59961 7.65685 8.31031 9 6.71973 9C5.12927 8.99985 3.83984 7.65676 3.83984 6C3.83984 4.34324 5.12927 3.00015 6.71973 3ZM6.71973 4.5C5.92456 4.50015 5.28027 5.17167 5.28027 6C5.28027 6.82833 5.92456 7.49985 6.71973 7.5C7.51502 7.5 8.16016 6.82843 8.16016 6C8.16016 5.17157 7.51502 4.5 6.71973 4.5Z" fill="#202733"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+7
View File
@@ -0,0 +1,7 @@
name: FocusRoom
category: Workplace
sub_category: Workplace
keywords:
- FocusRoom
variant: Stroke
created_at: 15.06.2026
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.71973 10.25C7.11737 10.25 7.44043 10.5858 7.44043 11V15C7.44043 15.138 7.54728 15.2498 7.67969 15.25H10.0801C11.0079 15.25 11.7598 16.0335 11.7598 17V21C11.7598 21.4142 11.4377 21.75 11.04 21.75C10.6424 21.75 10.3203 21.4142 10.3203 21V17C10.3203 16.862 10.2126 16.75 10.0801 16.75H7.67969C6.75199 16.7498 6 15.9664 6 15V11C6 10.5859 6.32221 10.2502 6.71973 10.25ZM18.7197 11.25C19.1174 11.25 19.4404 11.5858 19.4404 12C19.4404 12.4142 19.1174 12.75 18.7197 12.75H16.0801V21C16.0801 21.4141 15.7578 21.7498 15.3604 21.75C14.9627 21.75 14.6396 21.4142 14.6396 21V12.75H11.5195C11.1221 12.7497 10.7998 12.4141 10.7998 12C10.7998 11.5859 11.1221 11.2503 11.5195 11.25H18.7197ZM18.5742 5.62793C18.7715 5.26833 19.2124 5.14321 19.5576 5.34863C19.9026 5.55421 20.0223 6.01256 19.8252 6.37207L17.6982 10.25H13.9199C13.5223 10.25 13.2002 9.91419 13.2002 9.5C13.2002 9.08581 13.5223 8.75004 13.9199 8.75H16.8623L18.5742 5.62793ZM6.71973 3C8.31031 3 9.59961 4.34315 9.59961 6C9.59961 7.65685 8.31031 9 6.71973 9C5.12927 8.99985 3.83984 7.65676 3.83984 6C3.83984 4.34324 5.12927 3.00015 6.71973 3Z" fill="#202733"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,7 @@
name: FocusRoomFill
category: Workplace
sub_category: Workplace
keywords:
- FocusRoomFill
variant: Fill
created_at: 15.06.2026
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.25 3C18.4489 3 18.6396 3.07907 18.7803 3.21973C18.9209 3.36038 19 3.55109 19 3.75V7.18848L21.0303 8.91309C21.1709 9.05362 21.2499 9.2446 21.25 9.44336L21.2012 20.2461C21.2009 20.6601 20.8652 20.9961 20.4512 20.9961L3.5 20.9824C3.08579 20.9824 2.75 20.6466 2.75 20.2324V9.43066C2.75007 9.23181 2.82908 9.04099 2.96973 8.90039L5 7.18848V3.75C5 3.33579 5.33579 3 5.75 3H18.25ZM5.38086 19.5508H18.6191L13.6553 14.249H10.3447L5.38086 19.5508ZM15.1201 13.6533L19.75 18.3428L19.7012 10.9424L15.1201 13.6533ZM4.29883 18.2324L8.87891 13.6533L4.25 10.832L4.29883 18.2324ZM6.5 10.4307L10.2266 12.749H13.7734L17.5 10.4307V4.5H6.5V10.4307ZM13 9C13.4142 9 13.75 9.33579 13.75 9.75C13.75 10.1642 13.4142 10.5 13 10.5H9C8.58579 10.5 8.25 10.1642 8.25 9.75C8.25 9.33579 8.58579 9 9 9H13ZM15 6C15.4142 6 15.75 6.33579 15.75 6.75C15.75 7.16421 15.4142 7.5 15 7.5H9C8.58579 7.5 8.25 7.16421 8.25 6.75C8.25 6.33579 8.58579 6 9 6H15Z" fill="#202733"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+16
View File
@@ -0,0 +1,16 @@
name: Newsletter
category: Interface
sub_category: Communication
keywords:
- email
- message
- communication
- send
- e-post
- melding
- kommunikasjon
- sende
- lest
- read
variant: Stroke
created_at: 28.04.2026
Loaded 100 of 1840 files, more files were not shown because too many files have changed in this diff. Show more