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 element
- page_title: String -- Page title for tag
+ page_title: String -- Blog title (description, else name) for the tag; archive-specific labels come from archive_context
pico_stylesheet_href: String -- Path to Pico CSS theme
blog_languages: List
alternate_links: List
@@ -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{(.*?)}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, %{