Rams MCP · The full engine, now in your coding agent
calcom on GitHub

calcom/cal.com

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·July 9, 2026

Top fix

Add loading and not-found states so pages never render blank

See the fix

Verdict

Polished infrastructure hides a real gap: pages fetch data confidently but never plan for it failing. The biggest risk is silent blank screens when bookings or params don't resolve as expected.

Files Rams reviewed

apps/docs/app/layout.tsx

apps/web/app/(booking-page-wrapper)/[user]/[type]/embed/page.tsx

apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx

apps/web/app/(booking-page-wrapper)/[user]/page.tsx

apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx

apps/web/app/(booking-page-wrapper)/booking/[uid]/embed/page.tsx

apps/web/app/(booking-page-wrapper)/booking/[uid]/page.tsx

apps/web/app/(booking-page-wrapper)/d/[link]/[slug]/page.tsx

apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx

apps/web/app/(use-page-wrapper)/(main-nav)/booking/[uid]/logs/page.tsx

apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx

apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx

apps/web/app/(use-page-wrapper)/apps/(homepage)/page.tsx

apps/web/app/(use-page-wrapper)/apps/[slug]/page.tsx

apps/web/app/(use-page-wrapper)/apps/[slug]/setup/page.tsx

apps/web/app/(use-page-wrapper)/apps/installation/[[...step]]/page.tsx

apps/web/app/(use-page-wrapper)/apps/installed/[category]/page.tsx

apps/web/app/(use-page-wrapper)/auth/error/page.tsx

apps/web/app/(use-page-wrapper)/auth/forgot-password/[id]/page.tsx

apps/web/app/(use-page-wrapper)/auth/forgot-password/page.tsx

apps/web/app/(use-page-wrapper)/auth/setup/page.tsx

apps/web/app/(use-page-wrapper)/auth/verify-email-change/page.tsx

apps/web/app/(use-page-wrapper)/availability/[schedule]/page.tsx

apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx

apps/web/app/(use-page-wrapper)/getting-started/[[...step]]/page.tsx

apps/web/app/(use-page-wrapper)/layout.tsx

apps/web/app/(use-page-wrapper)/onboarding/getting-started/page.tsx

apps/web/app/(use-page-wrapper)/onboarding/personal/settings/page.tsx

apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx

apps/web/app/(use-page-wrapper)/payment/[uid]/page.tsx

94/100

UX

3 serious
UX·apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx:16Serious

No loading or not-found state, page just goes blank

When `useDecoyBooking(uid)` returns falsy, the component returns `null`. There is no distinction between 'still loading' and 'booking not found': both render an empty page.

Why it matters

A user landing on a booking confirmation link with a bad or stale uid sees a blank white screen with no explanation, and has no way to tell if the page is broken or still loading.

Fix

Render a designed loading state while data is in flight and a designed not-found/error state when the booking truly doesn't resolve.

if (!bookingData) {
  return null;
}
if (bookingData === undefined) {
  return <BookingSuccessSkeleton />;
}

if (!bookingData) {
  return <BookingNotFound uid={uid} />;
}
UX·apps/web/app/(booking-page-wrapper)/[user]/[type]/embed/page.tsx:26Serious

Component tree renders no loading, error, or empty state for booking data

`getData(context)` fetches `props` with no try/catch and no branch for a failed or empty `eventData`. If the fetch throws or `props` is malformed, `<TypePage {...props} />` renders directly with no fallback.

Why it matters

A failed booking-type lookup surfaces as a blank page or a raw framework error screen instead of a designed error state, leaving embedded booking widgets broken for the end user with no recovery path.

Fix

Wrap the data fetch in error handling and give the embed a designed fallback state when the event data is missing or the fetch fails.

const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);

const locale = props.eventData?.interfaceLanguage;
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(context);

if (!props?.eventData) {
  return <TypePage {...props} />;
}

const locale = props.eventData?.interfaceLanguage;
UX·apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx:23Serious

No fallback UI when title, date, or location are missing

