chore: replace cookiebot with c15t#334
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe pull request migrates the application's cookie consent system from Cookiebot to Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
📝 Changed routes:
Commit f364c7c (https://duecker-medizintechnik-h30cd4qs3-yl33ly.vercel.app). |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (9)
src/components/templates/DistributionProducts.tsx (1)
90-106: Consider adding explicit sizing hints tonext/image.The static import provides intrinsic dimensions, so this works, but since you're styling with
h-12 w-auto max-w-[200px], passingheight={48}(or explicitwidth/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: LGTMKeys mirror the German locale and match the translation lookups in the component.
Minor nit (pre-existing, not part of this diff):
meta.seo.titleat 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.tsxor aLOGO_PATHSconstant 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.
ImageResponsefromnext/ogaccepts aheadersoption. 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 bytitle/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. Aconsole.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
measurementconsent is granted. The current suite only exercises the happy path. Please add at least one test wherehas('measurement')returnsfalseand assert that#hotjaris not rendered (and ideally one forHOTJAR_IDundefined).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: Restoreglobal.fetchafter 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. Preferjest.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 theas jest.Mockcasts.🤖 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
trueformeasurement, so only the happy path is exercised. Given the component's new behavior returnsnullwhenhasStatsis false (persrc/components/helpers/GoogleAnalytics.tsx:15), adding a case wherehas('measurement')returnsfalsewould 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 setshasMock.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, andprimaryActions. 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-componentsAlso 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (37)
package.jsonpublic/locales/de/cookiePolicy.jsonpublic/locales/de/distribution.jsonpublic/locales/en/cookiePolicy.jsonpublic/locales/en/distribution.jsonsrc/app/[locale]/layout.tsxsrc/app/api/og/route.tsxsrc/app/layout.tsxsrc/app/not-found.tsxsrc/components/__tests__/ContactForm.test.tsxsrc/components/__tests__/GoogleAnalytics.test.tsxsrc/components/__tests__/Hotjar.test.tsxsrc/components/helpers/ConsentProvider.tsxsrc/components/helpers/GoogleAnalytics.tsxsrc/components/helpers/Hotjar.tsxsrc/components/helpers/consent/ConsentCategoryToggles.tsxsrc/components/helpers/consent/ConsentPreferencesSection.tsxsrc/components/helpers/consent/ConsentSurfaces.tsxsrc/components/layout/Footer.tsxsrc/components/layout/Header.tsxsrc/components/providers/FooterPostsContext.tsxsrc/components/providers/Providers.tsxsrc/components/templates/ContactForm.tsxsrc/components/templates/CookieControlCenter.tsxsrc/components/templates/CookiePolicyVendorList.tsxsrc/components/templates/DistributionProducts.tsxsrc/components/ui/RadixCheckbox.tsxsrc/components/ui/Switch.tsxsrc/components/ui/Typography/Title.tsxsrc/components/ui/index.tssrc/constant/env.tssrc/lib/__tests__/helper.test.tssrc/lib/footer-posts.tssrc/lib/helper.tssrc/types/Cookiebot.tssrc/utils/useConsent.tssrc/utils/useConsentState.ts
💤 Files with no reviewable changes (1)
- src/constant/env.ts
| "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", |
There was a problem hiding this comment.
🧩 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:
- 1: https://nextjs.org/blog/next-15
- 2: https://nextjs.org/blog/next-15-1
- 3: https://nextjs.org/docs/app/building-your-application/upgrading/version-15
- 4: https://nextjs.org/docs
- 5: https://www.npmjs.com/package/next/v/15.0.2
🏁 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=tsxRepository: 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 -20Repository: 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.
| "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.", |
There was a problem hiding this comment.
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.
| "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.
![Screenshot of /[locale]](https://images.weserv.nl?url=https://sjc.microlink.io/VNMYdEQj_zZRsVhYt_yOUUtpeYiVMM5RALgX13TsCdjTug6HNKTlX9o-kaKQm89YKBbvoBEMli6zF1WF-rrJDg.png&w=600)

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
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 topnpm/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
edgeOpen Graph image endpoint (/api/og) and switches OG URL generation to useNEXT_PUBLIC_APP_URLas 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
Improvements