Extract testimonials from a Testimonial.to wall

Site embed-v2.testimonial.toTask extract-testimonials-from-wallVersion v1Updated Aug 4, 2026Category data-extraction

Extract exact testimonial text, brand, role, and headshot URL for named people from a Testimonial.to wall. This skill was captured from a live agent session on embed-v2.testimonial.to and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Extract selected testimonials from a Testimonial.to wall, returning each person's exact review text, brand name, role, and headshot image URL without modifying the wall.

When to Use

Use when the caller supplies a Testimonial.to wall slug and one or more testimonial author names. This recipe is useful because the public wall embeds the actual content in an embed-v2.testimonial.to page and the embed URL's loading parameters control whether the complete collection is rendered.

Workflow

  1. Navigate directly to https://embed-v2.testimonial.to/w/{wall-slug}/?cc=off&showMore=off&card=undefined&autoLoadMore=on&loadMore=on, replacing {wall-slug} and the requested names with caller inputs. Wait for DOM content to load and for the testimonial cards to appear.
  2. In the same browser call, run this evaluator on the loaded embed page. Set names to the requested author names.
(() => {
  const names = ["{name-1}", "{name-2}", "{name-3}"];
  const norm = (s) => (s || "").replace(/\\s+/g, " ").trim();
  const low = (s) => norm(s).toLowerCase();
  const esc = (s) => s.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
  const text = (el) => norm(el?.innerText || el?.textContent || "");
  const visible = (el) =>
    !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
  const field = (root, patterns) => {
    const nodes = [...root.querySelectorAll("[class], [data-testid], [aria-label]")];
    for (const el of nodes) {
      const key = low(
        (el.className && typeof el.className === "string" ? el.className : "") +
          " " +
          (el.getAttribute("data-testid") || "") +
          " " +
          (el.getAttribute("aria-label") || ""),
      );
      if (patterns.some((p) => key.includes(p))) {
        const v = text(el);
        if (v) return v;
      }
    }
    return null;
  };
  const cardFor = (name) => {
    const wanted = low(name);
    const matches = [...document.querySelectorAll("body *")]
      .filter((el) => visible(el) && low(text(el)) === wanted)
      .sort((a, b) => text(a).length - text(b).length);
    const leaf = matches[0];
    if (!leaf) return null;
    let el = leaf;
    for (let i = 0; el && i < 8; i++, el = el.parentElement) {
      const t = text(el);
      const hasImage = el.querySelector("img");
      const looksLikeCard = el.matches(
        'article, li, [role="article"], [class*="card" i], [data-testid*="card" i]',
      );
      if (hasImage && (looksLikeCard || t.length >= 120)) return el;
    }
    return leaf.closest('article, li, [role="article"]') || leaf.parentElement;
  };
  const result = names.map((name) => {
    const card = cardFor(name);
    if (!card)
      return {
        name,
        review: null,
        brand: null,
        role: null,
        headshotUrl: null,
        rawText: null,
      };
    const lines = [...new Set(text(card).split(/\\n+/).map(norm).filter(Boolean))];
    const quote = card.querySelector(
      'blockquote, [class*="review" i], [class*="quote" i], [data-testid*="review" i], [data-testid*="quote" i]',
    );
    let review = text(quote) || null;
    if (review && low(review) === low(name)) review = null;
    const role = field(card, ["role", "title", "position", "job"]) || null;
    const brand =
      field(card, ["brand", "company", "organization", "business"]) || null;
    const nameIndex = lines.findIndex((x) => low(x) === low(name));
    const image =
      [...card.querySelectorAll("img")].find((img) =>
        low(img.alt).includes(low(name)),
      ) || card.querySelector("img");
    if (!review) {
      const candidates = [...card.querySelectorAll("p, q, blockquote")]
        .map(text)
        .filter((v) => v && low(v) !== low(name));
      review = candidates.sort((a, b) => b.length - a.length)[0] || null;
    }
    return {
      name,
      review,
      brand,
      role,
      headshotUrl: image ? image.currentSrc || image.src || null : null,
      rawText: text(card),
      textLines: lines,
      nearbyLines:
        nameIndex >= 0 ? lines.slice(Math.max(0, nameIndex - 2), nameIndex + 4) : [],
    };
  });
  return { wall: location.pathname, testimonials: result };
})();

Site-Specific Gotchas

  • The public wall is an outer page that embeds the content; navigating directly to the embed-v2.testimonial.to/w/{wall-slug}/ URL avoids an extra iframe lookup and makes the evaluator run against the testimonial DOM itself.
  • Keep autoLoadMore=on&loadMore=on so the wall can populate the full collection rather than only its initial cards. showMore=off prevents collapsed review text from hiding part of the exact quote.
  • Card markup may use generated class names. The evaluator therefore prefers semantic elements and class/data-attribute names containing review, quote, brand, company, role, or title, while also returning rawText and textLines for verification.
  • Headshot URLs should come from currentSrc before src, because responsive images may expose the effective image URL only through currentSrc.

Expected Output

Return one object per requested name under testimonials, with name, exact review, brand, role, and headshotUrl. rawText, textLines, and nearbyLines provide the complete card text and fallback context when a wall's generated markup does not expose explicit field labels; unavailable fields are null.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=embed-v2.testimonial.to&task=extract-testimonials-from-wall