Enumerate and compare the professional product catalogs exposed by First Trust Global Portfolios for multiple country/localized paths. The catalog is represented by product links under .FundsTable__group; each product URL normally carries its ISIN in the isin_code query parameter.
Optionally inspect product detail pages to determine whether an SFDR classification is actually present in the rendered DOM, rather than treating a missing scraper field as a literal dash value.
Use Cases
Use when verifying whether the same product universe is available in several country sites, identifying country-specific additions or omissions, or comparing product counts and ISIN coverage. Supply country path codes such as {country-codes} and compare the returned ISIN sets across locales.
Use the optional detail-page diagnostic when a static scraper reports - for SFDR on every product and you need to distinguish an absent/hidden field from a real displayed dash or an incorrect selector.
Automation Flow
- For each requested country code, navigate directly to:
https://www.ftglobalportfolios.com/{country-code}/professional/Products/ - If the site presents its consent/professional-investor gate, select the professional-investor option when shown, then select the visible
Acceptbutton. Wait briefly for the catalog to render. - On the loaded catalog page, run this self-contained evaluator:
(() => {
const links = [
...document.querySelectorAll('.FundsTable__group a[href*="/Products/"]'),
];
const products = [
...new Map(
links.map((a) => {
const href = (() => {
try {
return new URL(a.href, location.href).href;
} catch (_) {
return a.href;
}
})();
const url = (() => {
try {
return new URL(href);
} catch (_) {
return null;
}
})();
const isin = url ? (url.searchParams.get("isin_code") || "").trim() : "";
return [
href,
{
name: (a.textContent || "").replace(/\s+/g, " ").trim(),
url: href,
isin: isin || null,
},
];
}),
).values(),
];
const isins = [...new Set(products.map((p) => p.isin).filter(Boolean))];
return {
country: location.pathname.split("/").filter(Boolean)[0] || null,
url: location.href,
productCount: products.length,
uniqueIsinCount: isins.length,
products,
isins,
productsWithoutIsin: products
.filter((p) => !p.isin)
.map((p) => ({ name: p.name, url: p.url })),
};
})();Aggregate one evaluator result per country. Compare
isinsas sets, reporting the union, intersection, and each country’sonlyInCountryandmissingFromCountryvalues. Treat products without anisin_codeas separately reviewable rather than silently discarding them.Optionally diagnose SFDR for any product URL obtained from step 3. Navigate directly to the catalog-provided URL, preserving its
isin_codequery parameter; do not guess a product slug or opaque identifier. After the consent gate is accepted and the detail page has rendered, run this evaluator:
(() => {
const clean = (s) => (s || "").replace(/\s+/g, " ").trim();
const sets = [...document.querySelectorAll(".Values__set")];
const sfdrSets = sets.filter((set) =>
/SFDR\s*Classification/i.test(set.textContent || ""),
);
const exactLabels = [...document.querySelectorAll("*")].filter(
(el) =>
el.children.length === 0 &&
/^SFDR\s*Classification$/i.test(clean(el.textContent)),
);
const describe = (el) => {
const parent = el.parentElement;
const parentSet = el.closest(".Values__set");
const sibling = el.nextElementSibling;
const parentSibling = parent && parent.nextElementSibling;
return {
label: clean(el.textContent),
valueFromNextSibling: clean(sibling && sibling.textContent),
valueFromParentNextSibling: clean(parentSibling && parentSibling.textContent),
containerText: clean((parentSet || parent || el).textContent),
containerSelector: parentSet ? ".Values__set" : null,
};
};
return {
url: location.href,
isin: new URL(location.href).searchParams.get("isin_code"),
valuesSetCount: sets.length,
sfdrContainerCount: sfdrSets.length,
exactLabelCount: exactLabels.length,
sfdrContainers: sfdrSets.map((set) => clean(set.textContent)),
exactLabelMatches: exactLabels.map(describe),
bodyContainsSfdr: /SFDR\s*Classification/i.test(document.body.innerText || ""),
};
})();Interpret the result as follows: bodyContainsSfdr: false means the rendered page does not expose the field to a DOM-only static scraper; bodyContainsSfdr: true with zero .Values__set matches indicates the scraper’s container selector is wrong; a matching container or exact label with an empty sibling value indicates a structure/value-extraction issue; and an explicitly returned - in the container text is a genuine displayed dash. Repeat on representative products/locales before changing the scraper globally.
Possible Friction Points
- The localized country code is the first path segment, while the professional catalog path remains
/professional/Products/; do not assume the Luxembourg path applies to every country. - A consent/investor modal can prevent the catalog from rendering. The button text is localized; the German flow exposed
Professioneller AnlegerbeforeAccept, while other locales may expose a different professional-investor label. - Product rows can contain duplicate links, so deduplicate by absolute URL before counting. Use the
isin_codequery parameter for cross-country identity rather than product-link text or slug. - Wait for the catalog after dismissing the modal before extracting
.FundsTable__grouplinks; an early extraction can return an incomplete or empty table. - Product detail pages are reached with the catalog-provided product URL and its
isin_codequery parameter. Do not fabricate opaque product identifiers from names. - For SFDR diagnosis, search both the visible body text and
.Values__setcontainers. A selector that assumes every field is a.Values__setentry can report dashes when the label is absent, rendered elsewhere, or not exposed in the static DOM. - The catalog selector is based on the observed
FundsTable__groupstructure and the detail diagnostic uses observed.Values__setmarkup; both should be rechecked if the site redesigns its markup.