NimblePublisher
NimblePublisher — это минималистичный движок для публикации текстов на основе файловой системы с поддержкой Markdown и подсветкой кода.
Зачем использовать NimblePublisher?
NimblePublisher — простая библиотека, разработанная для публикации контента, который парсится из локальных файлов с использованием синтаксиса Markdown. Типичным вариантом использования является создание блога.
Эта библиотека инкапсулирует большую часть кода, который Dashbit использует для своего собственного блога, как представлено в их посте Добро пожаловать в наш блог: как это было сделано!, где они объясняют, почему они решили парсить контент из локальных файлов вместо использования базы данных или более сложной CMS.
Создание вашего контента
Давайте создадим свой блог. В нашем примере мы используем приложение на Phoenix’е, но Phoenix не является обязательным. Поскольку NimblePublisher занимается только парсингом локальных файлов, вы можете использовать его в любом приложении Elixir.
Сначала давайте создадим новое приложение Phoenix для нашего примера. Назовем проект NimbleSchool и создадим его с флагом –no-ecto, потому что нам не нужен Ecto в данном приложении:
mix phx.new nimble_school --no-ecto
Теперь давайте добавим несколько постов. Нам нужно создать директорию, в которой будут наши посты. Мы будем хранить их по годам в следующем формате:
/priv/posts/YEAR/MONTH-DAY-ID.md
Например, начнем с этих двух постов:
/priv/posts/2020/10-28-hello-world.md
/priv/posts/2020/11-04-exciting-news.md
Типичная запись в блоге будет написана с использованием синтаксиса Markdown, с разделом метаданных вверху и содержимым ниже, разделенным символами ---
, например:
%{
title: "Привет, Мир!",
author: "Джейми Иньеста",
tags: ~w(hello),
description: "Наш первый блог пост"
}
---
Да, это **тот пост**, которого вы ждали.
Я позволю вам проявить творческий подход к написанию собственных постов. Просто убедитесь, что вы следуете указанному выше формату метаданных и контента.
Разместив эти записи, давайте установим NimblePublisher, чтобы мы могли проанализировать контент и создать наш контекст Блог
.
Установка NimblePublisher
Сначала добавьте nimble_publisher
как зависимость. Вы можете по желанию включить подсветку синтаксиса, в нашем случае мы добавим поддержку подсветки кода Elixir и Erlang.
В нашем Phoenix приложении необходимо добавить следующий код в mix.exs
:
defp deps do
[
...,
{:nimble_publisher, "~> 0.1.1"},
{:makeup_elixir, ">= 0.0.0"},
{:makeup_erlang, ">= 0.0.0"}
]
end
После того, как вы запустили mix deps.get
для установки зависимостей, вы готовы продолжить создание блога.
Создание контекста блога
Мы определим структуру Post
, в которой будет содержимое парсинга файлов. Она будет ожидать ключ для каждого ключа метаданных, а также дата :date
из имени файла. Создайте файл lib/nimble_school/blog/post.ex
со следующим содержимым:
defmodule NimbleSchool.Blog.Post do
@enforce_keys [:id, :author, :title, :body, :description, :tags, :date]
defstruct [:id, :author, :title, :body, :description, :tags, :date]
def build(filename, attrs, body) do
[year, month_day_id] = filename |> Path.rootname() |> Path.split() |> Enum.take(-2)
[month, day, id] = String.split(month_day_id, "-", parts: 3)
date = Date.from_iso8601!("#{year}-#{month}-#{day}")
struct!(__MODULE__, [id: id, date: date, body: body] ++ Map.to_list(attrs))
end
end
Модуль Post
определяет структуру для метаданных и контента, а также функцию build/3
с логикой, необходимой для парсинга файла с содержимым поста.
С помощью структуры Post
мы можем определить наш контекст Blog
, который будет использовать NimblePublisher для парсинга локальных файлов в посты. Создайте lib/nimble_school/blog/blog.ex
со следующим содержимым:
defmodule NimbleSchool.Blog do
alias NimbleSchool.Blog.Post
use NimblePublisher,
build: Post,
from: Application.app_dir(:nimble_school, "priv/posts/**/*.md"),
as: :posts,
highlighters: [:makeup_elixir, :makeup_erlang]
# The @posts variable is first defined by NimblePublisher.
# Let's further modify it by sorting all posts by descending date.
@posts Enum.sort_by(@posts, & &1.date, {:desc, Date})
# Let's also get all tags
@tags @posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()
# And finally export them
def all_posts, do: @posts
def all_tags, do: @tags
end
Как видите, контекст Blog
использует NimblePublisher для создания коллекции Post
из указанного локального каталога, используя подсветку синтаксиса, которую мы хотим использовать.
NimblePublisher создаст переменную @posts
, которую мы позже обработаем для сортировки постов по убыванию :date
, как мы обычно делаем в блоге.
Мы также определяем переменную @tags
, беря её значение из @posts
.
Наконец, мы определяем функции all_posts/0
и all_tags/0
, которые будут возвращать только то, что было распаршено соответственно.
Давайте попробуем! Войдите в консоль с помощью команды iex -S mix
и запустите:
iex(1)> NimbleSchool.Blog.all_posts()
[
%NimbleSchool.Blog.Post{
author: "Джейми Иньеста",
body: "<p>\nОтлично, это наш второй пост в нашем замечательном блоге.</p>\n",
date: ~D[2020-11-04],
description: "Второй блог пост",
id: "exciting-news",
tags: ["exciting", "news"],
title: "Захватывающие новости!"
},
%NimbleSchool.Blog.Post{
author: "Джейми Иньеста",
body: "<p>\nДа, это <strong>тот пост</strong>, которого вы ждали.</p>\n",
date: ~D[2020-10-28],
description: "Наш первый блог пост",
id: "hello-world",
tags: ["hello"],
title: "Привет, Мир!"
}
]
iex(2)> NimbleSchool.Blog.all_tags()
["exciting", "hello", "news"]
Разве это не здорово? Все наши посты распаршены с интерпретацией Markdown и готовы к работе. И теги тоже!
Важно отметить, что NimblePublisher занимается разбором файлов и созданием переменной @posts
со всеми из них, а вы берете данные из этой переменной, чтобы создать нужные вам функции. Например, если нам нужна функция для получения последних постов, мы можем определить ее следующим образом:
def recent_posts(num \\ 5), do: Enum.take(all_posts(), num)
Как вы можете видеть, мы избежали использования @posts
внутри нашей новой функции и вместо этого вызвали функцию all_posts()
. В противном случае компилятор вычислил бы переменную @posts
дважды, создав полную копию всех постов.
Давайте определим еще несколько функций, чтобы получить наш полный пример блога. Нам нужно будет получить пост по его идентификатору, а также перечислить все посты для заданного тега. Определите следующие функции внутри контекста Blog
:
defmodule NotFoundError, do: defexception [:message, plug_status: 404]
def get_post_by_id!(id) do
Enum.find(all_posts(), &(&1.id == id)) ||
raise NotFoundError, "post with id=#{id} not found"
end
def get_posts_by_tag!(tag) do
case Enum.filter(all_posts(), &(tag in &1.tags)) do
[] -> raise NotFoundError, "posts with tag=#{tag} not found"
posts -> posts
end
end
Отображение вашего контента
Теперь, когда у нас есть способ получить все наши посты и теги, для отображения блога необходима обвязка в виде маршрутов, контроллеров, представлений и шаблонов, как мы обычно это делаем в приложении Phoenix. Для этого примера мы сделаем все просто: перечислим все посты и получим пост по его идентификатору. Читателю остается в качестве упражнения перечислить посты по тегу и расширить макет недавними постами.
Маршрутизация
Определите следующие маршруты в lib/nimble_school_web/router.ex
:
scope "/", NimbleSchoolWeb do
pipe_through :browser
get "/blog", BlogController, :index
get "/blog/:id", BlogController, :show
end
Контроллер
В файлеlib/nimble_school_web/controllers/blog_controller.ex
создайте контроллер для постов:
defmodule NimbleSchoolWeb.BlogController do
use NimbleSchoolWeb, :controller
alias NimbleSchool.Blog
def index(conn, _params) do
render(conn, "index.html", posts: Blog.all_posts())
end
def show(conn, %{"id" => id}) do
render(conn, "show.html", post: Blog.get_post_by_id!(id))
end
end
Отображение
Создайте вспомогательный модуль, необходимый для рендеринга представлений в
lib/nimble_school_web/controllers/blog_html.ex
.
На данный момент модуль будет иметь следующий вид:
defmodule NimbleSchoolWeb.BlogHTML do
use NimbleSchoolWeb, :html
embed_templates "blog_html/*"
end
Шаблон
Наконец, создайте HTML-файлы для отображения контента. В файле lib/nimble_school_web/controllers/blog_html/index.html.heex
определите шаблон для отображения списка блог-постов:
<h1>Все посты</h1>
<%= for post <- @posts do %>
<div id="{post.id}" style="margin-bottom: 3rem;">
<h2>
<.link href={~p"/blog/#{post.id}"}><%= post.title %></.link>
</h2>
<p>
<time><%= post.date %></time> by <%= post.author %>
</p>
<p>
Теги <%= Enum.join(post.tags, ", ") %>
</p>
<%= raw post.description %>
</div>
<% end %>
И создайте файл lib/nimble_school_web/controllers/blog_html/show.html.heex
для отображения одного блог-поста:
<.link href={~p"/blog"}>← Все посты</.link>
<h1><%= @post.title %></h1>
<p>
<time><%= @post.date %></time> by <%= @post.author %>
</p>
<p>
Тэги <%= Enum.join(@post.tags, ", ") %>
</p>
<%= raw @post.body %>
Просмотр ваших постов
Вы готовы к работе!
Запустите веб-сервер с помощью команды iex -S mix phx.server
и откройте страницу http://localhost:4000/blog, чтобы увидеть ваш новый блог в действии!
Расширение метаданных
NimblePublisher очень гибок, когда дело доходит до определения структуры и метаданных наших постов. Например, предположим, что мы хотим добавить ключ :published
, чтобы пометить посты, и показывать только те, где этот ключ true
.
Нам просто нужно добавить ключ :published
в структуру Post
, а также в метаданные постов. В модуле Blog
мы можем определить:
def all_posts, do: @posts
def published_posts, do: Enum.filter(all_posts(), &(&1.published == true))
def recent_posts(num \\ 5), do: Enum.take(published_posts(), num)
Подсветка синтаксиса
NimblePublisher использует библиотеку Makeup для подсветки синтаксиса. Вам нужно будет сгенерировать классы CSS для стилей, которые вы предпочитаете из перечисленных здесь.
Например, мы будем использовать :tango_style
. Из сеанса iex -S mix
вызовите:
Makeup.stylesheet(:tango_style, "makeup") |> IO.puts()
И поместите сгенерированные CSS-классы в свои таблицы стилей.
Отображение другого контента
NimblePublisher также можно использовать для создания других контекстов публикации с другой структурой.
Например, мы могли бы управлять коллекцией часто задаваемых вопросов (FAQ). В этом случае нам, вероятно, не понадобятся даты или авторы, и более простая структура с :id
, :question
и :answer
была бы просто замечательной.
Мы могли бы разместить наши файлы с контентом в другой директории, например:
/priv/faqs/is-there-a-free-trial.md
/priv/faqs/when-did-it-start.md
И мы могли бы определить нашу структуру Faq
и построить функцию в lib/nimble_school/faqs/faq.ex
следующим образом:
defmodule NimbleSchool.Faqs.Faq do
@enforce_keys [:id, :question, :answer]
defstruct [:id, :question, :answer]
def build(filename, attrs, body) do
[id] = filename |> Path.rootname() |> Path.split() |> Enum.take(-1)
struct!(__MODULE__, [id: id, answer: body] ++ Map.to_list(attrs))
end
end
Наш контекст Faqs
в lib/nimble_school/faqs/faqs.ex
будет выглядеть примерно так:
defmodule NimbleSchool.Faqs do
alias NimbleSchool.Faqs.Faq
use NimblePublisher,
build: Faq,
from: Application.app_dir(:nimble_school, "priv/faqs/*.md"),
as: :faqs
# The @faqs variable is first defined by NimblePublisher.
# Let's further modify it by sorting all posts by ascending question
@faqs Enum.sort_by(@faqs, & &1.question)
# And finally export them
def all_faqs, do: @faqs
end
Исходный код для примера блога
Код этого примера вы можете найти в https://github.com/jaimeiniesta/nimble_school
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!