Changelog

Changelog

All notable changes to this project will be documented in this file.

[1.20.0] - 2026-04-26

Added

  • /search now matches podcasts and formats in addition to episodes — typing "Stay" surfaces the Stay Forever podcast itself, "Heftkritik" surfaces the format card, and "Filmumsetzungen" still surfaces the matching episode. Each result is tagged with a colored type badge (Podcast / Format / Folge) and routes to the correct page (/[podcastId], /[podcastId]/[categorySlug], or /[podcastId]/[categorySlug]/[episodeId]).
  • Type filter chip group above the result list (Alle / Podcasts / Formate / Folgen) so users can narrow results to a single tier; the existing podcast filter dropdown still composes with it.

Changed

  • Search index search-index.json now emits a unified items array (12 podcasts + 149 formats + 7,162 episodes ≈ 4.4 MB) with a type discriminator and a shared name field, so a single Fuse instance covers all three tiers. Format and podcast records ship totalEpisodes plus year ranges so result cards can show "176 Folgen · 2010–2025" without hitting the full dataset.
  • Result ranking applies a small per-type penalty so a clean podcast or format hit floats above an episode-description fuzzy match at the same Fuse score.

[1.19.1] - 2026-04-26

Fixed

  • "Aktiv seit" column on the homepage Podcasts table no longer shows 0 for podcasts that contain an episode with an unparseable/empty pubDate (e.g. Spielvertiefung). The analyzer's year-0 sentinel for unknown publish dates is now ignored when computing first/last activity year.
  • Categories that contain no episodes (e.g. Spielvertiefung "Rezension", The Pod "Was tun?", Retrokompott "Echtzeitkompott", and 14 others) no longer appear anywhere in the UI: their per-category pages 404, the per-podcast Episoden Archiv skips the empty <details> accordions, and the sitemap drops the dead URLs. The findCategoryBySlug helper, generateStaticParams for /[podcastId]/[categorySlug], EpisodeArchive, and sitemap.ts all share a single hasEpisodes guard from lib/categories.ts.

[1.19.0] - 2026-04-26

Added

  • Homepage "Folgen pro Podcast" card now uses a real interactive Recharts horizontal bar chart — bars are clickable and route to the matching podcast page, tooltips and axis ticks use German number formatting (Intl.NumberFormat).
  • Build-time guard assertMetadataIdsMatch runs on the homepage render: any future feed name whose web getId(name) disagrees with the analyzer's metadata[id] (whitespace, casing, diacritics) now fails the build instead of silently breaking nav.

Changed

  • Tightened the contract between the web's Utils.getId and the Java analyzer's Main.getId — the web now uses replaceAll(' ', '') (matching Java byte-for-byte) instead of .replace(/\s/g, ''), with a comment block declaring the contract.

Removed

  • Stale planning doc 2026-02-20_replace-homepage-table-with-dashboard-stats.md at repo root (its decisions shipped in v1.10).

[1.18.0] - 2026-04-26

Added

  • Shared lib/dates.ts helper exposes locale-aware short month labels via Intl.DateTimeFormat so chart axes pick up future locale changes automatically.

Changed

  • All remaining English UI strings translated to German — dashboard stat cards (Podcasts gesamt, Folgen gesamt, Ø Folgen / Podcast, Formate gesamt, Aktivster Podcast, Zuletzt aktualisiert, Folgen pro Podcast), error and 404 pages, breadcrumbs (Übersicht instead of Dashboard), Paywall/Public badges, download button tooltips, page metadata (<title>, descriptions), and the sortable-table column header.
  • Number, date, and relative-time output now goes through Intl.NumberFormat / Intl.DateTimeFormat / Intl.RelativeTimeFormat instead of ad-hoc toFixed / toLocaleString / hand-rolled "vor X T." strings — episode file sizes render as 6,22 MB (German thousands/decimal), last-published cells show vor 3 Tagen-style relative dates, and chart month labels come from the locale rather than hardcoded arrays.
  • /changelog simplified to a single section that renders the canonical project CHANGELOG.md only — the separate Web/Backend tabs and the abandoned web/CHANGELOG.md stub were removed.

[1.17.0] - 2026-04-26

Added

  • Per-category pages now render the same chart suite as per-podcast pages — episodes per year, average episodes per month, cumulative growth, year × month activity heatmap, and duration histogram — scoped to the category's episodes.
  • Header navigation is now two-tiered: 🔍 Suche and the three primary podcasts (Mindestens 10 Zeichen, Stay Forever, The Pod) sit above a thin divider; all other podcasts appear below in a quieter secondary tier so the eye lands on the headline feeds first.

