@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
@@ -3,21 +3,21 @@
3
3
  <div
4
4
  v-if="showLanguageLinks"
5
5
  v-click-away="closeDropdown"
6
- @keydown.esc="closeDropdown"
7
6
  :class="themedClass('containerClass', 'relative py-2')"
7
+ @keydown.esc="closeDropdown"
8
8
  >
9
9
  <button
10
10
  type="button"
11
11
  aria-haspopup="menu"
12
12
  :aria-expanded="isLangDropdownOpen"
13
13
  aria-controls="language-dropdown"
14
- @click="toggleDropdown"
15
14
  :class="
16
15
  themedClass(
17
16
  'currentLanguageLinkClass',
18
17
  'flex cursor-pointer flex-nowrap items-center gap-1',
19
18
  )
20
19
  "
20
+ @click="toggleDropdown"
21
21
  >
22
22
  {{ currentLanguage.toUpperCase() }}
23
23
  <SpriteSymbol
@@ -104,7 +104,6 @@
104
104
  </template>
105
105
 
106
106
  <script lang="ts" setup>
107
-
108
107
  const themedClass = useTheme('SectionHeaderLanguageDropdown')
109
108
  const links = useLanguageLinks()
110
109
  const currentLanguage = useCurrentLanguage()
@@ -8,9 +8,9 @@
8
8
  <Media
9
9
  v-if="companyLogo"
10
10
  :media="companyLogo"
11
- :mediaClass="themedClass('mediaClass', 'h-8 2xl:h-10')"
12
- :figcaptionClass="themedClass('figcaptionClass')"
13
- :elementClass="themedClass('elementClass', 'h-8 2xl:h-10 w-auto')"
11
+ :media-class="themedClass('mediaClass', 'h-8 2xl:h-10')"
12
+ :figcaption-class="themedClass('figcaptionClass')"
13
+ :element-class="themedClass('elementClass', 'h-8 2xl:h-10 w-auto')"
14
14
  />
15
15
  </NuxtLink>
16
16
  </template>
@@ -11,17 +11,16 @@
11
11
  :link="link?.link?.uri?.path"
12
12
  :params="link?.params"
13
13
  :target-blank="link?.targetBlank"
14
- :buttonClass="themedClass('buttonClass', 'xs:w-fit')"
15
- :labelClass="themedClass('labelClass')"
16
- colorScheme="none"
17
- buttonStyle="focus"
14
+ :button-class="themedClass('buttonClass', 'xs:w-fit')"
15
+ :label-class="themedClass('labelClass')"
16
+ color-scheme="none"
17
+ button-style="focus"
18
18
  @click="close()"
19
19
  />
20
20
  </div>
21
21
  </template>
22
22
 
23
23
  <script lang="ts" setup>
24
-
25
24
  const { contactButtons } = await useGlobalConfig()
26
25
  const themedClass = useTheme('SectionHeaderMainButtons')
27
26
  const { close } = useMobileMenuOverlayBus()
@@ -10,18 +10,17 @@
10
10
  :title="link?.title"
11
11
  :link="link?.link?.uri?.path"
12
12
  :params="link?.params"
13
- :targetBlank="link?.targetBlank"
14
- :buttonClass="themedClass('buttonClass', 'xs:w-fit')"
15
- :labelClass="themedClass('labelClass')"
16
- colorScheme="none"
17
- buttonStyle="focus"
13
+ :target-blank="link?.targetBlank"
14
+ :button-class="themedClass('buttonClass', 'xs:w-fit')"
15
+ :label-class="themedClass('labelClass')"
16
+ color-scheme="none"
17
+ button-style="focus"
18
18
  @click="close()"
19
19
  />
20
20
  </div>
21
21
  </template>
22
22
 
23
23
  <script lang="ts" setup>
24
-
25
24
  const { metaLinks } = await useGlobalConfig()
26
25
  const themedClass = useTheme('SectionHeaderMetaButtons')
