pre.hn/lib/pre_dot_hn.ex
2024-11-30 13:25:21 -06:00

259 lines
6.9 KiB
Elixir

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 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()
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 %>
<div :if={!post.hidden} style="margin-bottom: 1rem">
<span class="text-yellow align-top"><%= post.date %></span>&nbsp;
<a class="text-ellipsis overflow-hidden inline-block" style="max-width: 40ch" href={"/#{post.slug}/"}><%= post.title %></a>
</div>
<% 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(
"""
<?xml version="1.0" encoding="UTF-8"?>
<feed version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<title>pre.hn</title>
<subtitle>Robert Prehn's personal blog.</subtitle>
<link rel="alternative" type="text/html">https://pre.hn</link>
<link rel="self" type="application/rss+xml">https://pre.hn/feed.rss</link>
<%= for post <- @posts do %>
<entry>
<title><%= HtmlSanitizeEx.strip_tags(post.title) %></title>
<content type="xhtml" xml:lang="en"
xml:base="https://pre.hn/"><![CDATA[<html><body>
<%= post.body %>
</body></html>]]>
</description>
<published><%= rss_date_format.(post.date) %></published>
<id><%= @base %><%= post.slug %>/</id>
<link rel="alternate" type="text/html"><%= @base %><%= post.slug %>/</link>
</entry>
<% end %>
</feed>
""",
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"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= @title %> | Robert Prehn</title>
<%= if assigns[:description] do %>
<meta name="description" content={@description} />
<% end %>
<meta name="author" content="Robert Prehn">
<link rel="alternate" type="application/rss+xml" title="Robert Prehn" href="https://pre.hn/feed.rss">
<link rel="alternate" type="application/rss+xml" title="Robert Prehn: Link Log" href="https://pre.hn/link-log/feed.rss">
<link
rel="icon"
href={"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%221.1em%22 font-size=%2272%22>⌨️</text></svg>"}
/>
<meta property="og:title" content={@title} />
<meta property="og:type" content="article" />
<meta property="og:url" content={"https://pre.hn/#{@slug}/"} />
<meta property="og:image" content="https://pre.hn/images/pre_dot_hn.png"} />
<link rel="stylesheet" href="/assets/app.css">
</head>
<body class="bg-dark font-mono text-light">
<.menu />
<%= render_slot(@inner_block) %>
</body>
</html>
"""
end
def menu(assigns) do
~H"""
<nav class="block bg-blue text-dark flex justify-center">
<div class="container flex ml-[-0.95rem]">
<a class="px-[0.6rem]" href="/">About Me</a>
<span>|</span>
<a class="px-[0.6rem]" href="/blog">Blog</a>
<span>|</span>
<a class="px-[0.6rem]" href="/link-log">Link Log</a>
</div>
</nav>
"""
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]}>
<main class="container mx-auto py-[2.6rem]">
<section class="box border border-light">
<h1><%= @title %></h1>
<%= {:safe, @body} %>
</section>
</main>
</.layout>
"""
|> rendered_to_string()
|> then(&File.write!(path, &1))
end
end