Packages
phoenix_kit
1.4.8
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/blogging/blogging.ex
defmodule PhoenixKitWeb.Live.Modules.Blogging do
@moduledoc """
Blogging module for managing site blogs and their posts.
This keeps content in the filesystem while providing an admin-friendly UI
for creating timestamped markdown blog posts.
"""
alias PhoenixKitWeb.Live.Modules.Blogging.Storage
# Delegate language info function to Storage
defdelegate get_language_info(language_code), to: Storage
@enabled_key "blogging_enabled"
@blogs_key "blogging_blogs"
@legacy_categories_key "blogging_categories"
@default_blog_mode "timestamp"
@type blog :: map()
@doc """
Returns true when the blogging module is enabled.
"""
@spec enabled?() :: boolean()
def enabled? do
settings_call(:get_boolean_setting, [@enabled_key, false])
end
@doc """
Enables the blogging module.
"""
@spec enable_system() :: {:ok, any()} | {:error, any()}
def enable_system do
settings_call(:update_boolean_setting, [@enabled_key, true])
end
@doc """
Disables the blogging module.
"""
@spec disable_system() :: {:ok, any()} | {:error, any()}
def disable_system do
settings_call(:update_boolean_setting, [@enabled_key, false])
end
@doc """
Returns all configured blogs.
"""
@spec list_blogs() :: [blog()]
def list_blogs do
case settings_call(:get_json_setting, [@blogs_key, nil]) do
%{"blogs" => blogs} when is_list(blogs) ->
normalize_blogs(blogs)
list when is_list(list) ->
normalize_blogs(list)
_ ->
legacy =
case settings_call(:get_json_setting, [@legacy_categories_key, %{"types" => []}]) do
%{"types" => types} when is_list(types) -> types
other when is_list(other) -> other
_ -> []
end
if legacy != [] do
settings_call(:update_json_setting, [@blogs_key, %{"blogs" => legacy}])
end
normalize_blogs(legacy)
end
end
@doc """
Adds a new blog.
"""
@spec add_blog(String.t(), String.t()) :: {:ok, blog()} | {:error, atom()}
def add_blog(name, mode \\ @default_blog_mode) when is_binary(name) do
trimmed = String.trim(name)
mode = normalize_mode(mode)
cond do
trimmed == "" ->
{:error, :invalid_name}
is_nil(mode) ->
{:error, :invalid_mode}
true ->
blogs = list_blogs()
slug = slugify(trimmed)
if Enum.any?(blogs, &(&1["slug"] == slug)) do
{:error, :already_exists}
else
blog = %{"name" => trimmed, "slug" => slug, "mode" => mode}
updated = blogs ++ [blog]
payload = %{"blogs" => updated}
with {:ok, _} <- settings_call(:update_json_setting, [@blogs_key, payload]),
:ok <- Storage.ensure_blog_root(slug) do
{:ok, blog}
end
end
end
end
@doc """
Removes a blog by slug.
"""
@spec remove_blog(String.t()) :: {:ok, any()} | {:error, any()}
def remove_blog(slug) when is_binary(slug) do
updated =
list_blogs()
|> Enum.reject(&(&1["slug"] == slug))
settings_call(:update_json_setting, [@blogs_key, %{"blogs" => updated}])
end
@doc """
Moves a blog to trash by renaming its directory with timestamp.
The blog is removed from the active blogs list and its directory is renamed to:
BLOGNAME-YYYY-MM-DD-HH-MM-SS
"""
@spec trash_blog(String.t()) :: {:ok, String.t()} | {:error, any()}
def trash_blog(slug) when is_binary(slug) do
with {:ok, _} <- remove_blog(slug),
{:ok, trashed_name} <- Storage.move_blog_to_trash(slug) do
{:ok, trashed_name}
end
end
@doc """
Looks up a blog name from its slug.
"""
@spec blog_name(String.t()) :: String.t() | nil
def blog_name(slug) do
Enum.find_value(list_blogs(), fn blog ->
if blog["slug"] == slug, do: blog["name"]
end)
end
@doc """
Returns the configured storage mode for a blog slug.
"""
@spec get_blog_mode(String.t()) :: String.t()
def get_blog_mode(blog_slug) do
list_blogs()
|> Enum.find(%{}, &(&1["slug"] == blog_slug))
|> Map.get("mode", @default_blog_mode)
end
@doc """
Lists blog posts for a given blog slug.
Accepts optional preferred_language to show titles in user's language.
"""
@spec list_posts(String.t(), String.t() | nil) :: [Storage.post()]
def list_posts(blog_slug, preferred_language \\ nil) do
case get_blog_mode(blog_slug) do
"slug" -> Storage.list_posts_slug_mode(blog_slug, preferred_language)
_ -> Storage.list_posts(blog_slug, preferred_language)
end
end
@doc """
Creates a new blog post for the given blog using the current timestamp.
"""
@spec create_post(String.t(), map() | keyword()) :: {:ok, Storage.post()} | {:error, any()}
def create_post(blog_slug, opts \\ %{}) do
case get_blog_mode(blog_slug) do
"slug" ->
title = fetch_option(opts, :title)
slug = fetch_option(opts, :slug)
Storage.create_post_slug_mode(blog_slug, title, slug)
_ ->
Storage.create_post(blog_slug)
end
end
@doc """
Reads an existing blog post.
"""
@spec read_post(String.t(), String.t(), String.t() | nil) ::
{:ok, Storage.post()} | {:error, any()}
def read_post(blog_slug, identifier, language \\ nil) do
case get_blog_mode(blog_slug) do
"slug" ->
{post_slug, inferred_language} = extract_slug_and_language(blog_slug, identifier)
Storage.read_post_slug_mode(blog_slug, post_slug, language || inferred_language)
_ ->
Storage.read_post(blog_slug, identifier)
end
end
@doc """
Updates a blog post and moves the file if the publication timestamp changes.
"""
@spec update_post(String.t(), Storage.post(), map()) ::
{:ok, Storage.post()} | {:error, any()}
def update_post(blog_slug, post, params) do
mode =
Map.get(post, :mode) ||
Map.get(post, "mode") ||
mode_atom(get_blog_mode(blog_slug))
case mode do
:slug -> Storage.update_post_slug_mode(blog_slug, post, params)
_ -> Storage.update_post(blog_slug, post, params)
end
end
@doc """
Adds a new language file to an existing post.
"""
@spec add_language_to_post(String.t(), String.t(), String.t()) ::
{:ok, Storage.post()} | {:error, any()}
def add_language_to_post(blog_slug, identifier, language_code) do
case get_blog_mode(blog_slug) do
"slug" ->
{post_slug, _} = extract_slug_and_language(blog_slug, identifier)
Storage.add_language_to_post_slug_mode(blog_slug, post_slug, language_code)
_ ->
Storage.add_language_to_post(blog_slug, identifier, language_code)
end
end
# Legacy wrappers (deprecated)
def list_entries(blog_slug, preferred_language \\ nil),
do: list_posts(blog_slug, preferred_language)
def create_entry(blog_slug), do: create_post(blog_slug)
def read_entry(blog_slug, relative_path), do: read_post(blog_slug, relative_path)
def update_entry(blog_slug, post, params), do: update_post(blog_slug, post, params)
def add_language_to_entry(blog_slug, post_path, language_code),
do: add_language_to_post(blog_slug, post_path, language_code)
@doc """
Generates a slug from a user-provided blog name.
"""
@spec slugify(String.t()) :: String.t()
def slugify(name) when is_binary(name) do
name
|> String.downcase()
|> String.replace(~r/[^a-z0-9]+/u, "-")
|> String.trim("-")
|> case do
"" -> "blog"
slug -> slug
end
end
defp settings_module do
Application.get_env(:phoenix_kit, :blogging_settings_module, PhoenixKit.Settings)
end
defp settings_call(fun, args) do
apply(settings_module(), fun, args)
end
defp normalize_blogs(blogs) do
Enum.map(blogs, fn
%{"mode" => mode} = blog when mode in ["timestamp", "slug"] ->
blog
blog ->
Map.put(blog, "mode", @default_blog_mode)
end)
end
defp normalize_mode(mode) when is_binary(mode) do
mode
|> String.downcase()
|> case do
"slug" -> "slug"
"timestamp" -> "timestamp"
_ -> nil
end
end
defp normalize_mode(mode) when is_atom(mode), do: normalize_mode(Atom.to_string(mode))
defp normalize_mode(_), do: nil
defp fetch_option(opts, key) when is_map(opts) do
Map.get(opts, key) || Map.get(opts, Atom.to_string(key))
end
defp fetch_option(opts, key) when is_list(opts) do
if Keyword.keyword?(opts) do
Keyword.get(opts, key)
else
nil
end
end
defp fetch_option(_, _), do: nil
defp mode_atom("slug"), do: :slug
defp mode_atom(_), do: :timestamp
defp extract_slug_and_language(_blog_slug, nil), do: {"", nil}
defp extract_slug_and_language(blog_slug, identifier) do
identifier
|> to_string()
|> String.trim()
|> String.trim_leading("/")
|> String.split("/", trim: true)
|> drop_blog_prefix(blog_slug)
|> case do
[] ->
{"", nil}
[slug] ->
{slug, nil}
[slug | rest] ->
language =
rest
|> List.first()
|> case do
nil -> nil
<<>> -> nil
lang_file -> String.replace_suffix(lang_file, ".phk", "")
end
{slug, language}
end
end
# Only drop blog prefix if there are more elements after it
# This prevents dropping the post slug when it matches the blog slug
defp drop_blog_prefix([blog_slug | rest], blog_slug) when rest != [], do: rest
defp drop_blog_prefix(list, _), do: list
end