fix: issue #14 wrong sorting in category and tag archives

This commit is contained in:
2026-07-05 16:09:33 +02:00
parent 7dfcd78abd
commit bb82f1d4a1
5 changed files with 109 additions and 5 deletions

View File

@@ -175,6 +175,7 @@ defmodule BDS.Generation.Data do
end) end)
end end
) )
|> sort_generation_index_posts()
end end
## --- internals ----------------------------------------------------------- ## --- internals -----------------------------------------------------------
@@ -398,4 +399,17 @@ defmodule BDS.Generation.Data do
defp append_generation_index(index, field, key, post) do defp append_generation_index(index, field, key, post) do
update_in(index[field], fn grouped -> Map.update(grouped, key, [post], &[post | &1]) end) update_in(index[field], fn grouped -> Map.update(grouped, key, [post], &[post | &1]) end)
end end
# Archive listings (categories, tags, dates) must render newest first. The
# index is built by prepending, so each grouped list is sorted explicitly by
# creation date (descending) to guarantee a stable newest-first order.
defp sort_generation_index_posts(index) do
Map.new(index, fn {field, grouped} ->
{field, Map.new(grouped, fn {key, posts} -> {key, sort_index_posts_desc(posts)} end)}
end)
end
defp sort_index_posts_desc(posts) do
Enum.sort_by(posts, &{-(&1.created_at || 0), -(&1.published_at || 0), to_string(&1.slug)})
end
end end

View File

@@ -273,7 +273,7 @@ defmodule BDS.Rendering.ListArchive do
|> DateTime.to_date() |> DateTime.to_date()
|> Date.to_iso8601()) |> Date.to_iso8601())
) )
|> Enum.sort_by(fn {label, _posts} -> label end) |> Enum.sort_by(fn {label, _posts} -> label end, :desc)
last_index = length(grouped_blocks) - 1 last_index = length(grouped_blocks) - 1
@@ -284,7 +284,7 @@ defmodule BDS.Rendering.ListArchive do
date_label: date_label, date_label: date_label,
show_date_marker: true, show_date_marker: true,
show_separator: index < last_index, show_separator: index < last_index,
posts: Enum.sort_by(grouped_posts, &Map.get(&1, :created_at)) posts: Enum.sort_by(grouped_posts, &Map.get(&1, :created_at), :desc)
} }
end) end)
|> case do |> case do

View File

@@ -0,0 +1,39 @@
defmodule BDS.Generation.ArchiveOrderingTest do
@moduledoc """
Category, tag and date archive listings must render newest first. The
generation post index is built by prepending, so this guards against the
regression where grouped archive lists came out in an unstable/oldest-first
order.
"""
use ExUnit.Case, async: true
alias BDS.Generation.Data
defp post(slug, created_at, categories, tags) do
%{
id: slug,
slug: slug,
created_at: created_at,
published_at: created_at,
categories: categories,
tags: tags
}
end
test "categories, tags and dates are ordered newest first regardless of input order" do
older = post("older", 1_700_000_000_000, ["article"], ["elixir"])
newest = post("newest", 1_700_000_200_000, ["article"], ["elixir"])
middle = post("middle", 1_700_000_100_000, ["article"], ["elixir"])
# Deliberately unordered input.
index = Data.build_generation_post_index([older, newest, middle])
expected = ["newest", "middle", "older"]
assert Enum.map(index.posts_by_category["article"], & &1.slug) == expected
assert Enum.map(index.posts_by_tag["elixir"], & &1.slug) == expected
{year, _month, _day} = BDS.Generation.Paths.local_date_parts!(newest.created_at)
assert Enum.map(index.posts_by_year[year], & &1.slug) == expected
end
end

View File

@@ -246,7 +246,7 @@ defmodule BDS.RenderingTest do
title: "Render Day Blocks", title: "Render Day Blocks",
kind: :list, kind: :list,
content: content:
"range={{ min_date }}-{{ max_date }}|heading={{ show_archive_range_heading }}|blocks={% for block in day_blocks %}[{{ block.date_label }}:{{ block.posts.size }}:{{ block.show_date_marker }}]{% endfor %}|archive={{ archive_context.kind }}:{{ archive_context.year }}:{{ archive_context.month }}" "range={{ min_date }}-{{ max_date }}|heading={{ show_archive_range_heading }}|blocks={% for block in day_blocks %}[{{ block.date_label }}:{% for p in block.posts %}{{ p.slug }};{% endfor %}:{{ block.show_date_marker }}]{% endfor %}|archive={{ archive_context.kind }}:{{ archive_context.year }}:{{ archive_context.month }}"
}) })
assert {:ok, published_list_template} = BDS.Templates.publish_template(list_template.id) assert {:ok, published_list_template} = BDS.Templates.publish_template(list_template.id)
@@ -289,6 +289,19 @@ defmodule BDS.RenderingTest do
tags: [], tags: [],
categories: [], categories: [],
href: "/2024/04/01/second/" href: "/2024/04/01/second/"
},
%{
id: "second-b",
slug: "second-b",
title: "Second B",
excerpt: "two-b",
language: "en",
created_at: second_day + 3_600,
updated_at: second_day + 3_600,
published_at: second_day + 3_600,
tags: [],
categories: [],
href: "/2024/04/01/second-b/"
} }
] ]
@@ -311,9 +324,9 @@ defmodule BDS.RenderingTest do
}) })
assert rendered =~ "heading=true" assert rendered =~ "heading=true"
assert rendered =~ "blocks=[2024-03-31:1:true][2024-04-01:1:true]" assert rendered =~ "blocks=[2024-04-01:second-b;second;:true][2024-03-31:first;:true]"
assert rendered =~ "archive=date:2024:4" assert rendered =~ "archive=date:2024:4"
assert rendered =~ "range=1711843200-1711929600" assert rendered =~ "range=1711843200-1711933200"
end end
test "render_post_page falls back to bundled starter template when the published default template file is missing", test "render_post_page falls back to bundled starter template when the published default template file is missing",

38
tmp_preview_check.exs Normal file
View File

@@ -0,0 +1,38 @@
Application.ensure_all_started(:ecto_sql)
Application.ensure_all_started(:exqlite)
{:ok, _} = BDS.Repo.start_link()
project_id = "aceac6d3-8f3f-4a9c-a2b4-517b59b20f44"
# 1) Replicate the preview data path exactly.
import Ecto.Query
alias BDS.Posts.Post
posts =
BDS.Repo.all(
from p in Post,
where: p.project_id == ^project_id and p.status in [:published, :draft],
order_by: [desc: p.created_at, desc: p.published_at, asc: p.slug]
)
picture = Enum.filter(posts, fn p -> "picture" in (p.categories || []) end)
IO.puts("== preview data path, first 5 (expect newest) ==")
picture |> Enum.take(5) |> Enum.each(fn p -> IO.puts(" #{p.slug} #{p.created_at} #{p.language}") end)
# 2) Call the actual router render and extract the first post links.
case BDS.Preview.Router.render_route(project_id, "/category/picture") do
{:ok, %{body: body}} ->
body = IO.iodata_to_binary(body)
links =
Regex.scan(~r{href="[^"]*/20\d\d/\d\d/[^"]*"}, body)
|> Enum.map(&hd/1)
|> Enum.take(6)
IO.puts("== render_route /category/picture, first link hrefs ==")
Enum.each(links, &IO.puts(" #{&1}"))
other ->
IO.inspect(other, label: "render_route returned")
end