This repo scrapes, normalises, and validates historic English league tables from Wikipedia so the resulting JSON can be embedded in other projects or visualisations.
- A supported Wikipedia scraping workflow that can resume after interruptions.
- Utilities to merge overlapping sources, verify season integrity, and minify the resulting datasets.
- Source-backed club identity metadata for active, historical, defunct, merged, relocated, and phoenix/successor cases.
- Jest unit + integration tests focused on the active Wikipedia pipeline.
wikipedia/is the actively supported ingestion path.rsssf/,scripts/csv-data/, and older reference exports should be treated as legacy tooling unless you are intentionally doing archive work. See RSSSF legacy tooling.- The overview scraper (
node wikipedia/cli/index.js overview) is now the primary maintained Wikipedia dataset flow across the full historical range. - The promotion/relegation scraper (
node wikipedia/cli/index.js build) remains available as a legacy/historical fallback for classic Football League season pages.
- Node.js
>= 20 pnpm >= 8(declared viapackageManager)- macOS/Linux shell or Windows WSL for the scraping scripts
Install dependencies once:
pnpm iThis repo keeps footy-data-kit-specific Codex guidance in tracked steering files and uses local AI Central symlinks for shared skills and reusable steering.
Tracked files:
AGENTS.md.codex/steering/README.md.codex/steering/repository-steering.md.codex/steering/data-contracts-steering.md.codex/steering/pipeline-testing-steering.md.codex/steering/javascript-steering.md
Ignored local links:
.codex/skills/.codex/steering/frontend-design-steering.md.codex/steering/javascript-esm-steering.md.codex/steering/testing-quality-gates-steering.md
Refresh local AI Central links with:
pnpm codex:linksBy default the script looks for AI Central at ../ai-central/templates. Set AI_CENTRAL_HOME if your checkout lives elsewhere.
- Generate Wikipedia data
# Primary maintained flow: overview parser across the full supported range pnpm -s wiki:build:overview - Merge and normalise
pnpm -s wiki:build:combined pnpm -s wiki:club-seed pnpm -s wiki:club-assets
- Validate and test
pnpm -s verify:data pnpm test:integration
- Minify for distribution (optional)
pnpm -s wiki:minify:combined pnpm -s wiki:minify:overview
All commands are resumable. If you stop a scraper with Ctrl+C, progress written to data-output stays intact.
The default maintained dataset workflow now uses the overview parser end to end. The promotion scraper is still useful for legacy comparison work and fixture repair, but it is no longer the main checked-in data path.
# Setup Repo, Install Deps
pnpm i
# Generate Data
pnpm -s wiki:build:overview
# Combine data into all-seasons file and regenerate club metadata
pnpm -s wiki:build:combined
pnpm -s wiki:club-seed
# Enrich the club metadata sidecar with crest asset candidates
pnpm -s wiki:club-assets
# Verify generated data plus club historical-reason metadata
pnpm -s verify:data
pnpm test:integration
# If all is good, finally minify data ready for external use
pnpm -s wiki:minify:combined
pnpm -s wiki:minify:overviewWhen data-output/wiki_promotion_relegations_by_season.json needs to be refreshed for historical comparison or legacy fixture coverage, rebuild it from code instead of patching individual seasons by hand:
pnpm wiki:build:promotion
pnpm wiki:minify:promotion
pnpm test:integration:promotionFor a single-season repair while preserving the checked-in dataset shape, use the same command with a narrow range and keep --ignore-war-years enabled. Example for the 1919-20 edge season:
node wikipedia/cli/index.js build --start 1919 --end 1919 --output ./data-output --force-update --ignore-war-years
node scripts/minify-json.js ./data-output/wiki_promotion_relegations_by_season.json
pnpm test:integration:promotiondata/– generated sidecar club metadata, review artifacts, raw reference files, and one-off exports.data-output/– canonical Wikipedia JSON outputs grouped by source.docs/roadmap.md– current release roadmap, v1 definition of done, and post-v1 feature plan.schemas/– JSON Schema Draft-07 contracts for the published season dataset and club metadata sidecar.scripts/– helper utilities such asminify-json.jsplus older one-off generators.wikipedia/– the main scraper, parsers, and FootballData models.rsssf/– legacy RSSSF parsing experiments. See RSSSF legacy tooling.shared/,club_names.json– shared helpers and canonicalised club naming.data/club-metadata.json– generated sidecar club metadata derived from the FootballData season outputs.
Run node wikipedia/cli/index.js <command> [options] to build FootballData-format JSON directly from Wikipedia tables.
| Command | Purpose | Default output |
|---|---|---|
build |
Legacy/historical promotion-relegation scraper for classic Football League season pages, mainly Tier 1 and Tier 2. | data-output/wiki_promotion_relegations_by_season.json |
overview |
Primary maintained parser. Reads overview pages (e.g. “2015–16 in English football”) and captures all listed tiers. | data-output/wiki_overview_tables_by_season.json |
combined |
Legacy bridge command: run build first, then backfill missing seasons with overview. |
Both files above, reusing the same --output directory. |
Common flags across commands:
| Flag | Default | Description |
|---|---|---|
-s, --start <year> |
varies | First season (inclusive). |
-e, --end <year> |
varies | Final season (inclusive). |
-o, --output <dir> |
./data-output |
Directory that will contain the JSON file(s). |
-u, --update-only |
false |
Skip seasons that already contain data on disk. |
-f, --force-update |
false |
Ignore cached entries and rebuild everything. |
--ignore-war-years |
false |
Skip WWI/WWII suspension years entirely. |
--include-war-placeholders |
false |
Emit metadata-only wartime placeholder seasons in overview output. |
Each run saves season-by-season progress immediately, so reruns are fast. The combined command exists for legacy mixed-source rebuilds, but the checked-in maintained path is now overview.
Tip: for the checked-in overview dataset we now run
overviewacross the full supported range. Keepbuildaround for legacy comparisons, targeted fixture repair, and classic-season parser regressions.
wikipedia/data/combine-output-files.js– merge multiple FootballData JSON files, drop war-year placeholders, prefer the richest tier record for each season, and show a grouped “missing seasons” summary. Use--include-emptyto keep placeholder entries,--compactfor minified JSON, and repeat--club-metadata <file>to merge sidecar club metadata.wikipedia/data/generate-club-metadata-seed.js– derive the club metadata sidecar from an existing FootballData JSON file plus curated source-backed lifecycle rules.wikipedia/data/generate-club-assets.js– discover and verify club crest candidates for the club metadata sidecar. Usepnpm -s wiki:club-assetsfor the default command; seedocs/club-assets.mdfor cache, review, licensing workflow, and consumer filtering guidance.wikipedia/data/verify-club-continuity.js– verify club metadata continuity and historical status reasons. Usepnpm -s wiki:club-historical-auditto write the repo review artifact atdata/club-historical-reason-audit.json; usepnpm -s wiki:club-historical-audit:checkorpnpm -s verify:datafor fail-on-issues checks.wikipedia/data/compare-football-data.js– compare two FootballData JSON files and report season, tier, table, outcome-list, and metadata changes between releases. Pass--jsonfor machine-readable output. Pass--markdownfor a release-note-friendly summary.wikipedia/data/build-release-notes.js– combine a curated note fromdocs/release-notes/vX.Y.Z.mdwith generated dataset counts, club metadata counts, validation notes, and the release diff. The release workflow publishes this asrelease-notes.mdand uses it as the GitHub release body.wikipedia/data/verify-json-schemas.js– validate generated data files against the JSON Schema contracts inschemas/. Run withpnpm -s schema:verify; this is also included inpnpm -s verify:data.scripts/minify-json.js– shrink JSON files in place or alongside (foo.min.json) so they are ready for publishing.wikipedia/data/verify-football-data.js– lint FootballData exports for empty tiers, duplicate teams, stat mismatches, or promotion/relegation inconsistencies. Pass--fail-on-issuesto exit non-zero when anomalies exist.
- The main season contract is the merged file:
data-output/all-seasons.json. - Club identity data is published separately as the sidecar file
data/club-metadata.json. - JSON Schema contracts live in
schemas/and are rendered into the docs site underdocs/schema/. - Every FootballData export may include a top-level
metadataobject with release provenance:schemaVersiongeneratorgeneratedAtgitShasourceFilesbuildOptions
- The club metadata sidecar has a top-level
clubsmap keyed by canonical club key. Club records are split into consumer-facing identity/status fields, source-backedhistory, and generatedderivedobservations:clubId– URL-safe unique slug for the club identity, such asmanchester-unitedcanonicalNamestatus.current– small status label such asactiveorunknownstatus.trackedFromSeasonstatus.trackedToSeasonstatus.hasUnexplainedGapshistory.nameHistory[]for source-backed name periodshistory.lifecycleEvents[]for events such asrenamed,merged,dissolved,not-re-elected, orphoenixhistory.trackedMembership[]for the season span where the club is expected in the tracked datasethistory.absenceExplanations[]for expected absences using broad reason codes such asofficial-competition-paused,outside-tracked-coverage,club-inactive,club-dissolved,club-reformed, orunknownderived.aliasesderived.identitySources[]with source URLs for curated identity/rename decisionsderived.relationships[]for sourced non-alias links such as phoenix clubs, mergers, relocations, and supporter-founded clubsderived.observedNames[]with exactrawName, cleanednormalizedName, observed seasons, and observed tiersderived.observedNamePeriodsderived.firstSeenSeasonderived.lastSeenSeasonderived.seasonsSeenderived.tiersSeenderived.tierSeasonsderived.coverageGapsassets.crestwith optional crest image candidates, license metadata, verification flags, and a preferred candidate when a usable or generated-placeholder asset is available
derived.coverageGapsmeans gaps in this dataset's observed league-table coverage, not confirmed inactivity or a financial interruption.historyis reserved for source-backed facts and explanations. Generated observations should stay underderived.assetsstores image URLs and provenance only. It does not embed or download image binaries. Crest candidates may beusable,placeholder,restricted,needs-review,needs-more-research, orfailed; seedocs/club-assets.mdfor status semantics, manual review rules, and when consumers should filter restricted club marks.clubIdis additive; season table rows still expose the scraped/canonicalteamtext and do not yet embedclubId.- Each season contains a
seasonInfosummary object plus one or moretierNobjects. seasonInfois not a league table. It is a season-level summary that currently stores:seasonpromotedrelegated- source metadata such as
seasonSlug,sourceUrl, ortableCount
seasonInfo.promotedmeans clubs moving into the top flight for the following season.seasonInfo.relegatedmeans clubs leaving the top flight at the end of that season.- Every
tierNentry is an object withseason,table,promoted, andrelegated. - Every
tierNentry now carries a singlemetadataobject:sourcesourceUrlseasonSlugleagueIdtitletableIndextableCounttierKey
# Build the maintained merged dataset from the overview export
pnpm -s wiki:build:combined
# Build sidecar club metadata
pnpm -s wiki:club-seed
# Discover club crest asset candidates and write data/club-assets-review.json
pnpm -s wiki:club-assets
# Run the data lint pass and historical club-reason check
pnpm -s verify:data
# Run only the JSON Schema drift check
pnpm -s schema:verify
# Write the historical club-reason audit review artifact
pnpm -s wiki:club-historical-audit
# Compare a previous release file against a freshly generated one
node wikipedia/data/compare-football-data.js ./releases/all-seasons-prev.json ./data-output/all-seasons.json
# Generate a markdown release summary
node wikipedia/data/compare-football-data.js --markdown ./releases/all-seasons-prev.json ./data-output/all-seasons.json
# Build the final user-facing release notes body
node wikipedia/data/build-release-notes.js \
--tag v0.8.2 \
--diff ./data-output/release-diff.json \
--current ./data-output/all-seasons.json \
--club-metadata ./data/club-metadata.json \
--manual ./docs/release-notes/v0.8.2.md \
--output ./data-output/release-notes.md
# Minify the merged dataset next to its original (writes all-seasons.min.json)
node scripts/minify-json.js ./data-output/all-seasons.jsonEach release can include a short curated note at docs/release-notes/vX.Y.Z.md. Keep that file focused on user-facing changes: what improved, whether schemas changed, and anything consumers should watch for.
The GitHub release body is generated from that curated note plus release facts from the rebuilt data files. The generated body includes coverage counts, club metadata counts, validation checks, published asset names, and the compact data diff. The raw diff remains attached as release-diff.json and release-diff.md.
Run the full Jest suite (unit + lightweight parsing checks):
pnpm testTarget just the integration suite (which exercises the supported Wikipedia scrapers end-to-end) when validating new data runs:
pnpm test:integration
pnpm test:integration:overview # primary maintained Wikipedia fixtures
pnpm test:integration:promotion # legacy promotion/relegation fixturesEstimate integration fixture breadth and tagged scenario coverage without fetching live pages:
pnpm integration:coverageCoverage is available via:
pnpm test:coverageEvery script sets NODE_OPTIONS=--experimental-vm-modules automatically so Jest can execute the ESM codebase without extra configuration.
- Keep output directories around; the CLIs skip existing seasons unless
--force-updateis provided, which significantly cuts rerun time. club_names.jsoncontains canonical spellings that the scrapers rely on when reconciling seasonal data – update it before running the cleaners if you expect new clubs to appear.- Extend
wikipedia/builders/parse-season-pages.jsorwikipedia/builders/parse-ext-season-overview-pages.jsif you need extra metadata (attendance, form, etc.); the FootballData schema is intentionally flexible.