Changed

  • Regenerated analyzer output against the v1.14.1 config so Stay Forever no longer surfaces stale 10x10, Flashback, or Sonderfolge categories on the rendered site (the orphan "10x10: Filmumsetzungen" episode now falls into _ Ohne Kategorie as intended).

[1.16.0] - 2026-04-26

Added

  • Per-category index pages at /[podcastId]/[categorySlug] — one static page per (podcast, format) pair, listing every episode in that format grouped by year. Sitemap and /llms.txt enumerate them.
  • Reusable <Breadcrumb> component, now rendered on the per-podcast, per-category, and per-episode pages so visitors can navigate the Dashboard → Podcast → Format → Episode hierarchy.

Changed

  • Episode permalinks moved from /[podcastId]/[episodeId] to /[podcastId]/[categorySlug]/[episodeId] (e.g. /spieleveteranen/regulare-folge/10f7q1z). Old 2-segment URLs are no longer generated and now return 404. Search results, sitemap entries, and the in-card "Podcast Format" link all use the new shape. The build asserts that no two formats within a podcast slugify to the same value.
  • The _ Ohne Kategorie bucket routes under the literal slug ohne-kategorie.

[1.15.0] - 2026-04-26

Added

  • New /search route: type-ahead full-text search over episode titles and descriptions across every analyzed podcast, powered by fuse.js against a slim /search-index.json (~3.4 MB / ~870 KB gzipped) generated at build time. Results can be filtered by podcast.
  • Per-episode permalink pages at /[podcastId]/[episodeId] — one static page per episode (~7,000 in total) with the sanitized description, an inline <audio> player, MP3 download, duration/size badges, breadcrumb navigation, and OpenGraph metadata. Episode IDs are stable FNV-1a hashes of the source URL (or title@pubDate fallback); the build asserts uniqueness.
  • Episode titles in the per-podcast archive now link to their permalink page.
  • Sitemap and /llms.txt enumerate every per-episode URL and the new /search page.
  • Homepage "Podcasts Overview" gained filters and sortable columns: filter by name, toggle "only podcasts that released a new episode in the last 12 months", and sort by episodes / first year / last-episode date.

Changed

  • Header navigation marks the current podcast or /search link as active (aria-current="page" plus a Bootstrap .active class).
  • SortableTable column headers are real <button> elements with aria-sort instead of clickable <th> cells, so they are keyboard-reachable and announced correctly by screen readers.
  • Episode-info modal now closes on Escape and on backdrop click, gets a real .modal-backdrop, traps body scroll while open, and exposes aria-modal / aria-labelledby.
  • Global :focus-visible outline so keyboard focus is visible across the lighter pill nav and table headers.

Fixed

  • Homepage "Podcasts Overview" previously built each podcast link by sanitizing the display name (name.replace(/\s/g, '').toLowerCase()) and looking it up in metadata — broken for any podcast whose ID did not happen to match its squashed name. Replaced with a new client PodcastsTable that uses real metadata IDs, so every row links to the correct podcast page.

[1.14.1] - 2026-04-26

Changed

  • Stay Forever feed: removed the 10x10, Flashback, and Sonderfolge category mappings. These captured only one or two episodes each and now fall through to _ Ohne Kategorie.

[1.14.0] - 2026-04-26

