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
- 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. - In the same browser call, run this evaluator on the loaded embed page. Set
namesto 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=onso the wall can populate the full collection rather than only its initial cards.showMore=offprevents 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, ortitle, while also returningrawTextandtextLinesfor verification. - Headshot URLs should come from
currentSrcbeforesrc, because responsive images may expose the effective image URL only throughcurrentSrc.