diff --git a/lib/bds/generation/data.ex b/lib/bds/generation/data.ex index 9bcf2f6..962053f 100644 --- a/lib/bds/generation/data.ex +++ b/lib/bds/generation/data.ex @@ -175,6 +175,7 @@ defmodule BDS.Generation.Data do end) end ) + |> sort_generation_index_posts() end ## --- internals ----------------------------------------------------------- @@ -398,4 +399,17 @@ defmodule BDS.Generation.Data do defp append_generation_index(index, field, key, post) do update_in(index[field], fn grouped -> Map.update(grouped, key, [post], &[post | &1]) 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 diff --git a/lib/bds/rendering/list_archive.ex b/lib/bds/rendering/list_archive.ex index 2c52868..8c2edde 100644 --- a/lib/bds/rendering/list_archive.ex +++ b/lib/bds/rendering/list_archive.ex @@ -273,7 +273,7 @@ defmodule BDS.Rendering.ListArchive do |> DateTime.to_date() |> 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 @@ -284,7 +284,7 @@ defmodule BDS.Rendering.ListArchive do date_label: date_label, show_date_marker: true, 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) |> case do diff --git a/test/bds/generation/archive_ordering_test.exs b/test/bds/generation/archive_ordering_test.exs new file mode 100644 index 0000000..28e9c47 --- /dev/null +++ b/test/bds/generation/archive_ordering_test.exs @@ -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 diff --git a/test/bds/rendering_test.exs b/test/bds/rendering_test.exs index 65b846f..f17dbbb 100644 --- a/test/bds/rendering_test.exs +++ b/test/bds/rendering_test.exs @@ -246,7 +246,7 @@ defmodule BDS.RenderingTest do title: "Render Day Blocks", kind: :list, 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) @@ -289,6 +289,19 @@ defmodule BDS.RenderingTest do tags: [], categories: [], 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 =~ "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 =~ "range=1711843200-1711929600" + assert rendered =~ "range=1711843200-1711933200" end test "render_post_page falls back to bundled starter template when the published default template file is missing", diff --git a/tmp_preview_check.exs b/tmp_preview_check.exs new file mode 100644 index 0000000..98b112f --- /dev/null +++ b/tmp_preview_check.exs @@ -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