layout
default
title
Home
description
Browse curated Minecraft mods, resource packs, datapacks, modpacks and plugins.
lang
en
{% assign t = site.data[page.lang] %}
{{ t.featured_subtitle }}
{% if site.data.modpacks and site.data.modpacks.size > 0 %}
{% assign sorted_modpacks = site.data.modpacks | sort: 'date_modified' | reverse %}
{% for modpack in sorted_modpacks limit:3 %}
{{ t.featured_badge }}
{{ modpack.description | truncate: 100 }}
{{ modpack.downloads | default: 0 | divided_by: 1000 }}K
{{ modpack.followers | default: 0 }}
{% if modpack.game_versions and modpack.game_versions.size > 0 %}
{{ modpack.game_versions[0] }}
{% endif %}
{% endfor %}
{% endif %}
{% include modpacks.html %}
{{ t.stats_subtitle }}
{{ t.stat_total_projects }}
—
{{ t.stat_loading }}
{{ t.stat_total_downloads }}
—
{{ t.stat_loading }}
{{ t.stat_supported_loaders }}
—
{{ t.stat_loading }}
{{ t.stat_game_versions }}
—
{{ t.stat_loading }}
{{ t.stat_top_categories }}
{{ t.stat_loading }}
{{ t.stat_top_downloads }}
{{ t.stat_downloads_note }}
{% assign featured_docs = site.docs | sort: 'nav_order' | slice: 0, 3 %}
{% if featured_docs and featured_docs.size > 0 %}
Quick links to key guides
{% for doc in featured_docs %}
{% if doc.description %}
{{ doc.description }}
{% else %}
Read more
{% endif %}
Docs
{% endfor %}
{% endif %}
All projects are stored as static data. No database needed.
Daily syncs from Modrinth keep your showcase fresh.
Deployed globally on GitHub Pages for instant loading.
Modern design with smooth animations and blur effects.
Configure branding, fetch projects, and deploy.
Open Docs
Run npm run fetch or use the scheduled workflow.
See Setup
Track updates, file issues, or contribute.
View Repo
Stay updated on releases across all projects.
Go to Modrinth
<script>
// Load stats from cached mods.json and populate snapshot cards
document.addEventListener('DOMContentLoaded', async () => {
const modsUrl = '{{ '/data/mods.json' | relative_url }}';
const statProjects = document.getElementById('stat-projects');
const statProjectsSub = document.getElementById('stat-projects-sub');
const statDownloads = document.getElementById('stat-downloads');
const statDownloadsSub = document.getElementById('stat-downloads-sub');
const statLoaders = document.getElementById('stat-loaders');
const statLoadersCount = document.getElementById('stat-loaders-count');
const statVersions = document.getElementById('stat-versions');
const statVersionsSub = document.getElementById('stat-versions-sub');
const statCategories = document.getElementById('stat-categories');
const downloadsChart = document.getElementById('downloads-chart');
const downloadsNote = document.getElementById('stat-downloads-note');
try {
const res = await fetch(modsUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const mods = await res.json();
if (!Array.isArray(mods) || mods.length === 0) throw new Error('No projects found');
const loaders = new Set();
const versions = new Set();
const categories = new Map();
let totalDownloads = 0;
mods.forEach(m => {
(m.loaders || []).forEach(l => loaders.add(l));
(m.game_versions || []).forEach(v => versions.add(v));
totalDownloads += Number(m.downloads || 0);
(m.categories || []).forEach(c => {
categories.set(c, (categories.get(c) || 0) + 1);
});
(m.display_categories || []).forEach(c => {
categories.set(c, (categories.get(c) || 0) + 1);
});
});
const formatNumber = (n) => n.toLocaleString(undefined, { maximumFractionDigits: 0 });
const t = {
projects_catalog: '{{ t.stat_projects_catalog }}',
avg_per_project: '{{ t.stat_avg_per_project }}',
unique_versions: '{{ t.stat_unique_versions }}',
no_categories: '{{ t.stat_no_categories }}',
no_data: '{{ t.stat_no_data }}',
unavailable: '{{ t.stat_unavailable }}'
};
statProjects.textContent = formatNumber(mods.length);
statProjectsSub.textContent = t.projects_catalog;
statDownloads.textContent = formatNumber(totalDownloads);
const avgDownloads = mods.length ? Math.round(totalDownloads / mods.length) : 0;
statDownloadsSub.textContent = `${formatNumber(avgDownloads)} ${t.avg_per_project}`;
const loaderList = Array.from(loaders).sort();
const versionCount = versions.size;
statLoadersCount.textContent = loaderList.length ? formatNumber(loaderList.length) : '—';
statLoaders.textContent = loaderList.length ? loaderList.join(', ') : '—';
statVersions.textContent = versionCount ? formatNumber(versionCount) : '—';
statVersionsSub.textContent = versionCount ? t.unique_versions : '—';
const topCats = Array.from(categories.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([c, count]) => `${c} (${count}) `) || [];
statCategories.innerHTML = topCats.length ? topCats.join(' ') : t.no_categories;
// Top downloads multi-line chart (simulated week data)
const topDownloads = [...mods]
.filter(m => typeof m.downloads === 'number')
.sort((a, b) => (b.downloads || 0) - (a.downloads || 0))
.slice(0, 11);
if (topDownloads.length && downloadsChart) {
downloadsChart.innerHTML = '';
const colors = [
'#1bd96f', '#10a8e0', '#22d3ee', '#a78bfa', '#f472b6',
'#fb923c', '#fbbf24', '#4ade80', '#60a5fa', '#c084fc', '#f87171'
];
// Simulate week data with realistic curves (7 days)
const days = 7;
const maxValue = Math.max(...topDownloads.map(m => m.downloads || 0));
const chartWrapper = document.createElement('div');
chartWrapper.style.cssText = 'display:flex;gap:20px;align-items:stretch;margin-bottom:12px;';
// Left side: chart
const chartContainer = document.createElement('div');
chartContainer.style.cssText = 'flex:1;background:var(--bg-primary);border:1px solid var(--border);padding:16px;box-sizing:border-box;position:relative;';
const svgWrapper = document.createElement('div');
svgWrapper.style.cssText = 'width:100%;height:240px;position:relative;';
// Tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = 'position:absolute;background:rgba(0,0,0,0.8);color:white;border:1px solid var(--border);padding:8px 12px;border-radius:6px;font-size:12px;pointer-events:none;opacity:0;transition:opacity 0.2s;z-index:10;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.4);';
chartContainer.appendChild(tooltip);
// Use actual pixel dimensions
const width = 800; // Fixed width for consistent rendering
const height = 200; // Fixed height
const svg = document.createElementNS('http://www.w3.org/2000/svg ', 'svg');
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.setAttribute('preserveAspectRatio', 'none');
svg.style.cssText = 'width:100%;height:100%;display:block;';
topDownloads.forEach((mod, idx) => {
const color = colors[idx % colors.length];
const baseValue = mod.downloads || 0;
// Generate smooth curve with variation
const points = [];
for (let day = 0; day < days; day++) {
const x = (day / (days - 1)) * width;
const variance = Math.sin(day * 0.8 + idx) * 0.3 + Math.cos(day * 1.2 - idx) * 0.2;
const normalizedValue = (baseValue / maxValue) * (0.4 + variance * 0.6);
const y = height - Math.max(4, Math.min(height - 4, normalizedValue * (height * 0.9)));
const simulatedDownloads = Math.round(baseValue * normalizedValue);
points.push({ x, y, downloads: simulatedDownloads, day });
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', color);
path.setAttribute('stroke-width', '2');
path.setAttribute('opacity', '0.85');
path.style.cursor = 'pointer';
// Add dots at each data point
points.forEach(p => {
const circle = document.createElementNS('http://www.w3.org/2000/svg ', 'circle');
circle.setAttribute('cx', p.x);
circle.setAttribute('cy', p.y);
circle.setAttribute('r', '4.5');
circle.setAttribute('fill', color);
circle.setAttribute('opacity', '0.9');
circle.style.cursor = 'pointer';
svg.appendChild(circle);
});
// Hover effect
path.addEventListener('mouseenter', () => {
path.setAttribute('stroke-width', '3');
path.setAttribute('opacity', '1');
});
path.addEventListener('mouseleave', () => {
path.setAttribute('stroke-width', '2');
path.setAttribute('opacity', '0.85');
tooltip.style.opacity = '0';
});
path.addEventListener('mousemove', (e) => {
const rect = chartContainer.getBoundingClientRect();
const svgRect = svg.getBoundingClientRect();
const relX = ((e.clientX - svgRect.left) / svgRect.width) * width;
const closestPoint = points.reduce((prev, curr) =>
Math.abs(curr.x - relX) < Math.abs(prev.x - relX) ? curr : prev
);
tooltip.innerHTML = `${mod.title || mod.name}
Day ${closestPoint.day + 1}: ${formatNumber(closestPoint.downloads)} downloads
`;
tooltip.style.opacity = '1';
tooltip.style.left = Math.min(rect.width - tooltip.offsetWidth - 10, Math.max(10, e.clientX - rect.left + 10)) + 'px';
tooltip.style.top = Math.max(10, e.clientY - rect.top - 40) + 'px';
});
svg.appendChild(path);
});
svgWrapper.appendChild(svg);
chartContainer.appendChild(svgWrapper);
// Right side: legend
const legendContainer = document.createElement('div');
legendContainer.style.cssText = 'width:200px;display:flex;flex-direction:column;gap:8px;overflow-y:auto;max-height:240px;padding-right:8px;';
legendContainer.className = 'line-legend';
legendContainer.innerHTML = topDownloads.map((m, idx) => {
const color = colors[idx % colors.length];
return `${m.title || m.name}
`;
}).join('');
chartWrapper.appendChild(chartContainer);
chartWrapper.appendChild(legendContainer);
downloadsChart.appendChild(chartWrapper);
} else if (downloadsNote) {
downloadsNote.textContent = t.no_data;
}
} catch (e) {
statProjects.textContent = '—';
statProjectsSub.textContent = t.unavailable;
statDownloads.textContent = '—';
statDownloadsSub.textContent = t.unavailable;
statLoadersCount.textContent = '—';
statLoaders.textContent = t.unavailable;
statVersions.textContent = '—';
statVersionsSub.textContent = t.unavailable;
statCategories.textContent = `{{ t.error_loading }} ${e.message}`;
if (downloadsNote) downloadsNote.textContent = `{{ t.error_loading }}`;
console.warn('{{ t.failed_to_load }}:', e);
}
});
</script>