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