27
26
  const { close } = useMobileMenuOverlayBus()
@@ -33,7 +33,6 @@
33
33
  "
34
34
  :target="menuItem?.target"
35
35
  :rel="menuItem?.rel"
36
- @click="close()"
37
36
  :exact-active-class="themedClass('linkActiveClass', 'text-primary')"
38
37
  :class="
39
38
  cn(
@@ -44,6 +43,7 @@
44
43
  menuItem?.additionalClass,
45
44
  )
46
45
  "
46
+ @click="close()"
47
47
  >
48
48
  {{ menuItem.label }}
49
49
  </NuxtLink>
@@ -8,7 +8,7 @@
8
8
  v-for="item in menuItems"
9
9
  :key="item.id"
10
10
  :item="item"
11
- :menuItemActive="menuItemActive"
11
+ :menu-item-active="menuItemActive"
12
12
  />
13
13
  </ul>
14
14
  </nav>
@@ -25,7 +25,6 @@
25
25
  :aria-controls="submenuId"
26
26
  role="button"
27
27
  tabindex="0"
28
- @click="isOpen = !isOpen"
29
28
  :class="[
30
29
  themedClass(
31
30
  'submenuToggleClass',
@@ -33,6 +32,7 @@
33
32
  ),
34
33
  { 'motion-safe:rotate-180': isOpen },
35
34
  ]"
35
+ @click="isOpen = !isOpen"
36
36
  />
37
37
  </div>
38
38
 
@@ -84,7 +84,7 @@
84
84
  v-for="item in item.children"
85
85
  :key="`submenu_${item.id}`"
86
86
  :item="item"
87
- :menuItemActive="menuItemActive"
87
+ :menu-item-active="menuItemActive"
88
88
  />
89
89
  </ul>
90
90
  </Transition>
@@ -25,7 +25,7 @@
25
25
  )
26
26
  "
27
27
  >
28
- <div ref="metaContainer" :class="themedClass('metaRefContainerClass')">
28
+ <div data-navigation-meta :class="themedClass('metaRefContainerClass')">
29
29
  <DesignSystemAtomsGridRow
30
30
  :class="
