@iqual/nuxt-icms 1.6.1 → 1.6.2-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +86 -0
  2. package/__tests__/anchors.spec.ts +43 -0
  3. package/__tests__/contract/blokkliComponents.spec.ts +59 -0
  4. package/__tests__/contract/componentFragments.spec.ts +24 -0
  5. package/__tests__/contract/languages.spec.ts +63 -0
  6. package/__tests__/contract/project.ts +144 -0
  7. package/__tests__/hreflang.spec.ts +39 -0
  8. package/{tests → __tests__}/pages.spec.ts +14 -5
  9. package/app.config.ts +15 -0
  10. package/app.vue +3 -21
  11. package/assets/css/backend.css +56 -0
  12. package/assets/css/base/base.css +2 -2
  13. package/assets/css/base/typography.css +2 -2
  14. package/assets/css/ckeditor.css +3 -4
  15. package/assets/css/components/decorations.css +1 -1
  16. package/assets/css/components/editor.css +1 -1
  17. package/assets/css/components/grid-container.css +19 -19
  18. package/assets/css/fe-fixes.css +6 -12
  19. package/assets/css/theme/colors/color-primary.css +33 -11
  20. package/assets/css/theme/colors/color-secondary.css +33 -11
  21. package/assets/css/utilities/utilities.css +1 -1
  22. package/assets/symbols/anchor.svg +3 -0
  23. package/blokkli.editAdapter.ts +56 -0
  24. package/bundles.ts +6 -6
  25. package/cache/__tests__/pageCache.spec.ts +540 -0
  26. package/cache/cacheTagRegistry.ts +124 -0
  27. package/cache/enabledForRequest.ts +21 -0
  28. package/cache/multiCache.serverOptions.ts +58 -0
  29. package/cache/pageCache.ts +360 -0
  30. package/components/Blokkli/Anchor/icon.svg +3 -0
  31. package/components/Blokkli/Anchor/index.vue +13 -0
  32. package/components/Blokkli/AnchorNavigation/icon.svg +8 -0
  33. package/components/Blokkli/AnchorNavigation/index.vue +9 -0
  34. package/components/Blokkli/Card/Element/index.vue +0 -1
  35. package/components/Blokkli/Collapsible/Element/index.vue +5 -0
  36. package/components/Blokkli/FAQ/Element/index.vue +0 -1
  37. package/components/Blokkli/Fragment/Separator/icon.svg +4 -0
  38. package/components/Blokkli/Fragment/Separator/index.vue +87 -0
  39. package/components/Blokkli/Gallery/index.vue +1 -3
  40. package/components/Blokkli/IconText/Element/index.vue +0 -1
  41. package/components/Blokkli/Introduction/index.vue +0 -1
  42. package/components/Blokkli/Listing/icon.svg +9 -0
  43. package/components/Blokkli/Listing/index.vue +24 -0
  44. package/components/Blokkli/Logo/Element/index.vue +0 -1
  45. package/components/Blokkli/Table/index.vue +0 -1
  46. package/components/Blokkli/Team/Element/index.vue +0 -1
  47. package/components/Blokkli/Team/Layout/index.vue +0 -1
  48. package/components/Blokkli/Testimonial/Element/index.vue +0 -1
  49. package/components/Blokkli/Text/index.vue +0 -1
  50. package/components/DesignSystem/Atoms/Button.vue +3 -0
  51. package/components/DesignSystem/Atoms/GridRow.vue +3 -1
  52. package/components/DesignSystem/Molecules/ButtonGroup.vue +5 -4
  53. package/components/DesignSystem/Molecules/IconText.vue +6 -6
  54. package/components/DesignSystem/Molecules/MediaLightbox.vue +8 -8
  55. package/components/DesignSystem/Molecules/MediaSlider.vue +9 -11
  56. package/components/DesignSystem/Molecules/Partnership.vue +7 -4
  57. package/components/DesignSystem/Molecules/Table.vue +35 -19
  58. package/components/DesignSystem/Molecules/TitleComposition.vue +46 -15
  59. package/components/DesignSystem/Molecules/iFrame.vue +2 -2
  60. package/components/DesignSystem/Organisms/CallToAction.vue +8 -7
  61. package/components/DesignSystem/Organisms/CallToActionFeature.vue +8 -7
  62. package/components/DesignSystem/Organisms/CallToActionFullwidth.vue +9 -8
  63. package/components/DesignSystem/Organisms/ContentBlock.vue +26 -16
  64. package/components/DesignSystem/Organisms/FacetedListing.vue +38 -17
  65. package/components/DesignSystem/Organisms/FactCard.vue +6 -7
  66. package/components/DesignSystem/Organisms/FeatureCard.vue +14 -14
  67. package/components/DesignSystem/Organisms/FlipTeaser.vue +19 -19
  68. package/components/DesignSystem/Organisms/HeaderHero.vue +11 -11
  69. package/components/DesignSystem/Organisms/HeaderTextImage.vue +10 -10
  70. package/components/DesignSystem/Organisms/LinkList.vue +20 -12
  71. package/components/DesignSystem/Organisms/LogoElement.vue +7 -4
  72. package/components/DesignSystem/Organisms/Map.vue +1 -1
  73. package/components/DesignSystem/Organisms/SearchResults.vue +15 -6
  74. package/components/DesignSystem/Organisms/SpotlightCard.vue +18 -18
  75. package/components/DesignSystem/Organisms/TeamElement.vue +19 -18
  76. package/components/DesignSystem/Organisms/TeaserCard.vue +16 -17
  77. package/components/DesignSystem/Organisms/TeaserSimple.vue +4 -31
  78. package/components/DesignSystem/Organisms/Testimonial.vue +13 -13
  79. package/components/DesignSystem/Organisms/TextCard.vue +10 -10
  80. package/components/DesignSystem/Organisms/TimelineElement.vue +20 -19
  81. package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +5 -5
  82. package/components/DesignSystem/README.md +59 -40
  83. package/components/Drupal/Breadcrumb/index.vue +25 -12
  84. package/components/Drupal/Messages/index.vue +9 -4
  85. package/components/Drupal/Webform/index.vue +271 -51
  86. package/components/Drupal/Webform/mutation.graphql +17 -1
  87. package/components/Form/HiddenInput.vue +1 -4
  88. package/components/Form/SelectInput.vue +1 -1
  89. package/components/Form/SubmitButton.vue +2 -2
  90. package/components/Form/TextAreaInput.vue +3 -3
  91. package/components/Form/TextEmailInput.vue +1 -1
  92. package/components/Form/TextInput.vue +1 -1
  93. package/components/Form/TextPhoneInput.vue +1 -1
  94. package/components/Form/WebformMarkup.vue +1 -1
  95. package/components/Form/composables/useFormDefaultValue.ts +9 -13
  96. package/components/Form/composables/useFormElement.ts +5 -3
  97. package/components/Form/utils/types.ts +3 -3
  98. package/components/Media/Image/fragment.graphql +4 -0
  99. package/components/Media/Image/index.vue +27 -17
  100. package/components/Media/RemoteVideo/index.vue +3 -3
  101. package/components/Media/Video/index.vue +1 -1
  102. package/components/Node/Icms/Page/SearchResult/fragment.graphql +1 -1
  103. package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
  104. package/components/Node/Icms/Page/Teaser/index.vue +1 -1
  105. package/components/Node/Icms/Page/index.vue +8 -2
  106. package/components/Paragraph/Anchor/fragment.graphql +4 -0
  107. package/components/Paragraph/Anchor/index.vue +48 -0
  108. package/components/Paragraph/AnchorNavigation/fragment.graphql +3 -0
  109. package/components/Paragraph/AnchorNavigation/index.vue +154 -0
  110. package/components/Paragraph/BlokkliFragment/fragment.graphql +5 -0
  111. package/components/Paragraph/Button/Element/fragment.graphql +1 -1
  112. package/components/Paragraph/Button/Element/index.vue +13 -8
  113. package/components/Paragraph/Card/Element/index.vue +1 -1
  114. package/components/Paragraph/Card/Layout/index.vue +9 -9
  115. package/components/Paragraph/Collapsible/Element/index.vue +2 -2
  116. package/components/Paragraph/Collapsible/Layout/index.vue +19 -18
  117. package/components/Paragraph/Contact/index.vue +17 -17
  118. package/components/Paragraph/Downloads/fragment.graphql +1 -1
  119. package/components/Paragraph/Downloads/index.vue +13 -13
  120. package/components/Paragraph/FAQ/Element/index.vue +4 -3
  121. package/components/Paragraph/FAQ/Layout/index.vue +42 -21
  122. package/components/Paragraph/FromLibrary/fragment.graphql +17 -0
  123. package/components/Paragraph/Gallery/index.vue +14 -14
  124. package/components/Paragraph/HeroImage/index.vue +26 -19
  125. package/components/Paragraph/IconText/Element/index.vue +4 -4
  126. package/components/Paragraph/IconText/Layout/index.vue +12 -12
  127. package/components/Paragraph/Introduction/index.vue +17 -17
  128. package/components/Paragraph/LinkLists/Element/fragment.graphql +1 -1
  129. package/components/Paragraph/LinkLists/Element/index.vue +5 -5
  130. package/components/Paragraph/LinkLists/Layout/index.vue +9 -9
  131. package/components/Paragraph/Listing/fragment.graphql +14 -0
  132. package/components/Paragraph/Listing/index.vue +91 -0
  133. package/components/Paragraph/Logo/Element/fragment.graphql +1 -1
  134. package/components/Paragraph/Logo/Element/index.vue +7 -4
  135. package/components/Paragraph/Media/index.vue +23 -21
  136. package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -1
  137. package/components/Paragraph/Partnerships/Element/index.vue +10 -7
  138. package/components/Paragraph/Partnerships/Layout/index.vue +13 -13
  139. package/components/Paragraph/SingleTestimonial/index.vue +13 -13
  140. package/components/Paragraph/Table/index.vue +12 -12
  141. package/components/Paragraph/Team/Element/index.vue +16 -14
  142. package/components/Paragraph/Team/Layout/index.vue +9 -9
  143. package/components/Paragraph/Teasers/index.vue +14 -17
  144. package/components/Paragraph/Testimonial/Element/index.vue +19 -18
  145. package/components/Paragraph/Testimonial/Layout/index.vue +24 -18
  146. package/components/Paragraph/Text/fragment.graphql +1 -1
  147. package/components/Paragraph/Text/index.vue +8 -8
  148. package/components/Paragraph/TextMedia/index.vue +22 -22
  149. package/components/Paragraph/Timeline/Element/index.vue +19 -19
  150. package/components/Paragraph/Timeline/Layout/index.vue +51 -20
  151. package/components/Paragraph/TwoColumnText/Element/index.vue +13 -13
  152. package/components/Paragraph/TwoColumnText/Layout/index.vue +1 -1
  153. package/components/Paragraph/iFrame/index.vue +8 -8
  154. package/components/Search/Facets/FacetDropdownUI.vue +25 -31
  155. package/components/Search/Facets/FacetedSearchUI.vue +72 -48
  156. package/components/Search/Facets/fragment.graphql +20 -15
  157. package/components/Section/Footer/index.vue +55 -32
  158. package/components/Section/Header/Buttons.vue +4 -5
  159. package/components/Section/Header/DesktopMainMenu.vue +5 -4
  160. package/components/Section/Header/DesktopMainMenuDropdown.vue +5 -5
  161. package/components/Section/Header/LanguageDropdown.vue +2 -3
  162. package/components/Section/Header/Logo.vue +3 -3
  163. package/components/Section/Header/MainButtons.vue +4 -5
  164. package/components/Section/Header/MetaButtons.vue +5 -6
  165. package/components/Section/Header/MetaMenu.vue +1 -1
  166. package/components/Section/Header/MobileMainMenu.vue +1 -1
  167. package/components/Section/Header/MobileMainMenuItem.vue +2 -2
  168. package/components/Section/Header/index.vue +6 -13
  169. package/composables/__tests__/useImageSources.spec.ts +91 -0
  170. package/composables/breadcrumb.ts +12 -7
  171. package/composables/useAccordionScroll.ts +12 -4
  172. package/composables/useAllowedFragments.ts +14 -0
  173. package/composables/useAnchors.ts +48 -0
  174. package/composables/useChildTitleWeight.ts +1 -1
  175. package/composables/useCurrentLangcodeEnum.ts +1 -1
  176. package/composables/useCurrentNodeId.ts +8 -0
  177. package/composables/useDrupalUrl.ts +15 -0
  178. package/composables/useEnrichFacetLabels.ts +8 -6
  179. package/composables/useEntityRegistry.ts +3 -3
  180. package/composables/useGlobalConfig.ts +1 -2
  181. package/composables/useGraphqlCacheability.ts +12 -1
  182. package/composables/useHreflangLinks.ts +54 -0
  183. package/composables/useImageSources.ts +70 -28
  184. package/composables/useInitData.ts +1 -2
  185. package/composables/useIsHomepage.ts +1 -1
  186. package/composables/useLinkUrl.ts +30 -0
  187. package/composables/useMediaLightbox.ts +62 -50
  188. package/composables/useMenu.ts +1 -3
  189. package/composables/useMessageBus.ts +14 -7
  190. package/composables/useMobileMenuOverlayBus.ts +1 -1
  191. package/composables/useNavigationOffset.ts +49 -6
  192. package/composables/usePageCache.ts +33 -0
  193. package/composables/useSiteShell.ts +11 -0
  194. package/composables/useTheme.ts +17 -13
  195. package/config/__tests__/stubs/nuxt-multi-cache-config.ts +2 -0
  196. package/config/blokkli.ts +7 -13
  197. package/config/easyTexts.ts +1 -1
  198. package/config/graphqlMiddleware.ts +11 -4
  199. package/config/index.ts +5 -2
  200. package/config/languageNegotiation.ts +1 -1
  201. package/config/multiCache.ts +6 -1
  202. package/config/postcss-drupal/postcss.config.cjs +2 -2
  203. package/config/runtimeConfig.ts +7 -2
  204. package/config/svgIconSprite.ts +1 -2
  205. package/config/vuepal.ts +16 -18
  206. package/error.vue +1 -22
  207. package/layouts/default.vue +2 -6
  208. package/layouts/error.vue +7 -15
  209. package/modules/blokkliFragments.ts +76 -0
  210. package/modules/dynamicEntityComponentResolver.ts +47 -47
  211. package/modules/dynamicParagraphResolver.ts +43 -8
  212. package/modules/pageCache.ts +106 -0
  213. package/nuxt.config.ts +62 -33
  214. package/package.json +23 -22
  215. package/pages/[...slug]/index.vue +25 -4
  216. package/pages/index.vue +3 -3
  217. package/pages/node/[nid]/index.vue +2 -3
  218. package/patches/@blokkli+editor+2.0.0-alpha.13.patch +15 -1
  219. package/plugins/currentNode.ts +17 -0
  220. package/plugins/graphqlMiddleware.ts +10 -2
  221. package/plugins/initData.ts +15 -7
  222. package/plugins/registerNodeIcmsPageComponents.ts +6 -2
  223. package/plugins/siteShell.client.ts +94 -0
  224. package/queries/facetTermLabels.graphql +1 -5
  225. package/queries/fragment.linkUrl.graphql +12 -0
  226. package/queries/query.contentSearch.graphql +17 -2
  227. package/queries/query.initData.graphql +1 -1
  228. package/scripts/create-shade.ts +13 -4
  229. package/scripts/update-icms.ts +3 -1
  230. package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +2 -2
  231. package/server/__tests__/turnstile.spec.ts +119 -0
  232. package/server/api/icms/blokkli/last-changed.get.ts +24 -0
  233. package/server/api/icon/[...params].ts +3 -9
  234. package/server/graphqlMiddleware.serverOptions.ts +9 -8
  235. package/server/helpers/index.ts +0 -1
  236. package/server/middleware/turnstile.ts +51 -0
  237. package/server/plugins/frameAncestors.ts +20 -0
  238. package/server/plugins/pageCache.ts +59 -0
  239. package/server/utils/__tests__/accessDeniedHandover.spec.ts +49 -0
  240. package/server/utils/__tests__/backendRequestHeaders.spec.ts +115 -0
  241. package/server/utils/accessDeniedHandover.ts +48 -0
  242. package/server/utils/backendRequestHeaders.ts +51 -0
  243. package/server/utils/cacheability.ts +4 -4
  244. package/shared/__tests__/blokkliState.spec.ts +54 -0
  245. package/shared/__tests__/csp.spec.ts +53 -0
  246. package/shared/__tests__/drupalUrl.spec.ts +50 -0
  247. package/shared/blokkliState.ts +40 -0
  248. package/shared/csp.ts +47 -0
  249. package/shared/drupalUrl.ts +30 -0
  250. package/tailwind.config.ts +0 -5
  251. package/types/breadcrumb.ts +22 -0
  252. package/types/global.ts +15 -14
  253. package/types/langcode.ts +16 -0
  254. package/types/layerConfig.ts +84 -0
  255. package/utils/pageCache.ts +95 -0
  256. package/utils/turnstile.ts +35 -0
  257. package/composables/useWebformEnrichment.ts +0 -68
  258. package/server/__tests__/graphqlMiddleware.ts +0 -41
  259. package/server/multiCache.serverOptions.ts +0 -32
  260. package/vitest.config.mts +0 -28
