PhoenixKit.Modules.Publishing (phoenix_kit v1.7.71)

Copy Markdown View Source

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.

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

group()

@type group() :: map()

Functions

add_group(name, opts \\ [])

@spec add_group(String.t(), keyword() | map()) :: {:ok, group()} | {:error, atom()}

Adds a new publishing group.

Parameters

  • name - Display name for the group
  • opts - 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"}}

add_language_to_entry(group_slug, post_path, language_code)

add_language_to_post(group_slug, post_uuid, language_code, version \\ nil)

@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.

cache_exists?(group_slug)

See PhoenixKit.Modules.Publishing.ListingCache.exists?/1.

check_primary_language_status(group_slug, post_slug)

Checks the primary language migration status for a post.

count_posts_on_date(group_slug, date)

Counts posts on a specific date for a group.

count_primary_language_status(group_slug)

See PhoenixKit.Modules.Publishing.ListingCache.count_primary_language_status/1.

create_entry(group_slug)

create_new_version(group_slug, source_post, params \\ %{}, opts \\ %{})

@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.

create_post(group_slug, opts \\ %{})

@spec create_post(String.t(), map() | keyword()) :: {:ok, map()} | {:error, any()}

Creates a new post for the given publishing group using the current timestamp.

create_version_from(group_slug, post_uuid, source_version, params \\ %{}, opts \\ %{})

@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 slug
  • post_slug - The post slug
  • source_version - Version to copy from, or nil for blank version
  • params - Optional parameters for the new version
  • opts - Options including :scope for 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, ...}}

db_post?(post)

@spec db_post?(map()) :: boolean()

Returns true when the given post is a DB-backed post (has a UUID).

delete_language(group_slug, post_uuid, language_code, version \\ nil)

@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.

delete_version(group_slug, post_uuid, version)

@spec delete_version(String.t(), String.t(), integer()) :: :ok | {:error, term()}

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.

disable_system()

@spec disable_system() :: {:ok, any()} | {:error, any()}

Disables the publishing module. Always writes to the new key.

enable_system()

@spec enable_system() :: {:ok, any()} | {:error, any()}

Enables the publishing module. Always writes to the new key.

enabled?()

@spec enabled?() :: boolean()

Returns true when the publishing module is enabled. Checks new key first, falls back to legacy key.

enabled_language_codes()

See PhoenixKit.Modules.Publishing.LanguageHelpers.enabled_language_codes/0.

find_by_previous_url_slug(group_slug, language, url_slug)

@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).

find_by_url_slug(group_slug, language, url_slug)

@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.

find_cached_post(group_slug, post_slug)

See PhoenixKit.Modules.Publishing.ListingCache.find_post/2.

find_cached_post_by_path(group_slug, date, time)

See PhoenixKit.Modules.Publishing.ListingCache.find_post_by_path/3.

generate_unique_slug(group_slug, title)

See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/2.

generate_unique_slug(group_slug, title, preferred_slug)

See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/3.

generate_unique_slug(group_slug, title, preferred_slug, opts)

See PhoenixKit.Modules.Publishing.SlugHelpers.generate_unique_slug/4.

get_display_code(language_code, enabled_languages)

See PhoenixKit.Modules.Publishing.LanguageHelpers.get_display_code/2.

get_group(slug)

@spec get_group(String.t()) :: {:ok, group()} | {:error, :not_found}

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}

get_group_mode(group_slug)

@spec get_group_mode(String.t()) :: String.t()

Returns the configured post mode for a publishing group slug.

get_language_info(language_code)

See PhoenixKit.Modules.Publishing.LanguageHelpers.get_language_info/1.

get_post_primary_language(group_slug, post_slug, version \\ nil)

Gets the primary language for a specific post from the database.

get_primary_language()

See PhoenixKit.Modules.Publishing.LanguageHelpers.get_primary_language/0.

get_primary_language_migration_status(group_slug)

@spec get_primary_language_migration_status(String.t()) :: map()

Returns count of posts by primary_language status. Alias for count_primary_language_status/1.

get_published_version(group_slug, post_slug)

Gets the published version number for a post.

get_version_metadata(group_slug, post_slug, version_number, language)

get_version_status(group_slug, post_slug, version_number, language)

Gets the status of a specific version/language.

group_name(slug)

@spec group_name(String.t()) :: String.t() | nil

Looks up a publishing group name from its slug.

has_legacy_groups?()

