Collect and curate client quotes, rendered as a wall or a carousel.
A testimonials system: quotes live as content the owner curates in Studio, and land on the site as either a quote wall or a swipeable carousel — placed on any page as a section. Optionally, a public "share your experience" form collects new quotes straight into the moderation queue.
A testimonial content type holds the quotes (draft = hidden, published = live). A testimonials section definition lets the client drop the block onto pages and choose wall vs carousel per placement. The optional collection form posts to a rate-limited custom route that stores submissions as drafts.
You are adding testimonials to a Prelo project.
1. **Content type.** In `cms.config.js`:
```js
testimonial: {
label: "Testimonials",
fields: {
quote: { type: "text", required: true },
person: { type: "text", required: true },
role: { type: "text" }, // "CEO, Acme"
avatar: { type: "image" },
featured: { type: "select", options: ["yes", "no"] },
},
studio: { columns: ["person", "role", "featured"] },
},
```
2. **Section.** Under `sections:`:
```js
testimonials: {
fields: {
heading: { type: "text" },
layout: { type: "select", options: ["wall", "carousel"], studio: "variant" },
only: { type: "select", options: ["all", "featured"] },
max: { type: "number" },
},
},
```
3. **Renderer.** In the project's section renderer map (`renderBody(body, { testimonials: … })` for one-process sites, or the frontend's section switch):
- Fetch published testimonials (`GET /api/testimonial`), filter to `featured: "yes"` when `only` = featured, cap at `max` (default 12), newest first.
- **Wall:** CSS multi-column layout (3 → 2 → 1 columns responsive), each card: quote, then avatar (`?w=96`, rounded) + person + role in the project's muted style. No JS.
- **Carousel:** a horizontal scroll-snap track of the same cards, with prev/next buttons that `scrollBy` one card and hide when scrolling isn't possible. Must degrade to plain horizontal scrolling without JS. No carousel library.
- Render quotes in `<figure>`/`<blockquote>`/`<figcaption>` for semantics; escape everything.
4. **Collection form (optional — confirm with the user before building).** If wanted:
- `hooks.boot`: `x_testimonial_hits (ip TEXT, at INTEGER)`; limit 2 per hour per IP.
- Route `"POST /testimonials"`: validate quote 10–600 chars, person required, honeypot `website` silently discarded; insert as a **draft** `testimonial` (title = person). Respond `{"ok":true}`.
- A small form component (quote textarea, name, role, honeypot) with a "Thanks — we review every quote before it appears" success state.
5. **Verify.** Seed 4–5 testimonials (draft one of them), add a section in Studio in each layout, confirm the draft never renders, the featured filter works, and the carousel is keyboard/scroll accessible.The owner curates in Studio: publish to show, draft to hide, flip featured to build a shortlist for the homepage while the full wall lives elsewhere. Submitted quotes (if enabled) wait as drafts until reviewed.
Signup form with double opt-in, delivering to Resend, Mailchimp or Buttondown.
Contact form with spam protection and email notifications, plus a submissions inbox in Studio.
Filterable gallery with lightbox, fed straight from the media library.