Packages
phoenix_kit
1.7.21
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/modules/publishing/web/settings.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@current_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button (Left aligned) --%>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin", locale: @current_locale)}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon name="hero-arrow-left" class="w-4 h-4 mr-2" /> Back to Dashboard
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">
{gettext("Publishing Settings")}
</h1>
<p class="text-lg text-base-content">
{gettext(
"Create dedicated publishing groups your team can publish to. Each group appears as its own tab in the publishing workspace."
)}
</p>
</div>
</header>
<div class="max-w-2xl mx-auto space-y-6">
<%!-- Language Configuration Information --%>
<div class="border-l-4 border-primary bg-base-200 p-4 rounded-r-lg">
<div class="flex gap-3">
<.icon name="hero-information-circle" class="w-5 h-5 shrink-0 text-primary" />
<div class="text-sm">
<p class="font-semibold mb-1">{gettext("Master Language")}</p>
<p>
{gettext("The")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/settings", locale: @current_locale)
}
class="link link-primary font-medium underline"
>
{gettext("Content Language")}
</.link>
{gettext(
"setting determines the master language for all content. This is the primary language used for versioning—translations in other languages follow the master's version history."
)}
<%= if @languages_enabled do %>
{gettext("Additional languages are configured in the")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/settings/languages",
locale: @current_locale
)
}
class="link link-primary font-medium underline"
>
{gettext("Languages Module")}
</.link>.
<% else %>
{gettext("To enable multi-language support, activate the")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/modules", locale: @current_locale)
}
class="link link-primary font-medium underline"
>
{gettext("Languages Module")}
</.link>.
<% end %>
</p>
</div>
</div>
</div>
<%!-- Publishing Groups Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">
{gettext("Publishing Groups")}
</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Create dedicated publishing groups like Product Updates, Release Notes, or Company News. Each group stores its content in structured folders on disk."
)}
</p>
</div>
<.link
href={Routes.path("/admin/settings/publishing/new", locale: @current_locale)}
class="btn btn-primary btn-sm whitespace-nowrap"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Create Publishing Group")}
</.link>
</div>
<%= if @blogs == [] do %>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">{gettext("No publishing groups yet")}</h3>
<p class="text-sm">
{gettext("Create your first publishing group to start publishing content.")}
</p>
</div>
</div>
<% else %>
<div class="space-y-4">
<%= for blog <- @blogs do %>
<% is_legacy = blog["is_legacy"] == true %>
<% storage_dir = if is_legacy, do: "blogging", else: "publishing" %>
<div class="border border-base-200 rounded-lg p-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div class="space-y-1">
<p class="flex items-center gap-2 text-base font-semibold text-base-content">
<.link
navigate={
Routes.path("/admin/publishing/#{blog["slug"]}",
locale: @current_locale
)
}
class="hover:text-primary transition-colors"
>
{blog["name"]}
</.link>
<span class="badge badge-ghost badge-lg text-xs px-4 py-3 whitespace-nowrap">
<%= if blog["mode"] == "slug" do %>
{gettext("Slug-based")}
<% else %>
{gettext("Timestamp-based")}
<% end %>
</span>
<%= if is_legacy do %>
<span
class="badge badge-warning badge-sm gap-1"
title={gettext("This group uses the legacy storage location")}
>
<.icon name="hero-archive-box" class="w-3 h-3" />
{gettext("Legacy")}
</span>
<% end %>
</p>
<p class="text-xs text-base-content/60">
{gettext("Slug")}: <code class="font-mono break-all">{blog["slug"]}</code>
</p>
<% blog_mode = blog["mode"] || "timestamp" %>
<% blog_path =
if blog_mode == "slug" do
"priv/#{storage_dir}/" <> blog["slug"] <> "/<slug>/<locale>.phk"
else
"priv/#{storage_dir}/" <>
blog["slug"] <> "/<YYYY-MM-DD>/<HH:mm>/<locale>.phk"
end %>
<p class="text-xs text-base-content/50">
{gettext("Files")}: <code class="font-mono break-all">{blog_path}</code>
</p>
</div>
<div class="flex items-center gap-2">
<%= if is_legacy do %>
<button
type="button"
aria-label={gettext("Migrate Storage")}
phx-click="migrate_storage"
phx-value-slug={blog["slug"]}
class="btn btn-warning btn-sm"
title={gettext("Move from priv/blogging to priv/publishing")}
data-confirm={
gettext(
"Migrate this group's storage from priv/blogging to priv/publishing?"
)
}
>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-1" />
{gettext("Migrate")}
</button>
<% end %>
<.link
aria-label={gettext("Open Admin")}
navigate={
Routes.path("/admin/publishing/#{blog["slug"]}", locale: @current_locale)
}
class="btn btn-outline btn-sm btn-square"
>
<.icon name="hero-arrow-right" class="w-4 h-4" />
</.link>
<.link
aria-label={gettext("Edit")}
navigate={
Routes.path(
"/admin/settings/publishing/#{blog["slug"]}/edit",
locale: @current_locale
)
}
class="btn btn-outline btn-sm btn-square"
>
<.icon name="hero-pencil-square" class="w-4 h-4" />
</.link>
<button
type="button"
aria-label={gettext("Delete")}
phx-click="remove_group"
phx-value-slug={blog["slug"]}
class="btn btn-sm btn-square btn-outline text-error hover:bg-error hover:text-error-content"
data-confirm={
gettext(
"Move this group to trash? The directory will be renamed with a timestamp."
)
}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Cache Management Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">
<.icon name="hero-bolt" class="w-6 h-6 inline-block mr-2" />
{gettext("Listing Cache")}
</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Cached listing data speeds up listing pages. Cache is automatically updated when posts change."
)}
</p>
</div>
<%= if @blogs != [] and (@file_cache_enabled or @memory_cache_enabled) do %>
<button
type="button"
phx-click="regenerate_all_caches"
class="btn btn-primary btn-sm whitespace-nowrap"
>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-1" />
{gettext("Regenerate All")}
</button>
<% end %>
</div>
<%!-- Cache Settings Toggles --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%!-- File Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-document" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("File Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Persist cache to JSON files for fast startup")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@file_cache_enabled}
phx-click="toggle_file_cache"
/>
</div>
<%!-- Memory Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-cpu-chip" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("Memory Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Store in :persistent_term for sub-microsecond reads")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@memory_cache_enabled}
phx-click="toggle_memory_cache"
/>
</div>
</div>
<%= if not @file_cache_enabled and not @memory_cache_enabled do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{gettext(
"Both caches are disabled. Listing pages will scan the filesystem on every request."
)}
</span>
</div>
<% end %>
<%= if @blogs == [] do %>
<div class="alert">
<.icon name="hero-information-circle" class="w-5 h-5" />
<span>{gettext("Create a publishing group to manage its cache.")}</span>
</div>
<% else %>
<%= if @file_cache_enabled or @memory_cache_enabled do %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>{gettext("Group")}</th>
<th class="text-center">{gettext("Posts")}</th>
<%= if @file_cache_enabled do %>
<th class="text-center">
<span class="flex items-center justify-center gap-1">
<.icon name="hero-document" class="w-4 h-4" />
{gettext("File")}
</span>
</th>
<% end %>
<%= if @memory_cache_enabled do %>
<th class="text-center">
<span class="flex items-center justify-center gap-1">
<.icon name="hero-cpu-chip" class="w-4 h-4" />
{gettext("Memory")}
</span>
</th>
<% end %>
<th class="text-right">{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<%= for blog <- @blogs do %>
<% cache = @cache_status[blog["slug"]] %>
<tr>
<td class="font-medium">{blog["name"]}</td>
<td class="text-center">
<%= if cache.post_count do %>
<span class="font-mono text-sm">{cache.post_count}</span>
<% else %>
<span class="text-base-content/50">—</span>
<% end %>
</td>
<%= if @file_cache_enabled do %>
<td class="text-center">
<%= if cache.exists do %>
<.icon name="hero-check-circle" class="w-5 h-5 text-success" />
<% else %>
<.icon name="hero-x-circle" class="w-5 h-5 text-base-content/30" />
<% end %>
</td>
<% end %>
<%= if @memory_cache_enabled do %>
<td class="text-center">
<%= if cache.in_memory do %>
<.icon name="hero-check-circle" class="w-5 h-5 text-success" />
<% else %>
<.icon name="hero-x-circle" class="w-5 h-5 text-base-content/30" />
<% end %>
</td>
<% end %>
<td class="text-right">
<div class="flex justify-end gap-2">
<%= if cache.exists or cache.in_memory do %>
<button
type="button"
phx-click="invalidate_cache"
phx-value-slug={blog["slug"]}
class="btn btn-ghost btn-xs text-error"
title={gettext("Clear cache")}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
<button
type="button"
phx-click="regenerate_cache"
phx-value-slug={blog["slug"]}
class="btn btn-ghost btn-xs"
title={gettext("Regenerate cache")}
>
<.icon name="hero-arrow-path" class="w-4 h-4" />
</button>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="text-xs text-base-content/50 space-y-1">
<p>
<.icon name="hero-information-circle" class="w-3 h-3 inline" />
{gettext(
"\"In Memory\" means the cache is loaded into :persistent_term for sub-microsecond reads."
)}
</p>
<p>
<.icon name="hero-arrow-path" class="w-3 h-3 inline" />
{gettext("Regenerate scans all posts and rebuilds the cache file.")}
</p>
</div>
<% end %>
<% end %>
</div>
</div>
<%!-- Render Cache Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-4">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">
<.icon name="hero-document-text" class="w-6 h-6 inline-block mr-2" />
{gettext("Render Cache")}
</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Cached rendered HTML for published posts. Uses content-hash keys so edits auto-invalidate."
)}
</p>
</div>
<%= if @render_cache_enabled do %>
<button
type="button"
phx-click="clear_render_cache"
class="btn btn-outline btn-error btn-sm whitespace-nowrap"
data-confirm={
gettext(
"Clear all cached rendered posts? They will be re-rendered on next view."
)
}
>
<.icon name="hero-trash" class="w-4 h-4 mr-1" />
{gettext("Clear All")}
</button>
<% end %>
</div>
<%!-- Global Render Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-bolt" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("Render Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Cache rendered HTML for published posts (6-hour TTL)")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@render_cache_enabled}
phx-click="toggle_render_cache"
/>
</div>
<%= if not @render_cache_enabled do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{gettext("Render cache is disabled. Posts will be rendered on every request.")}
</span>
</div>
<% else %>
<%!-- Stats Display --%>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Cache Hits")}</div>
<div class="stat-value text-lg font-mono">{@render_cache_stats[:hits] || 0}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Cache Misses")}</div>
<div class="stat-value text-lg font-mono">
{@render_cache_stats[:misses] || 0}
</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Entries Cached")}</div>
<div class="stat-value text-lg font-mono">{@render_cache_stats[:puts] || 0}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Hit Rate")}</div>
<div class="stat-value text-lg font-mono">
<%= if @render_cache_stats[:hit_rate] do %>
{Float.round(@render_cache_stats[:hit_rate] * 100, 1)}%
<% else %>
0%
<% end %>
</div>
</div>
</div>
<%!-- Per-Blog Render Cache Table --%>
<%= if @blogs != [] do %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>{gettext("Group")}</th>
<th class="text-center">{gettext("Enabled")}</th>
<th class="text-right">{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<%= for blog <- @blogs do %>
<% slug = blog["slug"] %>
<% enabled = @render_cache_per_blog[slug] %>
<tr>
<td class="font-medium">{blog["name"]}</td>
<td class="text-center">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked={enabled}
phx-click="toggle_blog_render_cache"
phx-value-slug={slug}
/>
</td>
<td class="text-right">
<%= if enabled do %>
<button
type="button"
phx-click="clear_blog_render_cache"
phx-value-slug={slug}
class="btn btn-ghost btn-xs text-error"
title={gettext("Clear cache for this group")}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<% end %>
<div class="text-xs text-base-content/50">
<p>
<.icon name="hero-information-circle" class="w-3 h-3 inline" />
{gettext(
"Render cache stores pre-rendered HTML for published posts. Cache keys include content hashes, so edits automatically use fresh renders. TTL: 6 hours."
)}
</p>
</div>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>