back to simple blog module, loading posts with elixir map frontmatter, avoiding yaml package

This commit is contained in:
Adam Piontek 2021-03-31 23:31:46 -04:00
parent 80da842416
commit f6c316d4fa
11 changed files with 200 additions and 149 deletions

3
.gitignore vendored
View file

@ -39,8 +39,5 @@ npm-debug.log
# for vscode elixir_ls extension files # for vscode elixir_ls extension files
/.elixir_ls /.elixir_ls
# blog content repo folder
/priv/content/
# dev # dev
TODO.md TODO.md

View file

@ -12,10 +12,9 @@ defmodule Home73k.Application do
# Start the PubSub system # Start the PubSub system
{Phoenix.PubSub, name: Home73k.PubSub}, {Phoenix.PubSub, name: Home73k.PubSub},
# Start the Endpoint (http/https) # Start the Endpoint (http/https)
Home73kWeb.Endpoint, Home73kWeb.Endpoint
# Start a worker by calling: Home73k.Worker.start_link(arg) # Start a worker by calling: Home73k.Worker.start_link(arg)
# {Home73k.Worker, arg} # {Home73k.Worker, arg}
Home73k.Blog
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html

View file

@ -1,107 +1,29 @@
defmodule Home73k.Blog do defmodule Home73k.Blog do
use GenServer alias Home73k.Blog.Post
#
# Setup
#
Application.ensure_all_started(:earmark) Application.ensure_all_started(:earmark)
@repo Home73k.Repo.get() @content_path Application.compile_env(:home73k, [:content_repo, :path], "./priv/content")
|> Path.expand()
# posts_paths =
# Client @content_path
# |> Path.join("**/*.md")
|> Path.wildcard()
def start_link(_opts) do posts =
GenServer.start_link(__MODULE__, %{posts: [], tags: [], files: []}, name: __MODULE__) for post_path <- posts_paths do
@external_resource Path.relative_to_cwd(post_path)
Post.parse!(post_path)
end end
def get_posts() do # @posts posts
GenServer.call(__MODULE__, :get_posts) @posts Enum.sort_by(posts, & &1.date, {:desc, NaiveDateTime})
end
def get_files() do @tags posts |> Stream.flat_map(& &1.tags) |> Stream.uniq() |> Enum.sort()
GenServer.call(__MODULE__, :get_files)
end
# def push(pid, element) do def list_posts, do: @posts
# GenServer.cast(pid, {:push, element}) def list_tags, do: @tags
# end
# def pop(pid) do
# GenServer.call(pid, :pop)
# end
# def put(server, key, value) do
# GenServer.cast(server, {:put, key, value})
# end
# def get(server, key) do
# GenServer.call(server, {:get, key})
# end
#
# Server
#
@impl true
def init(state) do
repo_all_paths = @repo.path |> Path.join("**/*.*") |> Path.wildcard()
repo_post_paths = repo_all_paths |> Enum.filter(fn f -> String.ends_with?(f, ".md") end)
repo_file_paths = repo_all_paths |> Enum.filter(fn f -> !String.ends_with?(f, ".md") end)
{:ok, %{state | posts: repo_post_paths, files: repo_file_paths}}
end
@impl true
def handle_call(:get_posts, _from, %{posts: posts} = state) do
{:reply, posts, state}
end
@impl true
def handle_call(:get_files, _from, %{files: files} = state) do
{:reply, files, state}
end
# @impl true
# def handle_call(:pop, _from, [head | tail]) do
# {:reply, head, tail}
# end
# @impl true
# def handle_cast({:push, element}, state) do
# {:noreply, [element | state]}
# end
# def handle_cast({:put, key, value}, state) do
# {:noreply, Map.put(state, key, value)}
# end
# def handle_call({:get, key}, _from, state) do
# {:reply, Map.fetch!(state, key), state}
# end
# alias Home73k.Blog.Post
# @posts_dir Application.compile_env(:home73k, :blog_posts_dir, "posts")
# posts_paths =
# @posts_dir
# |> Path.join("**/*.md")
# |> Path.wildcard()
# |> Enum.sort()
# posts =
# for post_path <- posts_paths do
# @external_resource Path.relative_to_cwd(post_path)
# Post.parse!(post_path)
# end
# @posts Enum.sort_by(posts, & &1.date, {:desc, Date})
# @tags posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()
# def list_posts, do: @posts
# def list_tags, do: @tags
# defmodule NotFoundError do # defmodule NotFoundError do
# defexception [:message, plug_status: 404] # defexception [:message, plug_status: 404]

71
lib/home73k/blog/post.ex Normal file
View file

