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 is published here as a reusable recipe for agents.

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.

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.

Use Cases

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.

Automation Flow

  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 };
})();

Possible Friction Points

  • 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.

Call it

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