defmodule PreDotHn do @moduledoc """ Documentation for `PreDotHn`. """ use Phoenix.Component require Logger import Phoenix.LiveViewTest, only: [rendered_to_string: 1] alias PreDotHn.Frontmatter alias PreDotHn.Markdown def read(path) do path |> File.read!() |> Frontmatter.front_matter_split() |> then(fn {frontmatter_text, body_text} -> {Frontmatter.make_frontmatter(path, frontmatter_text), body_text} end) |> then(fn {frontmatter, body_text} -> body = Markdown.render(body_text) Map.merge(frontmatter, %{"body" => body, "path" => path}) end) end def validate(%{"slug" => nil, "path" => path}) do {:error, "slug missing from #{path}"} end def validate(%{"slug" => "", "path" => path}) do {:error, "slug missing from #{path}"} end def validate(other), do: {:ok, other} def clean() do "priv/static/**" |> Path.wildcard() |> Enum.each(fn path -> if File.dir?(path) do if !File.exists?(Path.join(path, ".keep")) do File.exists?(path) && File.rm_rf!(path) end else File.exists?(path) && File.rm!(path) end end) end def run() do posts = "site/**/*.md" |> Path.wildcard() |> Enum.map(&read/1) |> Enum.map(&validate/1) |> Enum.filter(fn {:error, error} -> Logger.warning(error) false {:ok, _other} -> true end) |> Enum.map(&elem(&1, 1)) |> Enum.sort_by(&Map.get(&1, "date"), :desc) |> Enum.map(&atomize_keys/1) make_feed(posts) make_sitemap(posts) index = make_index(posts) PreDotHn.LinkLog.run() PreDotHn.Static.run() Enum.map([index | posts], &write_page/1) end def atomize_keys(page) do page |> Enum.map(fn {key, value} -> {String.to_atom(key), value} end) |> Enum.into(%{}) end def make_index(posts) do assigns = %{posts: posts} body = ~H""" <%= for post <- @posts do %>
<%= post.date %>  <%= post.title %>
<% end %> """ |> rendered_to_string() %{ title: "Blog", slug: "blog", date: "2023-09-23", body: body } end def make_feed(posts) do posts = posts |> Enum.reject(& &1.hidden) |> Enum.take(10) base = Application.get_env(:pre_dot_hn, :base, "/") assigns = %{base: base, posts: posts} body = EEx.eval_string( """ pre.hn Robert Prehn's personal blog. https://pre.hn https://pre.hn/feed.rss <%= for post <- @posts do %> <%= HtmlSanitizeEx.strip_tags(post.title) %> <%= post.body %> ]]> <%= rss_date_format.(post.date) %> <%= @base %><%= post.slug %>/ <%= @base %><%= post.slug %>/ <% end %> """, assigns: assigns, rss_date_format: &rss_date_format/1 ) path = Path.join(["priv", "static", "feed.rss"]) File.write!(path, body) end @days_of_week %{ 1 => "Mon", 2 => "Tue", 3 => "Wed", 4 => "Thu", 5 => "Fri", 6 => "Sat", 7 => "Sun" } @months %{ 1 => "Jan", 2 => "Feb", 3 => "Mar", 4 => "Apr", 5 => "May", 6 => "Jun", 7 => "Jul", 8 => "Aug", 9 => "Sep", 10 => "Oct", 11 => "Nov", 12 => "Dec" } def rss_date_format(date_string) do date = Date.from_iso8601!(date_string) day = Map.get(@days_of_week, Date.day_of_week(date)) month = Map.get(@months, date.month) "#{day}, #{date.day} #{month} #{date.year} 00:00:00 -0600" end def make_sitemap(posts) do posts |> Stream.reject(fn %{hidden: hidden} -> hidden end) |> Stream.map( &%Sitemapper.URL{lastmod: &1[:updated_at] || &1.date, loc: "https://pre.hn/#{&1.slug}/"} ) |> Sitemapper.generate(sitemap_url: "https://pre.hn/") |> Sitemapper.persist(store: Sitemapper.FileStore, store_config: [path: "priv/static/"]) |> Stream.run() end attr(:title, :string, required: true) attr(:slug, :string, required: true) attr(:description, :string) slot(:inner_block, required: true) def layout(assigns) do ~H""" <%= @title %> | Robert Prehn <%= if assigns[:description] do %> <% end %> ⌨️"} /> <.menu slug={@slug} /> <%= render_slot(@inner_block) %> """ end def menu(assigns) do ~H""" """ end def write_page(%{slug: slug} = assigns) do path = if slug == "index" do Path.join(["priv", "static", "index.html"]) else directory = Path.join(["priv", "static", slug]) File.mkdir_p!(directory) Path.join(["priv", "static", slug, "index.html"]) end ~H""" <.layout title={@title} slug={@slug} description={assigns[:description]}>

<%= @title %>

<%= {:safe, @body} %>
""" |> rendered_to_string() |> then(&File.write!(path, &1)) end end