defmodule BDS.Preview.Router do @moduledoc false import Ecto.Query alias BDS.Generation.Paths alias BDS.MapUtils alias BDS.Metadata, as: ProjectMetadata alias BDS.Posts alias BDS.Posts.Post alias BDS.Posts.Translation alias BDS.Rendering alias BDS.Repo @type route :: {:home, pos_integer()} | {:post, String.t(), integer(), integer(), integer()} | {:page, String.t()} | {:category, String.t(), pos_integer()} | {:tag, String.t(), pos_integer()} | {:year, integer(), pos_integer()} | {:month, integer(), integer(), pos_integer()} | {:day, integer(), integer(), integer(), pos_integer()} | :not_matched @spec render_route(String.t(), String.t()) :: {:ok, map()} | :not_matched def render_route(project_id, request_path) do {:ok, metadata} = ProjectMetadata.get_project_metadata(project_id) main_language = metadata.main_language || "en" blog_languages = metadata.blog_languages || [] additional_languages = Enum.reject(blog_languages, &(&1 == main_language)) segments = String.split(request_path, "/", trim: true) {language, route_segments} = extract_language_prefix(segments, additional_languages) effective_language = language || main_language case match_route(route_segments) do :not_matched -> :not_matched route -> case render(project_id, route, effective_language, main_language, metadata) do {:ok, body} -> {:ok, %{content_type: "text/html", body: body}} {:error, :not_found} -> :not_matched end end end @spec match_route([String.t()]) :: route() def match_route([]), do: {:home, 1} def match_route(["page", n]), do: {:home, parse_page(n)} def match_route(["category", name]), do: {:category, URI.decode(name), 1} def match_route(["category", name, "page", n]), do: {:category, URI.decode(name), parse_page(n)} def match_route(["tag", name]), do: {:tag, URI.decode(name), 1} def match_route(["tag", name, "page", n]), do: {:tag, URI.decode(name), parse_page(n)} def match_route([y, m, d, slug]) do with {year, ""} <- Integer.parse(y), {month, ""} <- Integer.parse(m), {day, ""} <- Integer.parse(d) do {:post, slug, year, month, day} else _ -> :not_matched end end def match_route([y, m, d, "page", n]) do with {year, ""} <- Integer.parse(y), {month, ""} <- Integer.parse(m), {day, ""} <- Integer.parse(d) do {:day, year, month, day, parse_page(n)} else _ -> :not_matched end end def match_route([y, m, d]) do with {year, ""} <- Integer.parse(y), {month, ""} <- Integer.parse(m), {day, ""} <- Integer.parse(d) do {:day, year, month, day, 1} else _ -> :not_matched end end def match_route([y, m, "page", n]) do with {year, ""} <- Integer.parse(y), {month, ""} <- Integer.parse(m) do {:month, year, month, parse_page(n)} else _ -> :not_matched end end def match_route([y, m]) do with {year, ""} <- Integer.parse(y), {month, ""} <- Integer.parse(m) do {:month, year, month, 1} else _ -> :not_matched end end def match_route([y, "page", n]) do with {year, ""} <- Integer.parse(y) do {:year, year, parse_page(n)} else _ -> :not_matched end end def match_route([y]) do case Integer.parse(y) do {year, ""} -> {:year, year, 1} _ -> {:page, y} end end def match_route(_segments), do: :not_matched ## Rendering defp render(project_id, {:home, page_number}, language, main_language, metadata) do posts = load_published_list_posts(project_id, metadata) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{kind: "core"}) end defp render(project_id, {:post, slug, year, month, day}, language, main_language, _metadata) do case find_post_by_slug_and_date(project_id, slug, year, month, day) do nil -> {:error, :not_found} post -> render_post(project_id, post, language, main_language) end end defp render(project_id, {:page, slug}, language, main_language, _metadata) do case find_page_by_slug(project_id, slug) do nil -> {:error, :not_found} post -> render_post(project_id, post, language, main_language) end end defp render(project_id, {:category, name, page_number}, language, main_language, metadata) do posts = load_published_posts_by_category(project_id, name) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "category", name: name }) end defp render(project_id, {:tag, name, page_number}, language, main_language, metadata) do posts = load_published_posts_by_tag(project_id, name) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "tag", name: name }) end defp render(project_id, {:year, year, page_number}, language, main_language, metadata) do posts = load_published_posts_by_year(project_id, year) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "date", year: year }) end defp render(project_id, {:month, year, month, page_number}, language, main_language, metadata) do posts = load_published_posts_by_month(project_id, year, month) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "date", year: year, month: month }) end defp render(project_id, {:day, year, month, day, page_number}, language, main_language, metadata) do posts = load_published_posts_by_day(project_id, year, month, day) posts = maybe_resolve_language(posts, language, main_language, project_id) render_list(project_id, posts, page_number, metadata, language, main_language, %{ kind: "date", year: year, month: month, day: day }) end ## Post rendering defp render_post(project_id, post, language, main_language) do {effective_record, body} = resolve_post_for_language(project_id, post, language, main_language) assigns = %{ id: effective_record.id, title: effective_record.title, content: body, slug: post.slug, language: Map.get(effective_record, :language, post.language), excerpt: Map.get(effective_record, :excerpt, post.excerpt), _post_record: effective_record } case Rendering.render_post_page(project_id, post.template_slug, assigns) do {:ok, rendered} -> {:ok, rendered} {:error, _reason} -> {:error, :not_found} end end defp resolve_post_for_language(project_id, post, language, main_language) do post_lang = String.downcase(to_string(post.language || main_language)) target_lang = String.downcase(to_string(language)) if post_lang == target_lang do {post, Posts.editor_body(post)} else case Repo.get_by(Translation, translation_for: post.id, language: language, project_id: project_id ) do %Translation{status: status} = translation when status in [:published, :draft] -> {translation, Posts.editor_body(translation)} _ -> {post, Posts.editor_body(post)} end end end ## List rendering defp render_list(project_id, posts, page_number, metadata, language, main_language, archive_ctx) do max_per_page = max(metadata.max_posts_per_page || 50, 1) total_items = length(posts) total_pages = Paths.page_count(total_items, max_per_page) if page_number > total_pages and page_number > 1 do {:error, :not_found} else page_posts = posts |> Enum.chunk_every(max_per_page) |> Enum.at(page_number - 1, []) |> Enum.map(&post_to_list_entry(project_id, &1, language, main_language)) language_prefix = Paths.language_prefix(language, main_language) route_language = Paths.route_language(main_language, language) segments = archive_context_to_segments(archive_ctx) pagination = %{ current_page: page_number, total_pages: total_pages, total_items: total_items, items_per_page: max_per_page, has_prev_page: page_number > 1, prev_page_href: Paths.archive_or_root_href(route_language, segments, page_number - 1), has_next_page: page_number < total_pages, next_page_href: Paths.archive_or_root_href(route_language, segments, page_number + 1) } assigns = %{ language: language, language_prefix: language_prefix, page_title: archive_page_title(archive_ctx), posts: page_posts, archive_context: archive_ctx, pagination: pagination } try do case Rendering.render_list_page(project_id, assigns) do {:ok, rendered} -> {:ok, rendered} {:error, _reason} -> {:ok, fallback_list_html(page_posts, archive_ctx)} end rescue _ -> {:ok, fallback_list_html(page_posts, archive_ctx)} end end end defp post_to_list_entry(_project_id, post, language, main_language) do route_language = Paths.route_language(main_language, language) %{ id: post.id, slug: post.slug, title: post.title, href: Paths.url_for_output(nil, Paths.post_output_path(post, route_language)), excerpt: post.excerpt, content: Posts.editor_body(post), language: post.language, author: post.author, created_at: post.created_at, updated_at: post.updated_at, published_at: post.published_at, tags: post.tags || [], categories: post.categories || [], template_slug: post.template_slug, do_not_translate: Map.get(post, :do_not_translate, false) } end defp archive_context_to_segments(%{kind: "core"}), do: [] defp archive_context_to_segments(%{kind: "category", name: name}), do: ["category", name] defp archive_context_to_segments(%{kind: "tag", name: name}), do: ["tag", name] defp archive_context_to_segments(%{kind: "date", year: y, month: m, day: d}) when is_integer(y) and is_integer(m) and is_integer(d) do [ Integer.to_string(y), String.pad_leading(Integer.to_string(m), 2, "0"), String.pad_leading(Integer.to_string(d), 2, "0") ] end defp archive_context_to_segments(%{kind: "date", year: y, month: m}) when is_integer(y) and is_integer(m) do [Integer.to_string(y), String.pad_leading(Integer.to_string(m), 2, "0")] end defp archive_context_to_segments(%{kind: "date", year: y}) when is_integer(y), do: [Integer.to_string(y)] defp archive_context_to_segments(_), do: [] defp fallback_list_html(posts, archive_ctx) do title = archive_page_title(archive_ctx) || "Archive" items = posts |> Enum.map(fn post -> ["