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