Scrape any Brookstone ETF product page and return its title, ticker, labeled ETF fields, performance tables, holdings tables, data blocks, and nearby fund-document links.
Use Cases
- Extract ticker and ETF metadata from a Brookstone product page.
- Retrieve quarter-end or other performance tables.
- Collect All Holdings or Top Holdings rows.
- Capture prospectus, reports, and other fund-document URLs.
Automation Flow
- Build
https://www.brookstoneam.com/{product-slug}from the caller-supplied slug, or use the supplied full URL. - Navigate with
waitUntil: "domcontentloaded", wait forh2, then wait an additional 3000 ms for asynchronous metadata and tables. - Run this
evaluate()on the loaded page:
(() => {
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
const text = el => clean(el ? (el.innerText || el.textContent) : '');
const headings = [...document.querySelectorAll('h2,h3,h4')];
const findHeading = name => headings.find(h => text(h).toLowerCase() === name.toLowerCase());
const section = name => {
const h = findHeading(name);
if (!h) return null;
let n = h;
for (let i = 0; i < 5 && n; i++, n = n.parentElement) {
if (text(n).length > text(h).length) return text(n);
}
return text(h.parentElement);
};
const precedingHeading = el => {
let result = null;
for (const h of headings) {
if (h.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) result = h;
}
return result ? text(result) : '';
};
const readRows = table => [...table.querySelectorAll('tr')]
.map(tr => [...tr.querySelectorAll('th,td')].map(text))
.filter(row => row.some(Boolean));
const tables = [...document.querySelectorAll('table')].map((table, index) => {
const rows = readRows(table);
const context = clean([
precedingHeading(table),
table.caption && text(table.caption),
table.parentElement && text(table.parentElement)
].filter(Boolean).join(' '));
const searchable = (context + ' ' + rows.flat().join(' ')).toLowerCase();
return {
index,
heading: precedingHeading(table) || null,
className: String(table.className || ''),
rows,
context,
kind: /quarter.?end|performance/.test(searchable) ? 'quarterEndPerformance' :
/all holdings|top holdings|holdings/.test(searchable) ? 'holdings' :
/etf data/.test(searchable) ? 'etfData' : 'other'
};
}).filter(table => table.rows.length);
const fields = {};
document.querySelectorAll('.text-style-tagline').forEach(label => {
const key = text(label);
if (!key || fields[key] != null || !label.parentElement) return;
const clone = label.parentElement.cloneNode(true);
clone.querySelectorAll('.text-style-tagline').forEach(node => node.remove());
const value = text(clone);
if (value) fields[key] = value;
});
const etfTables = tables.filter(table => table.kind === 'etfData');
etfTables.flatMap(table => table.rows).forEach(row => {
if (row.length >= 2 && row[0] && fields[row[0]] == null) fields[row[0]] = row.slice(1).join(' | ');
});
const tickerRow = etfTables.flatMap(table => table.rows).find(row => row.some(cell => /^ticker$/i.test(cell)));
const ticker = fields.Ticker || (tickerRow && tickerRow.slice(1).find(Boolean)) || text(document.querySelector('[data-ticker]')) || null;
const dataBlocks = [];
const seenBlocks = new Set();
document.querySelectorAll('.data-block-2col, .data-block.light, [class*="data-block"]').forEach(el => {
const value = text(el);
if (value && !seenBlocks.has(value)) {
seenBlocks.add(value);
dataBlocks.push({className: String(el.className || ''), text: value});
}
});
const documentHeading = findHeading('Fund Documents') || findHeading('Documents');
let documentRoot = documentHeading;
for (let i = 0; i < 6 && documentRoot; i++, documentRoot = documentRoot.parentElement) {
if (documentRoot.querySelectorAll && documentRoot.querySelectorAll('a[href]').length) break;
}
const links = root => root ? [...root.querySelectorAll('a[href]')]
.map(a => ({text: text(a), href: a.href}))
.filter(link => link.text || link.href) : [];
const fundDocuments = links(documentRoot);
const pdfLinks = [...document.querySelectorAll('a[href]')]
.map(a => ({text: text(a), href: a.href}))
.filter(link => /\.pdf(?:$|\?)/i.test(link.href));
return {
url: location.href,
title: text(document.querySelector('h1')) || document.title,
ticker,
fields,
sections: {
objective: section('Objective'),
etfData: section('ETF Data') || etfTables,
listingInformation: section('Listing Information'),
fundDocuments: section('Fund Documents') || section('Documents')
},
dataBlocks,
quarterEndPerformance: tables.filter(table => table.kind === 'quarterEndPerformance'),
topHoldings: tables.filter(table => table.kind === 'holdings'),
tables,
fundDocuments,
pdfLinks
};
})();Params
| Param | What it does | Example value |
|---|---|---|
product-slug | Brookstone ETF product-page path | brookstone-yield-etf |
product-url | Full product URL; use instead of constructing the slug URL | https://www.brookstoneam.com/brookstone-yield-etf |
Possible Friction Points
- Metadata, performance, or holdings are empty after
h2appears → wait an additional 3000 ms before extraction. - Holdings section is labeled
All Holdings→ usetopHoldingsor matchingtablesentries; do not require aTop Holdingsheading. - Documents section is labeled
Documents→ use the extractor's heading fallback andfundDocuments; usepdfLinksfor every PDF on the page. ObjectiveorListing Informationheadings are absent → treat those section values as null and usedataBlocks,fields, andtables.- Caller supplies only a product name without a slug or URL → resolve the product link from Brookstone navigation before constructing the direct URL; do not invent a slug.
- Navigation exposes Arlington Asset Management or other-domain links → remain on the Brookstone product URL and do not substitute another domain.
Example Result
{
"url": "https://example.com/fund",
"title": "Example Title",
"ticker": "EXMP",
"fields": {
"Ticker": "EXMP"
},
"sections": {
"objective": "Example investment objective",
"etfData": "ETF Data Ticker EXMP",
"listingInformation": "Example listing information",
"fundDocuments": "Fund Documents"
},
"dataBlocks": [
{
"className": "data-block-2col",
"text": "Example data"
}
],
"quarterEndPerformance": [],
"topHoldings": [],
"tables": [
{
"index": 0,
"heading": "ETF Data",
"className": "example-table",
"rows": [
[
"Ticker",
"EXMP"
]
],
"context": "ETF Data Ticker EXMP",
"kind": "etfData"
}
],
"fundDocuments": [
{
"text": "Example prospectus",
"href": "https://example.com/document.pdf"
}
],
"pdfLinks": [
{
"text": "Example prospectus",
"href": "https://example.com/document.pdf"
}
]
}