diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex
index 29f5b8e..5a96001 100644
--- a/lib/mv_web/components/layouts.ex
+++ b/lib/mv_web/components/layouts.ex
@@ -13,6 +13,39 @@ defmodule MvWeb.Layouts do
embed_templates "layouts/*"
+ @doc """
+ Builds the full browser tab title: "Mila", "Mila · Page", or "Mila · Page · Club".
+ Order is always: Mila · page title · club name.
+ Uses assigns[:club_name] and the short page label from assigns[:content_title] or
+ assigns[:page_title]. LiveViews should set content_title (same gettext as sidebar)
+ and then assign page_title to the result of this function so the client receives
+ the full title.
+ """
+ def page_title_string(assigns) do
+ club = assigns[:club_name]
+ page = assigns[:content_title] || assigns[:page_title]
+
+ parts =
+ [page, club]
+ |> Enum.filter(&(is_binary(&1) and String.trim(&1) != ""))
+
+ if parts == [] do
+ "Mila"
+ else
+ "Mila · " <> Enum.join(parts, " · ")
+ end
+ end
+
+ @doc """
+ Assigns content_title (short label for heading; same gettext as sidebar) and
+ page_title (full browser tab title). Call from LiveView mount after club_name
+ is set (e.g. from on_mount). Returns the socket.
+ """
+ def assign_page_title(socket, content_title) do
+ socket = assign(socket, :content_title, content_title)
+ assign(socket, :page_title, page_title_string(socket.assigns))
+ end
+
@doc """
Renders the public (unauthenticated) page layout: header with logo + "Mitgliederverwaltung" left,
club name centered, language selector right; plus main content and flash group. Use for sign-in, join, and join-confirm pages so they
diff --git a/lib/mv_web/components/layouts/root.html.heex b/lib/mv_web/components/layouts/root.html.heex
index e107d5b..5419b73 100644
--- a/lib/mv_web/components/layouts/root.html.heex
+++ b/lib/mv_web/components/layouts/root.html.heex
@@ -7,8 +7,8 @@
- <.live_title default="Mv" suffix=" · Phoenix Framework">
- {assigns[:page_title]}
+ <.live_title default="Mila">
+ {page_title_string(assigns)}