31
31
  themedClass(
@@ -52,6 +52,7 @@
52
52
 
53
53
  <!-- Main Container -->
54
54
  <DesignSystemAtomsContainer
55
+ data-navigation-main
55
56
  :spacing="{ top: 'none', bottom: 'none' }"
56
57
  :class="
57
58
  themedClass('mainContainerClass', 'border-b border-gray-100 bg-white')
@@ -90,8 +91,6 @@
90
91
  <!-- Mobile Menu Overlay -->
91
92
  <Teleport to="body">
92
93
  <Transition
93
- @after-enter="clickAwayArmed = true"
94
- @after-leave="clickAwayArmed = false"
95
94
  :enter-active-class="
96
95
  themedClass(
97
96
  'transitionEnterActiveClass',
@@ -128,11 +127,13 @@
128
127
  'opacity-0 md:opacity-100 md:translate-x-full',
129
128
  )
130
129
  "
130
+ @after-enter="clickAwayArmed = true"
131
+ @after-leave="clickAwayArmed = false"
131
132
  >
132
133
  <DesignSystemAtomsContainer
133
134
  v-show="isMobileMenuOpen"
134
- :spacing="{ top: 'none', bottom: 'none' }"
135
135
  v-click-away="clickAwayArmed && close"
136
+ :spacing="{ top: 'none', bottom: 'none' }"
136
137
  :class="
137
138
  themedClass(
138
139
  'mobileMenuContainerClass',
@@ -174,7 +175,6 @@
174
175
  </template>
175
176
 
176
177
  <script lang="ts" setup>
177
-
178
178
  const { metaLinks, contactButtons } = await useGlobalConfig()
179
179
  const { menuItems: metaMenuItems } = await useMenu('metaMenu')
180
180
  const languageLinks = useLanguageLinks()
@@ -185,8 +185,7 @@ const viewport = useViewport()
185
185
  const isMobileMenuOpen = ref(false)
186
186
  const clickAwayArmed = ref(false)
187
187
 
188
- const metaContainer = ref<HTMLElement | null>(null)
189
- const metaHeight = ref(0)
188
+ const { metaHeight } = useNavigationOffset()
190
189
 
191
190
  onOpen(() => {
192
191
  isMobileMenuOpen.value = true
@@ -203,10 +202,4 @@ watch(viewport.breakpoint, (newValue: string) => {
203
202
  isMobileMenuOpen.value = false
204
203
  }
205
204
  })
206
-
207
- onMounted(() => {
208
- requestAnimationFrame(() => {
209
- metaHeight.value = metaContainer.value?.offsetHeight ?? 0
210
- })
211
- })
212
205
  </script>
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi, beforeAll } from 'vitest'
2
+ import { computed, toRef } from 'vue'
3
+
4
+ /**
5
+ * The image component renders width/height attributes from the sources the
6
+ * composable builds, so the browser reserves the box before the image has
7
+ * loaded (no layout shift, stable screenshots). Art-directed images carry
8
+ * one pair per <source>; images without an aspect ratio use the original
9
+ * file's dimensions.
10
+ */
11
+
12
+ // Stand-in for @nuxt/image: one candidate per requested width, screens as
13
+ // configured in nuxt.config.ts of the layer.
14
+ const screens = { xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280, xxl: 1536 }
15
+ const img = Object.assign(
16
+ (src: string, modifiers: Record<string, unknown>) =>
17
+ `${src}?w=${modifiers.width}&h=${modifiers.height}`,
18
+ {
19
+ options: { screens },
20
+ getSizes: (src: string) => ({ sizes: '100vw', srcset: `${src} 1x`, src }),
21
+ },
22
+ )
23
+
24
+ type UseImageSources = typeof import('../useImageSources').useImageSources
25
+ let useImageSources: UseImageSources
26
+
27
+ beforeAll(async () => {
28
+ // The composable relies on Nuxt auto-imports; the unit suite is Nuxt-free.
29
+ vi.stubGlobal('useImage', () => img)
30
+ vi.stubGlobal('useRuntimeConfig', () => ({
31
+ public: { imageBaseUrl: 'https://backend.example' },
32
+ }))
33
+ vi.stubGlobal('toRef', toRef)
34
+ vi.stubGlobal('computed', computed)
35
+ useImageSources = (await import('../useImageSources')).useImageSources
36
+ })
37
+
38
+ const media = (dimensions: { width: number; height: number } | null) =>
39
+ ({
40
+ sourceUrl: { path: '/sites/default/files/image.jpg' },
41
+ dimensions,
42
+ }) as unknown as Parameters<UseImageSources>[0]['media']
43
+
44
+ describe('useImageSources dimensions', () => {
45
+ it('reserves the largest candidate of a single aspect ratio', () => {
46
+ const { imageSources } = useImageSources({
47
+ media: media({ width: 4000, height: 3000 }),
48
+ imageAspectRatios: '16/9',
49
+ imageSizes: 'xs:100vw md:50vw lg:600px',
50
+ })
51
+ expect(imageSources.value).toHaveLength(1)
52
+ expect(imageSources.value[0]).toMatchObject({ width: 600, height: 338 })
53
+ })
54
+
55
+ it('reserves one box per art-directed source', () => {
56
+ const { imageSources } = useImageSources({
57
+ media: media({ width: 4000, height: 3000 }),
58
+ imageAspectRatios: 'xs:1/1 md:16/9',
59
+ imageSizes: 'xs:100vw md:50vw lg:600px',
60
+ })
61
+ expect(imageSources.value).toHaveLength(2)
62
+ // Square up to the md breakpoint: 50vw of 768px.
63
+ expect(imageSources.value[0]).toMatchObject({
64
+ media: '(max-width: 768px)',
65
+ width: 384,
66
+ height: 384,
67
+ })
68
+ // Landscape from md on; the last source has no media query and is the
69
+ // fallback the <img> uses.
70
+ expect(imageSources.value[1].media).toBeUndefined()
71
+ expect(imageSources.value[1]).toMatchObject({ width: 600, height: 338 })
72
+ })
73
+
74
+ it('uses the original dimensions when the natural ratio is kept', () => {
75
+ const { imageSources } = useImageSources({
76
+ media: media({ width: 4000, height: 3000 }),
77
+ imageAspectRatios: '',
78
+ })
79
+ expect(imageSources.value).toHaveLength(1)
80
+ expect(imageSources.value[0]).toMatchObject({ width: 4000, height: 3000 })
81
+ })
82
+
83
+ it('leaves the attributes out when the file dimensions are unknown', () => {
84
+ const { imageSources } = useImageSources({
85
+ media: media(null),
86
+ imageAspectRatios: '',
87
+ })
88
+ expect(imageSources.value[0].width).toBeUndefined()
89
+ expect(imageSources.value[0].height).toBeUndefined()
90
+ })
91
+ })
@@ -1,4 +1,7 @@
1
- import type { BreadcrumbFragment, RouteQuery } from '#graphql-operations'
1
+ import type {
2
+ BreadcrumbLink,
3
+ RouteQueryWithBreadcrumb,
4
+ } from '@iqual/nuxt-icms/types/breadcrumb'
2
5
 
3
6
  /**
4
7
  * Extract the breadcrumb from a route query.
@@ -6,10 +9,12 @@ import type { BreadcrumbFragment, RouteQuery } from '#graphql-operations'
6
9
  * @see ~/pages/[language]/[...slug]/query.route.graphql
7
10
  */
8
11
  function getBreadcrumbFromRouteQuery(
9
- routeQuery: RouteQuery | undefined | null,
10
- ): BreadcrumbFragment[] {
12
+ routeQuery: RouteQueryWithBreadcrumb | undefined | null,
13
+ ): BreadcrumbLink[] {
11
14
  if (routeQuery && routeQuery.route && 'breadcrumb' in routeQuery.route) {
12
- return routeQuery.route.breadcrumb || []
15
+ return (routeQuery.route.breadcrumb || []).filter(
16
+ (link): link is BreadcrumbLink => !!link,
17
+ )
13
18
  }
14
19
  return []
15
20
  }
@@ -18,14 +23,14 @@ function getBreadcrumbFromRouteQuery(
18
23
  * Returns the current breadcrumb links.
19
24
  */
20
25
  export function useBreadcrumbLinks() {
21
- return useState<BreadcrumbFragment[]>('breadcrumbLinks', () => [])
26
+ return useState<BreadcrumbLink[]>('breadcrumbLinks', () => [])
22
27
  }
23
28
 
24
29
  /**
25
30
  * Set the breadcrumb links for the current page.
26
31
  */
27
32
  export function setBreadcrumbLinksFromRoute(
28
- routeQuery: RouteQuery | undefined | null,
33
+ routeQuery: RouteQueryWithBreadcrumb | undefined | null,
29
34
  ) {
30
35
  const links = useBreadcrumbLinks()
31
36
  links.value = getBreadcrumbFromRouteQuery(routeQuery)
@@ -35,7 +40,7 @@ export function setBreadcrumbLinksFromRoute(
35
40
  * Set the breadcrumb links directly.
36
41
  */
37
42
  export function setBreadcrumbLinks(
38
- newLinks?: BreadcrumbFragment[] | null | undefined,
43
+ newLinks?: BreadcrumbLink[] | null | undefined,
39
44
  ) {
40
45
  const links = useBreadcrumbLinks()
41
46
  links.value = newLinks || []
@@ -3,21 +3,29 @@ import { useNavigationOffset } from '@iqual/nuxt-icms/composables/useNavigationO
3
3
  export function useAccordionScroll() {
4
4
  const { navigationOffset } = useNavigationOffset()
5
5
 
6
- const scrollToTrigger = (accordionEl: HTMLElement | undefined, index: number): void => {
6
+ const scrollToTrigger = (
7
+ accordionEl: HTMLElement | undefined,
8
+ index: number,
9
+ ): void => {
7
10
  if (!accordionEl) return
8
11
 
9
- const triggers = accordionEl.querySelectorAll<HTMLElement>('[data-slot="trigger"]')
12
+ const triggers = accordionEl.querySelectorAll<HTMLElement>(
13
+ '[data-slot="trigger"]',
14
+ )
10
15
  const trigger = triggers[index]
11
16
 
12
17
  if (!trigger) return
13
18
 
14
19
  nextTick(() => {
15
20
  setTimeout(() => {
16
- const top = window.scrollY + trigger.getBoundingClientRect().top - navigationOffset.value
21
+ const top =
22
+ window.scrollY +
23
+ trigger.getBoundingClientRect().top -
24
+ navigationOffset.value
17
25
  window.scrollTo({ top, behavior: 'smooth' })
18
26
  }, 400)
19
27
  })
20
28
  }
21
29
 
22
30
  return { scrollToTrigger }
23
- }
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { BlokkliFragmentName } from '#blokkli-build/definitions'
2
+
3
+ /**
4
+ * The fragments editors may place in a page's paragraphs field.
5
+ *
6
+ * Passed as `allowed-fragments` to the page-level `<BlokkliField>`. The list
7
+ * holds every fragment blökkli discovered in the layer stack (collected at
8
+ * build time by the `blokkliFragments` module); it is empty when a project
9
+ * sets `icms.blokkliFragments: false`, in which case the editor's fragments
10
+ * feature is disabled altogether.
11
+ */
12
+ export default function useAllowedFragments(): BlokkliFragmentName[] {
13
+ return (useAppConfig().icms?.allowedFragments ?? []) as BlokkliFragmentName[]
14
+ }
@@ -0,0 +1,48 @@
1
+ import { useNavigationOffset } from '@iqual/nuxt-icms/composables/useNavigationOffset'
2
+
3
+ export type Anchor = { id: string; label: string }
4
+
5
+ const anchorNavigationHeight = 52
6
+
7
+ // Strips everything that is not safe in a URL fragment, e.g.
8
+ // "Öffnungszeiten & Preise" -> "Offnungszeiten-Preise".
9
+ const fragment = (text: string): string =>
10
+ text
11
+ .normalize('NFKD')
12
+ .replace(/[̀-ͯ]/g, '')
13
+ .trim()
14
+ .replace(/[^a-zA-Z0-9]+/g, '-')
15
+ .replace(/^-+|-+$/g, '')
16
+
17
+ // Generates an id from a label. Lowercase on top, so a link typed by hand
18
+ // ("#kontakt") hits the anchor no matter how the label is capitalised.
19
+ export function anchorId(text: string): string {
20
+ return fragment(text.toLowerCase())
21
+ }
22
+
23
+ // The id an anchor block scrolls to: the anchor field when the editor filled
24
+ // it in, kept as written apart from invalid characters, otherwise generated
25
+ // from the label.
26
+ export function resolveAnchorId(
27
+ label?: string | null,
28
+ anchor?: string | null,
29
+ ): string {
30
+ const explicit = anchor?.trim()
31
+
32
+ return explicit ? fragment(explicit) : anchorId(label ?? '')
33
+ }
34
+
35
+ // Both measured from the top of the viewport:
36
+ // headerOffset sits below the header, where the anchor navigation sticks.
37
+ // anchorOffset sits below the anchor navigation, where a section scrolls to.
38
+ export function useAnchorOffset(): {
39
+ headerOffset: ComputedRef<number>
40
+ anchorOffset: ComputedRef<number>
41
+ } {
42
+ const { headerHeight } = useNavigationOffset()
43
+
44
+ return {
45
+ headerOffset: headerHeight,
46
+ anchorOffset: computed(() => headerHeight.value + anchorNavigationHeight),
47
+ }
48
+ }
@@ -10,4 +10,4 @@ export function useChildTitleWeight(titleWeight: string) {
10
10
  } else {
11
11
  return 'p'
12
12
  }
13
- }
13
+ }
@@ -1,4 +1,4 @@
1
- import { Langcode } from '#graphql-operations'
1
+ import { Langcode } from '@iqual/nuxt-icms/types/langcode'
2
2
 
3
3
  /**
4
4
  * Return the current langcode enum for GraphQL requests.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The node ID of the node the current Drupal route resolves to.
3
+ *
4
+ * Set by the current-node plugin via the vuepal:drupal-route hook and
5
+ * null on routes that don't resolve to a node.
6
+ */
7
+ export const useCurrentNodeId = () =>
8
+ useState<string | null>('currentNodeId', () => null)
@@ -0,0 +1,15 @@
1
+ import { drupalOrigin, toDrupalUrl } from '../shared/drupalUrl'
2
+
3
+ /**
4
+ * Returns a function that turns a Drupal path into a browser-facing URL.
5
+ *
6
+ * The Drupal backend runs on the "backend." subdomain of the domain the
7
+ * frontend was requested on (see docs/routing.md), so the origin is derived
8
+ * per request and is right on every domain a site serves. Use it for every
9
+ * link or iframe pointing at Drupal (login, admin, entity forms) — it is the
10
+ * only place the backend URL is read from.
11
+ */
12
+ export default function useDrupalUrl() {
13
+ const origin = drupalOrigin(useRequestURL())
14
+ return (path: string): string => toDrupalUrl(path, origin)
15
+ }
@@ -10,14 +10,14 @@
10
10
  */
11
11
  export async function useEnrichFacetLabels(
12
12
  facets: any[],
13
- fieldsToEnrich: string[] = ['topics']
13
+ fieldsToEnrich: string[] = ['topics'],
14
14
  ): Promise<any[]> {
15
15
  if (!facets || facets.length === 0) {
16
16
  return facets
17
17
  }
18
18
 
19
19
  // Find facets that need enrichment
20
- const facetsToEnrich = facets.filter(f => fieldsToEnrich.includes(f.field))
20
+ const facetsToEnrich = facets.filter((f) => fieldsToEnrich.includes(f.field))
21
21
 
22
22
  if (facetsToEnrich.length === 0) {
23
23
  return facets
@@ -37,7 +37,9 @@ export async function useEnrichFacetLabels(
37
37
 
38
38
  // Fetch term labels via GraphQL
39
39
  try {
40
- const response: any = await useGraphqlQuery('facetTermLabels' as any, { ids: termIds })
40
+ const response: any = await useGraphqlQuery('facetTermLabels' as any, {
41
+ ids: termIds,
42
+ })
41
43
  const terms = response?.data?.terms?.items || []
42
44
 
43
45
  if (terms.length === 0) {
@@ -53,7 +55,7 @@ export async function useEnrichFacetLabels(
53
55
  }
54
56
 
55
57
  // Update facet filter labels
56
- return facets.map(facet => {
58
+ return facets.map((facet) => {
57
59
  if (fieldsToEnrich.includes(facet.field) && facet.filter?.id) {
58
60
  const label = termLabelMap.get(facet.filter.id)
59
61
  if (label) {
@@ -61,8 +63,8 @@ export async function useEnrichFacetLabels(
61
63
  ...facet,
62
64
  filter: {
63
65
  ...facet.filter,
64
- label
65
- }
66
+ label,
67
+ },
66
68
  }
67
69
  }
68
70
  }
@@ -1,6 +1,6 @@
1
1
  const nodeComponentRegistry = new Map<string, Component>()
2
2
 
3
- export const useEntityRegistry = () => {
3
+ export const useEntityRegistry = () => {
4
4
  const register = (type: string, component: Component) => {
5
5
  nodeComponentRegistry.set(type, component)
6
6
  }
@@ -16,6 +16,6 @@ export const useEntityRegistry = () => {
16
16
  return {
17
17
  register,
18
18
  get,
19
- getAll
19
+ getAll,
20
20
  }
21
- }
21
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  export async function useGlobalConfig() {
3
2
  const initData = useInitData()
4
3
 
@@ -44,7 +43,7 @@ export async function useGlobalConfig() {
44
43
 
45
44
  return {
46
45
  companyLogo,
47
- companyLogoAlt,
46
+ companyLogoAlt,
48
47
  companyName,
49
48
  companyDescription,
50
49
  address,
@@ -6,8 +6,9 @@ export function useGraphqlCacheability(
6
6
  response?: GraphqlResponseTyped,
7
7
  ) {
8
8
  if (import.meta.server) {
9
+ const cacheability = response?.extensions.cacheability
10
+
9
11
  useCDNHeaders((cdn) => {
10
- const cacheability = response?.extensions.cacheability
11
12
  if (!cacheability?.isCacheable) {
12
13
  cdn.private()
13
14
  return
@@ -18,5 +19,15 @@ export function useGraphqlCacheability(
18
19
  .setNumeric('sharedMaxAge', cacheability.sharedMaxAge)
19
20
  .addTags(cacheability.tagsCdn)
20
21
  }, event)
22
+
23
+ // The route cache is purged by Drupal with the encoded tags.
24
+ useRouteCache((route) => {
25
+ if (!cacheability?.isCacheable) {
26
+ route.setUncacheable()
27
+ return
28
+ }
29
+
30
+ route.setMaxAge(cacheability.sharedMaxAge).addTags(cacheability.tagsNuxt)
31
+ }, event)
21
32
  }
22
33
  }
@@ -0,0 +1,54 @@
1
+ export interface HreflangAlternate {
2
+ code: string
3
+ path: string
4
+ }
5
+
6
+ export type HreflangLink = {
7
+ rel: 'alternate'
8
+ hreflang: string
9
+ href: string
10
+ }
11
+
12
+ /**
13
+ * Builds the `<link rel="alternate" hreflang>` tags of a single page.
14
+ *
15
+ * Without them search engines rank the language versions of a page against
16
+ * each other as near-duplicates. No `x-default`: it would name the fallback
17
+ * of the `/` redirect, which lives in the router config (`routing.languages`)
18
+ * and is not visible here.
19
+ */
20
+ export function buildHreflangLinks(
21
+ alternates: HreflangAlternate[],
22
+ origin: string,
23
+ ): HreflangLink[] {
24
+ if (!origin || alternates.length < 2) {
25
+ return []
26
+ }
27
+
28
+ return alternates.map((alternate) => ({
29
+ rel: 'alternate',
30
+ hreflang: alternate.code,
31
+ href: origin + alternate.path,
32
+ }))
33
+ }
34
+
35
+ /**
36
+ * The hreflang link tags for the current route, for `useHead()`.
37
+ *
38
+ * Translations come from the route query's `languageSwitchLinks`, the same
39
+ * source as the language switcher.
40
+ */
41
+ export function useHreflangLinks() {
42
+ const languageLinks = useLanguageLinks()
43
+ const requestUrl = useRequestURL({ xForwardedHost: true })
44
+
45
+ return computed<HreflangLink[]>(() => {
46
+ const alternates = (languageLinks.value ?? []).flatMap((link) =>
47
+ link.enabled && typeof link.to === 'string'
48
+ ? [{ code: link.code, path: link.to }]
49
+ : [],
50
+ )
51
+
52
+ return buildHreflangLinks(alternates, requestUrl.origin)
53
+ })
54
+ }