← All components

Content · 8 of 13

Review

The attributed testimonial. It has no background of its own — only a 32px backdrop-filter — so it is the one component that cannot be judged on a plain surface.

Vue island src/components/common/Review.vue rendered live from src/components/common/Review.vue CMS-neutral

Props

PropTypeNotes
item *TestimonialCardThe six fields the component reads, declared in the component itself: photo.url, photo.name, name, position, review, linkedinurl, verification. A generated CMS entity satisfies it unchanged; the component carries no CMS schema.
resolveImage(url: string) => stringSupplied by the caller for relative media URLs; omit it when they are already absolute.
classNamestringInterpolated with no fallback — see findings.

Stories

Shown over testimonials-background.svg, the plate named for the section this card ships in. The blur is what separates the card from the art behind it — there is nothing else doing that job.

Full photo + linkedinurl + verification Verification row is --color-action — see findings. The LinkedIn icon renders because linkedinurl='#!' passes the length > 1 guard (also see findings).
Minimal no photo · no linkedinurl · no verification Photo is hidden by a literal filename check: v-if="item.photo.name !== 'no-image.svg'". This story also omits className — inspect the card's class attribute and the literal string undefined is in it (see findings).

In the grid

One card on a canvas shows the blur; it does not show what the blur is for. Laid two-up over a single continuous plate, the art runs behind each card and out the other side, the alleys between them stay unblurred, and the falloff in the plate's opacity ladder puts every card on a slightly different ground — which is the case the component has to survive and the one a flat swatch cannot pose.

Four testimonials, two-up mixed: with and without photo, LinkedIn and verification The 16px alley is the component's own mx-1 lg:mx-2, doubled where two cards meet — no wrapper supplies it. The section wrapper that positions this grid on the live site is an Astro page, not a harvested SFC, so its widths and vertical rhythm are not reproduced here; only the card is. The third card's review carries hard newlines to exercise whitespace-pre-wrap.

Findings

A sentinel filename controls rendering. The avatar is gated on item.photo.name !== 'no-image.svg'. The contract between CMS and component is a magic string: rename the placeholder asset in the CMS and every testimonial sprouts a broken avatar. There is also no guard on photo itself, so a testimonial with no photo relation throws on photo.name rather than falling back.

linkedinurl.length > 1 is a truthiness test in disguise. It guards against the empty string and a single character, not against an invalid URL. Any one-character-plus value renders a live external link. The same expression throws if linkedinurl is null rather than empty.

The verification row fails contrast. text-action on the site's #0A0A0B ground is 2.94:1, below the 4.5:1 floor for text this size (it was 3.03:1 against the pre-redesign #050505 — the rebuild nudged it further down). It is also the trust signal on a testimonial, so it is load-bearing copy. --color-hovered (≈8.4:1) is the in-palette fix. Same shape as DESIGN.md §10.5.

className has no fallback. The template interpolates ${className} directly, unlike ArticlePreview's ${className ?? ''}. Omitting the prop puts the literal string undefined into the class list. Harmless as a class name, but it ships in the markup.

Judge it on art, never on flat color. With no background of its own, previewing this card on #0A0A0B makes the blur invisible and the design look unfinished. That is a preview error, not a component defect.