尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Every NFL contract and team salary cap in plain English, for everyday fans.
src.html is the page (also published as the PlainStats artifact). Edit this one.build.sh wraps src.html into index.html, which Vercel serves. On a phone, edit index.html directly in Koder and make the same change in src.html.data/nfl.json holds all 32 teams' cap figures and every player contract. data/moves.json is the NFL news feed (contract moves, injuries, games, coaching, league news) and the trade deadline date. A daily scheduled task refreshes both.data/season.json holds standings (record, points for and against), head coaches, 2025 records, the rookie and comeback-candidate lists, and prevRanks (last week's power ranking, for the movement arrows). It feeds the Awards and Power rankings page (#awards, #rankings), which scores everything in the browser from data/stats.json plus this file, so a data refresh updates the page.api/contracts.js reads the featured deal breakdowns from Airtable (optional).data/contracts.csv is the starting data for the Airtable Contracts table.Add AIRTABLE_TOKEN (a personal access token with data.records:read on the Cap Tracker base). The function already points at the Cap Tracker base and its deals table; set AIRTABLE_BASE_ID or AIRTABLE_TABLE only to use a different one. Without them the page shows its built-in breakdowns.
Every section, tab, team, player, prospect and game has its own clean URL. On plainstatsnfl.com (and localhost or vercel.app) they are real paths; vercel.json rewrites them to index.html and the page routes on location.pathname (ROUTES, pathOf(), keyOfPath(), go() in src.html). In the artifact preview the same keys use the #hash instead. Old /#key links are moved to their path on load.
| Page | URL | Page | URL |
|---|---|---|---|
| Home | / | Contracts | /contracts |
| Scores | /scores | Best and worst deals | /best-worst-contracts |
| Standings | /standings | Rookie deals | /rookie-contracts |
| Playoff projections | /projections | Free agency | /free-agency, /free-agency/key-dates |
| News | /news, /news/<contracts|injuries|games|coaching|league> | Cap lab | /cap-lab |
| Team ratings | /team-ratings | Big board, mock draft | /big-board, /mock-draft |
| Weekly team tiers | /team-tiers | ||
| Cap room | /cap-room | Rookie pay, draft guide | /rookie-pay, /draft-guide |
| Power rankings, awards | /power-rankings, /awards | Roster builder, trade checker, compare | /roster-builder, /trade-checker, /compare |
| Player ratings, season stats | /player-ratings, /stats | Cap 101, about | /cap-101, /about |
| A team | /team/<code> (e.g. /team/kc) | A game | /game/<id> (e.g. /game/2026-07-ne-chi) |
A player card opens at /players/<name> when the player has a static search page, else /player/<first-last>-<team>; a prospect at /draft/<name>, else /prospect/<id>. Typing those static URLs directly gives the static page, which links into the app. /players, /teams and /draft stay the static hubs, so no app path may use them. When adding a page: add it to ROUTES (and PTITLE, SPAGES for search) in src.html, to rewrites in vercel.json and to APP_URLS in tools/build_seo.py (sitemap). index.html has <base href="/"> (build.sh) so relative data/ URLs work on deep paths.
The home search (searchEverything() in src.html) searches pages, teams, players, prospects, this and last week's games, news headlines and Cap 101 terms.
python3 -I tools/build_seo.py turns the data files into crawlable pages: one for every player with a confirmed contract or 2026 stats, three per team (overview, cap space, dead money), one per draft prospect, and hub pages for teams, players and the draft. Each has its own title, description, plain-English summary, FAQ and schema markup (Person, SportsTeam, FAQPage, BreadcrumbList), and the script rewrites sitemap.xml and tools/seo-nav.html (the team links build.sh adds to the bottom of the home page). Players whose contract is still being verified get noindex and stay out of the sitemap until a named report confirms the deal. Run it after every data refresh, then ./build.sh, then commit.
To submit to Google: add https://www.plainstatsnfl.com as a property in Search Console, open Sitemaps and submit sitemap.xml.
Every generated page has its own link-preview image (1200x630: player face or team logo on the team color, the name, one big number) so links look right on X, iMessage, Reddit and Discord. The pages point og:image at /api/og?k=<players|teams|draft|pages>&s=<slug>&v=<data date>. tools/build_seo.py writes the text for each card to og-data/<kind>.json (so a contract number appears only when the page would show it, and unverified players lead with a real 2026 stat and "Contract being verified"). api/og.js is a Vercel function (Node runtime, no runtime: edge: the edge build of @vercel/og fails to compile its wasm) that looks the slug up, fetches the headshot and logo from ESPN's image CDN (the build sandbox can't reach it; if an image is missing the card shows initials), draws the card with @vercel/og and Inter from assets/fonts/, and the CDN caches it for a day. Anything that goes wrong redirects to the static og-image.png. Only the slug comes from the URL, so nobody can make the domain draw a card with their own text. robots.txt must keep Allow: /api/og: X, Facebook and Reddit obey it for the image.
Check a card's layout without deploying: node tools/preview_og.mjs OUT_DIR [IMAGE_DIR] then python3 -I tools/shot_og.py OUT_DIR. The final check is the live /api/og?... URL, and a page you want X or Facebook to re-read after a change needs their card validator or a new v= value (the build bumps it every refresh).
Each panel that holds a chart or table gets a "Share this" button. It copies the panel offscreen (the live page never changes), drops the controls, keeps the filters as a small "Filter: ..." line, caps tables at 20 rows with a note, adds a small PlainStats watermark and the data date, and photographs the copy with html-to-image (loaded from jsDelivr on first click only). A preview opens with Share (phones), Save image and Copy image. ESPN logos and headshots are copied in only if ESPN's CDN allows it from the browser; otherwise they are left out of the image rather than breaking it. tools/check_pages.py fails if the buttons stop appearing.
PlainStats does not copy Over The Cap or Spotrac. data/build_contracts.py builds every player row and tags where it comes from:
r reported: data/ledger.csv has one row per deal from a named news report or team announcement, with its URL. Blank fields stay unverified and the player page says so.f calculated: rookie contracts for the last four draft classes, from the draft slot with PlainStats' rookie scale (data/rookie_scale.py, anchored to real 2026 signings listed in data/cba.json) and the CBA minimum salaries, including the proven performance escalator, fifth-year options and trades. Undrafted rookies get the minimum.m league minimum and p practice squad: valued with the CBA tables in data/cba.json.u unverified: no sourced figure yet. The dollar fields are blank in nfl.json and the page says "Contract being verified". The daily refresh moves these into the ledger (starting with quarterbacks and the best-known players, using nflverse snap counts to choose), and data/build_market.py waits to rebuild the price ladder until most contracts are verified.python3 -I data/build_contracts.py <folder with nflverse roster_2026.csv, draft_picks.csv, snap_counts_2023-2025.csv>, then data/fix_ol.py, then data/build_market.py.data/years_calc.json: rookie-deal cap hits by season, still written by build_contracts.py but no longer shown anywhere. The site shows no season-by-season cap hits, since reports rarely publish them.data/freeagency.json: PlainStats cap projection (2026 cap grown 7% a year), the offseason calendar and official franchise tag prices once announced.Built by python3 -I data/build_olwin.py <play_by_play_2026.csv.gz> from nflverse play-by-play: pass protection (dropbacks with no sack or QB hit) and run success (designed runs with positive expected points). PlainStats no longer uses ESPN win rates or PFF figures.
/team-tiers plots every team's offense EPA per play against defense EPA allowed per play (team logos, top-right = good at both), groups the 32 teams into five tiers by net EPA per play, and lists the splits (pass, run, hit rate) in a filterable table. python3 -I data/build_tiers.py <play_by_play_2026.csv.gz> writes data/tiers.json from nflverse play-by-play (CC BY 4.0) through the last week in which every game in data/schedule.json is final, plus the rank change from the week before. The weekly refresh (Tuesday) runs it in step 4e, right after build_olwin.py.
The Draft page covers the 2027 class: a big board, a full page for each prospect (grade, role, trait badges, 2026 college stats, NFL players in the same role, rookie contract, team fits), the projected first-round order with a model mock, and the rookie pay scale. data/draft.json is built by python3 -I data/build_draft.py from the prospect list in that script (WalterFootball and NFL Draft Buzz board ranks, cfbstats ids) the season totals in data/draft_stats.txt and the ESPN athlete ids in data/draft_espn.txt (prospect headshots; school logos use the ESPN team id at the end of each SCHOOLS row, the same ESPN CDN images the NFL cards use); the script's docstring explains the format. The page computes everything else at load: the consensus order, grades, roles, traits, team needs from the 2026 roster, the draft order from data/season.json with traded picks applied, and rookie pay grown with the projected 2027 cap from data/freeagency.json.
data/stats.json (the player card's On the field tab and Players > ratings): built weekly by python3 -I data/build_stats.py <dir> <season> data/stats.json [last week] from nflverse stats_player_week, snap_counts, pfr_advstats and players.csv. Players are joined across files by id (players.csv links the PFR id in snap counts to the gsis id in box scores), never by name, and each row carries gid; the page attaches a row to a card by gid first and never gives one player another's line. Pass the last week when a week is still being played. data/build_contracts.py merges contract rows that share a gsis id (the old list and the roster can spell a name differently), keeping the roster's spelling.data/history.json: regular-season box scores for every player from 2023 to the current season, plus this season's game log, built by python3 -I data/build_history.py <dir> <season> from nflverse stats_player_week_<S>.csv files. data/drafted.json (the last six draft classes, from nflverse draft_picks.csv) is written by the same script. data/history_old.json holds season totals (no game logs) from 1999 up to the season before history.json starts, only for players on a current roster; python3 -I data/build_history_old.py <dir> builds it from nflverse stats_player_reg_<S>.csv and re-matches the roster, so new signings pick up earlier seasons, and the page loads it only when a player's Stats tab opens. Weekly game logs are kept for the current season only. The page does not load history.json or history_old.json (they are build inputs, kept out of the deployment by .vercelignore; tools/build_seo.py still reads history.json). Both build scripts end by running python3 -I data/build_hist_split.py, which writes the small files the page loads from data/hist/: meta.json, one s<year>.json per season (totals, for Players > Season stats) and 64 shards p<NN>.json (a player's every season plus this year's game log, about 18 KB each, for the Stats tab). Players are keyed by gsis id, so a player traded mid-season stays one record (season line t = team with most games, tt = all teams). The page matches contract players to stats by gid (row index 19 of data/nfl.json). The files load only when someone opens the Season stats tab or a player's Stats tab. Players > Season stats also has a week range, a minimum-snaps qualifier and a garbage-time toggle (current season only). They read data/hist/wk<season>.json, written by build_hist_split.py from history.json plus , which builds from nflverse , and . Garbage time = offense win probability under 5% or over 95% (rbsdm's cut-off), split out of passing, rushing and receiving stats only. Run before after each weekly refresh; is kept out of the deployment.Every "Suggest a fix" link (the app footer, player sheets, and the footer of each generated page) can open a Tally or Google Form instead of an email. Set const FIX_FORM = '...' in src.html (one line, read by tools/build_seo.py too); {page}, {about} and {kind} in the address are filled in with the page address, the player or team and the fix type. Tally: add hidden fields named page and about. Google Forms: use the "Get pre-filled link" address with {page} as the value. Empty keeps the old email form. When it is set, update the "Suggest a fix" paragraph in privacy.html (it says the form only opens an email app), then rerun python3 -I tools/build_seo.py and ./build.sh.
data/schedule.json: every regular-season game with kickoff (Eastern) and final score, from nflverse schedules/games.csv. Built by python3 -I data/build_schedule.py <games.csv> 2026; the daily refresh runs it so scores stay current.data/projections.json: standings (records, division and conference records, streaks, seeds if the season ended today) and the PlainStats game model, built by python3 -I data/build_projections.py <games.csv> 2026 in the weekly refresh. The model is an Elo rating from every game since 1999 (K 20, home field 40, 40% regression to the mean each offseason), win chance and projected score for every game (played games keep their pre-kickoff pick), and 10,000 simulated seasons with simplified NFL tiebreakers and playoffs. It never uses betting lines. Backtest: about 65% of winners picked, 2012 to 2025.data/gameday.json: this week's games with the NFL's official injury report (nflverse injuries/injuries_<season>.csv, CC BY 4.0), stadium roof and location, and the model's pick adjusted for injuries. Built daily by python3 -I data/build_gameday.py <games.csv> <injuries.csv> 2026 after build_projections.py has run at least once. Each listed player counts by his chance of missing the game (Out 1, Doubtful 0.85, Questionable 0.3; practice-only listings 0.35 or 0.1) x his position's worth in Elo points x his snap share, capped at 150 per team. Weather is added in the page from the US National Weather Service hourly forecast (api.weather.gov, US public domain, fetched by the viewer's browser) for open-air US stadiums: wind over 12 mph, likely rain or snow and 25°F or colder take points off the projected total, and high wind narrows the spread. Tapping a game in the current week opens its preview, team stats and injury report.data/splits.jsonpython3 -I data/build_splits.py <nflverse dir> <season>play_by_play_<S>.csv.gzsnap_counts_<S>.csvplayers.csvbuild_splits.pybuild_hist_split.pysplits.json