Added

  • Per-podcast page: each "Podcast Format" row in the sortable table is now an anchor link that jumps to the matching section in the episode archive below and auto-expands it. Hash deep links (e.g. #archiv-talk) work the same way — the targeted <details> opens and scrolls into view on load and on hashchange.

[1.13.0] - 2026-04-26

Changed

  • Episode-to-category matcher rewritten to use title-only, boundary-aware prefix matching. A pattern only hits when it sits at a word/non-word boundary on both sides of the (lowercased) title, longer patterns win over shorter ones at the same boundary, and description-based TF-IDF semantic matching is removed entirely. The old semantic fallback was the source of false positives where any episode whose description mentioned common German words like "Spiel" or "Geschichte" leaked into the "Ein Spiel und seine Geschichte" category.

Added

  • New Stay Forever categories: Hommage, Filmpodcast, Down to the Detail, 10x10, Sonderfolge, Flashback. Episodes that previously fell through to Unclassified or were misrouted into "Ein Spiel und seine Geschichte" now land in their proper buckets.

Fixed

  • Episodes like Sonderfolge: Die Faszination des Sammelns, Filmpodcast: Bloodrayne, Hommage #1: Faith, and Down to the Detail: Gabriel Knight 2 #1 no longer appear under "Ein Spiel und seine Geschichte" — that category now contains only the four real Ein Spiel und seine Geschichte: … episodes.

[1.12.1] - 2026-04-24

Changed

  • /changelog page now shows the Web section first and the Backend section second, with a custom segmented-pill nav replacing the Bootstrap default, a proper "Changelog" title, and scroll-margin offsets so anchor jumps land cleanly below the sticky navbar.

[1.12.0] - 2026-04-24

Added

  • New /changelog route that renders both the project and the web-specific CHANGELOG.md files side-by-side at build time, with jump-nav pills between sections. Accessible by clicking the version number in the footer.

Changed

  • Bundle-size / load optimizations: swapped the full Bootstrap CSS import (~225 KB) for a selective SCSS bundle (−65 KB gzipped), DOMPurify is now dynamic-imported inside the episode info modal opener instead of shipped with the initial payload, and all five per-podcast charts are wrapped in a new <LazyMount> IntersectionObserver component so their recharts chunks only download as they scroll into view.
  • Silenced Bootstrap 5.3's internal Sass deprecations (import, global-builtin, color-functions, if-function, legacy-js-api) via sassOptions.silenceDeprecations in next.config.ts.

[1.11.0] - 2026-04-24

Added

  • "Episoden Archiv 📁" button in the per-podcast header that jumps to the archive section via a #archiv anchor.

Changed

  • All internal and external link wrappers in the podcast stats table and per-podcast header now use next/link instead of plain <a href> — dashboard navigation is client-side routed with automatic prefetching, no more full-page reloads.
  • Per-podcast header now keeps the title on the left and the "Episoden Archiv" / "Homepage" buttons grouped together on the right, instead of spreading all three across the row.

[1.10.0] - 2026-04-24

Added

  • Three new charts on every podcast detail page: a cumulative episode count over time (area chart), an activity heatmap showing episodes per calendar month per year, and a histogram of episode length in 30-minute buckets. The heatmap and duration histogram are hidden for feeds that lack the underlying data.
  • Anchor id on the episode archive heading so it can be deep-linked.

Changed

  • Consolidated the per-podcast chart aggregation into a single deduped episode walk (dedupe by url, fallback title@pubDate). All four charts derive from the same pass, keeping year/month computation consistent under Europe/Berlin.

Removed

  • Unused TableHeadline component.

[1.9.0] - 2026-04-24

Changed

  • All routes are now prerendered at build time. The per-podcast detail page uses generateStaticParams() to emit one static HTML file per podcast (with dynamicParams = false so unknown ids 404 at build), and /llms.txt is marked force-static so its response is baked in rather than evaluated per request. No more on-demand server rendering.

Fixed

  • Navigation items are now vertically centered on desktop. The featured pills no longer push plain links off-axis because the .navbar-nav flex container centers on the cross-axis and each .nav-link flex-centers its inner <li>. Mobile offcanvas layout is unchanged.

[1.8.0] - 2026-04-24

Added

  • Second chart on each podcast detail page showing the average number of episodes released in each calendar month (Jan–Dez), averaged across the podcast's active years. Complements the existing per-year view and reveals release cadence / seasonality.
  • Explanatory headlines above both per-podcast charts (Folgen & Formate pro Jahr and Ø Folgen pro Monat with an active-years subtitle).

Changed

  • Episode-archive avatars now load directly from their source CDNs instead of funnelling through /_next/image, so the browser fans out requests across many origins in parallel instead of queueing them through a single endpoint. WordPress/Jetpack-hosted covers (~87% of the catalogue) additionally get a ?w=64&h=64 edge resize, dropping per-avatar payloads from 100s of KB to 2–4 KB.
  • Each <details> section in the episode archive now lazy-mounts its list on first open, so collapsed sections no longer hydrate hundreds of components up front.

[1.7.0] - 2026-04-24

Added

  • Per-feed enabled flag in config.yaml: set enabled: false on any feed to skip it entirely (no fetch, no parse, no categorization, and it disappears from the frontend). Omitting the flag defaults to enabled: true, so existing configs are unaffected.
  • Navigation can now highlight selected podcasts via the FEATURED_PODCAST_IDS set in the header, rendered with dedicated nav-link-featured / nav-item-featured classes as a warm amber pill.

Changed

  • Disabled the Insert Moin, Kapitel Eins, and Last Game Standing feeds in the shipped config. They no longer appear in the exported results.
  • Mindestens 10 Zeichen, Stay Forever, and The Pod are now visually highlighted in the navigation.

[1.6.1] - 2026-04-24

Changed

  • Weekly GitHub Actions run now persists the analyzer feed cache between runs via actions/cache@v5, so conditional GETs and body-SHA short-circuits actually fire on the cron
  • Weekly run logs feed_cache_stats.json to the job log after the analyzer finishes for at-a-glance cache visibility

[1.6.0] - 2026-04-24

Added

  • RSS feed HTTP cache (Layer 1): analyzer now issues conditional If-None-Match / If-Modified-Since GETs, reuses cached XML bodies on 304, and falls back to SHA-256 body comparison on 200 responses. Stale cache is served on HTTP failures so weekly CI runs are resilient to transient 5xx errors. Honours -DrssAnalyzer.forceRefresh=true / RSS_ANALYZER_FORCE_REFRESH=true for forced re-downloads.
  • Parsed-items cache (Layer 2): keyed by the feed body SHA-256, skips XML parsing on warm re-runs when the body bytes haven't changed. Raw ParsedItem is now a denormalized JSON-serializable record so it can be rehydrated from disk without round-tripping through the rssreader library.
  • Categorization-result cache (Layer 3): keyed by matcher config hash + aggregated per-sub-feed SHAs; when both match, the entire match / count / countYear / countYearTitle pipeline is skipped for that feed and the cached result fragment is spliced straight into the final Result.
  • Per-layer cache observability: per-feed INFO log lines (304 / body-sha match / fresh / stale-on-failure / parsed-items hit / category-results hit), one-line run summaries for each cache layer, and a machine-readable feed_cache_stats.json written on every run.
  • Namespaced cache store: FileSystemCache now supports multiple independent namespaces plus side-file payloads, with transparent migration of legacy flat cache.yaml files into a default namespace.

Changed

  • Measured impact (29 feeds, today's config): cold run 45.9 s, warm re-run 25.1 s (-45 %). 27/30 feeds served from the HTTP cache, 27/27 parsed-items cache hits, 13/13 category-results cache hits; downloaded bytes drop from 41.2 MB to 12.9 MB on warm runs. The remaining runtime is dominated by the export phase (~22 s) which is addressable in a follow-up.

[1.5.0] - 2026-04-24

Added

  • Footer now displays the current app version

Changed

  • Bumped next to ^16.2.4 and dompurify to ^3.4.1

[1.4.0] - 2026-04-24

Added

  • Episode info modal now shows the release date and time above the description
  • "Was tun?" section on config values in config.yaml

Fixed

  • Episodes published late in the day now bucket into the correct year based on the publisher-local date (Europe/Berlin), preventing year-boundary leakage

[1.3.0] - 2026-04-20

Added

  • Enabled Umami session replay by default via recorder.js with data-sample-rate, data-mask-level, and data-max-duration attributes
  • Session replay tunables via NEXT_PUBLIC_UMAMI_SAMPLE_RATE, NEXT_PUBLIC_UMAMI_MASK_LEVEL, and NEXT_PUBLIC_UMAMI_MAX_DURATION

Changed

  • Migrated Umami script host to umami.lucanerlich.com

[1.2.3] - 2026-04-13

Added

  • Mindestens 10 Zeichen: added "Gaming Stories" category

[1.2.2] - 2026-04-13

Fixed

  • GitHub Actions workflow now also updates web/public/results.json so Nixpacks/Coolify builds pick up fresh podcast data

[1.2.1] - 2026-04-13

Changed

  • GitHub Actions workflow now only commits result.json (the only file the frontend reads) instead of all output files
  • Workflow skips commit when podcast data is unchanged, ignoring the createdAt timestamp that varies between runs

[1.2.0] - 2026-04-13

Added

  • Weekly GitHub Actions workflow to automatically refresh RSS data (runs Sunday-to-Monday night)
  • Gradle application plugin so the analyzer can be run via ./gradlew run

Fixed

  • Opted into Node.js 24 for GitHub Actions runners to resolve deprecation warnings

[1.1.1] - 2026-04-13

Changed

  • Improved episode categorization across 6 podcasts by adding missing keyword variants to config.yaml
    • Spielvertiefung: added "Über" keyword to catch ~20 deep-dive episodes, "Jahresrückblick" category, and "In Gespräch" typo variant
    • Stay Forever: added "10 Jahre Klüger"/"10 Jahre klüger" to match Patreon episode naming
    • Down to the Detail: added "Ambience" variant for Ambients category
    • Insert Moin: added "gamescom", "E3" to Event Recap, and new "Feriencamp" category
    • HookedFM: added "gamescom", "Jahresrückblick" to Special, and new "Hooked on" category
    • Mindestens 10 Zeichen: added "Spezial:" category

[1.1.0] - 2026-04-06

Added

  • Initial release with DownloadGoodies script for automated file downloads