PhoenixKit.Modules.Publishing.Web.Controller.Listing (PhoenixKitPublishing v0.5.1)

Copy Markdown View Source

Group listing functionality for the publishing controller.

Handles rendering post listings with:

  • Language filtering and fallback
  • Pagination
  • Translation link building for listings

Summary

Functions

The group's published posts for the requested language, title/excerpt-resolved and sorted newest-first — the shared source for the RSS feed and the post page's prev/next navigation. Deliberately independent of the group's listing_sort: feeds and chronological neighbors always mean "newest first".

Fetches group configuration by slug.

Filter posts to only include those that have matching language content. Handles both exact matches and base code matches (e.g., "en" matches "en-US"). Status comes from the post level; the matched language's CONTENT must also be real — the editor's "Add language" creates {title: "Untitled", content: ""} on the post's active version, which is public the instant it commits, so existence-only visibility rendered an Untitled card with an empty preview on that language's listing. Real = a non-default title OR a non-empty excerpt (a title-only translation still lists).

Strict version - only matches exact language, no fallback to base code.

Filters posts to only include published ones. Excludes timestamp-mode posts with a future post_date.

Find a matching language in available languages. Handles exact matches and base code matching.

Gets the page number from params.

Gets the posts per page setting.

The date→count map that decides the date-only vs date+time URL form for timestamp posts, computed over the group's published set across ALL languages — the same basis URL resolution (Posts.list_times_on_date/2) uses. Never build these counts from a language-filtered list: the card would emit a date-only URL whenever the same-day sibling merely lacks the viewer's translation, and that URL resolves to a different post.

The chronological neighbors of a post within its group + language, as %{newer:, older:, date_counts:} — either neighbor nil at the chronology's edge (or when the post isn't in the published set, e.g. a draft preview). date_counts covers the WHOLE published set, so a timestamp neighbor's URL correctly includes its time segment when the date has same-day siblings.

Paginates a list of posts.

Renders the group index page with resolved posts.

Renders the group listing as SEARCH RESULTS for query. Falls back to the normal listing when the group's search_enabled setting is off — a ?q= on a search-disabled group is ignored, not an error. The :ok shape matches render_group_index/3 plus :search_query, with the Featured/Latest bands suppressed and a single results page (capped at 50 matches, newest first).

Renders a category/tag archive: the listing shape (render_group_index/3 fields) with the term's posts, the bands suppressed, and a :term_filter map (%{type:, label:, count:}) for the heading. Single results page, newest first, capped like search.

Resolves posts for the requested language, handling exact match vs fallback.

chronological_posts/3 narrowed to a term scope: nil (whole group), {:category, slug} (the category AND its descendants — WordPress archive rule), or {:tag, tag} (case-insensitive tag match). Returns {:ok, posts, label} where label is the category's translated name (nil for the whole group, the raw tag for tags), or {:error, :not_found} for an unknown category / a tag no published post carries.

Search matches for a group's public listing: a DB substring pass (title + body of active PUBLISHED versions, candidate languages, ILIKE-escaped) intersected with the chronological cache maps — so results carry the same resolved titles/URLs/order as the listing. Returns {posts, date_counts}, the counts computed over the WHOLE published set so a matched timestamp post's URL keeps its time segment when non-matched same-day siblings exist.

Functions

chronological_posts(group_slug, language, limit \\ nil)

The group's published posts for the requested language, title/excerpt-resolved and sorted newest-first — the shared source for the RSS feed and the post page's prev/next navigation. Deliberately independent of the group's listing_sort: feeds and chronological neighbors always mean "newest first".

fetch_group(group_slug)

Fetches group configuration by slug.

filter_by_exact_language(posts, group_slug, language)

Filter posts to only include those that have matching language content. Handles both exact matches and base code matches (e.g., "en" matches "en-US"). Status comes from the post level; the matched language's CONTENT must also be real — the editor's "Add language" creates {title: "Untitled", content: ""} on the post's active version, which is public the instant it commits, so existence-only visibility rendered an Untitled card with an empty preview on that language's listing. Real = a non-default title OR a non-empty excerpt (a title-only translation still lists).

filter_by_exact_language_strict(posts, language)

Strict version - only matches exact language, no fallback to base code.

filter_published(posts)

Filters posts to only include published ones. Excludes timestamp-mode posts with a future post_date.

find_matching_language(language, available_languages)

Find a matching language in available languages. Handles exact matches and base code matching.

A full dialect that is itself ENABLED (a real sibling URL like /en-gb/) never falls back onto a sibling dialect — only onto a literal legacy base row ("en"). Base-code requests and non-enabled dialect requests keep the historical tolerant matching.

get_page_param(params)

Gets the page number from params.

get_per_page_setting()

Gets the posts per page setting.

group_date_counts(group_slug)

The date→count map that decides the date-only vs date+time URL form for timestamp posts, computed over the group's published set across ALL languages — the same basis URL resolution (Posts.list_times_on_date/2) uses. Never build these counts from a language-filtered list: the card would emit a date-only URL whenever the same-day sibling merely lacks the viewer's translation, and that URL resolves to a different post.

neighbor_posts(group_slug, language, post_uuid)

The chronological neighbors of a post within its group + language, as %{newer:, older:, date_counts:} — either neighbor nil at the chronology's edge (or when the post isn't in the published set, e.g. a draft preview). date_counts covers the WHOLE published set, so a timestamp neighbor's URL correctly includes its time segment when the date has same-day siblings.

paginate(posts, page, per_page)

Paginates a list of posts.

render_group_index(conn, ctx, all_posts)

Renders the group index page with resolved posts.

render_group_listing(conn, group_slug, language, params)

Renders a group listing page.

render_search_results(conn, group_slug, language, query, params)

Renders the group listing as SEARCH RESULTS for query. Falls back to the normal listing when the group's search_enabled setting is off — a ?q= on a search-disabled group is ignored, not an error. The :ok shape matches render_group_index/3 plus :search_query, with the Featured/Latest bands suppressed and a single results page (capped at 50 matches, newest first).

render_term_archive(conn, group_slug, language, term)

Renders a category/tag archive: the listing shape (render_group_index/3 fields) with the term's posts, the bands suppressed, and a :term_filter map (%{type:, label:, count:}) for the heading. Single results page, newest first, capped like search.

resolve_listing_posts_for_language(conn, ctx)

Resolves posts for the requested language, handling exact match vs fallback.

scoped_chronological_posts(group_slug, language, arg3)

chronological_posts/3 narrowed to a term scope: nil (whole group), {:category, slug} (the category AND its descendants — WordPress archive rule), or {:tag, tag} (case-insensitive tag match). Returns {:ok, posts, label} where label is the category's translated name (nil for the whole group, the raw tag for tags), or {:error, :not_found} for an unknown category / a tag no published post carries.

search_posts(group_slug, language, query)

Search matches for a group's public listing: a DB substring pass (title + body of active PUBLISHED versions, candidate languages, ILIKE-escaped) intersected with the chronological cache maps — so results carry the same resolved titles/URLs/order as the listing. Returns {posts, date_counts}, the counts computed over the WHOLE published set so a matched timestamp post's URL keeps its time segment when non-matched same-day siblings exist.