Skip to content

chore: replace cookiebot with c15t#334

Merged
dmnktoe merged 6 commits into
mainfrom
claude/replace-cookiebot-c15t-HngbO
Apr 19, 2026
Merged

chore: replace cookiebot with c15t#334
dmnktoe merged 6 commits into
mainfrom
claude/replace-cookiebot-c15t-HngbO

Conversation

@dmnktoe

@dmnktoe dmnktoe commented Apr 19, 2026

Copy link
Copy Markdown
Owner

Description & Technical Solution

Describe problems, if any, clearly and concisely.
Summarize the impact to the system.
Please also include relevant motivation and context.
Please include a summary of the technical solution and how it solves the problem.

Checklist

  • I have commented my code, particularly in hard-to-understand areas.
  • Already rebased against main branch.

Screenshots

Provide screenshots or videos of the changes made if any.


Note

Medium Risk
Consent/analytics gating is reworked around @c15t/nextjs, and the PR also upgrades Next.js and multiple UI/runtime dependencies, which could cause runtime or behavior regressions. CI/hooks/package-manager changes add some operational risk if environments aren’t aligned to pnpm/new Node version.

Overview
Replaces Cookiebot-based consent with a c15t-driven consent manager, including a headless banner/dialog UI, localized cookie policy/control-center content, and updated consent checks for analytics/embeds (e.g. Google Analytics/Hotjar and marketing-gated content).

Adds an edge Open Graph image endpoint (/api/og) and switches OG URL generation to use NEXT_PUBLIC_APP_URL as the canonical origin, removing the prior OG base env wiring.

Migrates project tooling to pnpm (Husky hooks, README, CI env cleanup) and bumps Next.js plus a set of related dependencies, adding Radix components used by the new consent UI.

Reviewed by Cursor Bugbot for commit 5cc7b15. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Redesigned cookie consent management with localized consent preferences UI
    • Added Open Graph image generation for enhanced social media sharing
    • Integrated new distribution partner (Micromed) to product offerings
  • Improvements

    • Enhanced analytics consent handling with proper user preferences
    • Expanded cookie policy documentation in German and English
    • Improved footer content management system

claude and others added 3 commits April 19, 2026 11:30
…te, add Micromed

- Replace Cookiebot (€7/month) with c15t offline consent management
  - ConsentProvider wraps app with ConsentManagerProvider + ConsentBanner
  - useConsent hook rewritten against useConsentManager API
  - CookieControlCenter uses inline ConsentWidget for cookie policy page
  - GoogleAnalytics/Hotjar now gated on measurement consent via c15t

- Remove axios and axios-mock-adapter; use native fetch in ContactForm
  - ContactForm.test.tsx mocks global.fetch instead of axios

- Create /api/og edge route for dynamic OG image generation
  - Hero-inspired design: navy background, hero image, Duecker logo SVG
  - helper.ts openGraph() now uses /api/og instead of external service

- Add Micromed logo to distribution partner marquee with link to micromed.com

https://claude.ai/code/session_014YjtAaGzMEhJG8HAKPhUgH
@vercel

vercel Bot commented Apr 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
duecker-medizintechnik Ready Ready Preview, Comment Apr 19, 2026 11:14pm

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The pull request migrates the application's cookie consent system from Cookiebot to @c15t/nextjs, introduces new consent UI components, updates multiple dependencies, adds Open Graph image generation, implements footer posts context management, and removes axios in favor of native fetch for form submissions.

Changes

