Context for managing posts, likes, tags, and groups.
Provides complete API for the social posts system including CRUD operations,
counter cache management, tag assignment, and group organization.
Comments are now handled by the standalone PhoenixKit.Modules.Comments module.
Features
- Post Management: Create, update, delete, publish, schedule posts
- Like System: Like/unlike posts, check like status
- Comment System: Nested threaded comments with unlimited depth
- Tag System: Hashtag categorization with auto-slugification
- Group System: User collections for organizing posts
- Media Attachments: Multiple images per post with ordering
- Publishing: Draft/public/unlisted/scheduled status management
- Analytics: View tracking (future feature)
Examples
# Create a post
{:ok, post} = Posts.create_post(user_uuid, %{
title: "My First Post",
content: "Hello world!",
type: "post",
status: "draft"
})
# Publish a post
{:ok, post} = Posts.publish_post(post)
# Like a post
{:ok, like} = Posts.like_post(post.uuid, user_uuid)
# Add a comment
{:ok, comment} = Posts.create_comment(post.uuid, user_uuid, %{
content: "Great post!"
})
# Create a group
{:ok, group} = Posts.create_group(user_uuid, %{
name: "Travel Photos",
description: "My adventures"
})
Summary
Functions
Adds a mention to a post.
Adds a post to a group.
Adds multiple posts to a group in a single transaction.
Adds tags to a post.
Attaches media to a post.
Creates a user group.
Creates a new post.
Decrements the comment counter for a post.
Decrements the dislike counter for a post.
Decrements the like counter for a post.
Deletes a group.
Deletes a post and all related data (cascades to media, likes, comments, etc.).
Detaches media from a post.
Detaches media from a post by PostMedia ID.
Disables the Posts module.
User dislikes a post.
Reverts a post to draft status.
Enables the Posts module.
Checks if the Posts module is enabled.
Finds or creates a tag by name.
Gets the current Posts module configuration and stats.
Gets the featured image for a post (PostMedia with position 1).
Gets a single group by ID with optional preloads.
Gets a single group by ID with optional preloads.
Gets a single post by ID with optional preloads.
Gets a single post by ID with optional preloads.
Gets a single post by slug.
Increments the comment counter for a post.
Increments the dislike counter for a post.
Increments the like counter for a post.
Increments the view counter for a post.
User likes a post.
Lists all groups ordered by name.
Lists popular tags by usage count.
Lists all dislikes for a post.
Lists all likes for a post.
Lists media for a post (ordered by position).
Lists mentioned users in a post.
Lists posts with optional filtering and pagination.
Lists posts in a group.
Lists public posts only.
Lists user's groups.
Lists user's posts.
Callback invoked by the Comments module when a comment is created on a post. Increments the post's denormalized comment_count.
Callback invoked by the Comments module when a comment is deleted from a post. Decrements the post's denormalized comment_count.
Parses hashtags from text.
Checks if a user has disliked a post.
Checks if a user has liked a post.
Processes scheduled posts that are ready to be published.
Publishes a post (makes it public).
Removes the featured image from a post.
Removes a mention from a post.
Removes a post from a group.
Removes a tag from a post.
Reorders user's groups.
Reorders media in a post.
Resolves post titles and admin paths for a list of resource IDs.
Schedules a post for future publishing.
Sets the featured image for a post (PostMedia with position 1).
User removes dislike from a post.
User unlikes a post.
Unschedules a post, reverting it to draft status.
Updates a group.
Updates an existing post.
Functions
Adds a mention to a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string) to mentionmention_type- "contributor" or "mention" (default: "mention")
Examples
iex> add_mention_to_post("018e3c4a-...", "019145a1-...", "contributor")
{:ok, %PostMention{}}
Adds a post to a group.
Increments the group's post counter.
Parameters
post_uuid- Post UUID (UUIDv7 string)group_uuid- Group UUID (UUIDv7 string)opts- Options:position- Display position (default: 0)
Examples
iex> add_post_to_group("018e3c4a-...", "018e3c4a-...")
{:ok, %PostGroupAssignment{}}
Adds multiple posts to a group in a single transaction.
Uses on_conflict: :nothing to skip duplicates, then increments the group's
post_count by the actual number of newly inserted rows.
Parameters
post_uuids- List of Post UUIDs (UUIDv7 strings)group_uuid- Group UUID (UUIDv7 string)opts- Options:position- Position in group (default: 0)
Examples
iex> add_posts_to_group(["018e3c4a-...", "018e3c4b-..."], "018e3c4a-...")
{:ok, 2}
Adds tags to a post.
Creates tags if they don't exist, then assigns them to the post. Updates usage counters for tags.
Parameters
post- Post to tagtag_names- List of tag names
Examples
iex> add_tags_to_post(post, ["elixir", "phoenix"])
{:ok, [%PostTag{}, %PostTag{}]}
Attaches media to a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)file_uuid- File UUID (UUIDv7 string, from PhoenixKit.Modules.Storage)opts- Options:position- Display position (default: 1):caption- Image caption
Examples
iex> attach_media("018e3c4a-...", "018e3c4a-...", position: 1)
{:ok, %PostMedia{}}
Creates a user group.
Parameters
user_uuid- Owner UUID (UUIDv7 string)attrs- Group attributes (name, description, etc.)
Examples
iex> create_group("019145a1-...", %{name: "Travel Photos"})
{:ok, %PostGroup{}}
Creates a new post.
Parameters
user_uuid- Owner UUID (UUIDv7 string)attrs- Post attributes (title, content, type, status, etc.)
Examples
iex> create_post("019145a1-...", %{title: "Test", content: "Content", type: "post"})
{:ok, %Post{}}
iex> create_post("019145a1-...", %{title: "", content: ""})
{:error, %Ecto.Changeset{}}
Decrements the comment counter for a post.
Examples
iex> decrement_comment_count(post)
{1, nil}
Decrements the dislike counter for a post.
Examples
iex> decrement_dislike_count(post)
{1, nil}
Decrements the like counter for a post.
Examples
iex> decrement_like_count(post)
{1, nil}
Deletes a group.
Parameters
group- Group to delete
Examples
iex> delete_group(group)
{:ok, %PostGroup{}}
Deletes a post and all related data (cascades to media, likes, comments, etc.).
Parameters
post- Post struct to delete
Examples
iex> delete_post(post)
{:ok, %Post{}}
iex> delete_post(post)
{:error, %Ecto.Changeset{}}
Detaches media from a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)file_uuid- File UUID (UUIDv7 string)
Examples
iex> detach_media("018e3c4a-...", "018e3c4a-...")
{:ok, %PostMedia{}}
Detaches media from a post by PostMedia ID.
Parameters
media_uuid- PostMedia record UUID (UUIDv7 string)
Examples
iex> detach_media_by_uuid("018e3c4a-...")
{:ok, %PostMedia{}}
Disables the Posts module.
Examples
iex> disable_system()
{:ok, %Setting{}}
User dislikes a post.
Creates a dislike record and increments the post's dislike counter. Returns error if user has already disliked the post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> dislike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostDislike{}}
iex> dislike_post("018e3c4a-...", "019145a1-...") # Already disliked
{:error, %Ecto.Changeset{}}
Reverts a post to draft status.
Examples
iex> draft_post(post)
{:ok, %Post{status: "draft"}}
Enables the Posts module.
Examples
iex> enable_system()
{:ok, %Setting{}}
Checks if the Posts module is enabled.
Examples
iex> enabled?()
true
Finds or creates a tag by name.
Automatically generates slug from name. Returns existing tag if slug already exists.
Parameters
name- Tag name (e.g., "Web Development")
Examples
iex> find_or_create_tag("Web Development")
{:ok, %PostTag{name: "Web Development", slug: "web-development"}}
iex> find_or_create_tag("web development") # Same slug
{:ok, %PostTag{name: "Web Development"}} # Returns existing
Gets the current Posts module configuration and stats.
Examples
iex> get_config()
%{enabled: true, total_posts: 42, published_posts: 30, ...}
Gets the featured image for a post (PostMedia with position 1).
Parameters
post_uuid- Post UUID (UUIDv7 string)
Examples
iex> get_featured_image("018e3c4a-...")
%PostMedia{position: 1}
iex> get_featured_image("018e3c4a-...")
nil
Gets a single group by ID with optional preloads.
Returns nil if group not found.
Parameters
id- Group ID (UUIDv7)opts- Options:preload- List of associations to preload (e.g., [:user, :posts])
Examples
iex> get_group("018e3c4a-...")
%PostGroup{}
iex> get_group("018e3c4a-...", preload: [:user])
%PostGroup{user: %User{}}
iex> get_group("nonexistent")
nil
Gets a single group by ID with optional preloads.
Raises Ecto.NoResultsError if group not found.
Parameters
id- Group ID (UUIDv7)opts- Options:preload- List of associations to preload (e.g., [:user, :posts])
Examples
iex> get_group!("018e3c4a-...")
%PostGroup{}
iex> get_group!("018e3c4a-...", preload: [:user])
%PostGroup{user: %User{}}
iex> get_group!("nonexistent")
** (Ecto.NoResultsError)
Gets a single post by ID with optional preloads.
Returns nil if post not found.
Parameters
id- Post ID (UUIDv7)opts- Options:preload- List of associations to preload
Examples
iex> get_post("018e3c4a-...")
%Post{}
iex> get_post("018e3c4a-...", preload: [:user, :media, :tags])
%Post{user: %User{}, media: [...], tags: [...]}
iex> get_post("nonexistent")
nil
Gets a single post by ID with optional preloads.
Raises Ecto.NoResultsError if post not found.
Parameters
id- Post ID (UUIDv7)opts- Options:preload- List of associations to preload
Examples
iex> get_post!("018e3c4a-...")
%Post{}
iex> get_post!("018e3c4a-...", preload: [:user, :media, :tags])
%Post{user: %User{}, media: [...], tags: [...]}
iex> get_post!("nonexistent")
** (Ecto.NoResultsError)
Gets a single post by slug.
Parameters
slug- Post slug (e.g., "my-first-post")opts- Options:preload- List of associations to preload
Examples
iex> get_post_by_slug("my-first-post")
%Post{}
iex> get_post_by_slug("nonexistent")
nil
Increments the comment counter for a post.
Examples
iex> increment_comment_count(post)
{1, nil}
Increments the dislike counter for a post.
Examples
iex> increment_dislike_count(post)
{1, nil}
Increments the like counter for a post.
Examples
iex> increment_like_count(post)
{1, nil}
Increments the view counter for a post.
Examples
iex> increment_view_count(post)
{1, nil}
User likes a post.
Creates a like record and increments the post's like counter. Returns error if user already liked the post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> like_post("018e3c4a-...", "019145a1-...")
{:ok, %PostLike{}}
iex> like_post("018e3c4a-...", "019145a1-...") # Already liked
{:error, %Ecto.Changeset{}}
Lists all groups ordered by name.
Parameters
opts- Options:preload- Associations to preload
Examples
iex> list_groups()
[%PostGroup{}, ...]
Lists popular tags by usage count.
Parameters
limit- Number of tags to return (default: 20)
Examples
iex> list_popular_tags(10)
[%PostTag{usage_count: 150}, %PostTag{usage_count: 120}, ...]
Lists all dislikes for a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_post_dislikes("018e3c4a-...")
[%PostDislike{}, ...]
iex> list_post_dislikes("018e3c4a-...", preload: [:user])
[%PostDislike{user: %User{}}, ...]
Lists all likes for a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_post_likes("018e3c4a-...")
[%PostLike{}, ...]
iex> list_post_likes("018e3c4a-...", preload: [:user])
[%PostLike{user: %User{}}, ...]
Lists media for a post (ordered by position).
Parameters
post_uuid- Post UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_post_media("018e3c4a-...")
[%PostMedia{}, ...]
Lists mentioned users in a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_post_mentions("018e3c4a-...")
[%PostMention{}, ...]
Lists posts with optional filtering and pagination.
Parameters
opts- Options:user_uuid- Filter by user:status- Filter by status (draft/public/unlisted/scheduled):type- Filter by type (post/snippet/repost):search- Search in title and content:page- Page number (default: 1):per_page- Items per page (default: 20):preload- Associations to preload
Examples
iex> list_posts()
[%Post{}, ...]
iex> list_posts(status: "public", page: 1, per_page: 10)
[%Post{}, ...]
iex> list_posts(user_uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890", type: "post")
[%Post{}, ...]
Lists posts in a group.
Parameters
group_uuid- Group UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_posts_by_group("018e3c4a-...")
[%Post{}, ...]
Lists public posts only.
Parameters
opts- Seelist_posts/1for options
Examples
iex> list_public_posts()
[%Post{}, ...]
Lists user's groups.
Parameters
user_uuid- User UUID (UUIDv7 string)opts- Options:preload- Associations to preload
Examples
iex> list_user_groups("019145a1-...")
[%PostGroup{}, ...]
Lists user's posts.
Parameters
user_uuid- User UUID (UUIDv7 string)opts- Seelist_posts/1for options
Examples
iex> list_user_posts("019145a1-...")
[%Post{}, ...]
Callback invoked by the Comments module when a comment is created on a post. Increments the post's denormalized comment_count.
Callback invoked by the Comments module when a comment is deleted from a post. Decrements the post's denormalized comment_count.
Parses hashtags from text.
Extracts all hashtags (#word) from text and returns list of tag names.
Parameters
text- Text to parse
Examples
iex> parse_hashtags("Check out #elixir and #phoenix!")
["elixir", "phoenix"]
iex> parse_hashtags("No tags here")
[]
Checks if a user has disliked a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> post_disliked_by?("018e3c4a-...", "019145a1-...")
true
iex> post_disliked_by?("018e3c4a-...", "019145a2-...")
false
Checks if a user has liked a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> post_liked_by?("018e3c4a-...", "019145a1-...")
true
iex> post_liked_by?("018e3c4a-...", "019145a2-...")
false
Processes scheduled posts that are ready to be published.
Finds all posts with status "scheduled" where scheduled_at <= now, and publishes them. Returns list of published posts.
Should be called periodically (e.g., via Oban job every minute).
Examples
iex> process_scheduled_posts()
{:ok, 2}
Publishes a post (makes it public).
Sets status to "public" and published_at to current time.
Examples
iex> publish_post(post)
{:ok, %Post{status: "public"}}
Removes the featured image from a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)
Examples
iex> remove_featured_image("018e3c4a-...")
{:ok, 1}
iex> remove_featured_image("018e3c4a-...")
{:ok, 0}
Removes a mention from a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> remove_mention_from_post("018e3c4a-...", "019145a1-...")
{:ok, %PostMention{}}
Removes a post from a group.
Decrements the group's post counter.
Parameters
post_uuid- Post UUID (UUIDv7 string)group_uuid- Group UUID (UUIDv7 string)
Examples
iex> remove_post_from_group("018e3c4a-...", "018e3c4a-...")
{:ok, %PostGroupAssignment{}}
Removes a tag from a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)tag_uuid- Tag UUID (UUIDv7 string)
Examples
iex> remove_tag_from_post("018e3c4a-...", "018e3c4a-...")
{:ok, %PostTagAssignment{}}
Reorders user's groups.
Updates position field for multiple groups.
Parameters
user_uuid- User UUID (UUIDv7 string)group_uuid_positions- Map of group_uuid => position
Examples
iex> reorder_groups("019145a1-...", %{"group1" => 0, "group2" => 1})
:ok
Reorders media in a post.
Parameters
post_uuid- Post UUID (UUIDv7 string)file_uuid_positions- Map of file_uuid => position
Examples
iex> reorder_media("018e3c4a-...", %{"file1" => 1, "file2" => 2})
:ok
Resolves post titles and admin paths for a list of resource IDs.
Called by the Comments module to display resource context in the admin UI.
Returns a map of resource_uuid => %{title: ..., path: ...}.
Schedules a post for future publishing.
Updates the post status to "scheduled" and creates an entry in the scheduled jobs table for execution by the cron worker.
Parameters
post- Post to schedulescheduled_at- DateTime to publish at (must be in future)attrs- Additional attributes to update (title, content, etc.)opts- Options:created_by_uuid- UUID of user scheduling the post
Examples
iex> schedule_post(post, ~U[2025-12-31 09:00:00Z])
{:ok, %Post{status: "scheduled"}}
iex> schedule_post(post, ~U[2025-12-31 09:00:00Z], %{title: "New Title"})
{:ok, %Post{status: "scheduled", title: "New Title"}}
Sets the featured image for a post (PostMedia with position 1).
Replaces any existing featured image (position 1) with the new one.
Parameters
post_uuid- Post UUID (UUIDv7 string)file_uuid- File UUID (UUIDv7 string, from PhoenixKit.Modules.Storage)
Examples
iex> set_featured_image("018e3c4a-...", "018e3c4a-...")
{:ok, %PostMedia{position: 1}}
User removes dislike from a post.
Deletes the dislike record and decrements the post's dislike counter. Returns error if dislike doesn't exist.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> undislike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostDislike{}}
iex> undislike_post("018e3c4a-...", "019145a1-...") # Not disliked
{:error, :not_found}
User unlikes a post.
Deletes the like record and decrements the post's like counter. Returns error if like doesn't exist.
Parameters
post_uuid- Post UUID (UUIDv7 string)user_uuid- User UUID (UUIDv7 string)
Examples
iex> unlike_post("018e3c4a-...", "019145a1-...")
{:ok, %PostLike{}}
iex> unlike_post("018e3c4a-...", "019145a1-...") # Not liked
{:error, :not_found}
Unschedules a post, reverting it to draft status.
Cancels any pending scheduled jobs for this post.
Parameters
post- Post to unschedule
Examples
iex> unschedule_post(post)
{:ok, %Post{status: "draft"}}
Updates a group.
Parameters
group- Group to updateattrs- Attributes to update
Examples
iex> update_group(group, %{name: "New Name"})
{:ok, %PostGroup{}}
Updates an existing post.
Parameters
post- Post struct to updateattrs- Attributes to update
Examples
iex> update_post(post, %{title: "Updated Title"})
{:ok, %Post{}}
iex> update_post(post, %{title: ""})
{:error, %Ecto.Changeset{}}