A lightweight static release catalog for browsing versioned builds, changelogs, and downloadable artifacts.
Built with Eleventy, Vite, Alpine.js, and Tailwind CSS.
Releases Catalog is a static web application designed to:
- List application releases from a CSV file
- Dynamically load and render versioned changelogs
- Provide downloadable build artifacts
- Support deep-linking to specific releases
- Work entirely without a backend
Perfect for:
- Internal QA build portals
- Client distribution pages
- Open-source release hubs
- Mobile APK / IPA distribution pages
- π Release metadata from
catalog.csv - π On-demand loading of
CHANGELOG.md - π Deep linking via
#v=version.id - π― Unique version identifiers (
versionCode) - πΎ Session-based changelog caching
- π Per-release refresh (clear cache & reload)
- π Dark mode with system preference detection and manual toggle
- π± Fully responsive layout
- β‘ Client-side pagination
- π§© Configurable via
window.catalogConfig - π Relative paths β works in any subdirectory without config
- π§± No backend required
- Eleventy β Static Site Generator
- Nunjucks β Templates
- Vite β Bundler
- Alpine.js β Client interactivity
- Tailwind CSS β Styling
src/
βββ _includes/ # Layouts & Nunjucks partials
βββ assets/
β βββ main.js # Vite entry
β βββ main.css # Tailwind styles
β βββ svg/ # Icons
βββ catalog/ # Alpine app logic
β βββ app.js
β βββ config.js
β βββ services.js
β βββ utils.js
βββ releases/ # Version folders (CHANGELOG + artifacts)
βββ catalog.csv # Release metadata (id, version, datetime)
βββ index.njk # Entry template
βββ favicon.svg
_site/ # Generated output
#id,version,datetime
240823526,0.0.1,2024-08-23T10:00:00
260225100,0.0.2,2025-02-26T10:00:00Each row generates a unique versionCode:
versionCode = `${version}.${id}`
// e.g. 0.0.2.260225100This ensures unique deep-linking, stable cache keys, and no collision if versions repeat.
Each release folder must match the version field:
releases/
βββ 0.0.2/
βββ CHANGELOG.md
βββ my-app-0.0.2.apk
Link directly to a specific release:
https://example.com/#v=0.0.2.260225100
Behavior:
- Automatically navigates to the correct page
- Scrolls to and highlights the release
- Expands the changelog section
When no hash is present, the latest release is automatically expanded without modifying the URL.
Dark mode is supported with:
- System preference detection β follows
prefers-color-schemeon first visit - Manual toggle β sun/moon button in the header
- Persistent preference β stored in
localStorage - No flash β inline script applies the class before CSS loads
All asset and data paths in the app use relative URLs (no leading /), so the site works out of the box when deployed under any subdirectory (e.g. https://example.com/myapp/) β no configuration or post-build edits needed.
Keep paths in
config.jsrelative (catalog.csv,releases/...,assets/svg/...) to preserve this behavior. Absolute paths (starting with/) will bypass the subdirectory and break.
Customize behavior by defining window.catalogConfig before the app loads. All methods have access to this (the merged config object), so this.releasesRelativePath and other properties are available inside any overridden function.
| Option | Type | Default | Description |
|---|---|---|---|
pageSize |
number |
5 |
Releases per page |
catalogCsv |
string |
"catalog.csv" |
Path or URL to catalog CSV file |
releasesRelativePath |
string |
"releases" |
Base path/URL used by URL builder functions |
getAssetIconUrl(asset) |
function |
β | Icon URL for a given asset filename |
getAssetDownloadUrl(version, asset) |
function |
β | Download URL for an artifact |
getAssetDownloadName(version, asset) |
function |
β | Filename hint for download |
getChangelogUrl(versionCode) |
function |
β | URL for CHANGELOG.md |
transformChangelogHtml(html) |
function |
β | Post-process rendered changelog HTML |
Files are served alongside the static site. Relative paths are resolved by the browser from the current page URL, so subdirectory deployments work automatically without any config change.
Do not use a leading
/β absolute paths ignore the deployment subdirectory and will break.
<script>
window.catalogConfig = {
pageSize: 10,
catalogCsv: "catalog.csv", // relative to page URL
releasesRelativePath: "releases", // relative to page URL
getAssetDownloadName(version, asset) {
return `myapp-${version}.apk`;
},
transformChangelogHtml(html) {
return html.replace(
/\b([A-Z]+-\d+)\b/g,
(match) => `<a href="https://your-jira/browse/${match}" target="_blank">${match}</a>`
);
}
};
</script>Full URLs (https://...) are used as-is by the browser. Setting catalogCsv and releasesRelativePath to full URLs is enough; the URL builder functions (getAssetDownloadUrl, getChangelogUrl) use this.releasesRelativePath automatically and require no override.
<script>
window.catalogConfig = {
catalogCsv: "https://cdn.example.com/my-app/catalog.csv",
releasesRelativePath: "https://cdn.example.com/my-app/releases",
};
</script>CORS: The CDN must allow cross-origin requests (
Access-Control-Allow-Origin) sincecatalog.csvandCHANGELOG.mdare loaded viafetch().Download: The HTML
downloadattribute only works for same-origin URLs. For cross-origin assets, configure the CDN to sendContent-Disposition: attachmentto force a file download.
npm install
npm run devnpm run build
# Output: _site/Static output in _site/ β deploy to any static host:
- GitHub Pages
- Netlify / Vercel / Cloudflare Pages
- Any static hosting provider
No server required.
catalog.csvprovides release metadata.- Releases are paginated client-side.
- CHANGELOG files are fetched on demand and cached in
sessionStorage. - Markdown is parsed and rendered dynamically.
- UI state (active release, page) is driven by the URL hash (
#v=). - Dark mode state is managed via
localStorageand a class on<html>.
Pull requests are welcome. For major changes, please open an issue first.