Cohort / File(s) Summary
Dependency Updates
package.json
Added @c15t/nextjs runtime dependency; bumped numerous package versions including @radix-ui/react-*, next, react-hook-form, Tailwind CSS, TypeScript, and testing libraries; removed axios; added pnpm.onlyBuiltDependencies configuration.
Consent System Migration
src/components/helpers/ConsentProvider.tsx, src/components/helpers/GoogleAnalytics.tsx, src/components/helpers/Hotjar.tsx, src/utils/useConsent.ts, src/types/Cookiebot.ts, src/constant/env.ts
Replaced Cookiebot with @c15t/nextjs consent manager; added ConsentProvider wrapper component; updated GoogleAnalytics and Hotjar to use useConsentManager() for consent checks; rewrote useConsent hook to use c15t API; removed Cookiebot TypeScript types and cookieBotId env constant.
Consent UI Components
src/components/helpers/consent/*, src/components/templates/CookieControlCenter.tsx, src/components/ui/RadixCheckbox.tsx, src/components/ui/Switch.tsx
Added new client-side consent UI layer: ConsentSurfaces (banner/modal), ConsentPreferencesSection, ConsentCategoryToggles; refactored CookieControlCenter to use new components; introduced RadixCheckbox and Switch UI components with Radix integration.
Localization Updates
public/locales/en/cookiePolicy.json, public/locales/de/cookiePolicy.json, public/locales/en/distribution.json, public/locales/de/distribution.json
Added comprehensive cookie consent UI strings and vendor policy details for both English and German; added Micromed distributor product entries and descriptions; expanded metadata and policy content.
Layout & Provider Integration
src/app/[locale]/layout.tsx, src/app/layout.tsx, src/components/providers/Providers.tsx, src/components/providers/FooterPostsContext.tsx
Moved third-party script initialization from root layout to locale layout; wrapped children with ConsentProvider; added footer posts context infrastructure and hydration via FooterPostsProvider; removed Cookiebot and direct script injection from root layout.
Footer Posts System
src/lib/footer-posts.ts, src/components/layout/Footer.tsx, src/components/providers/FooterPostsContext.tsx
Added server-side loadFooterPosts() utility and discriminated union type; created context and provider for sharing footer posts; refactored Footer to use useFooterPosts() hook instead of client-side fetching.
Request Handling Migration
src/components/templates/ContactForm.tsx, src/components/__tests__/ContactForm.test.tsx
Removed axios dependency; replaced with native fetch API using method: 'POST', JSON serialization, and native error handling; updated test to mock global.fetch instead of axios adapter.
Open Graph Image Generation
src/app/api/og/route.tsx, src/lib/helper.ts, src/lib/__tests__/helper.test.ts
Added new Edge runtime route for dynamic OG image generation; updated openGraph() helper to construct URLs with title/description query params instead of siteName/templateTitle/logo; removed ogBaseUrl env constant.
Navigation & Header Improvements
src/components/layout/Header.tsx, src/app/not-found.tsx
Changed LanguagePicker mount logic to use CSS hidden class for flag-based visibility instead of conditional rendering; migrated 404 page anchor to Next.js Link component.
Distribution Page Updates
src/components/templates/DistributionProducts.tsx
Added Micromed distributor section with logo image and descriptive text; restructured layout to always render product help sections; added MicromedLogo asset import.
Component Test Updates
src/components/__tests__/GoogleAnalytics.test.tsx, src/components/__tests__/Hotjar.test.tsx
Added mocks for @c15t/nextjs useConsentManager hook to simulate consent state; updated script content assertions to match new implementation.
Type System & Utilities
src/utils/useConsentState.ts, src/components/ui/Typography/Title.tsx, src/components/ui/index.ts
Removed Cookiebot-related types; re-exported ConsentState from consent hook; added optional id prop to Title component; added barrel exports for RadixCheckbox and Switch.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as App/Layout
    participant CP as ConsentProvider
    participant CM as useConsentManager<br/>(`@c15t/nextjs`)
    participant CS as ConsentSurfaces
    participant GA as GoogleAnalytics

    User->>App: Visit site
    App->>CP: Render ConsentProvider
    CP->>CM: Initialize consent manager
    CM-->>CP: Ready with stored/default consent
    CP->>CS: Render consent UI
    CS-->>User: Display banner/dialog
    User->>CS: Accept/Reject/Customize
    CS->>CM: Save consent preferences
    CM->>CM: Persist to storage
    App->>GA: Render GoogleAnalytics
    GA->>CM: Check measurement consent
    CM-->>GA: Return consent status
    alt Consent given
        GA->>GA: Inject GTM/GA script
    else Consent denied
        GA->>GA: Return null (no script)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 With consent now managed by c15t's gentle hand,
No more Cookiebot reigns across the land!
New surfaces bloom with choices so fair,
While RadixUI components dance in the air,
Footer posts flow and OG images gleam—
A fresher consent experience than you could dream!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description consists only of the template structure with all required sections empty; no concrete problems, impact, motivation, solution, or checklist items are provided. Fill in all template sections: clearly describe the problem(s) and system impact, provide motivation and context, summarize the technical solution, and complete the checklist items. The commit messages and objectives contain substantial context that should be incorporated.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: replace cookiebot with c15t' is clear, specific, and accurately summarizes the main technical change across the entire changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/replace-cookiebot-c15t-HngbO

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sentry

sentry Bot commented Apr 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.52174% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.80%. Comparing base (1e2aec8) to head (5cc7b15).
⚠️ Report is 31 commits behind head on main.

Files with missing lines Patch % Lines
src/components/templates/ContactForm.tsx 21.73% 18 Missing ⚠️
src/components/helpers/Hotjar.tsx 50.00% 8 Missing ⚠️
src/lib/helper.ts 77.77% 2 Missing ⚠️
src/components/ui/RadixCheckbox.tsx 83.33% 1 Missing ⚠️
src/components/ui/Switch.tsx 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #334      +/-   ##
==========================================
- Coverage   87.39%   82.80%   -4.60%     
==========================================
  Files          64       65       +1     
  Lines         611      634      +23     
  Branches      200      204       +4     
==========================================
- Hits          534      525       -9     
- Misses         77      109      +32     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dmnktoe

dmnktoe commented Apr 19, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (9)
src/components/templates/DistributionProducts.tsx (1)

90-106: Consider adding explicit sizing hints to next/image.

The static import provides intrinsic dimensions, so this works, but since you're styling with h-12 w-auto max-w-[200px], passing height={48} (or explicit width/height) makes the rendered output deterministic and avoids layout shift if the source asset changes. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/templates/DistributionProducts.tsx` around lines 90 - 106, Add
explicit size props to the next/image usage to make layout deterministic: in the
DistributionProducts component update the Image element that uses MicromedLogo
(the <Image ... /> inside the Link) to include explicit height (e.g.
height={48}) or width and height values that match the intended rendered size
(or the static import's intrinsic dimensions) so the visual sizing (h-12 w-auto
max-w-[200px]) remains but layout shift is prevented.
public/locales/en/distribution.json (1)

39-43: LGTM

Keys mirror the German locale and match the translation lookups in the component.

Minor nit (pre-existing, not part of this diff): meta.seo.title at line 55 is still in German (“Globaler Vertrieb: …”) in the English locale — worth localizing in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/locales/en/distribution.json` around lines 39 - 43, The English locale
file has meta.seo.title still in German—update the "meta.seo.title" key in
public/locales/en/distribution.json to an appropriate English string matching
the intent of the German version (e.g., "Global Distribution: …") so the SEO
title is localized and consistent with the rest of the en distribution keys;
make sure the key name remains "meta.seo.title" and only the value is changed.
src/app/api/og/route.tsx (2)

64-97: Optional: extract the inline SVG logo to a constant/module.

The ~30-line logo path list dominates the handler and obscures the layout logic. Moving it to a sibling file (e.g., src/app/api/og/logo.tsx or a LOGO_PATHS constant array mapped into <path> elements) would make future layout tweaks easier to review. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/og/route.tsx` around lines 64 - 97, Extract the large inline
<svg> block into a separate module and import it into the handler to declutter
the route: create a sibling export (e.g., a LogoSVG React component or a
LOGO_PATHS array) that contains the <svg> and all <path> elements (or the path
data), replace the inline <svg> in the route with the imported LogoSVG (or map
LOGO_PATHS to <path> elements in the route), and ensure any props like
width/height/style remain configurable where the original <svg> was used.

15-198: Add Cache-Control headers to the OG response.

ImageResponse from next/og accepts a headers option. Without explicit caching, each crawler/share will re-render the image (including the remote hero fetch + Satori SVG rasterization), which is expensive on the Edge and slow for consumers. Since the output only varies by title/description, long-lived caching is safe.

♻️ Proposed change
   return new ImageResponse(
     <div ...>
       ...
     </div>,
     {
       width: 1200,
       height: 630,
+      headers: {
+        'Cache-Control':
+          'public, immutable, no-transform, max-age=0, s-maxage=86400, stale-while-revalidate=604800',
+      },
     },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/og/route.tsx` around lines 15 - 198, The OG ImageResponse
currently has no caching headers; update the ImageResponse call in route.tsx to
pass a headers option that sets Cache-Control for long-lived CDN caching (for
example: "public, s-maxage=31536000, stale-while-revalidate=86400, immutable"),
ensuring generated images (ImageResponse) for given
title/description/heroImageUrl are cached at the edge; keep the rest of the
ImageResponse payload (SVG markup, heroImageUrl, title, description) unchanged.
src/lib/footer-posts.ts (1)

20-27: Consider logging the swallowed error.

The catch {} discards the error entirely, which will make production failures of the footer-posts fetch invisible. A console.error (or integration with your existing error reporter, e.g. Sentry which is already a dep) would preserve the graceful { kind: 'error' } fallback while making the underlying cause diagnosable.

Proposed change
-  } catch {
+  } catch (error) {
+    console.error('loadFooterPosts failed', error);
     return { kind: 'error' };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/footer-posts.ts` around lines 20 - 27, The catch block in the footer
posts fetch swallows errors; update the error handling in the async block that
calls fetchAPI (the code returning { kind: 'ready', posts: ... } or { kind:
'error' }) to log the caught error before returning { kind: 'error' } — use
console.error or the existing Sentry/reporting wrapper (whichever project
convention is used) to record the exception and include contextual info (e.g.,
the endpoint '/posts?...' and any error.message) while keeping the graceful
fallback.
src/components/__tests__/Hotjar.test.tsx (1)

1-19: Missing negative test for consent gating.

The whole purpose of the c15t migration is that Hotjar only loads when measurement consent is granted. The current suite only exercises the happy path. Please add at least one test where has('measurement') returns false and assert that #hotjar is not rendered (and ideally one for HOTJAR_ID undefined).

Sketch
it('does not render when measurement consent is denied', () => {
  jest.isolateModules(() => {
    jest.doMock('@c15t/nextjs', () => ({
      useConsentManager: () => ({ has: () => false }),
    }));
    const Hotjar = require('@/components/helpers/Hotjar').default;
    render(<Hotjar HOTJAR_ID='HJ_TEST_ID' />);
    expect(document.getElementById('hotjar')).toBeNull();
  });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/__tests__/Hotjar.test.tsx` around lines 1 - 19, Add negative
tests to src/components/__tests__/Hotjar.test.tsx to assert Hotjar is not
injected when measurement consent is denied and when HOTJAR_ID is undefined: use
jest.isolateModules plus jest.doMock to mock '@c15t/nextjs' so
useConsentManager().has returns false, require the Hotjar module
(Hotjar.default) inside the isolate, render it with HOTJAR_ID='HJ_TEST_ID' and
assert document.getElementById('hotjar') is null; then add a separate test that
renders Hotjar with undefined HOTJAR_ID (and with consent true) and assert no
`#hotjar` is rendered. Ensure you reference useConsentManager,
jest.isolateModules/jest.doMock, render and HOTJAR_ID in the new tests.
src/components/__tests__/ContactForm.test.tsx (1)

36-51: Restore global.fetch after the suite to avoid cross-file leakage.

Assigning global.fetch = jest.fn(...) at module scope replaces fetch for the entire Jest worker. If any other test file relies on a real fetch polyfill (MSW, undici, etc.) or its own fetch mock, ordering will determine which one wins. Prefer jest.spyOn(global, 'fetch') plus a teardown, so it's automatically restored.

Proposed tweak
-global.fetch = jest.fn(
-  (): Promise<any> =>
-    Promise.resolve({
-      ok: true,
-      status: 200,
-      statusText: 'OK',
-      json: (): Promise<any> => Promise.resolve({ message: 'success' }),
-    }),
-) as any;
+const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(
+  (): Promise<any> =>
+    Promise.resolve({
+      ok: true,
+      status: 200,
+      statusText: 'OK',
+      json: (): Promise<any> => Promise.resolve({ message: 'success' }),
+    }) as any,
+);
+
+afterAll(() => {
+  fetchMock.mockRestore();
+});

And then use fetchMock.mockClear() / fetchMock.mockResolvedValueOnce(...) below instead of the as jest.Mock casts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/__tests__/ContactForm.test.tsx` around lines 36 - 51, The test
currently assigns global.fetch at module scope which leaks across Jest worker;
replace the module-scope assignment with a spy using jest.spyOn(global, 'fetch')
(e.g., create a fetchMock variable from jest.spyOn in the test suite setup),
configure its mockResolvedValue/mockImplementation in beforeEach (where
createIntlWrapper is called) and call fetchMock.mockClear() between tests, and
restore the original by calling fetchMock.mockRestore() in afterAll (or
afterEach) so other test files are not affected; update uses in tests to call
fetchMock.mockResolvedValueOnce(...) instead of casting global.fetch to
jest.Mock.
src/components/__tests__/GoogleAnalytics.test.tsx (1)

1-26: Consider covering the "no measurement consent" branch.

The mock always returns true for measurement, so only the happy path is exercised. Given the component's new behavior returns null when hasStats is false (per src/components/helpers/GoogleAnalytics.tsx:15), adding a case where has('measurement') returns false would guard the consent gating going forward.

Optional sketch
-jest.mock('@c15t/nextjs', () => ({
-  useConsentManager: () => ({
-    has: (category: string) => category === 'measurement',
-  }),
-}));
+const hasMock = jest.fn((category: string) => category === 'measurement');
+jest.mock('@c15t/nextjs', () => ({
+  useConsentManager: () => ({ has: hasMock }),
+}));

Then add a second it(...) that sets hasMock.mockReturnValue(false) and asserts no <script> elements are rendered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/__tests__/GoogleAnalytics.test.tsx` around lines 1 - 26, The
test only covers the consent-true branch; change the jest.mock for
useConsentManager to expose a jest.fn (e.g., hasMock) instead of a hardcoded
function, then add a second test case that sets hasMock.mockReturnValue(false),
renders <GoogleAnalytics GA_MEASUREMENT_ID='GA_TEST_ID' />, and asserts that no
<script> elements are present (document.querySelectorAll('script') has length 0)
to cover the "no measurement consent" branch in the GoogleAnalytics component.
src/components/helpers/consent/ConsentSurfaces.tsx (1)

73-90: Prefer c15t policy action groups over hard-coded actions.

The custom UI always renders reject/customize/accept/save, bypassing allowedActions, actionGroups, and primaryActions. That can drift from c15t policy-pack decisions if geo/policy config changes. The c15t headless docs explicitly call out using those policy-aware values: https://c15t.com/docs/frameworks/next/building-headless-components

Also applies to: 117-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/helpers/consent/ConsentSurfaces.tsx` around lines 73 - 90, The
banner/footer currently renders hard-coded buttons ('reject', 'customize',
'accept') which bypass policy-driven values; update the UI in
ConsentSurfaces.tsx to iterate and render buttons from the policy-aware
props/values (e.g., allowedActions, actionGroups, primaryActions) instead of
hard-coding labels and actions, ensuring each rendered button calls
performBannerAction(actionId) or invokes openDialog() where the policy action
maps to a dialog; keep a safe fallback to the existing
'reject'/'customize'/'accept' behavior if those policy arrays are undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 41-47: The package.json pins react and react-dom to 18.3.1 which
conflicts with Next.js 15; update the "react" and "react-dom" entries to a React
19-compatible range (for example "^19") in package.json (the dependency keys
"react" and "react-dom"), then run npm install (or pnpm/yarn install) to update
lockfile and node_modules so peer dependencies align with Next.js 15 and the
app-router async params APIs used in the codebase.

In `@public/locales/en/cookiePolicy.json`:
- Around line 33-35: The locale key "cookieBotAvailability" contains stale
"CookieBot" wording; update its user-facing value to reference the new provider
(e.g., "c15t") or use a generic phrase like "cookie management service" while
keeping the key name intact so existing references (cookieBotAvailability)
continue to work; ensure the new string matches tone of other entries ("title",
"intro") and adjust capitalization/spacing accordingly.
- Around line 63-68: The "measurement" vendor entry currently states the service
as "Google Analytics 4 (via Google Tag Manager)" but the app loads GA directly
via gtag/js; update the JSON entry under the "measurement" array (the object
with "name": "Google Ireland Limited") to reflect the actual integration by
changing the "service" value to something like "Google Analytics 4" (remove the
"via Google Tag Manager" phrase) and adjust any related copy in that object if
needed to match direct GA usage.

In `@src/app/api/og/route.tsx`:
- Around line 6-13: The GET handler currently constructs heroImageUrl and passes
it to ImageResponse which will throw if the remote asset is missing; wrap the
ImageResponse construction in a try/catch inside the GET function (or pre-fetch
heroImageUrl with fetch/HEAD) and on failure return a fallback ImageResponse
variant that omits the hero image (or uses an embedded/local placeholder) so the
route doesn't 500; reference the heroImageUrl constant and the ImageResponse
creation in your changes and optionally log the caught error for observability.

In `@src/components/helpers/consent/ConsentCategoryToggles.tsx`:
- Around line 56-70: The toggle rendering currently reads staged state only, so
when selectedConsents is empty previous saved values get ignored; update the
value reads to use the fallback pattern: derive checked as
selectedConsents[name] ?? consents[name] ?? false (use the same fallback
anywhere the toggle reads the value, e.g., where checked is computed in
ConsentCategoryToggles and the two other places referenced around the existing
lines ~97 and ~104), ensuring you import/obtain consents from useConsentManager
if not already.

In `@src/components/helpers/consent/ConsentPreferencesSection.tsx`:
- Around line 24-32: The Save button handler currently discards the promise from
saveConsents('custom') and never closes the dialog; change the onClick to an
async handler that awaits saveConsents('custom') inside a try/catch, call
setActiveUI('none') after a successful await to dismiss the preferences UI, and
in the catch block surface the error (e.g., set an error state or call your
toast/notification helper) so failures aren’t swallowed; update the Button
onClick in ConsentPreferencesSection to use this async try/catch flow
referencing saveConsents and setActiveUI.

In `@src/components/helpers/ConsentProvider.tsx`:
- Around line 36-39: The onError callback currently sends errors to Sentry only
when isLocal is true, which inverts the intended behavior; change the condition
in callbacks.onError to report to Sentry when NOT local (i.e., if (!isLocal)
Sentry.captureException(new Error(error))); keep the existing use of new
Error(error) to normalize the payload and optionally guard against empty error
values before calling captureException.

In `@src/components/helpers/GoogleAnalytics.tsx`:
- Around line 19-34: The GA_MEASUREMENT_ID is injected unsafely into both the
Script src and the inline script string in the Script component; update the src
to use an encoded query param (e.g., encodeURIComponent(GA_MEASUREMENT_ID)) and
replace the raw interpolation inside dangerouslySetInnerHTML with a
JSON-stringified value (e.g., JSON.stringify(GA_MEASUREMENT_ID)) so the Script
component, its src attribute, and the inline config in the 'google-analytics'
Script are safe against malformed env values; locate the Script usages and the
GA_MEASUREMENT_ID interpolations to apply these changes.
- Line 15: The component currently short-circuits rendering with "if
(!GA_MEASUREMENT_ID || !hasStats) return null;" but doesn't revoke GA runtime
when consent is later withdrawn; update the GoogleAnalytics component to
subscribe to the consent/hasStats state (e.g., via useEffect watching the
hasStats/measurement consent prop or context) and when consent transitions from
granted to revoked call gtag('consent', 'update', { 'analytics_storage':
'denied' }) to explicitly revoke analytics_storage; ensure you still avoid
rendering the GA script when GA_MEASUREMENT_ID is absent but always run the
consent-update side effect on consent changes.

In `@src/components/helpers/Hotjar.tsx`:
- Line 15: The Hotjar snippet currently returns early when HOTJAR_ID or hasStats
is false but does not undo an already-injected runtime if consent is later
revoked; update the Hotjar component to watch hasStats (e.g., in a useEffect
dependent on hasStats) and when hasStats becomes false perform cleanup: call
Hotjar's opt-out if available (or clear window.h?.hj and window.h?._hjSettings
safely), remove any injected script elements whose src contains "hotjar" or that
were created by the component, and if you prefer simpler behavior you may
trigger a full page reload when consent is revoked; reference HOTJAR_ID,
hasStats, and the global window properties h.hj and h._hjSettings when locating
the logic to change.

In `@src/components/templates/ContactForm.tsx`:
- Around line 65-84: Replace the hardcoded German error strings and the direct
use of response.statusText in ContactForm.tsx: when the POST to /api/form fails,
parse the JSON and prefer errorData.errors?.[0] (or join errorData.errors) or
errorData.message if present, then call setResult with an i18n key like
t('content.contactForm.submit.error', { detail }) (or
t('content.contactForm.submit.error') when no detail) instead of concatenating
response.statusText; also replace the catch branch's 'Fehler beim Senden' with
t('content.contactForm.submit.networkError') and keep
setResultColor('text-red-500') and reset() behavior unchanged so all
user-visible strings come from t(...) and API error shapes from
src/app/api/form/route.ts (errors array) are handled.

In `@src/lib/helper.ts`:
- Around line 1-20: The SITE_URL constant is hardcoded and causes openGraph() to
always return a production absolute URL; change openGraph (and remove/replace
SITE_URL) so it returns a root-relative path (e.g. "/api/og?..." using the same
URLSearchParams) instead of "https://www.duecker-medizintechnik.de/..." so
previews/local hosts resolve images from their own origin; if you prefer an
absolute URL use an env-driven base (process.env.SITE_URL) and fall back to the
relative path; also update any tests that assume new URL(result) is absolute to
construct a URL with a base (e.g. new URL(result, 'http://example')) when
validating searchParams.

In `@src/utils/useConsent.ts`:
- Around line 35-36: The code calls setConsent (from useConsentManager()) before
calling saveConsents('custom'), causing immediate saves and duplicate callbacks;
replace the first setConsent call with setSelectedConsent so that changes are
staged and only persisted when saveConsents('custom') runs, i.e., use
setSelectedConsent(...) instead of setConsent(...) prior to
saveConsents('custom') while keeping the final saveConsents('custom') call to
persist and trigger callbacks.

---

Nitpick comments:
In `@public/locales/en/distribution.json`:
- Around line 39-43: The English locale file has meta.seo.title still in
German—update the "meta.seo.title" key in public/locales/en/distribution.json to
an appropriate English string matching the intent of the German version (e.g.,
"Global Distribution: …") so the SEO title is localized and consistent with the
rest of the en distribution keys; make sure the key name remains
"meta.seo.title" and only the value is changed.

In `@src/app/api/og/route.tsx`:
- Around line 64-97: Extract the large inline <svg> block into a separate module
and import it into the handler to declutter the route: create a sibling export
(e.g., a LogoSVG React component or a LOGO_PATHS array) that contains the <svg>
and all <path> elements (or the path data), replace the inline <svg> in the
route with the imported LogoSVG (or map LOGO_PATHS to <path> elements in the
route), and ensure any props like width/height/style remain configurable where
the original <svg> was used.
- Around line 15-198: The OG ImageResponse currently has no caching headers;
update the ImageResponse call in route.tsx to pass a headers option that sets
Cache-Control for long-lived CDN caching (for example: "public,
s-maxage=31536000, stale-while-revalidate=86400, immutable"), ensuring generated
images (ImageResponse) for given title/description/heroImageUrl are cached at
the edge; keep the rest of the ImageResponse payload (SVG markup, heroImageUrl,
title, description) unchanged.

In `@src/components/__tests__/ContactForm.test.tsx`:
- Around line 36-51: The test currently assigns global.fetch at module scope
which leaks across Jest worker; replace the module-scope assignment with a spy
using jest.spyOn(global, 'fetch') (e.g., create a fetchMock variable from
jest.spyOn in the test suite setup), configure its
mockResolvedValue/mockImplementation in beforeEach (where createIntlWrapper is
called) and call fetchMock.mockClear() between tests, and restore the original
by calling fetchMock.mockRestore() in afterAll (or afterEach) so other test
files are not affected; update uses in tests to call
fetchMock.mockResolvedValueOnce(...) instead of casting global.fetch to
jest.Mock.

In `@src/components/__tests__/GoogleAnalytics.test.tsx`:
- Around line 1-26: The test only covers the consent-true branch; change the
jest.mock for useConsentManager to expose a jest.fn (e.g., hasMock) instead of a
hardcoded function, then add a second test case that sets
hasMock.mockReturnValue(false), renders <GoogleAnalytics
GA_MEASUREMENT_ID='GA_TEST_ID' />, and asserts that no <script> elements are
present (document.querySelectorAll('script') has length 0) to cover the "no
measurement consent" branch in the GoogleAnalytics component.

In `@src/components/__tests__/Hotjar.test.tsx`:
- Around line 1-19: Add negative tests to
src/components/__tests__/Hotjar.test.tsx to assert Hotjar is not injected when
measurement consent is denied and when HOTJAR_ID is undefined: use
jest.isolateModules plus jest.doMock to mock '@c15t/nextjs' so
useConsentManager().has returns false, require the Hotjar module
(Hotjar.default) inside the isolate, render it with HOTJAR_ID='HJ_TEST_ID' and
assert document.getElementById('hotjar') is null; then add a separate test that
renders Hotjar with undefined HOTJAR_ID (and with consent true) and assert no
`#hotjar` is rendered. Ensure you reference useConsentManager,
jest.isolateModules/jest.doMock, render and HOTJAR_ID in the new tests.

In `@src/components/helpers/consent/ConsentSurfaces.tsx`:
- Around line 73-90: The banner/footer currently renders hard-coded buttons
('reject', 'customize', 'accept') which bypass policy-driven values; update the
UI in ConsentSurfaces.tsx to iterate and render buttons from the policy-aware
props/values (e.g., allowedActions, actionGroups, primaryActions) instead of
hard-coding labels and actions, ensuring each rendered button calls
performBannerAction(actionId) or invokes openDialog() where the policy action
maps to a dialog; keep a safe fallback to the existing
'reject'/'customize'/'accept' behavior if those policy arrays are undefined.

In `@src/components/templates/DistributionProducts.tsx`:
- Around line 90-106: Add explicit size props to the next/image usage to make
layout deterministic: in the DistributionProducts component update the Image
element that uses MicromedLogo (the <Image ... /> inside the Link) to include
explicit height (e.g. height={48}) or width and height values that match the
intended rendered size (or the static import's intrinsic dimensions) so the
visual sizing (h-12 w-auto max-w-[200px]) remains but layout shift is prevented.

In `@src/lib/footer-posts.ts`:
- Around line 20-27: The catch block in the footer posts fetch swallows errors;
update the error handling in the async block that calls fetchAPI (the code
returning { kind: 'ready', posts: ... } or { kind: 'error' }) to log the caught
error before returning { kind: 'error' } — use console.error or the existing
Sentry/reporting wrapper (whichever project convention is used) to record the
exception and include contextual info (e.g., the endpoint '/posts?...' and any
error.message) while keeping the graceful fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a114976-7b0e-4705-b787-05535270b11e

📥 Commits

Reviewing files that changed from the base of the PR and between f173b64 and 8b78fe9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (37)
  • package.json
  • public/locales/de/cookiePolicy.json
  • public/locales/de/distribution.json
  • public/locales/en/cookiePolicy.json
  • public/locales/en/distribution.json
  • src/app/[locale]/layout.tsx
  • src/app/api/og/route.tsx
  • src/app/layout.tsx
  • src/app/not-found.tsx
  • src/components/__tests__/ContactForm.test.tsx
  • src/components/__tests__/GoogleAnalytics.test.tsx
  • src/components/__tests__/Hotjar.test.tsx
  • src/components/helpers/ConsentProvider.tsx
  • src/components/helpers/GoogleAnalytics.tsx
  • src/components/helpers/Hotjar.tsx
  • src/components/helpers/consent/ConsentCategoryToggles.tsx
  • src/components/helpers/consent/ConsentPreferencesSection.tsx
  • src/components/helpers/consent/ConsentSurfaces.tsx
  • src/components/layout/Footer.tsx
  • src/components/layout/Header.tsx
  • src/components/providers/FooterPostsContext.tsx
  • src/components/providers/Providers.tsx
  • src/components/templates/ContactForm.tsx
  • src/components/templates/CookieControlCenter.tsx
  • src/components/templates/CookiePolicyVendorList.tsx
  • src/components/templates/DistributionProducts.tsx
  • src/components/ui/RadixCheckbox.tsx
  • src/components/ui/Switch.tsx
  • src/components/ui/Typography/Title.tsx
  • src/components/ui/index.ts
  • src/constant/env.ts
  • src/lib/__tests__/helper.test.ts
  • src/lib/footer-posts.ts
  • src/lib/helper.ts
  • src/types/Cookiebot.ts
  • src/utils/useConsent.ts
  • src/utils/useConsentState.ts
💤 Files with no reviewable changes (1)
  • src/constant/env.ts

Comment thread package.json
Comment on lines +41 to 47
"next": "^15.5.15",
"next-intl": "^4.9.1",
"nodemailer": "^8.0.5",
"nodemailer-express-handlebars": "^6.1.2",
"qs": "^6.14.0",
"qs": "^6.15.1",
"react": "18.3.1",
"react-dom": "18.3.1",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Next.js 15 App Router officially support React 18, or is React 19 required?

💡 Result:

No, Next.js 15 App Router does not officially support React 18. React 19 (stable or Canary) is required for the App Router. React 18 is supported only for the Pages Router.

Citations:


🏁 Script executed:

#!/bin/bash
# Confirm App Router usage and look for sync dynamic API call sites that would break under Next 15.
fd -t d '^app$' src
rg -nP '\b(cookies|headers|draftMode)\s*\(\s*\)' --type=ts --type=tsx -g '!**/node_modules/**'
rg -nP '\buseFormState\b' --type=ts --type=tsx

Repository: dmnktoe/duecker-medizintechnik

Length of output: 155


🏁 Script executed:

#!/bin/bash
# Verify versions in package.json
grep -E '(next|react|react-dom)' package.json | head -20

# Search for dynamic API calls that require React 19 in App Router
# Using grep as rg has file type recognition issues in sandbox
find src/app -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) -exec grep -l "cookies()\|headers()\|draftMode()\|useFormState\|searchParams\|params" {} \; 2>/dev/null | head -20

Repository: dmnktoe/duecker-medizintechnik

Length of output: 1362


🏁 Script executed:

#!/bin/bash
# Search for dynamic API calls in App Router files using grep
grep -r "cookies()" src/app --include="*.ts" --include="*.tsx" 2>/dev/null || echo "No cookies() found"
grep -r "headers()" src/app --include="*.ts" --include="*.tsx" 2>/dev/null || echo "No headers() found"
grep -r "draftMode()" src/app --include="*.ts" --include="*.tsx" 2>/dev/null || echo "No draftMode() found"
grep -r "useFormState" src/app --include="*.ts" --include="*.tsx" 2>/dev/null || echo "No useFormState found"
grep -r "searchParams" src/app --include="*.ts" --include="*.tsx" 2>/dev/null || echo "No searchParams found"

# Also check layout.tsx and page.tsx files which commonly use params
find src/app -name "layout.tsx" -o -name "page.tsx" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -50 {}'

Repository: dmnktoe/duecker-medizintechnik

Length of output: 6857


Next.js 15 requires React 19 — bump React to fix peer dependency conflict.

Next.js 15 App Router requires React 19 (per official upgrade guide). The current pinning of react and react-dom to 18.3.1 creates a peer dependency mismatch. Additionally, the codebase already uses the async params: Promise<{ locale: string }> pattern (visible in multiple page components), which is the React 19-compatible API. This indicates the code was written for React 19 but the dependency was not updated.

Upgrade react and react-dom to ^19 in package.json, then run npm install to align the dependency with the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 41 - 47, The package.json pins react and react-dom
to 18.3.1 which conflicts with Next.js 15; update the "react" and "react-dom"
entries to a React 19-compatible range (for example "^19") in package.json (the
dependency keys "react" and "react-dom"), then run npm install (or pnpm/yarn
install) to update lockfile and node_modules so peer dependencies align with
Next.js 15 and the app-router async params APIs used in the codebase.

Comment thread public/locales/en/cookiePolicy.json Outdated
Comment on lines +33 to +35
"cookieBotAvailability": "CookieBot is not available in development mode.",
"title": "Cookie Policy"
"title": "Cookie policy",
"intro": "This page explains which cookies and similar technologies we use and which providers are involved. For legal bases and further details on personal data, please see our privacy policy.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the stale Cookiebot wording.

The PR replaces Cookiebot with c15t, but this locale entry still says “CookieBot”. Keep the key if it is still referenced, but update the user-facing value.

Proposed copy update
-    "cookieBotAvailability": "CookieBot is not available in development mode.",
+    "cookieBotAvailability": "Cookie consent management is not available in development mode.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"cookieBotAvailability": "CookieBot is not available in development mode.",
"title": "Cookie Policy"
"title": "Cookie policy",
"intro": "This page explains which cookies and similar technologies we use and which providers are involved. For legal bases and further details on personal data, please see our privacy policy.",
"cookieBotAvailability": "Cookie consent management is not available in development mode.",
"title": "Cookie policy",
"intro": "This page explains which cookies and similar technologies we use and which providers are involved. For legal bases and further details on personal data, please see our privacy policy.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/locales/en/cookiePolicy.json` around lines 33 - 35, The locale key
"cookieBotAvailability" contains stale "CookieBot" wording; update its
user-facing value to reference the new provider (e.g., "c15t") or use a generic
phrase like "cookie management service" while keeping the key name intact so
existing references (cookieBotAvailability) continue to work; ensure the new
string matches tone of other entries ("title", "intro") and adjust
capitalization/spacing accordingly.

Comment thread public/locales/en/cookiePolicy.json
Comment thread src/app/api/og/route.tsx
Comment thread src/components/helpers/consent/ConsentCategoryToggles.tsx Outdated
Comment thread src/components/helpers/GoogleAnalytics.tsx
Comment thread src/components/helpers/Hotjar.tsx
Comment thread src/components/templates/ContactForm.tsx
Comment thread src/lib/helper.ts Outdated
Comment thread src/utils/useConsent.ts Outdated
@dmnktoe dmnktoe merged commit 31dc9fb into main Apr 19, 2026
10 of 11 checks passed
@dmnktoe dmnktoe deleted the claude/replace-cookiebot-c15t-HngbO branch April 19, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants