Publishing module for managing content groups and their posts.
Database-backed CMS for creating timestamped or slug-based posts with multi-language support and versioning.
Summary
Functions
Adds a new publishing group.
Adds a new language translation to an existing post.
Checks the primary language migration status for a post.
Counts posts on a specific date for a group.
Creates a new version of a slug-mode post by copying from the latest version.
Creates a new post for the given publishing group using the current timestamp.
Creates a new version from an existing version or blank.
Returns true when the given post is a DB-backed post (has a UUID).
Deletes a specific language translation from a post.
Deletes an entire version of a post.
Disables the publishing module. Always writes to the new key.
Enables the publishing module. Always writes to the new key.
Returns true when the publishing module is enabled. Checks new key first, falls back to legacy key.
Finds a post by a previous URL slug (for 301 redirects).
Finds a post by URL slug from the database.
Gets a publishing group by slug.
Returns the configured post mode for a publishing group slug.
Gets the primary language for a specific post from the database.
Returns count of posts by primary_language status.
Alias for count_primary_language_status/1.
Gets the published version number for a post.
Gets the status of a specific version/language.
Looks up a publishing group name from its slug.
Always returns false — DB-only mode has no legacy groups.
Always returns false — DB-only mode has no legacy groups.
Returns all configured publishing groups. Checks new key first, falls back to legacy keys.
Lists posts for a given publishing group slug.
Lists time values for posts on a specific date.
Lists version numbers for a post.
Migrates all posts in a group to use the current global primary_language.
Checks if any posts in a group need primary_language migration.
Returns the preset content types with their default item names.
Publishes a version, making it the only published version.
Dynamic children function for Publishing sidebar tabs.
Reads an existing post.
Reads a post by its database UUID.
Removes a publishing group by slug.
Sets a translation's status and marks it as manually overridden.
Always returns false — auto-versioning is disabled.
Generates a slug from a user-provided group name. Returns empty string if the name contains only invalid characters.
Enqueues an Oban job to translate a post to all enabled languages using AI.
Removes a publishing group. The group is removed from the active groups list and soft-deleted in the database.
Soft-deletes a post by UUID.
Updates a publishing group's display name and slug.
Updates a post in the database.
Updates the primary language for a post. Accepts a post UUID.
Returns true when the slug matches the allowed lowercase letters, numbers, and hyphen pattern, and is not a reserved language code.
Types
@type group() :: map()
Functions
Adds a new publishing group.
Parameters
name- Display name for the groupopts- Keyword list or map with options::mode- Post mode: "timestamp" or "slug" (default: "timestamp"):slug- Optional custom slug, auto-generated from name if nil:type- Content type: "blogging", "faq", "legal", or custom (default: "blogging"):item_singular- Singular name for items (default: based on type, e.g., "post"):item_plural- Plural name for items (default: based on type, e.g., "posts")
Examples
iex> Publishing.add_group("News")
{:ok, %{"name" => "News", "slug" => "news", "mode" => "timestamp", "type" => "blogging", ...}}
iex> Publishing.add_group("FAQ", type: "faq", mode: "slug")
{:ok, %{"name" => "FAQ", "slug" => "faq", "mode" => "slug", "type" => "faq", "item_singular" => "question", ...}}
iex> Publishing.add_group("Recipes", type: "custom", item_singular: "recipe", item_plural: "recipes")
{:ok, %{"name" => "Recipes", ..., "item_singular" => "recipe", "item_plural" => "recipes"}}
@spec add_language_to_post(String.t(), String.t(), String.t(), integer() | nil) :: {:ok, map()} | {:error, any()}
Adds a new language translation to an existing post.
Accepts an optional version parameter to specify which version to add the translation to. If not specified, defaults to the latest version.
Checks the primary language migration status for a post.
Counts posts on a specific date for a group.
See PhoenixKit.Modules.Publishing.ListingCache.count_primary_language_status/1.
@spec create_new_version(String.t(), map(), map(), map() | keyword()) :: {:ok, map()} | {:error, any()}
Creates a new version of a slug-mode post by copying from the latest version.
The new version starts as draft with status: "draft". Content and metadata updates from params are applied to the new version.
Note: For more control over which version to branch from, use create_version_from/5.
Creates a new post for the given publishing group using the current timestamp.
@spec create_version_from( String.t(), String.t(), integer() | nil, map(), map() | keyword() ) :: {:ok, map()} | {:error, any()}
Creates a new version from an existing version or blank.
Parameters
group_slug- The publishing group slugpost_slug- The post slugsource_version- Version to copy from, ornilfor blank versionparams- Optional parameters for the new versionopts- Options including:scopefor audit metadata
Examples
# Create blank version
iex> Publishing.create_version_from("blog", "my-post", nil, %{}, scope: scope)
{:ok, %{version: 3, ...}}
# Branch from version 1
iex> Publishing.create_version_from("blog", "my-post", 1, %{}, scope: scope)
{:ok, %{version: 3, ...}}
Returns true when the given post is a DB-backed post (has a UUID).
@spec delete_language(String.t(), String.t(), String.t(), integer() | nil) :: :ok | {:error, term()}
Deletes a specific language translation from a post.
For versioned posts, specify the version. For legacy posts, version is ignored. Refuses to delete the last remaining language file.
Returns :ok on success or {:error, reason} on failure.
Deletes an entire version of a post.
Archives the version instead of permanent deletion. Refuses to delete the last remaining version or the live version.
Returns :ok on success or {:error, reason} on failure.
Disables the publishing module. Always writes to the new key.
Enables the publishing module. Always writes to the new key.
@spec enabled?() :: boolean()
Returns true when the publishing module is enabled. Checks new key first, falls back to legacy key.
See PhoenixKit.Modules.Publishing.LanguageHelpers.enabled_language_codes/0.
@spec find_by_previous_url_slug(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, :not_found | :cache_miss}
Finds a post by a previous URL slug (for 301 redirects).
@spec find_by_url_slug(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, :not_found | :cache_miss}
Finds a post by URL slug from the database.
See PhoenixKit.Modules.Publishing.ListingCache.find_post_by_path/3.
See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/2.
See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/3.
See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/4.
See PhoenixKit.Modules.Publishing.LanguageHelpers.get_display_code/2.
Gets a publishing group by slug.
Examples
iex> Publishing.get_group("news")
{:ok, %{"name" => "News", "slug" => "news", ...}}
iex> Publishing.get_group("nonexistent")
{:error, :not_found}
Returns the configured post mode for a publishing group slug.
See PhoenixKit.Modules.Publishing.LanguageHelpers.get_language_info/1.
Gets the primary language for a specific post from the database.
See PhoenixKit.Modules.Publishing.LanguageHelpers.get_primary_language/0.
Returns count of posts by primary_language status.
Alias for count_primary_language_status/1.
Gets the published version number for a post.
Gets the status of a specific version/language.
Looks up a publishing group name from its slug.
Always returns false — DB-only mode has no legacy groups.
See PhoenixKit.Modules.Publishing.ListingCache.invalidate/1.
See PhoenixKit.Modules.Publishing.LanguageHelpers.language_enabled?/2.
Always returns false — DB-only mode has no legacy groups.
@spec list_groups() :: [group()]
Returns all configured publishing groups. Checks new key first, falls back to legacy keys.
Lists posts for a given publishing group slug.
Queries the database directly via DBStorage. The optional second argument is accepted for API compatibility but unused.
Lists time values for posts on a specific date.
Lists version numbers for a post.
Migrates all posts in a group to use the current global primary_language.
This updates the primary_language field in the database and regenerates
the listing cache. The migration is idempotent - running it multiple times
is safe and will skip posts that are already at the current primary language.
Returns {:ok, count} where count is the number of posts updated.
See PhoenixKit.Modules.Publishing.LanguageHelpers.order_languages_for_display/2.
See PhoenixKit.Modules.Publishing.LanguageHelpers.order_languages_for_display/3.
Checks if any posts in a group need primary_language migration.
See PhoenixKit.Modules.Publishing.ListingCache.posts_needing_primary_language_migration/1.
@spec preset_types() :: [map()]
Returns the preset content types with their default item names.
Examples
iex> Publishing.preset_types()
[
%{type: "blogging", label: "Blog", item_singular: "post", item_plural: "posts"},
%{type: "faq", label: "FAQ", item_singular: "question", item_plural: "questions"},
%{type: "legal", label: "Legal", item_singular: "document", item_plural: "documents"}
]
Publishes a version, making it the only published version.
- All files in the target version (primary and translations) →
status: "published" - All files in other versions that were "published" →
status: "archived" - Draft/archived files in other versions keep their current status
Options
:source_id- ID of the source (e.g., socket.id) to include in broadcasts, allowing receivers to ignore their own messages
Examples
iex> Publishing.publish_version("blog", "my-post", 2)
:ok
iex> Publishing.publish_version("blog", "my-post", 2, source_id: "phx-abc123")
:ok
iex> Publishing.publish_version("blog", "nonexistent", 1)
{:error, :not_found}
Dynamic children function for Publishing sidebar tabs.
@spec read_post(String.t(), String.t(), String.t() | nil, integer() | nil) :: {:ok, map()} | {:error, any()}
Reads an existing post.
For slug-mode groups, accepts an optional version parameter. If version is nil, reads the latest version.
Reads from the database.
Reads a post by its database UUID.
Resolves the UUID to a group slug and post slug, then delegates to read_post/4.
Invalid version/language params gracefully fall back to latest/primary.
See PhoenixKit.Modules.Publishing.ListingCache.regenerate/1.
Removes a publishing group by slug.
@spec set_translation_status( String.t(), String.t(), integer(), String.t(), String.t() ) :: :ok | {:error, any()}
Sets a translation's status and marks it as manually overridden.
When a translation status is set manually, it will NOT inherit status changes from the primary language when publishing.
Accepts a post UUID or slug as the post identifier.
Examples
iex> Publishing.set_translation_status("blog", "019cce93-...", 2, "es", "draft")
:ok
Always returns false — auto-versioning is disabled.
See PhoenixKit.Modules.Publishing.SlugHelpers.slug_exists?/2.
Generates a slug from a user-provided group name. Returns empty string if the name contains only invalid characters.
@spec translate_post_to_all_languages(String.t(), String.t(), keyword()) :: {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
Enqueues an Oban job to translate a post to all enabled languages using AI.
This creates a background job that will:
- Read the source post in the primary language
- Translate the content to each target language using the AI module
- Create or update translation files for each language
Options
:endpoint_uuid- AI endpoint UUID to use for translation (required if not set in settings):source_language- Source language to translate from (defaults to primary language):target_languages- List of target language codes (defaults to all enabled except source):version- Version number to translate (defaults to latest/published):user_uuid- User UUID for audit trail
Configuration
Set the default AI endpoint for translations:
PhoenixKit.Settings.update_setting("publishing_translation_endpoint_uuid", "endpoint-uuid")Examples
# Translate to all enabled languages using default endpoint
{:ok, job} = Publishing.translate_post_to_all_languages("docs", "019cce93-...")
# Translate with specific endpoint
{:ok, job} = Publishing.translate_post_to_all_languages("docs", "019cce93-...",
endpoint_uuid: "endpoint-uuid"
)
# Translate to specific languages only
{:ok, job} = Publishing.translate_post_to_all_languages("docs", "019cce93-...",
endpoint_uuid: "endpoint-uuid",
target_languages: ["es", "fr", "de"]
)Returns
{:ok, %Oban.Job{}}- Job was successfully enqueued{:error, changeset}- Failed to enqueue job
Removes a publishing group. The group is removed from the active groups list and soft-deleted in the database.
Soft-deletes a post by UUID.
Returns {:ok, post_uuid} on success or {:error, reason} on failure.
Updates a publishing group's display name and slug.
Updates a post in the database.
Updates the primary language for a post. Accepts a post UUID.
Returns true when the slug matches the allowed lowercase letters, numbers, and hyphen pattern, and is not a reserved language code.
Group slugs cannot be language codes (like 'en', 'es', 'fr') to prevent routing ambiguity.
See PhoenixKit.Modules.Publishing.SlugHelpers.validate_slug/1.
See PhoenixKit.Modules.Publishing.SlugHelpers.validate_url_slug/4.