From d944c0783805b8c88e0b9ec53134d5d61748b429 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 5 Jul 2026 17:33:48 +0200 Subject: [PATCH] fix: issue #15 calendar index not properly created, also some smaller bugs --- lib/bds/generation.ex | 2 +- lib/bds/generation/outputs.ex | 4 +- lib/bds/generation/paths.ex | 2 +- lib/bds/generation/sitemap.ex | 16 +++-- lib/bds/preview.ex | 16 +++-- lib/bds/preview/router.ex | 37 ++++++++++ mix.exs | 4 +- mix.lock | 14 ++-- specs/generation.allium | 6 +- test/bds/generation/calendar_json_test.exs | 53 ++++++++++++++ test/bds/generation_test.exs | 15 ++-- test/bds/preview_test.exs | 81 ++++++++++++++++++++++ tmp_preview_check.exs | 38 ---------- 13 files changed, 218 insertions(+), 70 deletions(-) create mode 100644 test/bds/generation/calendar_json_test.exs delete mode 100644 tmp_preview_check.exs diff --git a/lib/bds/generation.ex b/lib/bds/generation.ex index 88d786a..023b20a 100644 --- a/lib/bds/generation.ex +++ b/lib/bds/generation.ex @@ -1355,7 +1355,7 @@ defmodule BDS.Generation do ["sitemap.xml"] -> :core - ["feed.xml"] -> + ["rss.xml"] -> :core ["atom.xml"] -> diff --git a/lib/bds/generation/outputs.ex b/lib/bds/generation/outputs.ex index 3c050c4..5d9600d 100644 --- a/lib/bds/generation/outputs.ex +++ b/lib/bds/generation/outputs.ex @@ -375,7 +375,7 @@ defmodule BDS.Generation.Outputs do main_static_outputs = [ {"404.html", render_not_found_output(plan, language)}, - {"feed.xml", render_feed(plan, language, published_posts)}, + {"rss.xml", render_feed(plan, language, published_posts)}, {"atom.xml", render_atom(plan, language, published_posts)}, {"calendar.json", render_calendar(published_posts)} ] @@ -399,7 +399,7 @@ defmodule BDS.Generation.Outputs do [ {Path.join(localized_language, "404.html"), render_not_found_output(plan, localized_language)}, - {Path.join(localized_language, "feed.xml"), + {Path.join(localized_language, "rss.xml"), render_feed(plan, localized_language, localized_source_posts)}, {Path.join(localized_language, "atom.xml"), render_atom(plan, localized_language, localized_source_posts)} diff --git a/lib/bds/generation/paths.ex b/lib/bds/generation/paths.ex index 12cd9f0..6923f56 100644 --- a/lib/bds/generation/paths.ex +++ b/lib/bds/generation/paths.ex @@ -237,7 +237,7 @@ defmodule BDS.Generation.Paths do @spec sitemap_route_output?(String.t()) :: boolean() def sitemap_route_output?("404.html"), do: false - def sitemap_route_output?("feed.xml"), do: false + def sitemap_route_output?("rss.xml"), do: false def sitemap_route_output?("atom.xml"), do: false def sitemap_route_output?("calendar.json"), do: false def sitemap_route_output?(relative_path), do: String.ends_with?(relative_path, ".html") diff --git a/lib/bds/generation/sitemap.ex b/lib/bds/generation/sitemap.ex index 8fe8d92..86bdbe3 100644 --- a/lib/bds/generation/sitemap.ex +++ b/lib/bds/generation/sitemap.ex @@ -212,13 +212,19 @@ defmodule BDS.Generation.Sitemap do "#{xml_escape(plan.project_name)} (#{xml_escape(language || "default")})#{entries}" end - @doc "Render a JSON calendar of all published posts." + @doc """ + Render the JSON calendar archive consumed by the calendar runtime: + post counts keyed by year ("2024"), month ("2024-05"), and day ("2024-05-17"). + """ @spec render_calendar([map()]) :: String.t() def render_calendar(published_posts) do - published_posts - |> Enum.map(fn post -> - %{date: Paths.local_date_iso8601!(post.created_at), slug: post.slug, title: post.title} - end) + day_keys = Enum.map(published_posts, &Paths.local_date_iso8601!(&1.created_at)) + + %{ + years: Enum.frequencies_by(day_keys, &binary_part(&1, 0, 4)), + months: Enum.frequencies_by(day_keys, &binary_part(&1, 0, 7)), + days: Enum.frequencies(day_keys) + } |> Jason.encode!() end diff --git a/lib/bds/preview.ex b/lib/bds/preview.ex index ab41af3..c10203a 100644 --- a/lib/bds/preview.ex +++ b/lib/bds/preview.ex @@ -201,16 +201,21 @@ defmodule BDS.Preview do query_params: query_params ) + :static -> + serve_file(safe_join(Path.join(server.data_dir, "html"), relative_path), + server: server, + query_params: query_params + ) + :generated -> + # Content is always rendered dynamically; generated files on + # disk go stale and must never be served by the preview. case BDS.Preview.Router.render_route(server.project_id, request_path) do {:ok, response} -> {:ok, apply_response_overrides(response, query_params)} :not_matched -> - serve_file(safe_join(Path.join(server.data_dir, "html"), relative_path), - server: server, - query_params: query_params - ) + render_not_found_response(server.project_id, query_params) end end end @@ -319,6 +324,9 @@ defmodule BDS.Preview do match?(["media" | _], segments) -> {:ok, Path.join(tl(segments)), :media} + match?(["pagefind" | _], segments) -> + {:ok, Path.join(segments), :static} + normalized == "/" -> {:ok, "index.html", :generated} diff --git a/lib/bds/preview/router.ex b/lib/bds/preview/router.ex index b6e4376..8daeb5b 100644 --- a/lib/bds/preview/router.ex +++ b/lib/bds/preview/router.ex @@ -4,6 +4,7 @@ defmodule BDS.Preview.Router do import Ecto.Query alias BDS.Generation.Paths + alias BDS.Generation.Sitemap alias BDS.MapUtils alias BDS.Metadata, as: ProjectMetadata alias BDS.Posts @@ -23,6 +24,9 @@ defmodule BDS.Preview.Router do | {:year, integer(), pos_integer()} | {:month, integer(), integer(), pos_integer()} | {:day, integer(), integer(), integer(), pos_integer()} + | :calendar + | :feed + | :atom | :not_matched @spec render_route(String.t(), String.t()) :: {:ok, map()} | :not_matched @@ -40,6 +44,28 @@ defmodule BDS.Preview.Router do :not_matched -> :not_matched + :calendar -> + posts = load_published_list_posts(project_id, metadata) + {:ok, %{content_type: "application/json", body: Sitemap.render_calendar(posts)}} + + :feed -> + posts = load_published_list_posts(project_id, metadata) + + {:ok, + %{ + content_type: "application/rss+xml", + body: Sitemap.render_feed(feed_plan(metadata), effective_language, posts) + }} + + :atom -> + posts = load_published_list_posts(project_id, metadata) + + {:ok, + %{ + content_type: "application/atom+xml", + body: Sitemap.render_atom(feed_plan(metadata), effective_language, posts) + }} + route -> case render(project_id, route, effective_language, main_language, metadata) do {:ok, body} -> @@ -53,6 +79,9 @@ defmodule BDS.Preview.Router do @spec match_route([String.t()]) :: route() def match_route([]), do: {:home, 1} + def match_route(["calendar.json"]), do: :calendar + def match_route(["rss.xml"]), do: :feed + def match_route(["atom.xml"]), do: :atom def match_route(["page", n]), do: {:home, parse_page(n)} def match_route(["category", name]), do: {:category, URI.decode(name), 1} @@ -130,6 +159,14 @@ defmodule BDS.Preview.Router do ## Rendering + defp feed_plan(metadata) do + %{ + base_url: metadata.public_url, + project_name: metadata.name, + language: metadata.main_language || "en" + } + end + defp render(project_id, {:home, page_number}, language, main_language, metadata) do posts = load_published_list_posts(project_id, metadata) posts = maybe_resolve_language(posts, language, main_language, project_id) diff --git a/mix.exs b/mix.exs index 0648415..6ae8bad 100644 --- a/mix.exs +++ b/mix.exs @@ -28,8 +28,8 @@ defmodule BDS.MixProject do defp deps do [ - {:ecto_sql, "~> 3.13"}, - {:ecto_sqlite3, "~> 0.21"}, + {:ecto_sql, "~> 3.14"}, + {:ecto_sqlite3, "~> 0.24"}, {:luerl, "~> 1.5"}, {:jason, "~> 1.4"}, {:mdex, "~> 0.13"}, diff --git a/mix.lock b/mix.lock index 4a45d22..def39d4 100644 --- a/mix.lock +++ b/mix.lock @@ -9,16 +9,16 @@ "complex": {:hex, :complex, "0.7.0", "695632ef9487517aa5d57edd1697801079d622414cb2e1a7cf538b1f9a50f205", [:mix], [], "hexpm", "0ee39c0803129f546e7f3f640da8f021c9e659402bf59da6f7f2c4848f068f8d"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"}, - "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, + "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, "dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"}, "debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"}, - "decimal": {:hex, :decimal, "2.4.1", "6c0fbede12fb122ba685e9ab41c6a40c129e322b3aa192f9e072e61f3a6ffaf2", [:mix], [], "hexpm", "7e618897933a8455f19a727d7c5e50a2c071a544b700e5e724298ecb4340187f"}, + "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, "desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.45", "cba8369ab2a1342e419bc2760eec731b17be828941dcf494045d44766227e1d5", [:mix], [], "hexpm", "d3ec045bf122965db20c0bdb420e19ee1415843135327124918473feb4b328e8"}, - "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"}, + "ecto": {:hex, :ecto, "3.14.0", "2fa64521eebfcb2670d907a86e4ad947290e9933706bb315e6fb5c21b172cb26", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "130d69ffb4285f9ce4792b65dfbb994fd13ea4cbc3cbea2524b199aa3de84af3"}, + "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.24.1", "26665565f075aaf5f83a76a409ffdef4d2f0a2f3c6840bbf0995993fbea2ad32", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.14", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "681ca576c74a94944b962eeb7e0cf19aaea517decafd3213afb403ac8f4cd2e3"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "emlx": {:hex, :emlx, "0.2.0", "f844c5456a8051032da98276f1e5c2282ff822824b139e4788af6e48375d0e1e", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.10", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "24c674d716beca3daf422829ce7d5fc044981d3d0ca93a832a536016c543dd6f"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, @@ -29,7 +29,7 @@ "ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"}, "exla": {:hex, :exla, "0.10.0", "93e7d75a774fbc06ce05b96de20c4b01bda413b315238cb3c727c09a05d2bc3a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.9.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "16fffdb64667d7f0a3bc683fdcd2792b143a9b345e4b1f1d5cd50330c63d8119"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, - "exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"}, + "exqlite": {:hex, :exqlite, "0.38.0", "324bca1d9b26d34b02ea64bd3724302a7d11978c682bc4a945186aeae7f61adb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f3da7b6e7b08bd548c33a118890d0eb8c5395fe093b31c8b329663234d0e988e"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, @@ -68,7 +68,7 @@ "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"}, - "progress_bar": {:hex, :progress_bar, "3.0.0", "f54ff038c2ac540cfbb4c2bfe97c75e7116ead044f3c2b10c9f212452194b5cd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6981c2b25ab24aecc91a2dc46623658e1399c21a2ae24db986b90d678530f2b7"}, + "progress_bar": {:hex, :progress_bar, "3.1.0", "d277d8f58f90df41ec69adb77027881f817827283b45007ce21b5c77ebe4541e", [:mix], [], "hexpm", "72e8c5f1657b610891de01fb20e8ba7b07a327ca5a10358946f0cd54db5eab34"}, "req": {:hex, :req, "0.6.1", "7b904c8b42d0e08136a5c6aba024fd12fc79a1ed8856e7a3522b0917f7e75113", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "aaf11c9c80f2df2364630b3594e1857fe610d8ea7cb994e1ce3dcb55f204ff1c"}, "rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, diff --git a/specs/generation.allium b/specs/generation.allium index 6dd1335..1a3178d 100644 --- a/specs/generation.allium +++ b/specs/generation.allium @@ -136,8 +136,8 @@ rule GenerateCoreSectionPages { ensures: FileGenerated("index.html") ensures: FileGenerated("sitemap.xml") -- Multi-language sitemap with hreflang alternates - ensures: FileGenerated("feed.xml") - -- RSS 2.0 feed + ensures: FileGenerated("rss.xml") + -- RSS 2.0 feed (same output name as the old bDS app; templates link /rss.xml) ensures: FileGenerated("atom.xml") -- Atom feed ensures: FileGenerated("calendar.json") @@ -146,7 +146,7 @@ rule GenerateCoreSectionPages { -- Not-found page rendered from the not-found template for lang in generation.blog_languages - {generation.language}: ensures: FileGenerated(format("{lang}/index.html", lang: lang)) - ensures: FileGenerated(format("{lang}/feed.xml", lang: lang)) + ensures: FileGenerated(format("{lang}/rss.xml", lang: lang)) ensures: FileGenerated(format("{lang}/atom.xml", lang: lang)) ensures: FileGenerated(format("{lang}/404.html", lang: lang)) } diff --git a/test/bds/generation/calendar_json_test.exs b/test/bds/generation/calendar_json_test.exs new file mode 100644 index 0000000..a846e84 --- /dev/null +++ b/test/bds/generation/calendar_json_test.exs @@ -0,0 +1,53 @@ +defmodule BDS.Generation.CalendarJsonTest do + use ExUnit.Case, async: true + + alias BDS.Generation.Paths + alias BDS.Generation.Sitemap + + defp unix_ms(date_string) do + # Local-noon timestamp so the local-time date matches date_string regardless of TZ. + date = Date.from_iso8601!(date_string) + naive = NaiveDateTime.new!(date, ~T[12:00:00]) + :calendar.local_time_to_universal_time_dst(NaiveDateTime.to_erl(naive)) + |> List.first() + |> :calendar.datetime_to_gregorian_seconds() + |> Kernel.-(62_167_219_200) + |> Kernel.*(1000) + end + + defp post(date_string, slug) do + %{created_at: unix_ms(date_string), slug: slug, title: slug} + end + + test "render_calendar emits years/months/days count maps for the calendar runtime" do + posts = [ + post("2024-05-17", "a"), + post("2024-05-17", "b"), + post("2024-06-01", "c"), + post("2023-12-31", "d") + ] + + decoded = posts |> Sitemap.render_calendar() |> Jason.decode!() + + assert decoded == %{ + "years" => %{"2024" => 3, "2023" => 1}, + "months" => %{"2024-05" => 2, "2024-06" => 1, "2023-12" => 1}, + "days" => %{ + "2024-05-17" => 2, + "2024-06-01" => 1, + "2023-12-31" => 1 + } + } + end + + test "render_calendar of no posts emits empty count maps" do + assert Sitemap.render_calendar([]) |> Jason.decode!() == + %{"years" => %{}, "months" => %{}, "days" => %{}} + end + + test "day keys use the local date used for post routes" do + ts = unix_ms("2024-05-17") + decoded = Sitemap.render_calendar([%{created_at: ts, slug: "a", title: "a"}]) |> Jason.decode!() + assert Map.keys(decoded["days"]) == [Paths.local_date_iso8601!(ts)] + end +end diff --git a/test/bds/generation_test.exs b/test/bds/generation_test.exs index a61091e..c493554 100644 --- a/test/bds/generation_test.exs +++ b/test/bds/generation_test.exs @@ -103,7 +103,7 @@ defmodule BDS.GenerationTest do "404.html", "index.html", "sitemap.xml", - "feed.xml", + "rss.xml", "atom.xml", "calendar.json", "pagefind/index.json", @@ -111,7 +111,7 @@ defmodule BDS.GenerationTest do "pagefind/pagefind-ui.js", "de/404.html", "de/index.html", - "de/feed.xml", + "de/rss.xml", "de/atom.xml", "de/pagefind/index.json", "de/pagefind/pagefind-ui.css", @@ -214,7 +214,7 @@ defmodule BDS.GenerationTest do "https://example.com/blog" <> "/" <> String.trim_trailing(BDS.Generation.post_output_path(published_post), "index.html") - feed_xml = File.read!(Path.join([temp_dir, "html", "feed.xml"])) + feed_xml = File.read!(Path.join([temp_dir, "html", "rss.xml"])) atom_xml = File.read!(Path.join([temp_dir, "html", "atom.xml"])) assert feed_xml =~ "" @@ -854,12 +854,13 @@ defmodule BDS.GenerationTest do assert expected_paths -- Enum.map(result.generated_files, & &1.relative_path) == [] + # Archives paginate newest-first (issue #14), so page 1 holds the newest posts. assert File.read!(Path.join([temp_dir, "html", "category", "notes", "index.html"])) =~ - "Archive 1" + "Archive 3" assert File.read!( Path.join([temp_dir, "html", "category", "notes", "page", "2", "index.html"]) - ) =~ "Archive 3" + ) =~ "Archive 1" assert File.read!(Path.join([temp_dir, "html", "tag", "Elixir", "index.html"])) =~ "Elixir" assert File.read!(Path.join([temp_dir, "html", "2026", "04", "index.html"])) =~ "2026-04" @@ -936,7 +937,7 @@ defmodule BDS.GenerationTest do assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date]) - feed_path = Path.join([temp_dir, "html", "feed.xml"]) + feed_path = Path.join([temp_dir, "html", "rss.xml"]) atom_path = Path.join([temp_dir, "html", "atom.xml"]) not_found_path = Path.join([temp_dir, "html", "404.html"]) pagefind_path = Path.join([temp_dir, "html", "pagefind", "index.json"]) @@ -1146,7 +1147,7 @@ defmodule BDS.GenerationTest do # Ancillary artifacts are all present. assert File.exists?(Path.join([temp_dir, "html", "sitemap.xml"])) - assert File.exists?(Path.join([temp_dir, "html", "feed.xml"])) + assert File.exists?(Path.join([temp_dir, "html", "rss.xml"])) assert File.exists?(Path.join([temp_dir, "html", "calendar.json"])) assert File.exists?(Path.join([temp_dir, "html", "pagefind", "index.json"])) diff --git a/test/bds/preview_test.exs b/test/bds/preview_test.exs index e9fcf4e..39139a1 100644 --- a/test/bds/preview_test.exs +++ b/test/bds/preview_test.exs @@ -170,6 +170,87 @@ defmodule BDS.PreviewTest do assert :ok = BDS.Preview.stop_preview(project.id) end + test "preview renders calendar.json dynamically with count maps, ignoring stale files", + %{project: project, temp_dir: temp_dir} do + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Calendar Post", + content: "Calendar body", + language: "en" + }) + + assert {:ok, _published} = Posts.publish_post(post.id) + + # A stale calendar.json in the old (array) format must not be served. + File.mkdir_p!(Path.join(temp_dir, "html")) + File.write!(Path.join([temp_dir, "html", "calendar.json"]), ~s([{"date":"2000-01-01"}])) + + assert {:ok, _server} = BDS.Preview.start_preview(project.id) + + assert {:ok, %{body: body, content_type: "application/json"}} = + BDS.Preview.request(project.id, "/calendar.json") + + decoded = Jason.decode!(body) + day_key = BDS.Generation.Paths.local_date_iso8601!(Posts.get_post!(post.id).created_at) + + assert decoded["days"] == %{day_key => 1} + assert decoded["months"] == %{binary_part(day_key, 0, 7) => 1} + assert decoded["years"] == %{binary_part(day_key, 0, 4) => 1} + + assert :ok = BDS.Preview.stop_preview(project.id) + end + + test "preview renders content dynamically and never serves generated content files from disk", + %{project: project, temp_dir: temp_dir} do + assert {:ok, _metadata} = + Metadata.update_project_metadata(project.id, %{ + public_url: "https://example.com/blog", + main_language: "en" + }) + + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Dynamic Feed Post", + content: "Feed body", + language: "en" + }) + + assert {:ok, _published} = Posts.publish_post(post.id) + + # Stale generated content files must never be served. + html_dir = Path.join(temp_dir, "html") + File.mkdir_p!(Path.join(html_dir, "pagefind")) + File.write!(Path.join(html_dir, "rss.xml"), "stale feed") + File.write!(Path.join(html_dir, "atom.xml"), "stale atom") + File.write!(Path.join(html_dir, "sitemap.xml"), "stale") + File.write!(Path.join([html_dir, "pagefind", "pagefind-ui.js"]), "console.log('pf')") + + assert {:ok, _server} = BDS.Preview.start_preview(project.id) + + assert {:ok, %{body: feed_xml, content_type: "application/rss+xml"}} = + BDS.Preview.request(project.id, "/rss.xml") + + assert feed_xml =~ "Dynamic Feed Post" + refute feed_xml =~ "stale feed" + + assert {:ok, %{body: atom_xml, content_type: "application/atom+xml"}} = + BDS.Preview.request(project.id, "/atom.xml") + + assert atom_xml =~ "Dynamic Feed Post" + refute atom_xml =~ "stale atom" + + # Content files without a dynamic route render the 404 page instead. + assert {:ok, %{status: 404}} = BDS.Preview.request(project.id, "/sitemap.xml") + + # Generated static assets (pagefind search index) still come from disk. + assert {:ok, %{body: "console.log('pf')", content_type: "application/javascript"}} = + BDS.Preview.request(project.id, "/pagefind/pagefind-ui.js") + + assert :ok = BDS.Preview.stop_preview(project.id) + end + test "draft preview renders through the published post template", %{project: project} do assert {:ok, template} = BDS.Templates.create_template(%{ diff --git a/tmp_preview_check.exs b/tmp_preview_check.exs deleted file mode 100644 index 98b112f..0000000 --- a/tmp_preview_check.exs +++ /dev/null @@ -1,38 +0,0 @@ -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