`title={booking.title || "Booking"}`, `formattedDate`, and `location={booking.location || null}` all fall back to empty strings or generic text when data is missing, with no indication to the user why a field is blank.

Why it matters

A confirmation page with a blank date or blank time field reads as broken, not as 'no data available', and a user checking their booking details loses confidence the booking actually succeeded.

Fix

Show an explicit placeholder like 'Time unavailable' instead of an empty string so the missing field reads as intentional.

const formattedDate = startTime ? startTime.tz(timeZone).format("dddd, MMMM D, YYYY") : "";
const formattedDate = startTime ? startTime.tz(timeZone).format("dddd, MMMM D, YYYY") : "Date unavailable";
96/100

Accessibility

2 serious
Accessibility·apps/docs/app/layout.tsx:33Serious

Both logo images carry identical, non-distinguishing alt text

The `logo-light` and `logo-dark` `<img>` elements both use `alt="Cal.diy Docs"` with no distinction between them, and neither is marked `aria-hidden` even though only one is meant to be visible at a time based on theme.

Why it matters

If the theme-toggling CSS class fails to hide one image (print styles, forced-colors mode, a CSS load failure), assistive tech and search crawlers see the same logo name announced twice in the nav on every page.

Fix

Give the non-visible theme variant aria-hidden so only the active logo is ever exposed to assistive tech.

<img
  src="/cal-docs-logo.svg"
  alt="Cal.diy Docs"
  height={26}
  className="logo-light"
  style={{ height: 26 }}
/>
<img
  src="/cal-docs-logo.svg"
  alt="Cal.diy Docs"
  height={26}
  className="logo-light"
/>
Accessibility·apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx:11Serious

Unvalidated params.uid cast bypasses type safety for a URL param

`const uid = params?.uid as string;` force-casts a Next.js route param that can be `string | string[] | undefined` depending on routing, with no runtime check.

Why it matters

If `uid` resolves to an array or undefined at runtime, `useDecoyBooking` receives a malformed value silently, and the failure surfaces later as the blank-page problem above instead of a clear error at the source.

Fix

Validate the param shape before use instead of casting past the type system.

const uid = params?.uid as string;
const uid = typeof params?.uid === "string" ? params.uid : undefined;

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repo for free
98/100

Color

1 serious
Color·apps/docs/app/layout.tsx:78Serious

No color-scheme declared even though light and dark logos exist

The layout renders separate `logo-light` and `logo-dark` images, which only makes sense if the site supports a dark theme, but `<Head />` is rendered with no `<meta name="color-scheme">` and no `color-scheme` CSS is set anywhere in this file.

Why it matters

Without color-scheme, browser chrome (scrollbars, form controls, the page background flash before hydration) stays light even when the user's system or the site is in dark mode, producing a visible mismatch between page content and browser UI.

Fix

Declare color-scheme for both themes so browser-native UI matches the active theme.

<Head />
<Head>
  <meta name="color-scheme" content="light dark" />
</Head>

Typography

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • Loading calSans (400 weight, for display use) separately from calSansUI (300-700, for interface text) gives the docs site a clear heading/body font pairing instead of one typeface doing every job. This is the correct way to introduce a second family: one for expressive headings, one variable-weight family for everything else.
  • Locale handling is conditional and clean: the component only wraps children in CustomI18nProvider when a locale is resolved from eventData, falling back to rendering LegacyPage directly rather than forcing a provider with empty translations. This avoids a silent no-op wrapper in the common case where locale isn't set.
  • Deriving `timeZone` with a fallback chain (`booker?.timeZone || host?.timeZone || dayjs.tz.guess()`) is the right call: it prefers the attendee's own context, falls back to the host, and only guesses as a last resort, so the displayed time is as relevant as possible to whoever is viewing it.
  • Conditionally wrapping OldPage in CustomI18nProvider only when eventLocale exists (rather than always wrapping with a fallback) avoids loading and parsing translation namespaces the page doesn't need, which is the right call for a server component that runs on every booking page view.

Score your own repo with Rams.

Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.