diff --git a/lib/bds/generation.ex b/lib/bds/generation.ex index 7fbdb23..88d786a 100644 --- a/lib/bds/generation.ex +++ b/lib/bds/generation.ex @@ -59,6 +59,7 @@ defmodule BDS.Generation do %{ project_id: project_id, project_name: project.name, + project_description: metadata.description, base_url: normalize_base_url(metadata.public_url), language: metadata.main_language, blog_languages: normalize_blog_languages(metadata.main_language, metadata.blog_languages), diff --git a/lib/bds/generation/outputs.ex b/lib/bds/generation/outputs.ex index 37e13ca..3c050c4 100644 --- a/lib/bds/generation/outputs.ex +++ b/lib/bds/generation/outputs.ex @@ -6,6 +6,7 @@ defmodule BDS.Generation.Outputs do import BDS.Generation.Sitemap, only: [render_feed: 3, render_atom: 3, render_calendar: 1] alias BDS.Rendering.RenderContext + alias BDS.Rendering.Metadata, as: RenderMetadata alias BDS.Rendering.TemplateSelection @type output_callback :: ({String.t(), iodata()} -> any()) | nil @@ -214,6 +215,7 @@ defmodule BDS.Generation.Outputs do concurrent_flat_map(posts_by_category, fn {category, posts} -> paginated_posts = Enum.chunk_every(posts, max(plan.max_posts_per_page, 1)) category_slug = archive_route_segment(category) + category_title = RenderMetadata.category_display_name(plan.category_settings, category) total_pages = length(paginated_posts) total_items = length(posts) @@ -256,7 +258,7 @@ defmodule BDS.Generation.Outputs do ["category", category_slug], page_number ), - render_archive_page(plan, category, page_posts, language, "category", pagination) + render_archive_page(plan, category_title, page_posts, language, "category", pagination) } report_output(output, on_output) @@ -508,7 +510,7 @@ defmodule BDS.Generation.Outputs do render_list_output( plan, language, - plan.project_name, + blog_page_title(plan), page_posts, %{kind: "core"}, pagination_for_page( diff --git a/lib/bds/generation/renderers.ex b/lib/bds/generation/renderers.ex index a01bc87..8f0fce0 100644 --- a/lib/bds/generation/renderers.ex +++ b/lib/bds/generation/renderers.ex @@ -4,20 +4,23 @@ defmodule BDS.Generation.Renderers do alias BDS.Generation.Paths alias BDS.Projects alias BDS.Rendering + alias BDS.Rendering.Metadata, as: RenderMetadata alias BDS.Rendering.RenderContext @doc "Render the home page (HTML) using the project's template engine." @spec render_home(map(), String.t() | nil) :: String.t() def render_home(plan, language) do + title = blog_page_title(plan) + [ "", "", - plan.project_name, + title, "", "

", - plan.project_name, + title, "

", "" ] @@ -67,14 +70,15 @@ defmodule BDS.Generation.Renderers do end # Shared by every archive kind so category/tag pages get the same post - # payloads (loaded bodies, real hrefs) as date archives. + # payloads (loaded bodies, real hrefs) as date archives. The document title + # stays the blog title; the archive-specific label lives in `archive_context`. defp render_archive_list_page(plan, title, archive_context, posts, language, kind, pagination) do fallback = fn -> render_inline_list_page(title, posts, language, kind) end render_list_output( plan, language, - title, + blog_page_title(plan), build_list_posts(plan, posts, Paths.route_language(plan.language, language)), archive_context, pagination, @@ -128,6 +132,14 @@ defmodule BDS.Generation.Renderers do ) end + @doc "Blog document title (description, then name) shared by list/home pages." + @spec blog_page_title(map()) :: String.t() + def blog_page_title(plan) do + RenderMetadata.blog_page_title( + Map.get(plan, :project_description), + Map.get(plan, :project_name) + ) + end @doc "Render the project's 404 page via its template, falling back to a static page." @spec render_not_found_output(map(), String.t() | nil) :: String.t() def render_not_found_output( diff --git a/lib/bds/preview/router.ex b/lib/bds/preview/router.ex index b70b93d..b6e4376 100644 --- a/lib/bds/preview/router.ex +++ b/lib/bds/preview/router.ex @@ -10,6 +10,7 @@ defmodule BDS.Preview.Router do alias BDS.Posts.Post alias BDS.Posts.Translation alias BDS.Rendering + alias BDS.Rendering.Metadata, as: RenderMetadata alias BDS.Rendering.TemplateSelection alias BDS.Repo @@ -159,7 +160,8 @@ defmodule BDS.Preview.Router do render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "category", - name: name + name: RenderMetadata.category_display_name(metadata.category_settings, name), + slug: name }) end @@ -294,7 +296,7 @@ defmodule BDS.Preview.Router do assigns = %{ language: language, language_prefix: language_prefix, - page_title: archive_page_title(archive_ctx), + page_title: RenderMetadata.blog_page_title(metadata.description, metadata.name), posts: page_posts, archive_context: archive_ctx, pagination: pagination @@ -334,6 +336,10 @@ defmodule BDS.Preview.Router do end defp archive_context_to_segments(%{kind: "core"}), do: [] + + defp archive_context_to_segments(%{kind: "category", slug: slug}) when is_binary(slug), + do: ["category", slug] + defp archive_context_to_segments(%{kind: "category", name: name}), do: ["category", name] defp archive_context_to_segments(%{kind: "tag", name: name}), do: ["tag", name] diff --git a/lib/bds/rendering/metadata.ex b/lib/bds/rendering/metadata.ex index 3625188..9032351 100644 --- a/lib/bds/rendering/metadata.ex +++ b/lib/bds/rendering/metadata.ex @@ -25,6 +25,49 @@ defmodule BDS.Rendering.Metadata do metadata end + @doc """ + Resolve the document/page title (the `` and the home-page heading). + + Mirrors the old app's `resolvePageTitle`: the blog description wins, then the + blog name. The archive-specific label (category/tag/date) is carried + separately in `archive_context`, so every page's document title stays the + blog title. + """ + @spec blog_page_title(String.t() | nil, String.t() | nil) :: String.t() + def blog_page_title(description, name) do + cond do + is_binary(description) and String.trim(description) != "" -> String.trim(description) + is_binary(name) and String.trim(name) != "" -> String.trim(name) + true -> "" + end + end + + @doc """ + Resolve a category's descriptive display name for archive headings. + + Mirrors the old app: the category's configured `title` wins, falling back to + the raw category key when no title is set. + """ + @spec category_display_name(map() | nil, String.t()) :: String.t() + def category_display_name(category_settings, category) do + title = + category_settings + |> category_settings_for(category) + |> category_title() + + if is_binary(title) and String.trim(title) != "", do: String.trim(title), else: category + end + + defp category_settings_for(category_settings, category) when is_map(category_settings), + do: Map.get(category_settings, category) + + defp category_settings_for(_category_settings, _category), do: nil + + defp category_title(settings) when is_map(settings), + do: Map.get(settings, "title") || Map.get(settings, :title) + + defp category_title(_settings), do: nil + @spec menu_items(String.t()) :: [map()] def menu_items(project_id) do {:ok, %{items: items}} = Menu.get_menu(project_id) diff --git a/specs/template_context.allium b/specs/template_context.allium index 61c55fb..5210171 100644 --- a/specs/template_context.allium +++ b/specs/template_context.allium @@ -62,7 +62,7 @@ value RenderContext { language: String -- Current language code language_prefix: String? -- "/de" or "" depending on language html_theme_attribute: String -- Theme class for <html> element - page_title: String -- Page title for <title> tag + page_title: String -- Blog title (description, else name) for the <title> tag; archive-specific labels come from archive_context pico_stylesheet_href: String -- Path to Pico CSS theme blog_languages: List<BlogLanguage> alternate_links: List<AlternateLink> @@ -181,7 +181,7 @@ value LinkContext { value ArchiveContext { kind: category | tag | date - name: String? + name: String? -- Heading label: for categories the configured descriptive title (falls back to the category key), for tags the raw tag name month: Integer? year: Integer? day: Integer? diff --git a/test/bds/generation/page_title_parity_test.exs b/test/bds/generation/page_title_parity_test.exs new file mode 100644 index 0000000..1b1590d --- /dev/null +++ b/test/bds/generation/page_title_parity_test.exs @@ -0,0 +1,93 @@ +defmodule BDS.Generation.PageTitleParityTest do + use ExUnit.Case, async: false + + alias BDS.Generation + + @blog_description "My Wonderful Blog" + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) + temp_dir = Path.join(System.tmp_dir!(), "bds-page-title-#{System.unique_integer([:positive])}") + File.mkdir_p!(temp_dir) + on_exit(fn -> File.rm_rf(temp_dir) end) + + {:ok, project} = BDS.Projects.create_project(%{name: "PageTitleBlog", data_path: temp_dir}) + + {:ok, _} = + BDS.Metadata.update_project_metadata(project.id, %{ + main_language: "en", + description: @blog_description + }) + + {:ok, post} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "Hello Post", + content: "Body of the hello post", + categories: ["article"], + tags: ["news"], + language: "en" + }) + + {:ok, _} = BDS.Posts.publish_post(post.id) + + %{project: project, temp_dir: temp_dir} + end + + defp read_html(temp_dir, segments) do + path = Path.join([temp_dir, "html"] ++ segments ++ ["index.html"]) + assert File.exists?(path), "expected page at #{Enum.join(segments, "/")}" + File.read!(path) + end + + defp document_title(html) do + case Regex.run(~r{<title>(.*?)}s, html) do + [_, title] -> String.trim(title) + _ -> nil + end + end + + test "home page document title is the blog title", %{project: project, temp_dir: temp_dir} do + {:ok, _} = Generation.render_site_section(project.id, :core) + + home_html = read_html(temp_dir, []) + assert document_title(home_html) == @blog_description + end + + test "archive document titles are the blog title while headings show the label", %{ + project: project, + temp_dir: temp_dir + } do + {:ok, _} = Generation.render_site_section(project.id, :category) + {:ok, _} = Generation.render_site_section(project.id, :tag) + + category_html = read_html(temp_dir, ["category", "article"]) + tag_html = read_html(temp_dir, ["tag", "news"]) + + assert document_title(category_html) == @blog_description + assert document_title(tag_html) == @blog_description + + assert category_html =~ ~s(

article

) + assert tag_html =~ ~s(

news

) + end + + test "category archive heading uses the configured descriptive title", %{ + project: project, + temp_dir: temp_dir + } do + {:ok, _} = + BDS.Metadata.update_category_settings(project.id, "article", %{ + "render_in_lists" => true, + "show_title" => true, + "title" => "Long Form Articles" + }) + + {:ok, _} = Generation.render_site_section(project.id, :category) + + category_html = read_html(temp_dir, ["category", "article"]) + + assert category_html =~ ~s(

Long Form Articles

) + refute category_html =~ ~s(

article

) + assert document_title(category_html) == @blog_description + end +end diff --git a/test/bds/preview_test.exs b/test/bds/preview_test.exs index 55ca47d..e9fcf4e 100644 --- a/test/bds/preview_test.exs +++ b/test/bds/preview_test.exs @@ -562,6 +562,47 @@ defmodule BDS.PreviewTest do assert :ok = BDS.Preview.stop_preview(project.id) end + test "on-demand rendering: document title is the blog title on home and archives", %{ + project: project + } do + assert {:ok, _metadata} = + Metadata.update_project_metadata(project.id, %{ + main_language: "en", + blog_languages: ["en"], + description: "My Preview Blog" + }) + + assert {:ok, _} = + Metadata.update_category_settings(project.id, "article", %{ + "render_in_lists" => true, + "show_title" => true, + "title" => "Long Form Articles" + }) + + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Article Post", + content: "Article body", + language: "en", + categories: ["article"] + }) + + assert {:ok, _published} = Posts.publish_post(post.id) + assert {:ok, _server} = BDS.Preview.start_preview(project.id) + + assert {:ok, %{body: home_html}} = BDS.Preview.request(project.id, "/") + assert home_html =~ "My Preview Blog" + + assert {:ok, %{body: category_html}} = + BDS.Preview.request(project.id, "/category/article") + + assert category_html =~ "My Preview Blog" + assert category_html =~ ~s(

Long Form Articles

) + + assert :ok = BDS.Preview.stop_preview(project.id) + end + test "on-demand rendering: tag archive renders filtered posts", %{project: project} do assert {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{