@@ -18,6 +18,7 @@
18
18
  * icms: {
19
19
  * components: {
20
20
  * fragments: ['paragraphIcmsLayoutText', 'paragraphIcmsLayoutImage'],
21
+ * wrapperFragments: ['paragraphFromLibrary'],
21
22
  * teaserFragments: ['paragraphIcmsEventOccurrence'],
22
23
  * searchResultFragments: ['paragraphIcmsEventOccurrence']
23
24
  * }
@@ -32,6 +33,9 @@
32
33
  *
33
34
  * Each layer can define:
34
35
  * - `fragments`: Full paragraph fragments (e.g., 'paragraphIcmsLayoutText')
36
+ * - `wrapperFragments`: Fragments of paragraphs embedding another paragraph
37
+ * (e.g., 'paragraphFromLibrary'); spread into `paragraph` but not into
38
+ * `paragraphInLibrary`, the fragment the embedded paragraph uses
35
39
  * - `teaserFragments`: Teaser variant fragments (e.g., 'paragraphIcmsEventOccurrence')
36
40
  * - `searchResultFragments`: Search result variant fragments
37
41
  *
@@ -40,6 +44,7 @@
40
44
  * the fragments available in GraphQL queries throughout the application.
41
45
  */
42
46
  import { defineNuxtModule } from 'nuxt/kit'
47
+ import type { IcmsLayerConfig } from '../types/layerConfig'
43
48
 
44
49
  /**
45
50
  * Module definition for the dynamicParagraphResolver
@@ -51,9 +56,24 @@ export default defineNuxtModule({
51
56
  name: 'dynamicParagraphResolver',
52
57
  },
53
58
  setup(_options, nuxt) {
59
+ const layerConfigs: IcmsLayerConfig[] = nuxt.options._layers
60
+ .map((layer) => layer.config?.icms)
61
+ .filter((config): config is IcmsLayerConfig => !!config)
62
+
54
63
  // Collect all paragraph fragments from the Nuxt layers.
55
- const paragraphFragments = nuxt.options._layers
56
- .map(layer => layer.config?.icms?.components?.fragments)
64
+ const paragraphFragments = layerConfigs
65
+ .map((config) => config.components?.fragments)
66
+ .filter(Boolean)
67
+ .flat()
68
+ .map((fragmentName) => {
69
+ return `...${fragmentName}`
70
+ })
71
+
72
+ // Wrapper paragraphs (from_library) embed another paragraph. They belong
73
+ // in `paragraph` but not in the fragment the embedded paragraph uses:
74
+ // a fragment spreading itself is a GraphQL validation error.
75
+ const wrapperFragments = layerConfigs
76
+ .map((config) => config.components?.wrapperFragments)
57
77
  .filter(Boolean)
58
78
  .flat()
59
79
  .map((fragmentName) => {
@@ -62,6 +82,14 @@ export default defineNuxtModule({
62
82
 
63
83
  const fragment = `
64
84
  fragment paragraph on Paragraph {
85
+ id
86
+ parentId
87
+ ${[...paragraphFragments, ...wrapperFragments].join('\n ')}
88
+ }
89
+ `
90
+
91
+ const inLibraryFragment = `
92
+ fragment paragraphInLibrary on Paragraph {
65
93
  id
66
94
  parentId
67
95
  ${paragraphFragments.join('\n ')}
@@ -69,8 +97,8 @@ fragment paragraph on Paragraph {
69
97
  `
70
98
 
71
99
  // Collect teaser fragments from all layers
72
- const paragraphTeaserFragments = nuxt.options._layers
73
- .map(layer => layer.config?.icms?.components?.teaserFragments)
100
+ const paragraphTeaserFragments = layerConfigs
101
+ .map((config) => config.components?.teaserFragments)
74
102
  .filter(Boolean)
75
103
  .flat()
76
104
  .filter((fragmentName) => typeof fragmentName === 'string')
@@ -83,8 +111,8 @@ fragment paragraphTeaser on Paragraph {
83
111
  `
84
112
 
85
113
  // Collect search result fragments from all layers
86
- const paragraphSearchResultFragments = nuxt.options._layers
87
- .map(layer => layer.config?.icms?.components?.searchResultFragments)
114
+ const paragraphSearchResultFragments = layerConfigs
115
+ .map((config) => config.components?.searchResultFragments)
88
116
  .filter(Boolean)
89
117
  .flat()
90
118
  .filter((fragmentName) => typeof fragmentName === 'string')
@@ -98,8 +126,15 @@ fragment paragraphSearchResult on Paragraph {
98
126
 
99
127
  nuxt.hooks.hookOnce('nuxt-graphql-middleware:init', (context) => {
100
128
  context.addDocument('fragment.paragraph.graphql', fragment)
129
+ context.addDocument(
130
+ 'fragment.paragraphInLibrary.graphql',
131
+ inLibraryFragment,
132
+ )
101
133
  context.addDocument('fragment.paragraphTeaser.graphql', teaserFragment)
102
- context.addDocument('fragment.paragraphSearchResult.graphql', searchResultFragment)
134
+ context.addDocument(
135
+ 'fragment.paragraphSearchResult.graphql',
136
+ searchResultFragment,
137
+ )
103
138
  })
104
- }
139
+ },
105
140
  })
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Ships the ICMS multi-cache server options with the layer (ICMS-268).
3
+ *
4
+ * nuxt-multi-cache only looks for `multiCache.serverOptions.ts` in the app's
5
+ * own `server/` directory, never in a layer. It imports that file from the
6
+ * generated `#nuxt-multi-cache/server-options` template. When the app has no
7
+ * file of its own, this module replaces that template with one importing
8
+ * `cache/multiCache.serverOptions.ts` of this package — Nuxt's addTemplate()
9
+ * replaces a template with the same file name.
10
+ *
11
+ * A project that needs different server options keeps its own
12
+ * `server/multiCache.serverOptions.ts`; it then wins, as before.
13
+ *
14
+ * Like nuxt-multi-cache does for the app's file, the server options are
15
+ * replaced with an empty stub in the client build, which would otherwise
16
+ * bundle server-only code. The Vue app's SSR build gets only the ICMS
17
+ * `enabledForRequest`, the one option nuxt-multi-cache's app-side helpers
18
+ * need, with or without a project file: the full options import
19
+ * nitropack/runtime, which only resolves in Nitro's bundle — the Vite dev
20
+ * server can't load it. Nitro always uses the full options.
21
+ */
22
+ import { existsSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import {
25
+ addTemplate,
26
+ addVitePlugin,
27
+ createResolver,
28
+ defineNuxtModule,
29
+ useLogger,
30
+ } from 'nuxt/kit'
31
+
32
+ const TEMPLATE = 'nuxt-multi-cache/server-options.js'
33
+ const EXTENSIONS = ['.ts', '.mts', '.js', '.mjs']
34
+
35
+ export default defineNuxtModule({
36
+ meta: { name: 'icmsPageCache' },
37
+ setup(_options, nuxt) {
38
+ const logger = useLogger('icms')
39
+
40
+ const resolver = createResolver(import.meta.url)
41
+ addVitePlugin(
42
+ {
43
+ name: 'icms-page-cache:app-ssr',
44
+ enforce: 'pre',
45
+ transform(_code: string, id: string) {
46
+ // Also with a project file: it typically extends the ICMS options.
47
+ // Templates are virtual modules (virtual:nuxt:.nuxt%2F…).
48
+ const path = decodeURIComponent(id.split('?')[0]!)
49
+ if (path.endsWith('/' + TEMPLATE)) {
50
+ return [
51
+ `import { enabledForRequest } from ${JSON.stringify(resolver.resolve('../cache/enabledForRequest'))}`,
52
+ 'export const serverOptions = { enabledForRequest }',
53
+ ].join('\n')
54
+ }
55
+ },
56
+ },
57
+ { client: false },
58
+ )
59
+
60
+ const appServerOptions = EXTENSIONS.map((extension) =>
61
+ join(nuxt.options.serverDir, 'multiCache.serverOptions' + extension),
62
+ ).find((path) => existsSync(path))
63
+ if (appServerOptions) {
64
+ return
65
+ }
66
+
67
+ addVitePlugin(
68
+ {
69
+ name: 'icms-page-cache:server-only',
70
+ enforce: 'pre',
71
+ transform(_code: string, id: string) {
72
+ if (id.includes('nuxt-icms/cache/multiCache.serverOptions')) {
73
+ return 'export default function () { return {} }'
74
+ }
75
+ },
76
+ },
77
+ { server: false },
78
+ )
79
+
80
+ nuxt.hook('modules:done', () => {
81
+ const hasTemplate = nuxt.options.build.templates.some(
82
+ (template) => template.filename === TEMPLATE,
83
+ )
84
+ if (!hasTemplate) {
85
+ // Fail-safe: server/plugins/pageCache.ts disables the page cache
86
+ // when the server options have no bounded route storage.
87
+ logger.error(
88
+ `nuxt-multi-cache no longer generates "${TEMPLATE}"; the ICMS ` +
89
+ 'page cache stays disabled. See modules/pageCache.ts.',
90
+ )
91
+ return
92
+ }
93
+
94
+ addTemplate({
95
+ filename: TEMPLATE,
96
+ write: true,
97
+ getContents: () =>
98
+ [
99
+ "import serverOptionsFactory from '@iqual/nuxt-icms/cache/multiCache.serverOptions'",
100
+ 'const serverOptions = serverOptionsFactory()',
101
+ 'export { serverOptions }',
102
+ ].join('\n'),
103
+ })
104
+ })
105
+ },
106
+ })
package/nuxt.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path'
1
+ import { resolve, dirname, join } from 'node:path'
2
2
 
3
3
  import easyTexts from './config/easyTexts'
4
4
  import blokkli from './config/blokkli'
@@ -14,17 +14,10 @@ import vuepal from './config/vuepal'
14
14
  import { breakpoints } from './config/breakpoints'
15
15
 
16
16
  import { fileURLToPath } from 'node:url'
17
- import { dirname, join } from 'node:path'
18
17
 
19
-
20
-
21
- const ONE_YEAR = 31_536_000
22
- const ONE_MONTH = 2_592_000
23
18
  const IS_DEV = process.env.NODE_ENV === 'development'
24
- const NUXT_REQUEST_HOST = process.env.NUXT_REQUEST_HOST
25
- const NUXT_IMAGE_CLOUDFLARE_BASE_URL = process.env.NUXT_IMAGE_CLOUDFLARE_BASE_URL ?? 'https://iqual.cloud'
26
-
27
-
19
+ const NUXT_IMAGE_CLOUDFLARE_BASE_URL =
20
+ process.env.NUXT_IMAGE_CLOUDFLARE_BASE_URL ?? 'https://iqual.cloud'
28
21
 
29
22
  const currentDir = dirname(fileURLToPath(import.meta.url))
30
23
 
@@ -60,15 +53,6 @@ export default defineNuxtConfig({
60
53
  htmlAttrs: {
61
54
  tagDuplicateStrategy: 'merge',
62
55
  },
63
- link: [
64
- { rel: 'shortcut icon', type: 'image/x-icon', href: '/favicon/favicon.ico' },
65
- { rel: 'apple-touch-icon', href: '/favicon/favicon/apple-touch-icon.png' },
66
- { rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon/favicon-96x96.png' },
67
- { rel: 'icon', type: 'image/svg+xml', href: '/favicon/favicon.svg' },
68
- { rel: 'manifest', href: '/favicon/site.webmanifest' },
69
- { rel: 'icon', type: 'image/png', sizes: '192x192', href: '/favicon/web-app-manifest-192x192.png' },
70
- { rel: 'icon', type: 'image/png', sizes: '512x512', href: '/favicon/web-app-manifest-512x512.png' }
71
- ],
72
56
  },
73
57
  },
74
58
 
@@ -76,9 +60,7 @@ export default defineNuxtConfig({
76
60
  * Site-wide CSS (PostCSS compiled if within ~/assets)
77
61
  * @see https://nuxt.com/docs/getting-started/styling
78
62
  */
79
- css: [
80
- join(currentDir, './assets/css/fe-fixes.css'),
81
- ],
63
+ css: [join(currentDir, './assets/css/fe-fixes.css')],
82
64
 
83
65
  /**
84
66
  * Nuxt.js modules
@@ -90,9 +72,14 @@ export default defineNuxtConfig({
90
72
  'nuxt-easy-texts',
91
73
  'nuxt-page-dependencies',
92
74
  'nuxt-multi-cache',
75
+ // Ships the ICMS multi-cache server options; see modules/pageCache.ts.
76
+ '@iqual/nuxt-icms/modules/pageCache',
93
77
  'vuepal',
94
78
  'nuxt-language-negotiation',
95
79
  'nuxt-svg-icon-sprite',
80
+ // Registers the `blokkli:alter-features` hook, which @blokkli/editor runs
81
+ // during its own setup — so it has to be in place before that module.
82
+ '@iqual/nuxt-icms/modules/blokkliFragments',
96
83
  '@blokkli/editor',
97
84
  'nuxt-viewport',
98
85
  ...(IS_DEV ? ['@nuxt/eslint'] : []),
@@ -100,13 +87,25 @@ export default defineNuxtConfig({
100
87
  '@iqual/nuxt-icms/modules/dynamicParagraphResolver',
101
88
  '@iqual/nuxt-icms/modules/dynamicEntityComponentResolver',
102
89
  'nuxt-maplibre',
90
+ '@nuxtjs/turnstile',
103
91
  ],
104
92
 
105
93
  image: {
94
+ // Pin the @nuxt/image v1 default screens: v2 dropped `xs` and `xxl`, and
95
+ // useImageSources() derives every srcset from these keys.
96
+ screens: {
97
+ xs: 320,
98
+ sm: 640,
99
+ md: 768,
100
+ lg: 1024,
101
+ xl: 1280,
102
+ xxl: 1536,
103
+ '2xl': 1536,
104
+ },
106
105
  provider: 'cloudflare',
107
106
  cloudflare: {
108
- baseURL: NUXT_IMAGE_CLOUDFLARE_BASE_URL
109
- }
107
+ baseURL: NUXT_IMAGE_CLOUDFLARE_BASE_URL,
108
+ },
110
109
  },
111
110
 
112
111
  pageDependencies: {
@@ -114,7 +113,7 @@ export default defineNuxtConfig({
114
113
  },
115
114
 
116
115
  $meta: {
117
- name: 'icms'
116
+ name: 'icms',
118
117
  },
119
118
 
120
119
  icms: {
@@ -125,6 +124,7 @@ export default defineNuxtConfig({
125
124
  'paragraphIcmsLayoutText',
126
125
  'paragraphIcmsLayoutTextMedia',
127
126
  'paragraphIcmsLayoutTeasers',
127
+ 'paragraphIcmsLayoutListing',
128
128
  'paragraphIcmsLayoutHeroImage',
129
129
  'paragraphIcmsLayoutCallToAction',
130
130
  'paragraphIcmsLayoutCards',
@@ -141,11 +141,12 @@ export default defineNuxtConfig({
141
141
  'paragraphIcmsLayoutPartnerships',
142
142
  'paragraphIcmsLayoutDownloads',
143
143
  'paragraphIcmsLayoutMedia',
144
- 'paragraphIcmsLayoutDownloads',
145
144
  'paragraphIcmsLayoutIframe',
146
145
  'paragraphIcmsLayoutTimeline',
147
146
  'paragraphIcmsLayoutLinkLists',
148
147
  'paragraphIcmsLayoutCollapsible',
148
+ 'paragraphIcmsLayoutAnchor',
149
+ 'paragraphIcmsLayoutAnchorNavigation',
149
150
  // Element components
150
151
  'paragraphIcmsButtonElement',
151
152
  'paragraphIcmsCardElement',
@@ -159,8 +160,15 @@ export default defineNuxtConfig({
159
160
  'paragraphIcmsTimelineElement',
160
161
  'paragraphIcmsLinkListElement',
161
162
  'paragraphIcmsCollapsibleElement',
162
- ]
163
+ // Placeholder for frontend-defined fragments (defineBlokkliFragment)
164
+ 'paragraphBlokkliFragment',
165
+ ],
166
+ // Reusable blocks from the paragraph library
167
+ wrapperFragments: ['paragraphFromLibrary'],
163
168
  },
169
+ // Blökkli fragments (components/Blokkli/Fragment/*) are discovered and
170
+ // offered to editors automatically; a project switches the feature off
171
+ // with `blokkliFragments: false`. See modules/blokkliFragments.ts.
164
172
  },
165
173
 
166
174
  sourcemap: IS_DEV,
@@ -168,7 +176,7 @@ export default defineNuxtConfig({
168
176
  hooks: {
169
177
  'vite:extend'({ config }) {
170
178
  if (config.server && config.server.hmr) {
171
- // @ts-ignore
179
+ // @ts-expect-error `hmr` narrows to `true | HmrOptions`.
172
180
  config.server.hmr.protocol = 'wss'
173
181
  }
174
182
  },
@@ -184,9 +192,12 @@ export default defineNuxtConfig({
184
192
  sourcemap: false,
185
193
  minify: 'esbuild',
186
194
  },
187
- esbuild: {
188
- keepNames: true,
189
- },
195
+ // Never enable esbuild's `keepNames` here: it wraps every function in a
196
+ // `__name()` helper that is defined once per chunk, and MapLibre builds its
197
+ // web worker by stringifying functions into a blob. The serialised source
198
+ // then calls a helper that does not exist in the worker, the worker dies on
199
+ // its first statement, and every map renders as an empty background with no
200
+ // tiles and no error (ICMS-642). It breaks production builds only.
190
201
  css: {
191
202
  devSourcemap: true,
192
203
  },
@@ -238,8 +249,26 @@ export default defineNuxtConfig({
238
249
  },
239
250
 
240
251
  maplibre: {
241
- css: false
252
+ css: false,
253
+ },
254
+
255
+ /**
256
+ * Cloudflare Turnstile — spam protection for decoupled webforms.
257
+ *
258
+ * Both keys default to an empty string, which disables the check end to end:
259
+ * no widget is rendered and the server middleware lets submissions through.
260
+ * A project turns it on by setting NUXT_PUBLIC_TURNSTILE_SITE_KEY and
261
+ * NUXT_TURNSTILE_SECRET_KEY, which also override the values at runtime
262
+ * because the module maps them onto runtimeConfig.
263
+ *
264
+ * The empty strings are deliberate: without them the module falls back to
265
+ * Cloudflare's "always passes" test keys in development, which loads the
266
+ * Turnstile script on every local page that carries a form.
267
+ */
268
+ turnstile: {
269
+ siteKey: process.env.NUXT_PUBLIC_TURNSTILE_SITE_KEY || '',
270
+ secretKey: process.env.NUXT_TURNSTILE_SECRET_KEY || '',
242
271
  },
243
272
 
244
273
  compatibilityDate: '2024-08-14',
245
- })
274
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iqual/nuxt-icms",
3
- "version": "1.6.1",
3
+ "version": "1.6.2-rc.2",
4
4
  "description": "ICMS Base",
5
5
  "type": "module",
6
6
  "private": false,
@@ -13,54 +13,55 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@blokkli/editor": "2.0.0-alpha.13",
16
- "@elastic/elasticsearch": "~8.19.1",
16
+ "@elastic/elasticsearch": "~8.19.2",
17
17
  "@iframe-resizer/child": "~5.5.9",
18
18
  "@iframe-resizer/vue": "~5.5.9",
19
- "@nuxt/image": "~1.11.0",
20
- "@nuxt/kit": "~4.4.8",
19
+ "@nuxt/image": "~2.2.0",
20
+ "@nuxt/kit": "~4.6.2",
21
21
  "@nuxt/scripts": "~0.11.13",
22
- "@nuxt/schema": "~4.4.8",
23
- "@nuxt/ui": "~4.7.1",
22
+ "@nuxt/schema": "~4.6.2",
23
+ "@nuxt/ui": "~4.12.0",
24
24
  "@nuxtjs/color-mode": "~3.5.2",
25
25
  "@nuxtjs/turnstile": "1.0.0",
26
26
  "@vueuse/core": "~12.8.2",
27
- "caniuse-lite": "~1.0.30001806",
27
+ "caniuse-lite": "~1.0.30001817",
28
28
  "clsx": "~2.1.1",
29
29
  "culori": "~4.0.2",
30
30
  "h3": "~1.15.11",
31
31
  "lucide-vue-next": "~0.469.0",
32
- "nitropack": "~2.13.4",
33
- "nuxt": "~4.4.8",
34
- "nuxt-easy-texts": "~2.1.2",
32
+ "nitropack": "~2.13.5",
33
+ "nuxt": "~4.6.2",
34
+ "nuxt-easy-texts": "~2.2.0",
35
35
  "nuxt-graphql-middleware": "5.4.0",
36
36
  "nuxt-language-negotiation": "~2.0.2",
37
37
  "nuxt-maplibre": "~1.2.2",
38
38
  "nuxt-multi-cache": "~4.0.3",
39
39
  "nuxt-page-dependencies": "~1.2.0",
40
40
  "nuxt-svg-icon-sprite": "~2.0.2",
41
- "nuxt-viewport": "~2.3.1",
41
+ "nuxt-viewport": "~2.6.1",
42
42
  "ofetch": "~1.5.1",
43
43
  "patch-package": "~8.0.1",
44
- "svgo": "~3.3.2",
44
+ "svgo": "~3.3.5",
45
45
  "swiper": "~10.3.1",
46
- "tailwind-merge": "~2.6.1",
47
- "tailwind-variants": "~0.3.1",
46
+ "tailwind-merge": "~3.7.0",
47
+ "tailwind-variants": "~3.3.1",
48
48
  "tailwind-animate": "~0.2.10",
49
+ "typescript": "~5.9.3",
49
50
  "ufo": "~1.6.4",
50
- "unstorage": "~1.17.5",
51
- "vite": "~7.3.6",
52
- "vue": "~3.5.41",
51
+ "unstorage": "~1.17.6",
52
+ "vite": "~7.3.7",
53
+ "vue": "~3.5.44",
53
54
  "vue3-carousel": "0.3.1",
54
55
  "vue3-click-away": "~1.2.4",
55
- "vuepal": "~3.0.5",
56
- "zod": "~4.4.3"
56
+ "vuepal": "~3.0.6",
57
+ "zod": "~4.6.7"
57
58
  },
58
59
  "devDependencies": {
59
- "@tailwindcss/postcss": "^4.2.4",
60
+ "@tailwindcss/postcss": "^4.3.3",
60
61
  "@types/iframe-resizer": "^4.0.0",
61
62
  "postcss-cli": "^11.0.1",
62
63
  "postcss-hexrgba": "github:thormeier/postcss-hexrgba",
63
- "postcss-nesting": "^14.0.0",
64
- "sass": "^1.89.0"
64
+ "postcss-nesting": "^14.0.3",
65
+ "sass": "^1.106.0"
65
66
  }
66
67
  }
@@ -2,11 +2,9 @@
2
2
  <div>
3
3
  <component :is="entityComponent" v-bind="entity" />
4
4
  </div>
5
-
6
5
  </template>
7
6
 
8
7
  <script lang="ts" setup>
9
-
10
8
  const { get } = useEntityRegistry()
11
9
 
12
10
  const entityComponent = computed(() => get(entity.value?.__typename))
@@ -20,7 +18,30 @@ definePageMeta({
20
18
  path: '/:slug(.*)*',
21
19
  })
22
20
 
23
- const { entity } = await useDrupalRouteQuery('slug')
21
+ usePageCache()
22
+
23
+ const { entity, drupalRoute } = await useDrupalRouteQuery('slug', {
24
+ noError: true,
25
+ })
26
+
27
+ if (!entity.value) {
28
+ // A Drupal route without an entity to render (admin, user, forms) is a
29
+ // backend page. Links to such paths are router links, so a click reaches
30
+ // this page instead of the router in front of the frontend: hand over to
31
+ // the backend domain rather than showing a 404.
32
+ if (drupalRoute.value.name) {
33
+ await navigateTo(useDrupalUrl()(useRoute().fullPath), {
34
+ external: true,
35
+ redirectCode: 302,
36
+ })
37
+ } else {
38
+ throw createError({
39
+ statusCode: 404,
40
+ statusMessage: 'Page not found',
41
+ fatal: true,
42
+ })
43
+ }
44
+ }
24
45
 
25
46
  await renderPageDependencies()
26
- </script>
47
+ </script>
package/pages/index.vue CHANGED
@@ -2,11 +2,9 @@
2
2
  <div>
3
3
  <component :is="entityComponent" v-bind="entity" />
4
4
  </div>
5
-
6
5
  </template>
7
6
 
8
7
  <script lang="ts" setup>
9
-
10
8
  const { get } = useEntityRegistry()
11
9
 
12
10
  const entityComponent = computed(() => get(entity.value?.__typename))
@@ -20,7 +18,9 @@ definePageMeta({
20
18
  hideBreadcrumb: true,
21
19
  })
22
20
 
21
+ usePageCache()
22
+
23
23
  const { entity } = await useDrupalRouteQuery('slug')
24
24
 
25
25
  await renderPageDependencies()
26
- </script>
26
+ </script>
@@ -1,9 +1,8 @@
1
1
  <template>
2
- <component :is="entityComponent" v-bind="node"/>
2
+ <component :is="entityComponent" v-bind="node" />
3
3
  </template>
4
4
 
5
5
  <script lang="ts" setup>
6
-
7
6
  defineOptions({
8
7
  name: 'PageNodeCanonical',
9
8
  })
@@ -17,4 +16,4 @@ const { entity: node } = await useDrupalRouteQuery('nodeCanonical')
17
16
  const entityComponent = computed(() => get(node.value?.__typename))
18
17
 
19
18
  await renderPageDependencies()
20
- </script>
19
+ </script>
@@ -1,5 +1,5 @@
1
1
  diff --git a/node_modules/@blokkli/editor/dist/module.mjs b/node_modules/@blokkli/editor/dist/module.mjs
2
- index 0ff0d3b..5329e9f 100644
2
+ index 0ff0d3b..2dee9c3 100644
3
3
  --- a/node_modules/@blokkli/editor/dist/module.mjs
4
4
  +++ b/node_modules/@blokkli/editor/dist/module.mjs
5
5
  @@ -271,16 +271,31 @@ class CollectedBlockFile extends CollectedFile {
@@ -162,3 +162,17 @@ index 0ff0d3b..5329e9f 100644
162
162
  await Promise.all(allFiles.map((filePath) => this.addFile(filePath)));
163
163
  }
164
164
  runHooks() {
165
+ @@ -7417,6 +7531,13 @@ const runtimeOptions = defineCodeTemplate(
166
+ item.variations.forEach((variation) => {
167
+ if (hasOptions) {
168
+ OPTIONS.set(variation, item.varName);
169
+ + // Fragments: defineBlokkli() looks up their options as
170
+ + // `fragment:<name>__default`, while getVariations() only yields
171
+ + // `fragment:<name>`. Without the suffixed alias the lookup misses
172
+ + // and a fragment never receives its (global) options.
173
+ + if (!isBlock(item.definition)) {
174
+ + OPTIONS.set(variation + "__default", item.varName);
175
+ + }
176
+ }
177
+ });
178
+ if (isBlock(item.definition)) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tracks the node ID of the currently viewed Drupal route so components
3
+ * deep in the tree (e.g. faceted listings) can reference it without each
4
+ * node component having to provide it.
5
+ */
6
+ export default defineNuxtPlugin({
7
+ name: 'starterkit:current-node',
8
+ setup(app) {
9
+ const currentNodeId = useCurrentNodeId()
10
+ app.hooks.hook('vuepal:drupal-route', (data) => {
11
+ currentNodeId.value =
12
+ data.drupalRoute?.entityType === 'node' && data.drupalRoute.entityId
13
+ ? String(data.drupalRoute.entityId)
14
+ : null
15
+ })
16
+ },
17
+ })
@@ -44,7 +44,15 @@ export default defineNuxtPlugin({
44
44
  }
45
45
 
46
46
  if (import.meta.server && event) {
47
- state.fetchOptions.headers = buildProxyRequestHeaders(event)
47
+ const headers = buildProxyRequestHeaders(event)
48
+ // The internal request to the GraphQL server route has no Host header of
49
+ // its own; pass on the public host, from which the route derives the
50
+ // forwarded host for Drupal (see server/utils/backendRequestHeaders.ts).
51
+ const host = event.node.req.headers.host
52
+ if (host) {
53
+ headers.host = host
54
+ }
55
+ state.fetchOptions.headers = headers
48
56
  }
49
57
 
50
58
  state.fetchOptions.onResponse = (result) => {
@@ -71,4 +79,4 @@ export default defineNuxtPlugin({
71
79
  }
72
80
  }
73
81
  },
74
- })
82
+ })
@@ -1,4 +1,3 @@
1
-
2
1
  import type {
3
2
  GlobalConfigFragment,
4
3
  InitDataQuery,
@@ -79,11 +78,20 @@ export default defineNuxtPlugin({
79
78
  if (!event) {
80
79
  throw new Error('Missing request event.')
81
80
  }
82
- const { value: cachedResponse, addToCache } =
83
- await useDataCache<InitDataResponse>(
84
- 'initData_' + language.value,
85
- event,
86
- )
81
+ const {
82
+ value: cachedResponse,
83
+ addToCache,
84
+ cacheTags,
85
+ } = await useDataCache<InitDataResponse>(
86
+ 'initData_' + language.value,
87
+ event,
88
+ )
89
+ // A cached response skips the GraphQL request, so pass its tags on to
90
+ // the page in the route cache (only the tags: bubbling the whole cache
91
+ // item would make the page as permanent as the init data).
92
+ if (cachedResponse) {
93
+ useRouteCache((route) => route.addTags(cacheTags))
94
+ }
87
95
  const response = cachedResponse || (await useGraphqlQuery('initData'))
88
96
  data.value = mapInitData(response)
89
97
  if (!cachedResponse && response.extensions.cacheability?.isCacheable) {
@@ -105,4 +113,4 @@ export default defineNuxtPlugin({
105
113
  watch(language, loadInitData)
106
114
  }
107
115
  },
108
- })
116
+ })