PhoenixKit.Modules.Publishing.Web.HTML (PhoenixKitPublishing v0.6.0)

Copy Markdown View Source

HTML rendering functions for Publishing.Web.Controller.

Summary

Functions

Pre-computes date counts for timestamp-mode posts to avoid per-post DB queries.

Builds a post URL based on mode. Omits the locale prefix when the site is effectively single-language. Can also omit the default-language prefix when that setting is enabled.

Builds a public path with explicit date and time (always includes time). Used when redirecting from date-only URLs to full timestamp URLs.

Builds language data for the publishing_language_switcher component on public pages. Converts the @translations assign to the format expected by the component.

Extracts and renders an excerpt from post content. Returns content before <!-- more --> tag, or first paragraph if no tag. Renders markdown and strips HTML tags for plain text display.

Resolves a featured image URL for a post, falling back to the original variant.

Builds the public URL for a group's RSS feed (…/<group>/feed.xml) or a term archive's (…/<group>/category/<slug>/feed.xml, …/tag/<tag>/feed.xml), with the same locale-prefix rules as group_listing_path/3.

Whether group RSS feeds are served (publishing_feeds_enabled, default on). Read by the controller's feed branch and by the listing's autodiscovery link.

Formats a date for display using locale-aware month names.

Formats a date for URL.

Formats a date with time for display. Used when multiple posts exist on the same date.

Formats a post's publication date, including time only when multiple posts exist on the same date.

Formats time for URL (HH:MM).

Builds the public URL for a group listing page. Omits the locale prefix when the site is effectively single-language. Can also omit the default-language prefix when that setting is enabled.

Checks if a post has a publication date to display. For timestamp mode, the date comes from the DB fields. For slug mode, it comes from metadata.published_at.

Drives <Gallery motion="scroll"> where the browser has no scroll-driven animations.

Renders the JSON-LD Article structured-data script for a post page, built from the same :og map the meta tags use (title/description/image/url are already override- and plugin-refined there) plus the post's publish date. Rendered in-page for the same reason as the OG tags: search engines read body placement, and the host owns <head>.

The right-side slide-out panels for "panel" notes style. CSS-only: hidden off-canvas until the panel is the URL :target (the note ref in the body links to it); the backdrop and ✕ link back to the ref, which both closes the panel and returns the reader to the annotated phrase. Public so the editor preview can render the same panels (with commenting off) — preview must match production.

Renders the OpenGraph + Twitter Card meta tags for a public page from the :og map the controller builds.

Pluralizes a word based on count.

Renders a post's publication date (calendar icon + formatted date).

Resolves the exact language-switcher code to highlight on public pages.

Renders a slim heading-anchor rail on post pages: a fixed side rail with a tick per <h2>/<h3>/<h4> in the article; hover reveals the heading text, click smooth-scrolls to it, and the current section is highlighted as you scroll. Built client-side (assigns heading ids in the browser, so the cached render pipeline is untouched) as a real <nav> of links — keyboard and screen-reader accessible. Hidden on narrow screens; honors reduced-motion.

Renders a thin reading-progress bar fixed to the top of the viewport that fills as the reader scrolls the article. Decorative (aria-hidden), pointer transparent, and honors prefers-reduced-motion.

Renders a date-timeline rail on the group listing: a fixed side rail with a marker per distinct year found across the rendered post cards (which carry a data-post-date); click a year to smooth-scroll to its first post, and the current year highlights as you scroll. Only appears when 2+ years are present. Built client-side as an accessible <nav> of links; hidden on narrow screens.

Emits a <style> that recolors the page's native scrollbar to the daisyUI theme when the group opts into "branded"/"thin". "default" renders nothing (the browser's native bar is untouched). Only recolors/resizes the real scrollbar — scrolling stays native.

Public URL for a category archive (…/<group>/category/<slug>) or tag archive (…/<group>/tag/<tag>).

Functions

build_date_counts(posts)

Pre-computes date counts for timestamp-mode posts to avoid per-post DB queries.

Returns a map of %{date_string => count} for use with build_post_url/4 and format_post_date/3.

build_post_url(group_slug, post, language, date_counts \\ nil)

Builds a post URL based on mode. Omits the locale prefix when the site is effectively single-language. Can also omit the default-language prefix when that setting is enabled.

For slug mode posts, uses the language-specific URL slug (from post.url_slug or post.language_slugs[language]) for SEO-friendly localized URLs.

For timestamp mode posts:

  • If only one post exists on the date, uses date-only URL (e.g., /group/2025-12-09)
  • If multiple posts exist on the date, includes time (e.g., /group/2025-12-09/16:26)

build_public_path_with_time(language, group_slug, date, time)

Builds a public path with explicit date and time (always includes time). Used when redirecting from date-only URLs to full timestamp URLs.

build_public_translations(translations, current_language)

Builds language data for the publishing_language_switcher component on public pages. Converts the @translations assign to the format expected by the component.

extract_excerpt(content)

Extracts and renders an excerpt from post content. Returns content before <!-- more --> tag, or first paragraph if no tag. Renders markdown and strips HTML tags for plain text display.

feed_path(language, group_slug, scope \\ nil)

Builds the public URL for a group's RSS feed (…/<group>/feed.xml) or a term archive's (…/<group>/category/<slug>/feed.xml, …/tag/<tag>/feed.xml), with the same locale-prefix rules as group_listing_path/3.