@ -0,0 +1,71 @@
defmodule Home73k.Blog.Post do
@enforce_keys [:title, :slug, :date, :author, :tags, :summary, :body]
defstruct [:title, :slug, :date, :author, :tags, :summary, :body]
@title_slug_regex ~r/[^a-zA-Z0-9 ]/
def parse!(post_path) do
post_path
|> File.read()
|> parse_raw_file_data()
end
defp parse_raw_file_data({:ok, post_data}) do
post_data
|> String.split("---", parts: 3)
|> parse_split_file_data()
end
defp parse_raw_file_data(_), do: nil
defp parse_split_file_data(["", fm, md]) do
Code.eval_string(fm)
|> parse_summary(md)
end
defp parse_split_file_data(_), do: nil
defp parse_summary({%{summary: summ} = fm, _}, md) do
Earmark.as_html(md)
|> parse_post(Earmark.as_html(summ), fm)
end
defp parse_summary({%{} = fm, _}, md) do
String.split(md, "<!--more-->", parts: 2)
|> parse_summary(fm)
end
defp parse_summary([summ, _] = parts, fm) do
parts
|> Enum.join(" ")
|> Earmark.as_html()
|> parse_post(Earmark.as_html(summ), fm)
end
defp parse_summary(md, fm) do
Earmark.as_html(md)
|> parse_post({:ok, nil, []}, fm)
end
defp parse_title_to_slug(title) do
Regex.replace(@title_slug_regex, title, "")
|> String.replace(" ", "-")
|> String.downcase()
end
defp build_post(main_html, summ_html, fm) do
fm
|> Map.put_new(:slug, parse_title_to_slug(fm.title))
|> Map.put_new(:author, "Author Name")
|> Map.put_new(:tags, [])
|> Map.put(:summary, summ_html)
|> Map.put(:body, main_html)
end
defp parse_post({:ok, main_html, _}, {:ok, summ_html, _}, fm) do
post = build_post(main_html, summ_html, fm)
struct!(__MODULE__, post)
end
defp parse_post(_, _, _), do: nil
end

View file

@ -1,30 +0,0 @@
defmodule Home73kWeb.BlogFileController do
use Home73kWeb, :controller
@moduledoc """
This controller handles path requests that didn't match an existing route,
including the main Plug.Static for the / root path.
To handle this situation, we'll check if the requested resource exists as
a file under the blog content repo folder, by querying the Blog genserve.
If it exists, we'll redirect to the blog content static path under /_
Otherwise, we'll return 404 not found.
"""
alias Home73k.Repo
alias Home73k.Blog
def index(conn, _params) do
# What would be the content path of this requested resource?
content_path = Repo.content_path() |> Path.join(conn.request_path)
# Check if it exists in the Blog's known files
Blog.get_files()
|> Enum.member?(content_path)
|> case do
true -> redirect(conn, to: "/_#{conn.request_path}")
false -> send_resp(conn, 404, "not found")
end
end
end

View file

@ -26,18 +26,6 @@ defmodule Home73kWeb.Endpoint do
gzip: true, gzip: true,
only: ~w(css fonts images js favicon.ico robots.txt DF185CEE29A3D443_public_key.asc) only: ~w(css fonts images js favicon.ico robots.txt DF185CEE29A3D443_public_key.asc)
# Blog static path handler
if File.dir?(Home73k.Repo.content_path()) do
plug Plug.Static,
at: "/_",
from: Home73k.Repo.content_path(),
gzip: false,
only:
Home73k.Repo.content_path()
|> File.ls!()
|> Enum.filter(fn f -> !String.starts_with?(f, ".") end)
end
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
if code_reloading? do if code_reloading? do

View file

@ -43,9 +43,4 @@ defmodule Home73kWeb.Router do
live_dashboard "/dashboard", metrics: Home73kWeb.Telemetry live_dashboard "/dashboard", metrics: Home73kWeb.Telemetry
end end
end end
# Wildcard path for handling Blog Files from repo
scope "/", Home73kWeb do
get "/*path", BlogFileController, :index
end
end end

View file

@ -45,8 +45,6 @@ defmodule Home73k.MixProject do
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:tzdata, "~> 1.1"}, {:tzdata, "~> 1.1"},
{:git_cli, "~> 0.3.0"},
{:yaml_elixir, "~> 2.6"},
{:earmark, "~> 1.4"}, {:earmark, "~> 1.4"},
# Additional packages # Additional packages

View file

@ -0,0 +1,20 @@
---
%{
title: "Markdown for blog posts is nice, says Sebastian",
slug: "markdown-for-blog-posts",
date: ~N[2016-05-01 13:30:00],
author: "Adam Piontek",
tags: ["sample", "demo"],
summary: """
This summary could get long.
We might even have multiple lines!
"""
}
---
All the **Markdown** here...
```javascript
console.log('hello world!')
```

View file

