agentleFS
karerokarero/website-builder/skills/website-testimonials/SKILL.md

website-testimonials

Skill9 starsChanged 30 days agoMITView on GitHub

Encode customer / attendee testimonials as proper schema.org `Review` JSON-LD AND render the "What people say" UI from ONE typed source of truth (`src/data/testimonials.ts`). Individual Review nodes only — author + 5/5 reviewRating + reviewBody + inLanguage + itemReviewed pointing at the SPECIFIC thing reviewed (Event / Product / Service), never a fabricated org-wide AggregateRating. Use when adding quotes, testimonials, social proof, ratings, or star reviews to a site built with the new-website kit (pipeline step 5, alongside `website-seo-geo`). Delegates other schema types to `schema-markup`. Trigger phrases: "add testimonials", "customer quotes", "attendee feedback", "review schema", "star ratings", "social proof section", "encode the quotes", "schema.org Review", "what people say section".

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Quotes are verbatim: Never invent, embellish, or "tidy" a quote or a name.

    line 41
  2. Very specific:

    Add one entry to src/data/testimonials.ts (set lang.

    line 58
  3. Distinctive:

    Ship to PREVIEW first, not prod: Put the section on a preview deploy (a branch build, off the production branch) so the team can review …

    line 77
  4. Very specific:

    Promote per person — ship only the approved subset: If two of three consent, the live TESTIMONIALS array carries those two.

    line 80
  5. Very specific:

    Withhold, don't delete: Keep an unconsented quote out of the array but leave a // NOTE: comment (the verbatim text also survives in git …

    line 82
  6. Very specific:

    Every NAMED quote on production has consent.

    line 154

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Website Testimonials & Review Schema

Turn real quotes into (1) a UI section and (2) valid Review structured data, both from one typed data file so the visible card and the JSON-LD can never drift. This skill owns the testimonial pattern; it hands generic schema work (Product, FAQ, Breadcrumb, Article…) to schema-markup.

Read this first — what schema can and can't buy you

  • Google does NOT show ⭐ rich-results for self-hosted reviews about your own Organization / LocalBusiness. It treats them as self-serving and may ignore — or, for a fabricated AggregateRating, penalise — them. So the value here is truthful semantic markup + GEO (LLMs / AI answer engines citing your social proof), not blue-link stars. Set expectations honestly (Rule 12).
  • itemReviewed is the specific thing the person reviewed — the Event (talk/workshop), Product, Service, Course — not a blanket org rating. This keeps the markup accurate and is what review-snippet-eligible types want.
  • Individual Review nodes only by default. No AggregateRating unless the reviewed item is a genuinely rich-results-eligible type (e.g. Product) AND the rating reflects all real ratings. Few hand-picked 5-stars + an aggregate reads as spam.

Accuracy rules (non-negotiable)

  • Quotes are verbatim. Never invent, embellish, or "tidy" a quote or a name.
  • Keep the original language. A quote written in another language stays in that language (e.g. a German quote on an English page) — translating a testimonial misrepresents it. Set lang on the element AND inLanguage on the Review so screen readers + parsers get it right.
  • Real ratings only. ratingValue is what the person actually gave.

Sourcing quotes (where they come from)

Testimonials come from wherever real feedback lives: post-purchase emails, post-event surveys, support tickets, sales calls, or review sites (G2, Trustpilot, app stores). Most sources are private to you, with no public feed or API to pull from, so the workflow is manual curation — which is also better editorially, since you pick the strongest, on-message quotes:

  1. Open the source and copy the best verbatim quote + the rating + the person's name/affiliation.
  2. Add one entry to src/data/testimonials.ts (set lang; map itemName/itemUrl to the specific item reviewed — the product, service, or event). No sync script is possible for most sources — don't build one.
  3. Get consent for named quotes before they go live — see Consent & shipping below.

Example (event sites): Luma keeps ratings organizer-private in Manage Event → Insights, exportable only as CSV; its per-response "share card" is a one-off image, not a live source — so you transcribe by hand. Expect the same of most platforms.

Consent & shipping named quotes

A person left feedback for one purpose, not blanket permission to be quoted by name on your marketing site. Treat consent as a publish gate, applied per person:

  1. Ask before production. "OK to feature this quote, with your name and affiliation, on our site?" Anonymous/handle-only quotes are lower-risk — but still ask if the wording itself is identifying. Keeps you clean on GDPR/consent.
  2. Ship to PREVIEW first, not prod. Put the section on a preview deploy (a branch build, off the production branch) so the team can review wording and layout while consent is pending. Keep testimonials off production until approved.
  3. Promote per person — ship only the approved subset. If two of three consent, the live TESTIMONIALS array carries those two; the third stays out.
  4. Withhold, don't delete. Keep an unconsented quote out of the array but leave a // NOTE: comment (the verbatim text also survives in git history), so re-adding on approval is a one-line edit.

Caveat: a static preview is usually not auto-noindexed, and a *.pages.dev URL is public — so a named, unconsented quote sitting on a preview is still exposure. Keep previews unlinked and short-lived (or add a branch-only noindex); the real guarantee is keeping it off production until consent lands.

The data model — one source of truth

src/data/testimonials.ts exports the typed list and the schema builder, so a page imports both and nothing is duplicated:

import { SITE } from '../config';

export interface Testimonial {
  author: string;        // name or handle, verbatim
  org?: string;          // affiliation, shown after the name
  rating: number;        // 1–5, as actually given
  quote: string;         // verbatim
  lang: string;          // BCP-47 tag, e.g. 'en' / 'de' → `lang` attr + inLanguage
  itemName: string;      // the thing reviewed → itemReviewed.name
  itemUrl: string;       // canonical URL of that item (absolute)
  itemType?: string;     // schema.org type of the reviewed thing; default 'Event'
}

// Only consent-approved entries ship here; withhold the rest with a // NOTE.
export const TESTIMONIALS: Testimonial[] = [ /* … */ ];

// Individual Review nodes — no self-serving AggregateRating.
export function reviewSchemas(): Record<string, unknown>[] {
  return TESTIMONIALS.map((t) => ({
    '@context': 'https://schema.org',
    '@type': 'Review',
    author: { '@type': 'Person', name: t.author },
    reviewRating: { '@type': 'Rating', ratingValue: t.rating, bestRating: 5, worstRating: 1 },
    reviewBody: t.quote,
    inLanguage: t.lang,
    itemReviewed: { '@type': t.itemType ?? 'Event', name: t.itemName, url: t.itemUrl },
  }));
}

Wiring it in (Astro / new-website kit)

  1. Schema — pass the builder to Base's schemas prop; the Base layout already serialises each entry to <script type="application/ld+json">:
    ---
    import { TESTIMONIALS, reviewSchemas } from '../data/testimonials';
    ---
    <Base titleExact="…" description="…" schemas={reviewSchemas()}>
    (Do NOT hand-write JSON-LD in the page — go through schemas so it stays in sync with the data file and Base's other schema nodes.)
  2. UI — render cards from the same TESTIMONIALS array. Stars are decorative (aria-label="Rated 5 out of 5", the glyphs themselves hidden from the name); foreign-language quotes get lang={t.lang} so AT pronounces them correctly.
  3. Theme — reuse the site's tokens (--dark-card, --text, --text-muted, --primary-light); a gold star (#F5A623) clears AA on the dark card.

Done means

  • npm test green — a11y.spec.ts (star contrast + lang attrs), tone, seo.
  • JSON-LD parses and passes the schema.org validator / Google Rich Results Test with no errors; each Review has author, reviewRating (with bestRating/worstRating), reviewBody, inLanguage, and an itemReviewed with a real name + absolute url.
  • The rendered card text equals the reviewBody (same source — verify one).
  • No AggregateRating unless justified per the rules above.
  • Every NAMED quote on production has consent; un-approved ones are withheld (with a // NOTE:), and anything pending consent shipped to preview only.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.