feeds_enabled?()

Whether group RSS feeds are served (publishing_feeds_enabled, default on). Read by the controller's feed branch and by the listing's autodiscovery link.

format_date(datetime)

Formats a date for display using locale-aware month names.

format_date_for_url(datetime)

Formats a date for URL.

format_date_with_time(datetime)

Formats a date with time for display. Used when multiple posts exist on the same date.

format_post_date(post, group_slug, date_counts \\ nil)

Formats a post's publication date, including time only when multiple posts exist on the same date.

format_time_for_url(datetime)

Formats time for URL (HH:MM).

group_listing_path(language, group_slug, params \\ [])

Builds the public URL for a group listing page. Omits the locale prefix when the site is effectively single-language. Can also omit the default-language prefix when that setting is enabled.

has_publication_date?(post)

Checks if a post has a publication date to display. For timestamp mode, the date comes from the DB fields. For slug mode, it comes from metadata.published_at.

helix_scroll_fallback(assigns)

Drives <Gallery motion="scroll"> where the browser has no scroll-driven animations.

The gallery expresses scroll-linking in pure CSS (animation-timeline), and that is the real implementation — this does nothing at all where it works. But support is only around 84% of browsers: Firefox has it from 156, Safari from 26. Everywhere else the @supports gate falls through to the drift animation, which is a perfectly good gallery but silently not the feature that was asked for.

So this is enhancement in the strict sense: no JS still gives you a turning helix, a modern browser gives you the CSS one, and only the gap in between is filled here.

It works by pausing the drift animations and setting their currentTime from scroll position rather than re-implementing the geometry. Every card's phase already lives in its own negative delay, so a single virtual clock driven by scroll reproduces the exact arrangement CSS would have produced — there is no second copy of the maths to drift out of step with the first.

index(assigns)

jsonld_script(assigns)

Renders the JSON-LD Article structured-data script for a post page, built from the same :og map the meta tags use (title/description/image/url are already override- and plugin-refined there) plus the post's publish date. Rendered in-page for the same reason as the OG tags: search engines read body placement, and the host owns <head>.

Attributes

  • og (:map) - Defaults to nil.
  • post (:map) (required)
  • language (:string) (required)

note_panels(assigns)

The right-side slide-out panels for "panel" notes style. CSS-only: hidden off-canvas until the panel is the URL :target (the note ref in the body links to it); the backdrop and ✕ link back to the ref, which both closes the panel and returns the reader to the annotated phrase. Public so the editor preview can render the same panels (with commenting off) — preview must match production.

og_meta_tags(assigns)

Renders the OpenGraph + Twitter Card meta tags for a public page from the :og map the controller builds.

These are emitted in-page (inside the rendered body) so a social preview works out of the box even when the host's root layout doesn't render the forwarded :og assign in <head> — most host apps ship their own root layout. The same :og map is ALSO forwarded via module_assigns, so a host that does render it in <head> gets the strictly-correct placement; such a host disables the in-page copy via publishing_render_og_tags to avoid duplicate tags. Renders nothing when :og is absent (e.g. the groups index).

Attributes

  • og (:map) - Defaults to nil.

pluralize(count, singular, plural)

Pluralizes a word based on count.

post_date(assigns)

Renders a post's publication date (calendar icon + formatted date).

Attributes

  • post (:map) (required)
  • group_slug (:string) (required)
  • class (:any) - Defaults to nil.

public_current_language(translations, fallback)

Resolves the exact language-switcher code to highlight on public pages.

reading_headings(assigns)

Renders a slim heading-anchor rail on post pages: a fixed side rail with a tick per <h2>/<h3>/<h4> in the article; hover reveals the heading text, click smooth-scrolls to it, and the current section is highlighted as you scroll. Built client-side (assigns heading ids in the browser, so the cached render pipeline is untouched) as a real <nav> of links — keyboard and screen-reader accessible. Hidden on narrow screens; honors reduced-motion.

Attributes

  • enabled (:boolean) - Defaults to false.

reading_progress(assigns)

Renders a thin reading-progress bar fixed to the top of the viewport that fills as the reader scrolls the article. Decorative (aria-hidden), pointer transparent, and honors prefers-reduced-motion.

Attributes

  • enabled (:boolean) - Defaults to false.

scroll_timeline(assigns)

Renders a date-timeline rail on the group listing: a fixed side rail with a marker per distinct year found across the rendered post cards (which carry a data-post-date); click a year to smooth-scroll to its first post, and the current year highlights as you scroll. Only appears when 2+ years are present. Built client-side as an accessible <nav> of links; hidden on narrow screens.

Attributes

  • enabled (:boolean) - Defaults to false.
  • granularity (:string) - Defaults to "auto".

scrollbar_style_tag(assigns)

Emits a <style> that recolors the page's native scrollbar to the daisyUI theme when the group opts into "branded"/"thin". "default" renders nothing (the browser's native bar is untouched). Only recolors/resizes the real scrollbar — scrolling stays native.

Attributes

  • style (:string) - Defaults to "default".

show(assigns)

term_archive_path(language, group_slug, arg)

Public URL for a category archive (…/<group>/category/<slug>) or tag archive (…/<group>/tag/<tag>).