Zum Inhalt springen

Command Palette

Search for a command to run...

Changelog

Changelog

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

[2.9.0] - 2026-05-02

Removed

  • OPML feed export: the /feeds.opml endpoint and its sidebar button have been removed.

[2.8.1] - 2026-04-30

Fixed

  • Removed redundant note about duplicate counting from supporter stats display.

[2.8.0] - 2026-04-30

Added

  • Revenue comparison chart on the compare page, showing estimated monthly Patreon and Steady earnings per podcast grouped by currency.

Fixed

  • Supporter history chart now renders with a single data point instead of requiring at least two.

Changed

  • Removed per-platform patron breakdown from the supporter overview table for a cleaner display.

[2.7.0] - 2026-04-30

Added

  • Auto-gather Patreon & Steady supporter stats for podcasts (#28).
  • Show KPI cards as 2×2 grid on mobile (#27).

[2.6.1] - 2026-04-30

Removed

  • Playwright and the entire E2E test suite (web/e2e/, web/playwright.config.ts, web/playwright-report/, web/test-results/, the e2e.yml GitHub Actions workflow, and the @playwright/test devDependency). Vitest unit tests remain unchanged.
  • Badges on the homepage cards: the four KPI cards (Podcasts gesamt, Folgen gesamt, Ø Folgen / Podcast, Formate gesamt) lost their delta/info badges, and the "Aktivster Podcast" and "Zuletzt aktualisiert" cards lost their pill badges. The episode count for the most active podcast is still shown inline next to the name.

[2.6.0] - 2026-04-30

Added

  • Each podcast detail page now shows the homepage favicon next to the podcast title. The icon is derived at render time from the existing metadata.homepage URL via Google's favicon service (s2/favicons, sz=64), with graceful fallback when the site has no favicon and silent hiding on load errors.

Changed

  • Bumped lucide-react from ^1.12.0 to ^1.14.0.

[2.5.0] - 2026-04-30

Added

  • New noPaywallAccess flag on each podcast in the backend config.yaml. When set, the podcast detail page shows an info banner ("Eingeschränkte Auswertung") letting visitors know that the statistics are based only on publicly available episodes because we have no access to the paid feed. Initially set for OKCool, Spieleveteranen, Mindestens 10 Zeichen, and HookedFM.

[2.4.0] - 2026-04-30

Added

  • Custom equalizer brand mark (RSS dot + three ascending bars) now serves as the favicon, sidebar logo, and header home button. Replaces the default Next.js favicon and the lucide Rss/Home icons. Adds an icon.svg with prefers-color-scheme light/dark variants and a 180x180 apple-icon route for iOS home-screen.

Fixed

  • Row selection checkboxes in the "Podcasts Übersicht" table now visually toggle when clicked. The React Compiler memoized DataTable based on the stable table reference from useReactTable, so the body did not re-render when only rowSelection changed; opted the file out with 'use no memo'.

Changed

  • Re-enabled the insertmoin podcast feed in the scraper config; regenerated reports, results, and cached outputs accordingly.

[2.3.0] - 2026-04-30

Added

  • Comprehensive Playwright E2E test suite extended from 3 tests to 71 tests across 11 files, covering home, search, podcast detail, compare, changelog, category, and episode routes, plus tab persistence, theme toggle, Cmd+K palette navigation, and RSS/OPML feeds. (#25)
  • Expanded schema.org JSON-LD across the full site: @graph on the home page with WebSite + SearchAction, stable @id anchors, PodcastSeries with @id and cover image on podcast pages, CollectionPage with isPartOf on category pages, BreadcrumbList and aligned partOfSeries on episode pages, and WebPage + SearchAction on the search and changelog pages. SearchClient now reads an optional ?q= on load so the URL template matches real behavior. (#24)

Changed

  • Search page URL parameter aligned to ?q= (was ?query=) so JSON-LD SearchAction URL template and actual client routing agree. (#24)
  • Home, search, changelog, podcast, category, and not-found pages now carry explicit alternates.canonical and full openGraph metadata. Not-found pages additionally set robots: noindex. (#23)
  • Podcast and category page main titles use a semantic <h1> instead of the former CardTitle (div); podcast chart section headings promoted from h3 to h2. Compare page main card title is an <h1>. (#23)
  • Sitemap: /changelog added with weekly change frequency. llms.txt updated to reference changelog. (#23)

Fixed

  • Existing Playwright tab-persistence test corrected to match the current 'Episoden' tab label. (#25)

Tests

  • Extensive JUnit 5 + AssertJ + Mockito unit tests added for the java/rss-analyzer module: cache namespace/YAML/migration, FeedFingerprint, Config.Feed, ParsedItem normalization and mapping, domain records, JsonTransformer, RssExporter pipeline, SmartCategoryMatcher, and CategoryResultsCache. (#22)

[2.2.9] - 2026-04-29

Fixed

  • Next.js production build failed TypeScript check when both utils.test.ts and Utils.test.ts existed (Linux-only casing clash). Renamed the cn() Vitest file to cn.test.ts so it no longer collides with Utils.test.ts (@/components/Utils).

[2.2.8] - 2026-04-29

Fixed

  • "Folgen & Formate pro Jahr" chart no longer shows negative Y-axis values. The linear regression trend line could produce negative fitted values for early years (when the intercept is below zero), causing Recharts to auto-scale the axis floor into negative territory (e.g. down to -55). Fixed by adding domain={[0, 'auto']} to all YAxis instances in YearSumChart and CategoryYearChart, clamping the axis minimum to zero.

[2.2.7] - 2026-04-29

Added

  • "Zum Format" outline button in each Episoden-Archiv accordion header — one click to the dedicated category page from the collapsed state. Right-aligned, icon-only on small screens.

[2.2.6] - 2026-04-29

Added

  • Persistent Home / Übersicht button in the top header bar — always visible regardless of sidebar state, highlights when active, icon-only on small screens.

Fixed

  • Mobile sidebar now closes automatically after tapping a nav link. The shadcn Sidebar Sheet was previously left open after navigation, requiring a second tap to dismiss.

[2.2.5] - 2026-04-29

Changed

  • Chart palette reshuffled so adjacent series contrast: --chart-2 is now teal (was amber) and --chart-3 is violet (was yellow). The "Folgen & Formate pro Jahr" chart now reads as orange × teal instead of orange × amber, and the homepage distribution / /compare overlay chart cycles warm/cool/warm/cool for clearer separation.

[2.2.4] - 2026-04-28

Fixed

  • Tests no longer fail in sandboxed CI/Coolify builds with Error: No such built-in module: node:. The lib/trends.ts module was split into a pure-math half (safe to import from anywhere, no Node built-ins) and a server-only trends-server.ts half that still loads result-history.jsonl via fs/path (without the node: prefix that some sandboxes choke on).

[2.2.3] - 2026-04-28

Changed

  • Empty / zero / null cell values across every table (homepage Podcasts overview, per-podcast Format-by-Year matrix, comparison metrics) now render as blank space instead of an em-dash placeholder, so the eye can scan rows for actual data without the dashes adding noise. Year ranges, duration buckets, and the year-group divider in the category episode list are unchanged — those dashes represent real ranges, not missing data.

[2.2.2] - 2026-04-28

Changed

  • Homepage Verlauf sparkline now excludes the current (in-progress) year so the trend isn't artificially pulled downward. The column header carries an asterisk and the cell tooltip explains "Ohne laufendes Jahr".

[2.2.1] - 2026-04-28

Changed

  • Homepage KPI cards trimmed to label, value, and delta badge. Removed the static footer copy ("Inhalte über alle Podcasts", "Verteilung der Produktion", "Über alle Podcasts gemittelt", etc.) — the badge already conveys the trend.

[2.2.0] - 2026-04-28

Added

  • Podcast compare mode — select 2-3 podcasts in the homepage table (new checkbox column) and a sticky "Vergleichen" pill routes to /compare?ids=…, which renders side-by-side summary cards, an overlaid year-by-year line chart, and a metric table.
  • Real KPI trend deltas on the homepage: Podcasts gesamt, Folgen gesamt, and Formate gesamt now show signed Δ vs the previous analyzer run with TrendingUp/TrendingDown icons. Backed by a new web/public/result-history.jsonl journal that grows by one line per run.
  • Tiny sparkline column in the homepage Podcasts table (recharts <LineChart>, no axes/dots) showing each podcast's per-year episode counts at a glance.
  • JSON-LD PodcastEpisode schema.org block embedded on every episode page (full title, description, ISO 8601 duration, audio URL, cover image, parent series) for richer Google Podcasts / generic crawler results.
  • Generated Open Graph image per podcast (Tangerine accent, podcast name, total episodes + format counts) at /[podcastId]/opengraph-image — child routes (categories, episodes) inherit it automatically. Shared episode links now render distinctive cards on Slack/Twitter/Mastodon.
  • OPML 2.0 feed export at /feeds.opml, linked from the sidebar footer — one-click subscribe across podcatchers.
  • Playwright smoke suite (pnpm test:e2e) covering tab persistence (?tab=…), Cmd+K palette navigation, and theme toggle. Runs in CI on every web/** PR.
  • Skip-to-content keyboard link at the top of the layout for screen-reader / keyboard users.
  • pnpm build:analyze script wires @next/bundle-analyzer for one-shot bundle inspection.

Changed

  • Cmd+K palette now matches against episode descriptions (not just titles), mirroring the /search page. Searching for "metacritic" or "braid" now surfaces the relevant Tri-bunal episodes.
  • The full stripped episode description (previously truncated to 280 chars) is now indexed for matching; a separate 280-char snippet field carries the visible preview. Search index grew to 9.7 MB raw / 2.34 MB gzipped.
  • EpisodeArchive on per-podcast pages restructured as a server component with a thin client Accordion wrapper. Per-podcast HTML payloads dropped dramatically (gamenotover from ~13 MB → 808 KB; the largest, thepod, from ~14 MB → 8.8 MB).
  • Per-podcast DownloadButtons now fetch /results.json on click and slice out their data, instead of embedding the per-podcast categoryCountYearTitle blob into every page bundle.
  • Light-mode --primary darkened from oklch(0.7 0.18 55) to oklch(0.6 0.2 50) so primary buttons / sidebar accent meet WCAG AA contrast (~4.7:1) against the near-white foreground. Dark mode unchanged. Documented in globals.css.
  • metadataBase set on the root layout so OG / Twitter image URLs resolve against the canonical https://rssanalyzer.org (overridable via NEXT_PUBLIC_BASE_URL) instead of http://localhost:3000.
  • The per-podcast page header dropped the now-redundant "Episoden Archiv" button (the new tab covers the same affordance) and renamed the third tab from "Archiv" to "Episoden Archiv".

Fixed

  • Switching to the Übersicht tab on a per-podcast page no longer expands the layout horizontally — SidebarInset and the inner content column gained min-w-0, so the wide format-by-year table now scrolls inside its overflow-x-auto wrapper.
  • Recharts no longer logs width(-1) and height(-1) of chart should be greater than 0 on mobile — the homepage Sparkline column dropped its ResponsiveContainer (it always renders at fixed pixel dimensions inside hidden md:flex cells).
  • Tab persistence (?tab=…) now reliably clears when navigating back to the Übersicht tab — the soft-navigation race between router.replace and useSearchParams was replaced with a direct history.replaceState call.

Removed

  • Vendored shadcn UI primitives that the dashboard-01 block install pulled in but the app never used: ui/toggle.tsx, ui/toggle-group.tsx. Dropped peer deps @tabler/icons-react, @dnd-kit/{core,modifiers,sortable,utilities}, and zod.

[2.1.0] - 2026-04-28

Added

  • Per-podcast page restructured around shadcn Tabs (Übersicht / Charts / Episoden Archiv / Daten). The active tab persists in the URL via ?tab=… (bookmarkable, browser-back works), and any #archiv-… deep-link auto-switches to the Episoden Archiv tab and opens the matching Accordion item.
  • Cmd+K / Ctrl+K palette now shows per-result-type shortcut chips (P / F / E) and a sticky kbd footer (↑↓ navigieren · öffnen · esc schließen).
  • Pagination on the per-category episode list (Pagination + page-size Select 10/25/50/Alle), spanning all years with inline year separators so the page stays scannable on prolific formats.
  • HoverCard previews on long episode titles in the Episoden Archiv (cover thumbnail, date, duration, file size) and on truncated breadcrumb segments above 40 chars.
  • Toast notifications (Sonner) on JSON downloads from per-podcast pages.
  • Shape-aware Skeleton placeholders for every chart while LazyMount defers hydration — bar bars, area gradient, heatmap grid.

Changed

  • Brand palette retuned to a Tangerine accent — --primary, --ring, and --chart-1..5 shifted to a warm orange/amber/yellow/teal/violet ramp (Slate neutrals untouched). Both light and dark modes adopted.
  • PodcastsTable and PodcastFormatTable now use @tanstack/react-table for sort/filter/pagination, replacing hand-rolled useMemo sort logic. Adds page-size selector (10/25/50/100/Alle) on the homepage table and a unified ColumnDef typed surface.
  • Homepage DashboardStats adopts the shadcn dashboard-01 block visual pattern — KPI tiles use Card + CardDescription + tabular-nums CardTitle + CardAction Badge + CardFooter headline/caption with a soft from-primary/5 to-card gradient.
  • Per-podcast page header trimmed to a single Homepage button (the Archiv tab supersedes the old "Episoden Archiv" anchor button).
  • Episode info modal in the archive now uses a ResponsiveDialog wrapper that renders a centered shadcn Dialog on >=md viewports and a bottom shadcn Drawer (vaul) on mobile.
  • Episode covers in the archive switched to next/image (with unoptimized for the many third-party hosts), removing the bespoke optimizeCoverUrl + Photon hostname helper.
  • All "Keine Folgen" / "Keine Treffer" / "Mindestens 2 Zeichen" placeholders now render as shadcn Alert blocks with lucide icons; error.tsx and not-found.tsx rewritten as Alert + Button instead of bespoke layouts.
  • Subtle 280 ms card-in mount animation on top-level Cards for calmer page transitions (respects prefers-reduced-motion).

Fixed

  • Switching to the Übersicht tab no longer expands the layout horizontally — SidebarInset and the inner main column gained min-w-0, so the wide format-by-year table now scrolls within its overflow-x-auto wrapper instead of pushing the whole page wider than the viewport.

[2.0.0] - 2026-04-28

Changed

  • Full UI redesign on Tailwind CSS v4 + shadcn/ui (New York / Slate). Bootstrap 5 and the custom SCSS layer are gone. Every page, every component, and every chart now uses shadcn primitives — Card, Table, Tabs, Dialog, Accordion, Breadcrumb, Badge, Input, Select, Switch, Button, Sidebar, Chart, and DropdownMenu. URL structure is preserved (/, /[podcastId], /[podcastId]/[categorySlug], /[podcastId]/[categorySlug]/[episodeId], /search, /changelog).
  • Navigation moved from a top navbar to a collapsible app Sidebar with two grouped sections — "Featured" (Mindestens 10 Zeichen, Stay Forever, The Pod) and "Alle Podcasts" (alphabetical) — plus a top bar with the sidebar trigger, a global Cmd+K search trigger, and the theme toggle.
  • Episode Archive replaced the native <details> accordion + hand-rolled Bootstrap modal with shadcn Accordion (type="multiple", hash-deep-linking preserved) + Dialog for the description modal. Episode rows now use Avatar + Badge (Paywall / MP3 / Duration / Filesize) + an Info Button.
  • Charts (PodcastDistributionChart, YearSumChart, MonthlyAverageChart, CumulativeChart, DurationHistogram, CategoryYearChart) wrap recharts in shadcn ChartContainer / ChartTooltipContent / ChartLegendContent with var(--chart-1..5) CSS variables, so light/dark theming is automatic — no more useChartTheme hook or MutationObserver.
  • YearMonthHeatmap rebuilt on Tailwind utilities + color-mix(in oklch, var(--chart-1) …) for theme-aware cell intensity.
  • Search page (/search) uses Input + Select + Tabs filters with Card result cells, type chips, and Skeleton placeholders while the index loads.
  • Theme handling switched from Bootstrap's data-bs-theme attribute + custom localStorage logic to next-themes (Light / Dark / System) on <html class="dark">. The pre-hydration inline script is gone.
  • Changelog page wraps the rendered Markdown in a shadcn Card + Tailwind prose (via @tailwindcss/typography) for proper typography in both themes.

Added

  • Cmd+K / Ctrl+K global command palette (shadcn Command over cmdk) that fuzzes across podcasts, formats, and episodes from the existing search-index.json and routes directly to the relevant page.
  • Sortable shadcn Table for the homepage Podcasts overview, with name search (Input), still-releasing toggle (Switch), and per-column sort buttons. The same pattern powers the per-podcast Format-by-Year matrix in the new PodcastFormatTable (replaces the former SortableTable.jsx).

Fixed

  • Per-page HTML payload reduced ~290× (from 21 MB to ~73 KB) by passing only a small {id, name, featured}[] shape into the client-side AppSidebar instead of leaking the full 19 MB Result blob through RSC into every static page.
  • Recharts axis labels and tooltips already render correctly in dark mode on every chart — re-implemented via shadcn chart vars rather than the previous useChartTheme workaround (folds in the unreleased 1.22.1 fix).

Removed

  • bootstrap, sass, all custom SCSS (styles/bootstrap.scss, styles/elements/*.css, styles/resets.css).
  • lib/useChartTheme.ts (superseded by shadcn chart CSS variables).
  • components/SortableTable.jsx, components/Header.tsx (replaced by AppSidebar + SiteHeader).
  • The inline pre-hydration themeBootstrap script in app/layout.tsx.

[1.22.1] - 2026-04-26

Fixed

  • Recharts bar chart axis ticks, labels, gridlines, tooltips and legends are now readable in dark mode. Previously, Recharts rendered them with hard-coded near-black SVG fills that Bootstrap's data-bs-theme="dark" cascade never reached, leaving the labels nearly invisible against the dark panel. A new useChartTheme hook (lib/useChartTheme.ts) reads data-bs-theme off <html> and live-updates via a MutationObserver so the palette flips instantly when the user toggles the theme. Applied to all five chart components (PodcastDistributionChart, MonthlyAverageChart, DurationHistogram, YearSumChart, CumulativeChart, CategoryYearChart) — axis stroke + tick fill, gridline stroke, tooltip background/border/text, legend text, hover cursor, and the CumulativeChart area fill all swap between light and dark presets.

[1.22.0] - 2026-04-26

Added

  • Linear-regression trendlines on the year-over-year bar charts: dotted grey line over YearSumChart ("Folgen & Formate pro Jahr" on each podcast page) and CategoryYearChart ("Folgen pro Jahr" on each category page). Makes growth/decline visually obvious at a glance. Trendline is suppressed for series with fewer than 2 data points.
  • New lib/stats.ts exports a tiny linearRegression helper, covered by 6 vitest cases (perfect line, noisy series, zero-variance fallback, decreasing slope, short-input edge cases). Test suite now stands at 76 across 6 files.

Changed

  • Production build now runs npm test as part of prebuild, so the contract/parity/logic tests gate every next build.

[1.21.1] - 2026-04-26

Changed

  • Test suite expanded from 11 to 70 tests across 5 files, covering every JS-only logic file in the web project — lib/dates.ts, lib/categories.ts, lib/episodes.ts, the helper functions in Utils.tsx, and a parity test that asserts the build-time generate-search-index.js episodeId/categorySlug implementations agree byte-for-byte with their TS counterparts on a broad sample of inputs. React components remain out of scope.
  • generate-search-index.js now exports its helpers via module.exports and guards the side-effect block with if (require.main !== module) return; so vitest can require() it without triggering a file write. The CLI entrypoint behaviour (npm run generate-search-index) is unchanged.
  • Vitest test environment switched from node to jsdom so the DOM-dependent helpers in Utils.tsx (getDescriptionHtml and its DOMParser-based linkifier chain) are testable; jsdom added as a devDependency.

[1.21.0] - 2026-04-26

Added

  • Dark mode — header toggle button cycles light → dark → auto with localStorage persistence, an inline pre-hydration script applies the saved theme before paint to avoid flash-of-wrong-theme, and a [data-bs-theme="dark"] override block covers every custom color (tier nav badges, search type pills, filter chips, episode-neighbor cards, changelog code blocks, focus outline).
  • Next/previous episode navigation on episode permalink pages — the strip below the audio surfaces the chronological neighbors within the same format (with rel="prev"/rel="next" for SEO), or shows a muted "Erste Folge / Aktuellste Folge dieses Formats" placeholder at the edges. Collapses to single column under 576 px.
  • Vitest test suite covering the slug/id/contract helpers — getId Java parity, categorySlug diacritic handling and fallback, archiveSectionId, episodeId determinism, hasEpisodes edge cases, and assertMetadataIdsMatch drift detection. Run with npm test.

Fixed

  • Episode descriptions are readable in dark mode — publisher-supplied descriptions routinely embed style="color: #..." declarations that were invisible on a dark background. A scoped [data-bs-theme="dark"] .episode-description *:not(a) { color: inherit !important } override neutralizes inline colors without touching light mode.

[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