@ -0,0 +1,52 @@
---
%{
title: "Enable Visual Studio CLI environment in PowerShell",
date: ~N[2020-08-01 02:00:00],
author: "Adam Piontek",
tags: ["coding", "tech", "elixir", "windows", "powershell", "scripting"],
}
---
# Enable Visual Studio CLI environment in PowerShell
#coding #tech #elixir #windows #powershell #scripting
My initial problem: I have an elixir project I'm building primarily on linux, but I want it to work on Windows, too, and I'm using [bcrypt](https://github.com/riverrun/bcrypt_elixir), which [needs nmake to compile](https://github.com/riverrun/comeonin/wiki/Requirements#windows) on Windows.
One must install Visual Studio (VS), but that's not enough.<!--more--> Your terminal/PowerShell CLI environment won't know about VS by default. VS includes batch files to set up a dev environment, but if you run them in PowerShell, they bring you into a CMD environment, which is no help if you want to use PowerShell.
Luckily, thanks to [Michael Teter's blog](https://michaelteter.com/2018/07/06/compiling-Elixir-modules-in-Windows.html), and posts on github that mentioned the [VS Setup PS Module](https://github.com/microsoft/vssetup.powershell), I was able to put together a solution that works for me:
The built-in PowerShell (5.1 right now) will load a [profile](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-5.1) from (under user Documents folder): `WindowsPowerShell\Microsoft.PowerShell_profile.ps1` — and PS 7 (pwsh.exe) will load its [profile](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7) from the slightly different (under user Documents folder): `PowerShell\Microsoft.PowerShell_profile.ps1`
For each version where I want the environment set up, I place the following code in the profile:
```powershell
function Invoke-CmdScript {
param(
[String] $scriptName
)
$cmdLine = """$scriptName"" $args & set"
& $Env:SystemRoot\system32\cmd.exe /c $cmdLine |
select-string '^([^=]*)=(.*)$' | foreach-object {
$varName = $_.Matches[0].Groups[1].Value
$varValue = $_.Matches[0].Groups[2].Value
set-item Env:$varName $varValue
}
}
$installationPath = (Get-VSSetupInstance -All | Select-Object InstallationPath).InstallationPath
$vcvars64Path = Join-Path -Path $installationPath -ChildPath "VC\Auxiliary\Build\vcvars64.bat"
if ($installationPath -and (Test-Path -Path $vcvars64Path)) {
Invoke-CmdScript $vcvars64Path
}
```
As Michael explains:
> The function allows you to run a script, the one that makes the build tools visible, but instead of losing the context after that script ends, the settings are propogated back to the calling shell (Powershell).
> The profile then uses that Invoke-CmdScript function to call the vcvars64.bat script...
My addition is just to get the VS installation path from that VS Setup PS Module's `Get-VSSetupInstance` and build the script path from that. Hopefully that will survive future VS upgrades?

View file

@ -0,0 +1,39 @@
---
%{
title: "Mom's Meatloaf",
date: ~N[2020-12-29 01:00:00],
author: "Adam Piontek",
tags: ["food", "recipe", "mealprep", "pandemiceats", "plaguecooking"],
summary: "A meatloaf recipe from a very special mom"
}
---
Preheat oven to 350
So as you know, I just throw my meatloaf together, but here are my estimates based on a bunch of published recipes:
- Put 2 pounds ground beef (80/20) in a large bowl.
- Squirt about 1/2 cup ketchup over it.
- Sprinkle about a tablespoon of Worcestershire sauce over.
- Throw in one egg (or 2, if you feel the need - I usually just use one).
- Put in about 1 teaspoon horseradish
- Put in about 1 teaspoon mustard
- Sprinkle 1 packet of dry onion soup mix over
- I have used bread (2 slices soaked like you're making meatballs and squeezed out) or oats (about 3/4 cup), or sometimes both depending on what I have or need to use up
You don't need any extra salt, everything you put in is pretty salty, grind some pepper on top
If you like you can put in a bit of mild hot sauce like Franks, Crystal, Louisiana hot sauce
Mix and squish everything together with your hands really well (make sure you wash them first!) (I don't think Justine will like this part). (Some recipes say to only mix loosely, I disagree.) Mixture should be nice and soft, but not so soft that you can't form it into a loaf (not as soft as I do for meatballs). I don't use a loaf pan, I shape it into an oval loaf. Do this in the pan you'll bake it in and then you don't have to grease it.
Squirt more ketchup over top, and spread all over the loaf (I use the back of a teaspoon to do this).
I like to bake it in a cast iron skillet, easier to clean and also you can make gravy in it if you like.
Bake at 350 for about an hour and 15 minutes. Internal temp should be 160. Let it stand a few minutes before slicing and serving with the classic accompaniments of mashed potatoes and corn.
If you want to make a gravy, pour off most of the fat from the pan, stir in flour to make a roux, add unsalted broth (beef or chicken or vegetable), cook and scrape the good stuff from the bottom of the pan. Drippings are pretty salty so unsalted liquid is essential, keep adding till flavor suits you.
Well, now you know all my secrets!