back to simple blog module, loading posts with elixir map frontmatter, avoiding yaml package
This commit is contained in:
parent
80da842416
commit
f6c316d4fa
11 changed files with 200 additions and 149 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
71
lib/home73k/blog/post.ex
Normal 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
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -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
|
||||||
|
|
20
priv/content/2016/2016-05-01_initial-test-post.md
Normal file
20
priv/content/2016/2016-05-01_initial-test-post.md
Normal 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!')
|
||||||
|
```
|
52
priv/content/2020/08/2020-08-01_enable_vs_cli_env_in_ps.md
Normal file
52
priv/content/2020/08/2020-08-01_enable_vs_cli_env_in_ps.md
Normal 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?
|
39
priv/content/2020/2020-12-29_moms-meatloaf.md
Normal file
39
priv/content/2020/2020-12-29_moms-meatloaf.md
Normal 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!
|
||||||
|
|
Loading…
Reference in a new issue