Automated analysis and visualization of top-performing Elder Scrolls Online trial builds from ESO Logs.
Live Site: esobuild.com
ESO Build-O-Rama scans the top-ranked ESO Logs trial reports, identifies common builds among elite players, and generates static web pages showcasing these builds. Each trial is scanned independently on a staggered schedule to manage API load and ensure fresh data.
A build is featured if it appears frequently in the top 10 ranked logs for a specific boss:
- DPS Builds: 5 or more occurrences
- Tank/Healer Builds: 3 or more occurrences
A "build" consists of:
- Three Subclasses: Skill lines identified from ability usage patterns
- Two 5-Piece Gear Sets: The dominant armor/weapon sets
- Analyzes top 10 ESO Logs reports per boss encounter
- Consolidates identical builds across multiple reports
- Deduplicates same character appearing in multiple reports
- Applies role-based occurrence thresholds
- Extracts per-player mundus stones via source-filtered buff queries
- Gear (all equipment pieces with traits and enchantments)
- Abilities (both skill bars including ultimates)
- Subclasses (detected from skill usage patterns)
- Gear Sets (5-piece, 2-piece, and arena weapons)
- Mundus Stones (from buff uptime data)
- Performance metrics (DPS, rankings, report links)
- Occurrence frequency across top logs
- Desktop: Full-width tables with sortable columns
- Mobile: Card-based layout for screens ≤768px width
- Responsive design with proper touch targets
- Ability icons and visual set indicators
- Direct links to source ESO Logs reports
- Interactive Gear Tooltips: Hover over gear sets and mundus stones for detailed information
- Powered by ESO-Hub's external tooltip system
- Shows set bonuses, stats, and acquisition info
- Works on all gear set names, mundus stones
- Links open detailed ESO-Hub pages
- WCAG 2.1 Level AA compliant for screen reader users
- Skip-to-main-content link for keyboard navigation
- ARIA landmarks and semantic HTML throughout
- Proper table headers, captions, and scope attributes
- Meaningful alt text for all images
- High-contrast focus indicators for keyboard navigation
- Accessible breadcrumb navigation
- Screen reader text for visual indicators (role emojis)
- See docs/accessibility.md for full details
- Advanced Caching System: 95% cache effectiveness reduces API calls by 97%
- Organized cache structure with subdirectories (buffs, tables, rankings, reports)
- Persistent cache across runs (46+ MB cache with 1,500+ files)
- Cache performance monitoring with hit/miss tracking
- See docs/cache_system.md for full documentation
- Incremental data storage between scans
- Staggered trial scanning (one trial every hour)
- Rate limit compliance (2-second delays, automatic retry)
- Static HTML generation for fast page loads
- Comprehensive SEO Implementation for better search engine discoverability
- Automatically generated sitemap.xml with all pages
- robots.txt for crawler guidance
- Structured data (Schema.org JSON-LD) on all pages
- Canonical URLs to prevent duplicate content issues
- Keyword-rich meta descriptions tailored to each page
- BreadcrumbList schema for navigation context
- HowTo schema on build pages for rich results
- WebSite schema with Elder Scrolls Online context
- GitHub Actions for automated scanning
- Staggered schedule (each trial updated every 14 hours)
- GitHub Pages hosting with CDN distribution
- Branch isolation (develop for testing, main for production)
- Python 3.9 or higher
- ESO Logs API credentials from esologs.com/api/clients
- Git
# Clone repository
git clone https://github.com/brainsnorkel/eso-build-o-rama.git
cd eso-build-o-rama
# Install dependencies
pip install -r requirements.txt
# Configure API credentials (add to shell profile)
export ESOLOGS_ID="your_client_id"
export ESOLOGS_SECRET="your_client_secret"
source ~/.zshrc # or ~/.bashrcMain Branch (main):
- Production code
- Outputs to
output/directory - Triggers GitHub Actions on push
- Deploys to esobuild.com
Development Branch (develop):
- Feature development and testing
- Outputs to
output-dev/directory (gitignored) - Does not trigger GitHub Actions
- Safe for local testing
# Scan all trials
python -m src.eso_build_o_rama.main
# Scan specific trial by ID
python -m src.eso_build_o_rama.main --trial-id 15 # Rockgrove
# Scan specific trial by name
python -m src.eso_build_o_rama.main --trial "Dreadsail Reef"
# Test mode (first trial only)
python -m src.eso_build_o_rama.main --test# View cache statistics
python -m src.eso_build_o_rama.main --cache-stats
# Clear cache before scan
python -m src.eso_build_o_rama.main --trial-id 17 --clear-cache
# Disable caching (force API calls)
python -m src.eso_build_o_rama.main --trial-id 17 --no-cache
# Migrate existing cache files to new structure
python utils/migrate_cache.py --dry-run # Preview changes
python utils/migrate_cache.py # Perform migrationNote: See docs/cache_system.md for comprehensive cache documentation.
# Trigger specific trial
gh workflow run "Generate ESO Builds (Staggered)" -f trial_id=15
# Check status
gh run list --workflow="Generate ESO Builds (Staggered)" --limit 5
# View logs
gh run view <run_id> --log# On develop branch
git checkout develop
python -m src.eso_build_o_rama.main --trial-id 1
# Serve locally
cd output-dev
python3 -m http.server 8080 &
ngrok http 8080| ID | Trial Name | Abbr |
|---|---|---|
| 1 | Aetherian Archive | AA |
| 2 | Hel Ra Citadel | HRC |
| 3 | Sanctum Ophidia | SO |
| 5 | Maw of Lorkhaj | MoL |
| 6 | The Halls of Fabrication | HoF |
| 7 | Asylum Sanctorium | AS |
| 8 | Cloudrest | CR |
| 12 | Sunspire | SS |
| 14 | Kyne's Aegis | KA |
| 15 | Rockgrove | RG |
| 16 | Dreadsail Reef | DSR |
| 17 | Sanity's Edge | SE |
| 18 | Lucent Citadel | LC |
| 19 | Ossein Cage | OC |
GitHub Actions (Staggered Schedule)
|
v
ESO Build-O-Rama Application
|
+-- API Client (GraphQL)
+-- Data Parser (Gear, Abilities)
+-- Build Analyzer (Consolidation)
+-- Trial Scanner (Orchestration)
+-- Cache Manager (Response Cache)
+-- Data Store (builds.json)
+-- Page Generator (HTML Templates)
|
v
GitHub Pages Deployment (esobuild.com)
- Fetch rankings from ESO Logs API
- Download top 10 reports per boss
- Parse gear, abilities, and performance data
- Detect subclasses from ability usage
- Identify gear sets from equipped items
- Query buff table for per-player mundus stones
- Consolidate identical builds across reports
- Apply role-based occurrence thresholds
- Save trial data to builds.json
- Generate HTML from templates
- Deploy to GitHub Pages
Build Identification:
# Normalize build for comparison
slug = sorted(subclasses) + sorted(top_two_sets)
slug = slug.lower().replace(' ', '-').replace("'", "")Build Consolidation:
- Group by (trial, boss, build_slug)
- Merge all players with matching build
- Count unique reports (not total players)
- Select highest DPS as representative player
- Preserve mundus from any instance of same character
Subclass Detection:
- Count ability casts per skill line
- Weight ultimates more heavily
- Select top 3 skill lines by usage
- Normalize to abbreviated names
Authentication: OAuth 2.0
API Version: v2
Primary Queries:
worldData.zones- Trial and encounter IDscharacterRankings(leaderboard: LogsOnly)- Top-ranked players per bossreportData.report- Full report datareportData.report.table(dataType: Summary)- Player performance and gearreportData.report.table(dataType: Buffs, sourceID: X)- Per-player buff uptime
Rate Limiting:
- API limit: 18,000 points/hour
- Report queries: 5-10 points each
- Request delay: 2 seconds minimum
- Retry attempts: 3 with exponential backoff (120s, 240s, 360s)
- Typical scan: ~200 points (well under limit)
- Location:
cache/directory (gitignored, persistent) - Duration: Indefinite (manual clear only)
- Effectiveness: 95-97% cache hit rate
- Key Format:
{type}_{parameters_hash}
Cache Types:
- Zone/encounter data
- Rankings
- Report data
- Player buffs
Every build generation automatically creates sitemap.xml with:
- All build pages, trial pages, and home page
- Last modified dates
- Change frequency hints for crawlers
- Priority values (1.0 for home, 0.8 for trials, 0.6 for builds)
Automatically generated robots.txt file:
- Allows all search engine crawlers
- Points to sitemap.xml location
- Disallows crawling of cache directory
JSON-LD structured data on all pages:
- Home page: WebSite schema with Elder Scrolls Online context
- Trial pages: BreadcrumbList for navigation hierarchy
- Build pages: HowTo schema with gear supplies and ability steps, plus BreadcrumbList
Comprehensive meta tags on every page:
- Unique, keyword-rich meta descriptions
- Keywords meta tag with relevant ESO search terms
- Canonical URLs to prevent duplicate content
- Open Graph and Twitter Card tags for social sharing
After deploying to production:
Google Search Console:
- Visit Google Search Console
- Add property:
esobuild.com - Verify ownership (DNS TXT record or HTML file)
- Submit sitemap:
https://esobuild.com/sitemap.xml - Monitor index coverage and search performance
Bing Webmaster Tools:
- Visit Bing Webmaster Tools
- Add site:
esobuild.com - Verify ownership
- Submit sitemap:
https://esobuild.com/sitemap.xml
Validation Tools:
- Google Rich Results Test - Test structured data
- Schema.org Validator - Validate JSON-LD
- XML Sitemap Validator - Check sitemap
base.html- Base template with header, footer, responsive CSS, structured dataindex_page.html- Home page listing all trials and buildstrial.html- Per-trial page with boss-grouped buildsbuild_page.html- Detailed build page
- Desktop (≥1024px): Full tables, all columns visible
- Tablet (769-1023px): Tables with reduced padding
- Mobile (≤768px): Card-based layout with labeled fields
Tables convert to vertical cards on mobile:
- Each row becomes a standalone card
- Labels appear on left (40% width)
- Data appears on right (60% width)
- Touch-friendly links with padding
- No horizontal scrolling required
Workflow runs every hour, cycling through 14 trials. Each trial updates approximately every 14 hours.
Reference time: 20:00 UTC (8:00 PM UTC)
Index 0 (20:00 UTC) → Aetherian Archive
Index 1 (21:00 UTC) → Hel Ra Citadel
Index 2 (22:00 UTC) → Sanctum Ophidia
Index 3 (23:00 UTC) → Maw of Lorkhaj
Index 4 (00:00 UTC) → Halls of Fabrication
Index 5 (01:00 UTC) → Asylum Sanctorium
Index 6 (02:00 UTC) → Cloudrest
Index 7 (03:00 UTC) → Sunspire
Index 8 (04:00 UTC) → Kyne's Aegis
Index 9 (05:00 UTC) → Rockgrove
Index 10 (06:00 UTC) → Dreadsail Reef
Index 11 (07:00 UTC) → Sanity's Edge
Index 12 (08:00 UTC) → Lucent Citadel
Index 13 (09:00 UTC) → Ossein Cage
- Schedule:
0 * * * *(every hour at :00) - Manual: Workflow dispatch with optional trial_id parameter
- Push: Main branch only (develop branch excluded)
- Determine which trial to scan based on current time
- Checkout latest code from main branch
- Install Python 3.9 and dependencies
- Download existing builds.json from live site
- Run scan for determined trial
- Copy static assets
- Upload artifact to GitHub Pages
- Deploy
Develop branch is prevented from deploying:
- Workflow triggers only on main branch
- Code checks for develop branch in GitHub Actions environment
- Exits with error if develop detected in CI/CD
Individual player's build in a specific fight.
Fields:
- Character and account names
- Player ID (API source ID)
- Class and role
- DPS and performance metrics
- Gear with traits and enchantments
- Abilities (two skill bars)
- Subclasses (detected from abilities)
- Equipped sets
- Mundus stone
- Report and fight references
Build appearing multiple times across reports.
Fields:
- Build slug (normalized identifier)
- Subclasses and gear sets
- Count (total players)
- Report count (unique reports)
- Best player (highest DPS)
- All players list
- Trial and boss information
Methods:
get_display_name()- Human-readable name with sorted subclassesget_sorted_sets()- Alphabetically sorted set listmeets_threshold()- Role-based occurrence check
Individual equipment piece.
Fields:
- Slot identifier
- Set name
- Item name
- Armor weight (L/M/H)
- Trait and trait ID
- Enchantment and enchant ID
- Item ID and quality
Mundus stones are identified through buff analysis:
- Query Buffs table filtered by player's source_id
- Match buff ability IDs against known mundus IDs (13940-13985)
- Verify high uptime (mundus buffs persist throughout fight)
- Map ability ID to mundus name
Mundus Ability IDs:
13940: The Warrior
13943: The Mage
13974: The Serpent
13975: The Thief
13976: The Lady
13977: The Steed
13978: The Lord
13979: The Apprentice
13980: The Ritual
13981: The Lover
13982: The Atronach
13984: The Shadow
13985: The Tower
eso-build-o-rama/
├── .github/workflows/
│ └── generate-builds.yml # CI/CD workflow
├── cache/ # API response cache (gitignored)
├── data/
│ ├── trials.json # Trial definitions
│ └── trial_bosses.json # Boss lists per trial
├── docs/ # Additional documentation
├── output/ # Production output (main)
│ └── builds.json # Persistent data (tracked)
├── output-dev/ # Development output (gitignored)
├── src/eso_build_o_rama/
│ ├── main.py # Application orchestrator
│ ├── api_client.py # ESO Logs API client
│ ├── trial_scanner.py # Scan orchestration
│ ├── data_parser.py # Parse API responses
│ ├── build_analyzer.py # Build identification
│ ├── subclass_analyzer.py # Skill line detection
│ ├── page_generator.py # HTML generation
│ ├── data_store.py # JSON persistence
│ ├── cache_manager.py # Response caching
│ └── models.py # Data structures
├── static/icons/ # Ability icons
├── templates/ # Jinja2 templates
├── tests/ # Test suite
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
└── requirements.txt
Required for API access:
export ESOLOGS_ID="your_client_id"
export ESOLOGS_SECRET="your_client_secret"data/trials.json
List of trials with IDs, names, and abbreviations.
data/trial_bosses.json
Maps trial names to boss encounters in encounter order.
.github/workflows/generate-builds.yml
GitHub Actions workflow configuration with scheduling, triggers, and deployment steps.
# Switch to develop branch
git checkout develop
# Make changes and test locally
python -m src.eso_build_o_rama.main --trial-id 1
# Verify output in output-dev/
ls output-dev/
# Commit changes
git add .
git commit -m "description"
git push origin develop
# Test with local server
cd output-dev
python3 -m http.server 8080 &
# When ready, merge to main
git checkout main
git merge develop
git push origin main| Branch | Output Dir | Git Tracked | Deployed |
|---|---|---|---|
| main | output/ |
builds.json only | Yes (GitHub Pages) |
| develop | output-dev/ |
No (gitignored) | No |
- Test on develop branch first
- Verify correct output directory
- Check logs for errors
- Test responsive layout (browser dev tools)
- Verify desktop layout unchanged
- Confirm data accuracy
- Review builds.json structure
- Merge to main when stable
This is expected for older trials with diverse meta. Top players in trials like Sanctum Ophidia or Hel Ra Citadel may use varied builds that don't meet the 5+/3+ threshold. These trials won't appear on the index page.
Indicates player had no mundus active during the fight or data parsing issue. Verify on ESO Logs directly and check application logs.
Solutions:
- Increase
min_request_delayin APIClient (default: 2.0s) - Avoid
--clear-cacheunless necessary - Wait 2-3 minutes between manual workflow triggers
- Automated schedule handles this automatically
When triggering multiple workflows manually, wait 2-3 minutes between them. Each workflow downloads builds.json from the live site. If workflows overlap, the later one may download stale data before the earlier deployment completes.
CDN caching on esobuild.com has 10-minute TTL (max-age=600). To see changes immediately:
- Hard refresh browser (Cmd/Ctrl + Shift + R)
- Wait 10 minutes for cache expiration
- Check deployment completed successfully
Typical Trial Scan:
- API calls: 10-15 new requests, 300+ cached
- Duration: 5-12 minutes per trial
- Data volume: 100-500KB per trial
- Page generation: Under 1 second
Resource Usage:
- API points: ~200 per scan (1% of hourly limit)
- Storage: ~5MB per trial in builds.json
- Cache size: Varies, typically 50-200MB
async def _retry_on_rate_limit(self, func, *args, **kwargs):
for attempt in range(self.max_retries):
await self._wait_for_rate_limit() # 2s minimum
try:
return await func(*args, **kwargs)
except RateLimitError:
delay = self.retry_delay * (2 ** attempt) # 120s, 240s, 360s
await asyncio.sleep(delay)Fetch Top Rankings:
query GetRankings($zoneID: Int!, $encounterID: Int!) {
characterRankings(
zoneID: $zoneID
encounterID: $encounterID
leaderboard: LogsOnly
)
}Fetch Player Buffs:
query GetBuffs($code: String!, $startTime: Float!, $endTime: Float!, $sourceID: Int!) {
reportData {
report(code: $code) {
table(
startTime: $startTime
endTime: $endTime
dataType: Buffs
sourceID: $sourceID
)
}
}
}- Type hints on all function signatures
- Docstrings for all public methods
- Logging at appropriate levels (INFO, WARNING, ERROR, DEBUG)
- Specific exception handling (KeyError, ValueError, TypeError)
- Async/await for all API operations
type: Brief description
Longer explanation if needed.
CHANGES:
- Specific change 1
- Specific change 2
RESULT:
- Outcome or effect
Types: feat, fix, docs, refactor, test, chore
Per-Player Mundus Detection
Fixed incorrect mundus stones by implementing source_id filtering in Buffs table queries.
Mobile Responsiveness
Added responsive CSS that converts tables to card layout on screens ≤768px.
UI Improvements
- Sorted subclasses alphabetically
- Updated page title to "BS ESO Meta Build Explorer"
- Accurate "About This Data" section
- Static generation timestamps
- Improved terminology
Development Environment
- Branch-based output directories
- GitHub Actions filtering (main only)
- Safety checks for develop branch
- output-dev/ gitignored
Build Consolidation
- Merge identical builds across reports
- Count unique reports
- Prevent duplicate players
See CHANGELOG.md for complete history.
This project uses data and resources from:
- ESO Logs - Combat log data and rankings API
- UESP.net - ESO reference data
- LibSets by Baertram - Gear set information
- esoskillbarbuilder by sheumais - Ability icons
- ESO-Log-Build-and-Buff-Summary - Subclass detection logic
- The Elder Scrolls Online - Game content, Copyright ZeniMax Media Inc.
MIT License - see LICENSE file for details.
Copyright 2025 Chris Gentle (@brainsnorkel)
This is a fan-made tool for the Elder Scrolls Online community. The Elder Scrolls Online and all related content are Copyright ZeniMax Media Inc. This project is not affiliated with or endorsed by ZeniMax Media Inc.
Report issues or request features: GitHub Issues