From ce6c9c8e7646d5da99a34a498d02a252c10857e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Thu, 13 Mar 2025 13:56:40 -0400 Subject: [PATCH 1/3] feat(rss): multiple feeds --- lib/mix/tasks/tableau.build.ex | 7 +- lib/tableau.ex | 1 + lib/tableau/config.ex | 2 + lib/tableau/extensions/rss_extension.ex | 141 +++++++++++++----- lib/tableau/extensions/sitemap_extension.ex | 6 +- lib/tableau_dev_server/router.ex | 5 +- .../tableau/extensions/rss_extension_test.exs | 133 +++++++++++++++++ 7 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 test/tableau/extensions/rss_extension_test.exs diff --git a/lib/mix/tasks/tableau.build.ex b/lib/mix/tasks/tableau.build.ex index 53eb54c..e4ef9f2 100644 --- a/lib/mix/tasks/tableau.build.ex +++ b/lib/mix/tasks/tableau.build.ex @@ -15,9 +15,14 @@ defmodule Mix.Tasks.Tableau.Build do token = %{site: %{config: config}, graph: Graph.new()} Mix.Task.run("app.start", ["--preload-modules"]) + dbg(argv) {opts, _argv} = OptionParser.parse!(argv, strict: [out: :string]) - out = Keyword.get(opts, :out, "_site") + out = opts[:out] || config.out_dir + + dbg(config) + dbg(opts) + dbg(out) mods = :code.all_available() diff --git a/lib/tableau.ex b/lib/tableau.ex index 6cc2449..65b9750 100644 --- a/lib/tableau.ex +++ b/lib/tableau.ex @@ -3,6 +3,7 @@ defmodule Tableau do ## Global Site Configuration * `:include_dir` - string - Directory that is just copied to the output directory. Defaults to `extra`. + * `:out_dir` - string - The directory to output your website to. Defaults to `_site`. * `:timezone` - string - Timezone to use when parsing date times. Defaults to `Etc/UTC`. * `:base_path` - string - Development server root. Defaults to '/'. * `:url` - string (required) - The URL of your website. diff --git a/lib/tableau/config.ex b/lib/tableau/config.ex index 37bf410..63238d4 100644 --- a/lib/tableau/config.ex +++ b/lib/tableau/config.ex @@ -7,6 +7,7 @@ defmodule Tableau.Config do :url, base_path: "", include_dir: "extra", + out_dir: "_site", timezone: "Etc/UTC", reload_log: false, converters: [md: Tableau.MDExConverter], @@ -30,6 +31,7 @@ defmodule Tableau.Config do __MODULE__, %{ optional(:include_dir) => str(), + optional(:out_dir) => str(), optional(:timezone) => str(), optional(:reload_log) => bool(), optional(:converters) => keyword(atom()), diff --git a/lib/tableau/extensions/rss_extension.ex b/lib/tableau/extensions/rss_extension.ex index 1f64687..8eb4d94 100644 --- a/lib/tableau/extensions/rss_extension.ex +++ b/lib/tableau/extensions/rss_extension.ex @@ -1,76 +1,137 @@ defmodule Tableau.RSSExtension do @moduledoc """ - Generate RSS data and write to `_site/feed.xml`. + Generate one or more RSS feeds. ## Configuration + Configuration is a keyword list of configuration lists with the key being the name of the XML file to be generated. + - `:enabled` - boolean - Extension is active or not. - `:title` - string (required) - Title of your feed. - `:description` - string (required) - Description of your feed. - - `:language` - string - Language to use in the `` tag. Defaults to "en-us" + - `:language` - string - Language to use in the `` tag. Defaults to "en-us". + - `:include` - keyword list - List of front matter keys and values to include in the feed. If a post has any value in the list, it'll be included. Defaults to all posts. ### Example ```elixir config :tableau, Tableau.RSSExtension, enabled: true, - language: "pt-BR", - title: "My Elixir Devlog", - description: "My Journey on Becoming the Best Elixirist" + feeds: [ + super: [ + enabled: true, + language: "pt-BR", + title: "The Super Feed", + description: "Includes all posts on the site" + ], + feed: [ + enabled: true, + language: "pt-BR", + title: "My Elixir Devlog", + description: "My Journey on Becoming the Best Elixirist", + include: [tags: ["elixir", "erlang"]] # includes any posts that include one of these tags + ], + til: [ + enabled: true, + language: "en-US", + title: "Today I Learned", + description: "Short log of what I learn every day", + include: [category: ["til"]] + ] + ] ``` """ use Tableau.Extension, key: :rss, type: :post_write, priority: 200 import Schematic + @impl Tableau.Extension def config(config) do unify( map(%{ optional(:enabled, true) => bool(), - optional(:language, "en-us") => str(), - title: str(), - description: str() + optional(:feeds) => list(feed_s()) }), config ) end - def run(%{site: %{config: %{url: url}}, posts: posts, extensions: %{rss: %{config: rss}}} = token) do - prelude = - """ - - - - #{HtmlEntities.encode(rss.title)} - #{url} - #{HtmlEntities.encode(rss.description)} - #{rss.language} - Tableau v#{version()} - """ - - # html - items = - for post <- posts, into: "" do + defp feed_s do + tuple([ + atom(), + keyword(%{ + optional(:enabled, true) => bool(), + optional(:language, "en-us") => str(), + optional(:include) => include_s(), + title: str(), + description: str() + }) + ]) + end + + defp keyword(options) do + list(oneof(options)) + end + + defp include_s do + list(tuple([atom(), list()])) + end + + @doc """ + The default filter function that is used when including the `:include` option in a feeds configuration. + """ + def filter(post, include_filter) do + Enum.any?(include_filter, fn {front_matter_key, values_to_accept} -> + Enum.any?(post[front_matter_key], fn front_matter_key -> front_matter_key in values_to_accept end) + end) + end + + @impl Tableau.Extension + def run(%{site: %{config: %{url: url, out_dir: out_dir}}, posts: posts, extensions: %{rss: %{config: feeds}}} = token) do + feeds = + if is_list(feeds) do + feeds + else + [feed: feeds] + end + + for {name, feed} <- feeds, feed[:enabled] do + prelude = """ - - #{HtmlEntities.encode(post.title)} - #{URI.merge(url, post.permalink)} - #{Calendar.strftime(post.date, "%a, %d %b %Y %X %Z")} - #{URI.merge(url, post.permalink)} - - + + + + #{HtmlEntities.encode(feed.title)} + #{url} + #{HtmlEntities.encode(feed.description)} + #{feed.language} + Tableau v#{version()} """ - end - # html - postlude = - """ - - - """ + # html + items = + for post <- posts, feed[:include] == nil or filter(post, feed[:include]), into: "" do + """ + + #{HtmlEntities.encode(post.title)} + #{URI.merge(url, post.permalink)} + #{Calendar.strftime(post.date, "%a, %d %b %Y %X %Z")} + #{URI.merge(url, post.permalink)} + + + """ + end + + # html + postlude = + """ + + + """ - File.mkdir_p!("_site") - File.write!("_site/feed.xml", prelude <> items <> postlude) + File.mkdir_p!(out_dir) + File.write!("#{out_dir}/#{name}.xml", prelude <> items <> postlude) + end {:ok, token} end diff --git a/lib/tableau/extensions/sitemap_extension.ex b/lib/tableau/extensions/sitemap_extension.ex index f2457c0..0377a91 100644 --- a/lib/tableau/extensions/sitemap_extension.ex +++ b/lib/tableau/extensions/sitemap_extension.ex @@ -48,7 +48,7 @@ defmodule Tableau.SitemapExtension do require Logger - def run(%{site: %{config: %{url: root}, pages: pages}} = token) do + def run(%{site: %{config: %{url: root, out_dir: out_dir}, pages: pages}} = token) do urls = for page <- pages, uniq: true do loc = @@ -72,8 +72,8 @@ defmodule Tableau.SitemapExtension do "" ] - File.mkdir_p!("_site") - File.write!("_site/sitemap.xml", xml) + File.mkdir_p!(out_dir) + File.write!("#{out_dir}/sitemap.xml", xml) {:ok, token} rescue diff --git a/lib/tableau_dev_server/router.ex b/lib/tableau_dev_server/router.ex index 279372e..a808589 100644 --- a/lib/tableau_dev_server/router.ex +++ b/lib/tableau_dev_server/router.ex @@ -6,6 +6,7 @@ defmodule TableauDevServer.Router do require Logger @base_path Path.join("/", Application.compile_env(:tableau, [:config, :base_path], "")) + @out_dir Application.compile_env(:tableau, [:config, :out_dir]) @not_found ~s''' Not Found @@ -15,7 +16,7 @@ defmodule TableauDevServer.Router do plug :rerender plug TableauDevServer.IndexHtml - plug Plug.Static, at: @base_path, from: "_site", cache_control_for_etags: "no-cache" + plug Plug.Static, at: @base_path, from: @out_dir, cache_control_for_etags: "no-cache" plug :match plug :dispatch @@ -57,7 +58,7 @@ defmodule TableauDevServer.Router do defp rerender(conn, _) do if conn.request_path != "/ws" do - Mix.Task.rerun("tableau.build", ["--out", "_site"]) + Mix.Task.rerun("tableau.build", ["--out", @out_dir]) end conn diff --git a/test/tableau/extensions/rss_extension_test.exs b/test/tableau/extensions/rss_extension_test.exs new file mode 100644 index 00000000..92cc107 --- /dev/null +++ b/test/tableau/extensions/rss_extension_test.exs @@ -0,0 +1,133 @@ +defmodule Tableau.RSSExtensionTest do + use ExUnit.Case, async: true + + alias Tableau.RSSExtension + + describe "run/1" do + @describetag :tmp_dir + # NOTE: this is deprecated, but we want backwards compatibility for a while + test "creates a single feed", %{tmp_dir: tmp_dir} do + posts = [ + post(1, tags: ["post"]), + post(2, tags: ["til"]), + post(3, tags: ["recipe"]) + ] + + rss_config = %{ + enabled: true, + language: "pt-BR", + title: "My Elixir Devlog", + description: "My Journey on Becoming the Best Elixirist", + include: [tags: ["post"]] + } + + token = %{ + site: %{ + config: %{ + out_dir: tmp_dir, + url: "https://example.com" + } + }, + posts: posts, + extensions: %{ + rss: %{config: rss_config} + } + } + + assert {:ok, _} = RSSExtension.run(token) + + feed_path = Path.join(tmp_dir, "feed.xml") + assert File.exists?(feed_path) + feed_content = File.read!(feed_path) + + assert feed_content =~ "Post 1" + refute feed_content =~ "Post 2" + refute feed_content =~ "Post 3" + end + + test "creates multiple feeds", %{tmp_dir: tmp_dir} do + posts = [ + post(1, tags: ["post"]), + post(2, tags: ["til"]), + post(3, tags: ["recipe"]) + ] + + rss_config = [ + super: %{ + enabled: true, + language: "en-US", + title: "The feed to rule them all", + description: "this is a super feed which comprises all the other feeds" + }, + feed: %{ + enabled: true, + language: "en-US", + title: "My Elixir Devlog", + description: "My Journey on Becoming the Best Elixirist", + include: [tags: ["post"]] + }, + til: %{ + enabled: true, + language: "en-US", + title: "Today I Learned", + description: "Short log of what I learn everyday", + include: [tags: ["til"]] + } + ] + + token = %{ + site: %{ + config: %{ + out_dir: tmp_dir, + url: "https://example.com" + } + }, + posts: posts, + extensions: %{ + rss: %{config: rss_config} + } + } + + assert {:ok, _} = RSSExtension.run(token) + + feed_path = Path.join(tmp_dir, "feed.xml") + assert File.exists?(feed_path) + feed_content = File.read!(feed_path) + + assert feed_content =~ "Post 1" + refute feed_content =~ "Post 2" + refute feed_content =~ "Post 3" + + til_path = Path.join(tmp_dir, "til.xml") + assert File.exists?(til_path) + til_content = File.read!(til_path) + + assert til_content =~ "Post 2" + refute til_content =~ "Post 1" + refute til_content =~ "Post 3" + + super_path = Path.join(tmp_dir, "super.xml") + assert File.exists?(super_path) + super_content = File.read!(super_path) + + assert super_content =~ "Post 1" + assert super_content =~ "Post 2" + assert super_content =~ "Post 3" + end + end + + defp post(idx, overrides) do + base = %{ + title: "Post #{idx}", + permalink: "/posts/post-#{1}", + date: DateTime.utc_now(), + body: """ + ## Welcome to Post #{idx} + + Here, we post like crazy. + """ + } + + Map.merge(base, Map.new(overrides)) + end +end From ba8ab2f294a406e3728eac1a731092120837fce0 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 26 Mar 2025 20:52:30 -0400 Subject: [PATCH 2/3] fixup! feat(rss): multiple feeds --- config/config.exs | 2 + lib/mix/tasks/tableau.build.ex | 5 - lib/tableau/config.ex | 8 +- lib/tableau/extensions/rss_extension.ex | 66 ++++---- lib/tableau_dev_server/router.ex | 2 +- mix.exs | 2 +- mix.lock | 2 +- .../tableau/extensions/rss_extension_test.exs | 151 +++++++++++++----- 8 files changed, 158 insertions(+), 80 deletions(-) diff --git a/config/config.exs b/config/config.exs index 8631e5f..4298a9e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,4 +2,6 @@ import Config config :elixir, :time_zone_database, Tz.TimeZoneDatabase +config :tableau, :config, url: "http://localhost:4999", out_dir: "_site" + import_config "#{Mix.env()}.exs" diff --git a/lib/mix/tasks/tableau.build.ex b/lib/mix/tasks/tableau.build.ex index e4ef9f2..0bf0fc2 100644 --- a/lib/mix/tasks/tableau.build.ex +++ b/lib/mix/tasks/tableau.build.ex @@ -15,15 +15,10 @@ defmodule Mix.Tasks.Tableau.Build do token = %{site: %{config: config}, graph: Graph.new()} Mix.Task.run("app.start", ["--preload-modules"]) - dbg(argv) {opts, _argv} = OptionParser.parse!(argv, strict: [out: :string]) out = opts[:out] || config.out_dir - dbg(config) - dbg(opts) - dbg(out) - mods = :code.all_available() |> Task.async_stream(fn {mod, _, _} -> Module.concat([to_string(mod)]) end) diff --git a/lib/tableau/config.ex b/lib/tableau/config.ex index 63238d4..45f67b0 100644 --- a/lib/tableau/config.ex +++ b/lib/tableau/config.ex @@ -22,10 +22,6 @@ defmodule Tableau.Config do Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{}))) end - defp keyword(value) do - list(tuple([atom(), value])) - end - defp schematic do schema( __MODULE__, @@ -34,8 +30,8 @@ defmodule Tableau.Config do optional(:out_dir) => str(), optional(:timezone) => str(), optional(:reload_log) => bool(), - optional(:converters) => keyword(atom()), - optional(:markdown) => keyword(list()), + optional(:converters) => keyword(values: atom()), + optional(:markdown) => keyword(values: list()), optional(:base_path) => str(), url: str() }, diff --git a/lib/tableau/extensions/rss_extension.ex b/lib/tableau/extensions/rss_extension.ex index 8eb4d94..ed86a52 100644 --- a/lib/tableau/extensions/rss_extension.ex +++ b/lib/tableau/extensions/rss_extension.ex @@ -10,7 +10,8 @@ defmodule Tableau.RSSExtension do - `:title` - string (required) - Title of your feed. - `:description` - string (required) - Description of your feed. - `:language` - string - Language to use in the `` tag. Defaults to "en-us". - - `:include` - keyword list - List of front matter keys and values to include in the feed. If a post has any value in the list, it'll be included. Defaults to all posts. + - `:include` - keyword list - List of front matter keys and values to include in the feed. If a post has any value in the list, it'll be included. Defaults to including everything. + - `:exclude` - keyword list - List of front matter keys and values to exclude from the feed. If a post has any value in the list, it'll be included. Defaults to not excluding anything. ### Example @@ -24,19 +25,19 @@ defmodule Tableau.RSSExtension do title: "The Super Feed", description: "Includes all posts on the site" ], - feed: [ + posts: [ enabled: true, language: "pt-BR", title: "My Elixir Devlog", description: "My Journey on Becoming the Best Elixirist", - include: [tags: ["elixir", "erlang"]] # includes any posts that include one of these tags + exclude: [category: "til"] # excludes posts with this category ], til: [ enabled: true, language: "en-US", title: "Today I Learned", description: "Short log of what I learn every day", - include: [category: ["til"]] + include: [category: "til"] ] ] ``` @@ -48,54 +49,59 @@ defmodule Tableau.RSSExtension do @impl Tableau.Extension def config(config) do unify( - map(%{ - optional(:enabled, true) => bool(), - optional(:feeds) => list(feed_s()) - }), + oneof([ + map(%{enabled: false}), + feed_s(&map/1), + map(%{ + enabled: true, + feeds: keyword(values: feed_s(&keyword/1)) + }) + ]), config ) end - defp feed_s do - tuple([ - atom(), - keyword(%{ - optional(:enabled, true) => bool(), - optional(:language, "en-us") => str(), - optional(:include) => include_s(), - title: str(), - description: str() - }) - ]) - end - - defp keyword(options) do - list(oneof(options)) + defp feed_s(type) do + type.(%{ + optional(:enabled, true) => bool(), + optional(:language, "en-us") => str(), + optional(:include) => include_s(), + optional(:exclude) => include_s(), + title: str(), + description: str() + }) end defp include_s do - list(tuple([atom(), list()])) + keyword(values: list(str())) end @doc """ The default filter function that is used when including the `:include` option in a feeds configuration. """ + def filter(post, include_filter) do Enum.any?(include_filter, fn {front_matter_key, values_to_accept} -> - Enum.any?(post[front_matter_key], fn front_matter_key -> front_matter_key in values_to_accept end) + post[front_matter_key] + |> List.wrap() + |> Enum.any?(fn front_matter_key -> + front_matter_key in List.wrap(values_to_accept) + end) end) end @impl Tableau.Extension def run(%{site: %{config: %{url: url, out_dir: out_dir}}, posts: posts, extensions: %{rss: %{config: feeds}}} = token) do feeds = - if is_list(feeds) do - feeds + if Map.has_key?(feeds, :feeds) do + feeds.feeds else [feed: feeds] end for {name, feed} <- feeds, feed[:enabled] do + feed = Map.new(feed) + prelude = """ @@ -108,9 +114,11 @@ defmodule Tableau.RSSExtension do Tableau v#{version()} """ - # html items = - for post <- posts, feed[:include] == nil or filter(post, feed[:include]), into: "" do + for post <- posts, + feed[:include] == nil || filter(post, feed[:include]), + feed[:exclude] == nil || not filter(post, feed[:exclude]), + into: "" do """ #{HtmlEntities.encode(post.title)} diff --git a/lib/tableau_dev_server/router.ex b/lib/tableau_dev_server/router.ex index a808589..8e236ac 100644 --- a/lib/tableau_dev_server/router.ex +++ b/lib/tableau_dev_server/router.ex @@ -6,7 +6,7 @@ defmodule TableauDevServer.Router do require Logger @base_path Path.join("/", Application.compile_env(:tableau, [:config, :base_path], "")) - @out_dir Application.compile_env(:tableau, [:config, :out_dir]) + @out_dir Application.compile_env(:tableau, [:config, :out_dir], "_site") @not_found ~s''' Not Found diff --git a/mix.exs b/mix.exs index f6be486..2284170 100644 --- a/mix.exs +++ b/mix.exs @@ -38,7 +38,7 @@ defmodule Tableau.MixProject do {:html_entities, "~> 0.5.2"}, {:libgraph, "~> 0.16.0"}, {:mdex, "~> 0.2.0"}, - {:schematic, "~> 0.4"}, + {:schematic, "~> 0.5"}, {:tz, "~> 0.28.1"}, {:web_dev_utils, "~> 0.3"}, {:websock_adapter, "~> 0.5"}, diff --git a/mix.lock b/mix.lock index 8dd9d6d..d2d535f 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, - "schematic": {:hex, :schematic, "0.4.0", "2b3c4865c919bb9392251aba4198982f7be8785bc37c732ccfe672a3e19e4591", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "692747901601f4b511171fbd3a4032bf13d84dc81d52e827f041373ce2035588"}, + "schematic": {:hex, :schematic, "0.5.0", "e9df4e9ad2074ad803cc82612cdfddaa57fbded9698b27f94e9d3aeb0b48e88e", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6cc50951999548506b9f99bfd5c707af70624ec2c4cb85a772da1be12be1b06"}, "styler": {:hex, :styler, "1.1.1", "ccb55763316915b5de532bf14c587c211ddc86bc749ac676e74dfacd3894cc0d", [:mix], [], "hexpm", "80ce12fb862e13d998589eea7c1932f4e6ce9d6ded2182cb322f8f9b2b8d3632"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, diff --git a/test/tableau/extensions/rss_extension_test.exs b/test/tableau/extensions/rss_extension_test.exs index 92cc107..186314c 100644 --- a/test/tableau/extensions/rss_extension_test.exs +++ b/test/tableau/extensions/rss_extension_test.exs @@ -44,36 +44,35 @@ defmodule Tableau.RSSExtensionTest do refute feed_content =~ "Post 2" refute feed_content =~ "Post 3" end + end - test "creates multiple feeds", %{tmp_dir: tmp_dir} do + describe "multiple feeds" do + @describetag :tmp_dir + setup %{tmp_dir: tmp_dir} do posts = [ post(1, tags: ["post"]), - post(2, tags: ["til"]), - post(3, tags: ["recipe"]) + post(2, tags: ["til"], category: "important"), + post(3, tags: ["recipe"], category: "casual") ] - rss_config = [ - super: %{ - enabled: true, - language: "en-US", - title: "The feed to rule them all", - description: "this is a super feed which comprises all the other feeds" - }, - feed: %{ - enabled: true, - language: "en-US", - title: "My Elixir Devlog", - description: "My Journey on Becoming the Best Elixirist", - include: [tags: ["post"]] - }, - til: %{ - enabled: true, - language: "en-US", - title: "Today I Learned", - description: "Short log of what I learn everyday", - include: [tags: ["til"]] - } - ] + rss_config = %{ + enabled: true, + feeds: [ + super: %{ + enabled: true, + language: "en-US", + title: "The feed to rule them all", + description: "this is a super feed which comprises all the other feeds" + }, + til: %{ + enabled: true, + language: "en-US", + title: "Today I Learned", + description: "Short log of what I learn everyday", + include: [tags: ["til"]] + } + ] + } token = %{ site: %{ @@ -88,8 +87,31 @@ defmodule Tableau.RSSExtensionTest do } } + [token: token] + end + + test "include works with various frontmatter", %{token: token, tmp_dir: tmp_dir} do + token = + put_in(token.extensions.rss.config.feeds, + feed: %{ + enabled: true, + language: "en-US", + title: "My Elixir Devlog", + description: "My Journey on Becoming the Best Elixirist", + include: [tags: ["post"]] + }, + casual: %{ + enabled: true, + language: "en-US", + title: "Casual posts", + description: "", + include: [category: "casual"] + } + ) + assert {:ok, _} = RSSExtension.run(token) + # only contains posts tagged as "post" feed_path = Path.join(tmp_dir, "feed.xml") assert File.exists?(feed_path) feed_content = File.read!(feed_path) @@ -97,22 +119,77 @@ defmodule Tableau.RSSExtensionTest do assert feed_content =~ "Post 1" refute feed_content =~ "Post 2" refute feed_content =~ "Post 3" + # only contains posts with category as "casual" + casual_path = Path.join(tmp_dir, "casual.xml") + assert File.exists?(casual_path) + casual_content = File.read!(casual_path) + + refute casual_content =~ "Post 2" + refute casual_content =~ "Post 1" + assert casual_content =~ "Post 3" + end + + test "includes everything if there is no includes key", %{token: token, tmp_dir: tmp_dir} do + token = + put_in(token.extensions.rss.config.feeds, + feed: %{ + enabled: true, + language: "en-US", + title: "My Elixir Devlog", + description: "My Journey on Becoming the Best Elixirist" + } + ) + + assert {:ok, _} = RSSExtension.run(token) + + # contains all posts + feed_path = Path.join(tmp_dir, "feed.xml") + assert File.exists?(feed_path) + feed_content = File.read!(feed_path) + + assert feed_content =~ "Post 1" + assert feed_content =~ "Post 2" + assert feed_content =~ "Post 3" + end + + test "excludes various frontmatter", %{token: token, tmp_dir: tmp_dir} do + token = + put_in(token.extensions.rss.config.feeds, + not_posts: %{ + enabled: true, + language: "en-US", + title: "", + description: "", + exclude: [tags: ["post"]] + }, + not_casual: %{ + enabled: true, + language: "en-US", + title: "", + description: "", + exclude: [category: "casual"] + } + ) + + assert {:ok, _} = RSSExtension.run(token) - til_path = Path.join(tmp_dir, "til.xml") - assert File.exists?(til_path) - til_content = File.read!(til_path) + # only contains posts not tagged as "post" + not_posts_path = Path.join(tmp_dir, "not_posts.xml") + assert File.exists?(not_posts_path) + not_posts_content = File.read!(not_posts_path) - assert til_content =~ "Post 2" - refute til_content =~ "Post 1" - refute til_content =~ "Post 3" + refute not_posts_content =~ "Post 1" + assert not_posts_content =~ "Post 2" + assert not_posts_content =~ "Post 3" - super_path = Path.join(tmp_dir, "super.xml") - assert File.exists?(super_path) - super_content = File.read!(super_path) + # only contains posts without category as "bar" + not_casual_path = Path.join(tmp_dir, "not_casual.xml") + assert File.exists?(not_casual_path) + not_casual_content = File.read!(not_casual_path) - assert super_content =~ "Post 1" - assert super_content =~ "Post 2" - assert super_content =~ "Post 3" + assert not_casual_content =~ "Post 2" + assert not_casual_content =~ "Post 1" + refute not_casual_content =~ "Post 3" end end From 6da07fc487e4c74246f8c7d1dc012d776b0191a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 26 Mar 2025 21:00:46 -0400 Subject: [PATCH 3/3] fixup! feat(rss): multiple feeds --- lib/tableau/extensions/rss_extension.ex | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/tableau/extensions/rss_extension.ex b/lib/tableau/extensions/rss_extension.ex index ed86a52..d3c3ce5 100644 --- a/lib/tableau/extensions/rss_extension.ex +++ b/lib/tableau/extensions/rss_extension.ex @@ -76,20 +76,6 @@ defmodule Tableau.RSSExtension do keyword(values: list(str())) end - @doc """ - The default filter function that is used when including the `:include` option in a feeds configuration. - """ - - def filter(post, include_filter) do - Enum.any?(include_filter, fn {front_matter_key, values_to_accept} -> - post[front_matter_key] - |> List.wrap() - |> Enum.any?(fn front_matter_key -> - front_matter_key in List.wrap(values_to_accept) - end) - end) - end - @impl Tableau.Extension def run(%{site: %{config: %{url: url, out_dir: out_dir}}, posts: posts, extensions: %{rss: %{config: feeds}}} = token) do feeds = @@ -150,4 +136,14 @@ defmodule Tableau.RSSExtension do _ -> "dev" end end + + defp filter(post, include_filter) do + Enum.any?(include_filter, fn {front_matter_key, values_to_accept} -> + post[front_matter_key] + |> List.wrap() + |> Enum.any?(fn front_matter_key -> + front_matter_key in List.wrap(values_to_accept) + end) + end) + end end