Always returns false — DB-only mode has no legacy groups.

invalidate_cache(group_slug)

See PhoenixKit.Modules.Publishing.ListingCache.invalidate/1.

language_enabled?(language_code, enabled_languages)

See PhoenixKit.Modules.Publishing.LanguageHelpers.language_enabled?/2.

legacy_group?(group_slug)

Always returns false — DB-only mode has no legacy groups.

list_entries(group_slug, preferred_language \\ nil)

list_groups()

@spec list_groups() :: [group()]

Returns all configured publishing groups. Checks new key first, falls back to legacy keys.

list_posts(group_slug, preferred_language \\ nil)

@spec list_posts(String.t(), String.t() | nil) :: [map()]

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.

list_times_on_date(group_slug, date)

Lists time values for posts on a specific date.

list_versions(group_slug, post_slug)

Lists version numbers for a post.

migrate_posts_to_current_primary_language(group_slug)

@spec migrate_posts_to_current_primary_language(String.t()) ::
  {:ok, integer()} | {:error, any()}

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.

order_languages_for_display(available_languages, enabled_languages)

See PhoenixKit.Modules.Publishing.LanguageHelpers.order_languages_for_display/2.

order_languages_for_display(available_languages, enabled_languages, primary)

See PhoenixKit.Modules.Publishing.LanguageHelpers.order_languages_for_display/3.

posts_need_primary_language_migration?(group_slug)

@spec posts_need_primary_language_migration?(String.t()) :: boolean()

Checks if any posts in a group need primary_language migration.

posts_needing_primary_language_migration(group_slug)

See PhoenixKit.Modules.Publishing.ListingCache.posts_needing_primary_language_migration/1.

preset_types()

@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"}
]

publish_version(group_slug, post_uuid, version, opts \\ [])

@spec publish_version(String.t(), String.t(), integer(), keyword()) ::
  :ok | {:error, any()}

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}

publishing_children(scope)

Dynamic children function for Publishing sidebar tabs.

read_entry(group_slug, relative_path)

read_post(group_slug, identifier, language \\ nil, version \\ nil)

@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.

read_post_by_uuid(post_uuid, language \\ nil, version \\ nil)

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.

regenerate_cache(group_slug)

See PhoenixKit.Modules.Publishing.ListingCache.regenerate/1.

remove_group(slug)

@spec remove_group(String.t()) :: {:ok, any()} | {:error, any()}

Removes a publishing group by slug.

set_translation_status(group_slug, post_identifier, version, language, status)

@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

should_create_new_version?(post, params, editing_language)

Always returns false — auto-versioning is disabled.

slug_exists?(group_slug, post_slug)

See PhoenixKit.Modules.Publishing.SlugHelpers.slug_exists?/2.

slugify(name)

@spec slugify(String.t()) :: String.t()

Generates a slug from a user-provided group name. Returns empty string if the name contains only invalid characters.

translate_post_to_all_languages(group_slug, post_uuid, opts \\ [])

@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:

  1. Read the source post in the primary language
  2. Translate the content to each target language using the AI module
  3. 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

trash_group(slug)

@spec trash_group(String.t()) :: {:ok, String.t()} | {:error, any()}

Removes a publishing group. The group is removed from the active groups list and soft-deleted in the database.

trash_post(group_slug, post_uuid)

@spec trash_post(String.t(), String.t()) :: {:ok, String.t()} | {:error, term()}

Soft-deletes a post by UUID.

Returns {:ok, post_uuid} on success or {:error, reason} on failure.

update_entry(group_slug, post, params)

update_group(slug, params)

@spec update_group(String.t(), map() | keyword()) :: {:ok, group()} | {:error, atom()}

Updates a publishing group's display name and slug.

update_post(group_slug, post, params, opts \\ %{})

@spec update_post(String.t(), map(), map(), map() | keyword()) ::
  {:ok, map()} | {:error, any()}

Updates a post in the database.

update_post_primary_language(group_slug, post_uuid, new_primary_language)

Updates the primary language for a post. Accepts a post UUID.

valid_slug?(slug)

@spec valid_slug?(String.t()) :: boolean()

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.

validate_slug(slug)

See PhoenixKit.Modules.Publishing.SlugHelpers.validate_slug/1.

validate_url_slug(group_slug, url_slug, language, exclude)

See PhoenixKit.Modules.Publishing.SlugHelpers.validate_url_slug/4.