Build an ETF comparison website with a page per ISIN
Comparison sites help investors weigh ETFs side by side: what each one tracks, what it costs, how big it is and how it has behaved. The best ones have a page per ISIN and a comparison view for any pair. This page covers the data model, the SEO structure and how to keep thousands of pages current from a single API.The problem
An ETF comparison site is a content site with a data problem. Readers arrive from a search for a specific ETF or a specific pairing ("A vs B"), and they expect the page to show current facts: TER, fund size, structure, distribution policy, index tracked, top holdings and recent performance. Multiply that by the number of UCITS ETFs in Europe and by the number of pairs people search for, and hand-maintained pages are impossible.
The second problem is trust. A comparison page that shows stale fees or a holdings table from eighteen months ago is worse than no page at all.
What you need from the data
Per ETF:
- Name, ISIN, share class, currency and distribution policy
- Index tracked (benchmark name) and structure
- TER, fund size, inception date
- Top holdings, sector and country breakdowns
- Calendar returns, cumulative returns and an indexed series for a chart
- Risk rating and volatility
- As-of date
For the comparison view you need the same fields for both ETFs and a consistent layout so differences are readable at a glance.
How it works with FundFacts API
Use static generation with periodic revalidation. Each ETF page and each comparison page is built from cached data objects; a scheduled job re-loads ISINs when their expiresAt passes and triggers a rebuild of the affected pages.
typescriptconst HEADERS = { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` };async function fund(isin: string) {const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, { headers: HEADERS });if (!res.ok) throw new Error(`${res.status} loading ${isin}`);return res.json();}function pick(f: any) {return {isin: f.isin as string,name: f.name as string,index: f.data.benchmarkName,structure: f.data.structure,ter: f.data.headlineMetrics?.ter,aum: f.data.headlineMetrics?.aum,distribution: f.data.keyFacts?.distribution,inception: f.data.keyFacts?.inception,riskRating: f.data.riskRating,holdings: f.data.keyFacts?.holdings,oneYear: f.data.cumulativePerformance?.["1y"],dataAsOf: f.data.dataAsOf,};}export async function comparison(a: string, b: string) {const [fa, fb] = await Promise.all([fund(a), fund(b)]);return { left: pick(fa), right: pick(fb) };}// comparison("IE00B4L5Y983", "IE00BJ0KDQ92") -> two MSCI World trackers side by side
Render left and right as two columns of the same rows. Where both funds track the same index, the interesting rows are TER, size, structure and distribution; where they track different indices, lead with the benchmark and the exposure breakdowns.
Design notes
SEO structure. One canonical URL per ISIN and one per pair, with the alphabetically smaller ISIN first so "A vs B" and "B vs A" resolve to the same page. Put the fund name, ISIN and as-of date in the title and first paragraph. The ISIN guide explains why the share-class ISIN, not the ticker, should be the key.
Caching. The API refreshes every 24 hours. Persist each response with expiresAt, rebuild pages from the store and never call the API during a page request. A site covering a few thousand ISINs needs a Scale or Enterprise plan for a full daily refresh; alternatively refresh popular pages daily and the long tail weekly.
Quota and errors. Run the refresh as a queue that respects X-RateLimit-Remaining and pauses on 429. Cold ISINs take one to three minutes on first load, so build the initial catalogue over several days rather than in one burst. A 404 fund_not_found means the page should be removed or marked as unavailable; a 502 upstream_error means keep the previous version and retry later.
Editorial content. The API provides facts; your value is the explanation around them. Write about what an index covers, what a replication method means, why TER differs from total cost of ownership. Keep hand-written text and API data in separate blocks so a data refresh never overwrites prose.
Disclaimers. Show dataAsOf on every page, state that figures are read from the documents each fund house publishes, and include the standard wording that this is not investment advice and past performance is not a guide to future returns. Avoid "best ETF" framing; present differences and let the reader decide.
The fund comparison tool guide walks through the comparison table in more detail.