fix: incremental rendering more on par with bDS

This commit is contained in:
2026-07-02 18:31:17 +02:00
parent 43f04dcd86
commit 1487c0337e
14 changed files with 2003 additions and 504 deletions

View File

@@ -263,27 +263,39 @@ defmodule BDS.Desktop.ShellCommands do
end
defp dispatch("apply_site_validation", project, params) do
report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{}))
validation_report = normalize_apply_validation_report(BDS.MapUtils.attr(params, :report, %{}))
group_id = task_group_id("apply_site_validation")
attrs = %{group_id: group_id, group_name: "Apply Site Validation"}
queue_task(
project,
"apply_site_validation",
"Apply Site Validation",
"Generation",
fn report_fn ->
{:ok, _apply} =
Generation.apply_validation(project.id, report,
on_progress: scaled_progress_reporter(report_fn, 0.0, 0.85)
)
{:ok, prepare_task} =
Tasks.submit_task(
"Prepare Validation Apply",
fn report ->
{:ok, preparation} =
Generation.prepare_validation_apply(project.id, validation_report,
on_progress: report
)
{:ok, validation} =
Generation.validate_site(project.id, @site_sections,
on_progress: scaled_progress_reporter(report_fn, 0.85, 1.0)
)
Map.put(preparation, :project_id, project.id)
end,
attrs
)
site_validation_result(project.id, validation)
end
)
Task.start(fn ->
run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task.id)
end)
{:ok,
%{
kind: "task_queued",
action: "apply_site_validation",
title: "Apply Site Validation",
message: "Apply Site Validation tasks queued",
project_id: project.id,
task_id: prepare_task.id,
task_group_id: group_id,
panel_tab: "tasks"
}}
end
defp dispatch("metadata_diff", project, _params) do
@@ -463,6 +475,124 @@ defmodule BDS.Desktop.ShellCommands do
end
end
defp run_apply_validation_sequence(project, validation_report, group_id, attrs, prepare_task_id) do
with :ok <-
wait_for_group_phase(group_id, ["Prepare Validation Apply"], @rebuild_phase_timeout),
{:ok, preparation} <- completed_task_result(prepare_task_id),
:ok <-
run_apply_validation_render_tasks(
project,
validation_report,
group_id,
attrs,
preparation
) do
if validation_apply_changed?(preparation) do
:ok = run_apply_validation_refresh_task(project, group_id, attrs, :calendar)
end
submit_apply_validation_check_task(project, attrs)
else
_other -> :ok
end
end
defp run_apply_validation_render_tasks(project, validation_report, group_id, attrs, preparation) do
sections = Map.get(preparation, :sections_to_render, [])
task_names = Enum.map(sections, &apply_validation_section_task_name/1)
Enum.each(sections, fn section ->
task_name = apply_validation_section_task_name(section)
{:ok, _task} =
Tasks.submit_task(
task_name,
fn report ->
{:ok, _result} =
Generation.apply_validation_section(project.id, validation_report, section,
on_progress: report
)
report.(1.0, apply_validation_section_complete_message(section))
:ok
end,
attrs
)
end)
case task_names do
[] -> :ok
names -> wait_for_group_phase(group_id, names, @rebuild_phase_timeout)
end
end
defp run_apply_validation_refresh_task(project, group_id, attrs, kind) do
task_name = apply_validation_refresh_task_name(kind)
{:ok, _task} =
Tasks.submit_task(
task_name,
fn report ->
{:ok, _result} =
Generation.refresh_validation_ancillary_outputs(project.id, kind, on_progress: report)
report.(1.0, apply_validation_refresh_complete_message(kind))
:ok
end,
attrs
)
wait_for_group_phase(group_id, [task_name], @rebuild_phase_timeout)
end
defp submit_apply_validation_check_task(project, attrs) do
{:ok, _task} =
Tasks.submit_task(
"Validate Site",
fn report ->
{:ok, validation} =
Generation.validate_site(project.id, @site_sections, on_progress: report)
site_validation_result(project.id, validation)
end,
attrs
)
:ok
end
defp completed_task_result(task_id) do
case Tasks.get_task(task_id) do
%{status: :completed, result: result} when is_map(result) -> {:ok, result}
%{status: :failed, error: error} -> {:error, error}
_other -> {:error, :task_result_unavailable}
end
end
defp validation_apply_changed?(preparation) do
Map.get(preparation, :sections_to_render, []) != [] or
Map.get(preparation, :deleted_url_count, 0) > 0
end
defp apply_validation_section_task_name(:core), do: "Render Site Core"
defp apply_validation_section_task_name(:single), do: "Render Single Posts"
defp apply_validation_section_task_name(:category), do: "Render Category Archives"
defp apply_validation_section_task_name(:tag), do: "Render Tag Archives"
defp apply_validation_section_task_name(:date), do: "Render Date Archives"
defp apply_validation_section_complete_message(:core), do: "Render Site Core complete"
defp apply_validation_section_complete_message(:single), do: "Render Single Posts complete"
defp apply_validation_section_complete_message(:category),
do: "Render Category Archives complete"
defp apply_validation_section_complete_message(:tag), do: "Render Tag Archives complete"
defp apply_validation_section_complete_message(:date), do: "Render Date Archives complete"
defp apply_validation_refresh_task_name(:calendar), do: "Regenerate Calendar"
defp apply_validation_refresh_complete_message(:calendar), do: "Regenerate Calendar complete"
defp translation_fill_enabled?(metadata) do
([metadata.main_language] ++ metadata.blog_languages)
|> Enum.map(fn language ->

View File

@@ -21,6 +21,7 @@ defmodule BDS.Generation do
import BDS.Generation.Progress
import BDS.Generation.Outputs
import BDS.Generation.Data
import BDS.Generation.Renderers, only: [build_list_posts: 3]
import BDS.Generation.Validation
alias BDS.Generation.GeneratedFileHash
@@ -227,62 +228,127 @@ defmodule BDS.Generation do
apply_validation(project_id, report, [])
end
@spec prepare_validation_apply(String.t(), map(), generation_opts()) ::
{:ok, map()} | {:error, term()}
def prepare_validation_apply(project_id, report, opts \\ [])
def prepare_validation_apply(project_id, report, opts)
when is_binary(project_id) and is_map(report) and is_list(opts) do
on_progress = callback(opts)
:ok = report_validation_progress(on_progress, 0.0, "Planning validation apply steps...")
with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do
sections_to_render = validation_apply_sections_to_render(apply_plan.targeted_plan)
project = Projects.get_project!(project_id)
:ok = report_validation_progress(on_progress, 0.2, "Deleting extra URLs...")
{deleted_url_count, removed_empty_dir_count} =
delete_extra_validation_paths(
project_id,
project,
Map.get(report, :extra_url_paths, []),
on_progress
)
:ok =
report_validation_progress(
on_progress,
1.0,
"Validation apply prepared"
)
{:ok,
%{
sections_to_render: sections_to_render,
deleted_url_count: deleted_url_count,
removed_empty_dir_count: removed_empty_dir_count
}}
end
end
@spec apply_validation_section(String.t(), map(), section(), generation_opts()) ::
{:ok, map()} | {:error, term()}
def apply_validation_section(project_id, report, section, opts \\ [])
def apply_validation_section(project_id, report, section, opts)
when is_binary(project_id) and is_map(report) and section in @core_sections and
is_list(opts) do
on_progress = callback(opts)
:ok = report_validation_progress(on_progress, 0.0, validation_section_task_label(section))
with {:ok, apply_plan} <- validation_apply_plan(project_id, [section], report) do
outputs_to_render =
targeted_apply_outputs(apply_plan.plan, apply_plan.targeted_plan, [section])
rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress)
{:ok, %{section: section, rendered_url_count: rendered_url_count}}
end
end
@spec refresh_validation_ancillary_outputs(String.t(), :calendar, generation_opts()) ::
{:ok, map()} | {:error, term()}
def refresh_validation_ancillary_outputs(project_id, kind, opts \\ [])
def refresh_validation_ancillary_outputs(project_id, kind, opts)
when is_binary(project_id) and kind in [:calendar] and is_list(opts) do
on_progress = callback(opts)
:ok = report_validation_progress(on_progress, 0.0, ancillary_validation_task_label(kind))
with {:ok, plan} <- plan_generation(project_id, @core_sections) do
expected_output_map = refresh_validation_output_map(plan, kind, on_progress)
paths = ancillary_validation_paths(expected_output_map, kind)
label = ancillary_validation_progress_label(kind)
total = length(paths)
:ok = report_generation_started(on_progress, total, label)
paths
|> Enum.with_index(1)
|> Enum.each(fn {relative_path, index} ->
_ =
write_generated_file(
project_id,
relative_path,
Map.fetch!(expected_output_map, relative_path)
)
:ok = report_generation_progress(on_progress, index, total, label)
end)
{:ok, %{kind: kind, written_count: length(paths)}}
end
end
defp refresh_validation_output_map(plan, :calendar, _on_progress) do
data = generation_data(plan)
%{"calendar.json" => BDS.Generation.Sitemap.render_calendar(data.published_list_posts)}
end
@spec apply_validation(String.t(), map(), generation_opts()) :: {:ok, map()} | {:error, term()}
def apply_validation(project_id, report, opts)
when is_binary(project_id) and is_map(report) and is_list(opts) do
on_progress = callback(opts)
with {:ok, plan} <- plan_generation(project_id, @core_sections) do
expected_outputs = build_outputs(plan)
expected_output_map = Map.new(expected_outputs)
with {:ok, apply_plan} <- validation_apply_plan(project_id, @core_sections, report) do
plan = apply_plan.plan
project = Projects.get_project!(project_id)
published_posts = list_published_posts(project_id)
targeted_plan =
build_targeted_validation_plan(
plan_validation_paths(report_paths(report), additional_languages(plan)),
published_posts
)
outputs_to_render = targeted_apply_outputs(plan, apply_plan.targeted_plan, plan.sections)
outputs_to_render =
expected_outputs
|> Enum.filter(fn {relative_path, _content} ->
targeted_output?(
relative_path,
targeted_plan,
plan.language,
additional_languages(plan)
)
end)
total_to_render = length(outputs_to_render)
:ok = report_generation_started(on_progress, total_to_render, "validation routes")
outputs_to_render
|> Enum.with_index(1)
|> Enum.each(fn {{relative_path, content}, index} ->
_ =
write_generated_file(project_id, relative_path, content,
refresh_timestamp_on_unchanged: route_html_path?(relative_path)
)
:ok =
report_generation_progress(on_progress, index, total_to_render, "validation routes")
end)
rendered_url_count = write_validation_outputs(project_id, outputs_to_render, on_progress)
{deleted_url_count, removed_empty_dir_count} =
delete_extra_validation_paths(project_id, project, Map.get(report, :extra_url_paths, []))
if outputs_to_render != [] or deleted_url_count > 0 do
write_ancillary_validation_outputs(project_id, expected_output_map)
{:ok, _} = refresh_validation_ancillary_outputs(project_id, :calendar)
end
{:ok,
%{
rendered_url_count:
Enum.count(outputs_to_render, fn {relative_path, _content} ->
route_html_path?(relative_path)
end),
rendered_url_count: rendered_url_count,
deleted_url_count: deleted_url_count,
removed_empty_dir_count: removed_empty_dir_count
}}
@@ -372,7 +438,7 @@ defmodule BDS.Generation do
:ok
end
defp build_outputs(plan) do
defp build_outputs(plan, on_output \\ nil) do
data = generation_data(plan)
published_translations = flattened_generation_translations(data.translations_by_post)
translations_by_post_language = translation_lookup_map(published_translations)
@@ -419,7 +485,8 @@ defmodule BDS.Generation do
build_core_outputs(
plan,
data.published_list_posts,
localized_list_posts_by_language
localized_list_posts_by_language,
on_output
)
else
[]
@@ -432,7 +499,8 @@ defmodule BDS.Generation do
plan.language,
data.published_posts,
translations_by_post_language,
localized_posts_by_language
localized_posts_by_language,
on_output
)
else
[]
@@ -445,14 +513,15 @@ defmodule BDS.Generation do
plan.language,
data.published_posts,
translations_by_post_language,
localized_posts_by_language
localized_posts_by_language,
on_output
)
else
[]
end
archive_outputs =
build_archive_outputs(plan, data.post_index, localized_post_indexes)
build_archive_outputs(plan, data.post_index, localized_post_indexes, on_output)
urls =
(core_outputs ++ page_outputs ++ single_outputs ++ archive_outputs)
@@ -606,6 +675,346 @@ defmodule BDS.Generation do
end
end
defp validation_apply_plan(project_id, sections, report) do
with {:ok, plan} <- plan_generation(project_id, sections) do
published_posts = list_published_posts(project_id)
targeted_plan =
build_targeted_validation_plan(
plan_validation_paths(report_paths(report), additional_languages(plan)),
published_posts
)
{:ok, %{plan: plan, targeted_plan: targeted_plan}}
end
end
# Render ONLY the routes targeted by the validation report for the given
# sections, matching the old app's behaviour of filtering the render inputs
# rather than rendering the whole site and discarding the surplus. Falls back
# to a full section render only when a report path could not be classified.
defp targeted_apply_outputs(plan, targeted_plan, sections) do
if fallback_validation_section_render?(targeted_plan) do
Enum.filter(build_outputs(plan), fn {relative_path, _content} ->
path_section(relative_path) in sections
end)
else
render_data = validation_render_data(plan)
Enum.flat_map(sections, fn section ->
build_targeted_section_outputs(plan, render_data, targeted_plan, section)
end)
end
end
# Write the targeted outputs, streaming one progress message per rewritten URL
# (exactly like the old app's `Generated /url` reporting) so the task shows the
# actual URLs and advances by the number of URLs rewritten.
defp write_validation_outputs(project_id, outputs, on_progress) do
route_total = Enum.count(outputs, fn {relative_path, _content} -> route_html_path?(relative_path) end)
:ok = report_validation_progress(on_progress, 0.0, validation_rewrite_started_message(route_total))
{rendered, _total} =
Enum.reduce(outputs, {0, route_total}, fn {relative_path, content}, {rendered, total} ->
_ =
write_generated_file(project_id, relative_path, content,
refresh_timestamp_on_unchanged: route_html_path?(relative_path)
)
if route_html_path?(relative_path) do
rewritten = rendered + 1
progress = if total > 0, do: rewritten / total, else: 1.0
:ok =
report_validation_progress(
on_progress,
progress,
"Rewrote #{relative_path_to_url_path(relative_path)} (#{rewritten}/#{total})"
)
{rewritten, total}
else
{rendered, total}
end
end)
rendered
end
defp validation_rewrite_started_message(0), do: "No URLs to rewrite"
defp validation_rewrite_started_message(1), do: "Rewriting 1 URL..."
defp validation_rewrite_started_message(total), do: "Rewriting #{total} URLs..."
defp validation_render_data(plan) do
data = generation_data(plan)
translations_by_post_language =
data.translations_by_post
|> flattened_generation_translations()
|> translation_lookup_map()
translatable_posts =
Enum.reject(data.published_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
translatable_list_posts =
Enum.reject(data.published_list_posts, &truthy_flag?(Map.get(&1, :do_not_translate)))
localized_posts_by_language =
Map.new(additional_languages(plan), fn language ->
{language,
resolve_posts_for_language(
translatable_posts,
language,
translations_by_post_language,
plan.language
)}
end)
localized_list_posts_by_language =
Map.new(additional_languages(plan), fn language ->
{language,
resolve_posts_for_language(
translatable_list_posts,
language,
translations_by_post_language,
plan.language
)}
end)
localized_post_indexes =
Map.new(localized_list_posts_by_language, fn {language, posts} ->
{language, build_generation_post_index(posts)}
end)
%{
data: data,
translations_by_post_language: translations_by_post_language,
localized_posts_by_language: localized_posts_by_language,
localized_list_posts_by_language: localized_list_posts_by_language,
localized_post_indexes: localized_post_indexes
}
end
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :core) do
data = render_data.data
main_root =
if targeted_plan.request_root_routes do
posts = build_list_posts(plan.base_url, data.published_list_posts, nil)
build_root_outputs(plan, plan.language, posts)
else
[]
end
language_root =
Enum.flat_map(additional_languages(plan), fn language ->
language_plan = language_targeted_plan(targeted_plan, language)
if language_plan.request_root_routes do
source = Map.get(render_data.localized_list_posts_by_language, language, [])
prefix = route_language(plan.language, language)
posts = build_list_posts(plan.base_url, source, prefix)
build_root_outputs(plan, language, posts)
else
[]
end
end)
main_page_posts =
Enum.filter(
data.published_posts,
&MapSet.member?(targeted_plan.requested_page_slugs, &1.slug)
)
localized_page_posts =
Map.new(additional_languages(plan), fn language ->
language_plan = language_targeted_plan(targeted_plan, language)
source = Map.get(render_data.localized_posts_by_language, language, [])
{language, Enum.filter(source, &MapSet.member?(language_plan.requested_page_slugs, &1.slug))}
end)
page_outputs =
build_page_outputs(
plan.project_id,
plan.language,
main_page_posts,
render_data.translations_by_post_language,
localized_page_posts
)
main_root ++ language_root ++ page_outputs
end
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :single) do
data = render_data.data
main_posts =
filter_route_posts(data.published_posts, targeted_plan.requested_post_routes)
localized_posts =
Map.new(additional_languages(plan), fn language ->
language_plan = language_targeted_plan(targeted_plan, language)
source = Map.get(render_data.localized_posts_by_language, language, [])
{language, filter_route_posts(source, language_plan.requested_post_routes)}
end)
build_single_outputs(
plan.project_id,
plan.language,
main_posts,
render_data.translations_by_post_language,
localized_posts
)
end
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :category) do
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
index,
language_plan,
languages ->
build_category_outputs(
plan_arg,
filter_collection_by_segment(index.posts_by_category, language_plan.requested_category_slugs),
languages
)
end)
end
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :tag) do
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
index,
language_plan,
languages ->
build_tag_outputs(
plan_arg,
filter_collection_by_segment(index.posts_by_tag, language_plan.requested_tag_slugs),
languages
)
end)
end
defp build_targeted_section_outputs(plan, render_data, targeted_plan, :date) do
build_targeted_archive_outputs(plan, render_data, targeted_plan, fn plan_arg,
index,
language_plan,
languages ->
build_date_outputs(plan_arg, filter_date_index(index, language_plan), languages)
end)
end
defp build_targeted_archive_outputs(plan, render_data, targeted_plan, builder) do
main_outputs = builder.(plan, render_data.data.post_index, targeted_plan, [plan.language])
language_outputs =
Enum.flat_map(additional_languages(plan), fn language ->
language_plan = language_targeted_plan(targeted_plan, language)
index = Map.get(render_data.localized_post_indexes, language, empty_generation_post_index())
builder.(plan, index, language_plan, [language])
end)
main_outputs ++ language_outputs
end
defp language_targeted_plan(targeted_plan, language) do
Map.get(targeted_plan.language_plans, language, empty_validation_path_plan())
end
defp filter_route_posts(posts, route_keys) do
Enum.filter(posts, fn post ->
{year, month, day} = local_date_parts!(post.created_at)
MapSet.member?(route_keys, route_key(year, month, day, post.slug))
end)
end
defp filter_collection_by_segment(collection, segment_slugs) do
collection
|> Enum.filter(fn {key, _posts} ->
MapSet.member?(segment_slugs, archive_route_segment(key))
end)
|> Map.new()
end
defp filter_date_index(index, language_plan) do
%{
posts_by_year: filter_map_by_keys(index.posts_by_year, language_plan.requested_years),
posts_by_year_month:
filter_map_by_keys(index.posts_by_year_month, language_plan.requested_year_months),
posts_by_year_month_day:
filter_map_by_keys(index.posts_by_year_month_day, language_plan.requested_year_month_days)
}
end
defp filter_map_by_keys(map, keys) do
map
|> Enum.filter(fn {key, _value} -> MapSet.member?(keys, key) end)
|> Map.new()
end
defp empty_generation_post_index do
%{
posts_by_category: %{},
posts_by_tag: %{},
posts_by_year: %{},
posts_by_year_month: %{},
posts_by_year_month_day: %{}
}
end
defp validation_apply_sections_to_render(targeted_plan) do
if fallback_validation_section_render?(targeted_plan) do
[:category, :tag, :date, :core, :single]
else
targeted_plan
|> validation_apply_section_plans()
|> Enum.reduce([], &append_validation_apply_sections/2)
end
end
defp fallback_validation_section_render?(%{requires_fallback_section_render: true}), do: true
defp fallback_validation_section_render?(%{language_plans: language_plans}) do
Enum.any?(Map.values(language_plans), &fallback_validation_section_render?/1)
end
defp validation_apply_section_plans(targeted_plan) do
[targeted_plan | Map.values(Map.get(targeted_plan, :language_plans, %{}))]
end
defp append_validation_apply_sections(plan, sections) do
sections
|> maybe_append_validation_apply_section(:core, validation_apply_core_section?(plan))
|> maybe_append_validation_apply_section(
:single,
not MapSet.equal?(plan.requested_post_routes, MapSet.new())
)
|> maybe_append_validation_apply_section(
:category,
not MapSet.equal?(plan.requested_category_slugs, MapSet.new())
)
|> maybe_append_validation_apply_section(
:tag,
not MapSet.equal?(plan.requested_tag_slugs, MapSet.new())
)
|> maybe_append_validation_apply_section(:date, validation_apply_date_section?(plan))
end
defp validation_apply_core_section?(plan) do
plan.request_root_routes or not MapSet.equal?(plan.requested_page_slugs, MapSet.new())
end
defp validation_apply_date_section?(plan) do
not MapSet.equal?(plan.requested_years, MapSet.new()) or
not MapSet.equal?(plan.requested_year_months, MapSet.new()) or
not MapSet.equal?(plan.requested_year_month_days, MapSet.new())
end
defp maybe_append_validation_apply_section(sections, _section, false), do: sections
defp maybe_append_validation_apply_section(sections, section, true) do
if section in sections, do: sections, else: sections ++ [section]
end
defp path_section(relative_path) do
segments = String.split(relative_path, "/", trim: true)
@@ -698,50 +1107,70 @@ defmodule BDS.Generation do
:ok
end
defp validation_section_task_label(:core), do: "Render Site Core"
defp validation_section_task_label(:single), do: "Render Single Posts"
defp validation_section_task_label(:category), do: "Render Category Archives"
defp validation_section_task_label(:tag), do: "Render Tag Archives"
defp validation_section_task_label(:date), do: "Render Date Archives"
defp ancillary_validation_progress_label(:calendar), do: "calendar files"
defp ancillary_validation_task_label(:calendar), do: "Regenerate Calendar"
defp delete_extra_validation_paths(project_id, project, extra_url_paths) do
Enum.reduce(extra_url_paths, {0, 0}, fn url_path, {deleted_count, removed_dir_count} ->
delete_extra_validation_paths(project_id, project, extra_url_paths, nil)
end
defp delete_extra_validation_paths(project_id, project, extra_url_paths, on_progress) do
total = length(extra_url_paths)
if total == 0 do
:ok = report_validation_progress(on_progress, 0.5, "No extra URLs to delete")
end
extra_url_paths
|> Enum.with_index(1)
|> Enum.reduce({0, 0}, fn {url_path, index}, {deleted_count, removed_dir_count} ->
relative_path = url_path_to_relative_index_path(url_path)
full_path = output_path(project, relative_path)
case File.rm(full_path) do
:ok ->
Repo.delete_all(
from generated_file in GeneratedFileHash,
where:
generated_file.project_id == ^project_id and
generated_file.relative_path == ^relative_path
)
result =
case File.rm(full_path) do
:ok ->
Repo.delete_all(
from generated_file in GeneratedFileHash,
where:
generated_file.project_id == ^project_id and
generated_file.relative_path == ^relative_path
)
{pruned_count, _last_dir} =
prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
{pruned_count, _last_dir} =
prune_empty_parent_dirs(Path.dirname(full_path), output_path(project, ""))
{deleted_count + 1, removed_dir_count + pruned_count}
{deleted_count + 1, removed_dir_count + pruned_count}
{:error, :enoent} ->
{deleted_count, removed_dir_count}
{:error, :enoent} ->
{deleted_count, removed_dir_count}
{:error, _reason} ->
{deleted_count, removed_dir_count}
end
{:error, _reason} ->
{deleted_count, removed_dir_count}
end
:ok =
report_validation_progress(
on_progress,
0.2 + 0.3 * index / max(total, 1),
"Deleted #{index}/#{total} extra URLs"
)
result
end)
end
defp write_ancillary_validation_outputs(project_id, expected_output_map) do
ancillary_paths =
Enum.filter(Map.keys(expected_output_map), fn relative_path ->
relative_path == "calendar.json" or String.contains?(relative_path, "pagefind/")
end)
Enum.each(ancillary_paths, fn relative_path ->
_ =
write_generated_file(
project_id,
relative_path,
Map.fetch!(expected_output_map, relative_path)
)
end)
:ok
defp ancillary_validation_paths(expected_output_map, :calendar) do
expected_output_map
|> Map.keys()
|> Enum.filter(&(&1 == "calendar.json"))
end
defp output_path(project, relative_path) do

View File

@@ -7,6 +7,8 @@ defmodule BDS.Generation.Outputs do
alias BDS.Rendering.TemplateSelection
@type output_callback :: ({String.t(), iodata()} -> any()) | nil
@spec additional_languages(map()) :: [String.t()]
def additional_languages(plan) do
Enum.reject(plan.blog_languages, &(&1 == plan.language))
@@ -149,16 +151,17 @@ defmodule BDS.Generation.Outputs do
end)
end
@spec build_archive_outputs(map(), map(), map()) :: [{String.t(), iodata()}]
def build_archive_outputs(plan, post_index, localized_post_indexes) do
@spec build_archive_outputs(map(), map(), map(), output_callback()) :: [{String.t(), iodata()}]
def build_archive_outputs(plan, post_index, localized_post_indexes, on_output \\ nil) do
category_outputs =
if :category in plan.sections do
build_category_outputs(plan, post_index.posts_by_category, [plan.language]) ++
build_category_outputs(plan, post_index.posts_by_category, [plan.language], on_output) ++
Enum.flat_map(additional_languages(plan), fn language ->
build_category_outputs(
plan,
Map.get(localized_post_indexes, language, %{posts_by_category: %{}}).posts_by_category,
[language]
[language],
on_output
)
end)
else
@@ -167,12 +170,13 @@ defmodule BDS.Generation.Outputs do
tag_outputs =
if :tag in plan.sections do
build_tag_outputs(plan, post_index.posts_by_tag, [plan.language]) ++
build_tag_outputs(plan, post_index.posts_by_tag, [plan.language], on_output) ++
Enum.flat_map(additional_languages(plan), fn language ->
build_tag_outputs(
plan,
Map.get(localized_post_indexes, language, %{posts_by_tag: %{}}).posts_by_tag,
[language]
[language],
on_output
)
end)
else
@@ -181,7 +185,7 @@ defmodule BDS.Generation.Outputs do
date_outputs =
if :date in plan.sections do
build_date_outputs(plan, post_index, [plan.language]) ++
build_date_outputs(plan, post_index, [plan.language], on_output) ++
Enum.flat_map(additional_languages(plan), fn language ->
build_date_outputs(
plan,
@@ -190,7 +194,8 @@ defmodule BDS.Generation.Outputs do
language,
%{posts_by_year: %{}, posts_by_year_month: %{}, posts_by_year_month_day: %{}}
),
[language]
[language],
on_output
)
end)
else
@@ -200,8 +205,10 @@ defmodule BDS.Generation.Outputs do
category_outputs ++ tag_outputs ++ date_outputs
end
@spec build_category_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
def build_category_outputs(plan, posts_by_category, languages) do
@spec build_category_outputs(map(), map(), [String.t()], output_callback()) :: [
{String.t(), iodata()}
]
def build_category_outputs(plan, posts_by_category, languages, on_output \\ nil) do
Enum.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)
@@ -240,34 +247,46 @@ defmodule BDS.Generation.Outputs do
)
}
{
archive_path(
route_language(plan.language, language),
["category", category_slug],
page_number
),
render_archive_page(plan, category, page_posts, language, "category", pagination)
}
output =
{
archive_path(
route_language(plan.language, language),
["category", category_slug],
page_number
),
render_archive_page(plan, category, page_posts, language, "category", pagination)
}
report_output(output, on_output)
end)
end)
end)
end
@spec build_tag_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
def build_tag_outputs(plan, posts_by_tag, languages) do
@spec build_tag_outputs(map(), map(), [String.t()], output_callback()) :: [
{String.t(), iodata()}
]
def build_tag_outputs(plan, posts_by_tag, languages, on_output \\ nil) do
Enum.flat_map(posts_by_tag, fn {tag, posts} ->
tag_slug = archive_route_segment(tag)
build_paginated_archive_outputs(plan, languages, ["tag", tag_slug], posts, fn page_posts,
language,
pagination ->
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
end)
build_paginated_archive_outputs(
plan,
languages,
["tag", tag_slug],
posts,
fn page_posts, language, pagination ->
render_archive_page(plan, tag, page_posts, language, "tag", pagination)
end,
on_output
)
end)
end
@spec build_date_outputs(map(), map(), [String.t()]) :: [{String.t(), iodata()}]
def build_date_outputs(plan, post_index, languages) do
@spec build_date_outputs(map(), map(), [String.t()], output_callback()) :: [
{String.t(), iodata()}
]
def build_date_outputs(plan, post_index, languages, on_output \\ nil) do
year_outputs =
Enum.flat_map(post_index.posts_by_year, fn {year, posts} ->
build_paginated_archive_outputs(
@@ -284,7 +303,8 @@ defmodule BDS.Generation.Outputs do
language,
pagination
)
end
end,
on_output
)
end)
@@ -292,59 +312,73 @@ defmodule BDS.Generation.Outputs do
Enum.flat_map(post_index.posts_by_year_month, fn {year_month, posts} ->
[year, month] = String.split(year_month, "/", parts: 2)
build_paginated_archive_outputs(plan, languages, [year, month], posts, fn page_posts,
language,
pagination ->
render_date_archive_page(
plan,
"#{year}-#{month}",
%{kind: "month", year: String.to_integer(year), month: String.to_integer(month)},
page_posts,
language,
pagination
)
end)
build_paginated_archive_outputs(
plan,
languages,
[year, month],
posts,
fn page_posts, language, pagination ->
render_date_archive_page(
plan,
"#{year}-#{month}",
%{kind: "month", year: String.to_integer(year), month: String.to_integer(month)},
page_posts,
language,
pagination
)
end,
on_output
)
end)
day_outputs =
Enum.flat_map(post_index.posts_by_year_month_day, fn {year_month_day, posts} ->
[year, month, day] = String.split(year_month_day, "/", parts: 3)
build_paginated_archive_outputs(plan, languages, [year, month, day], posts, fn page_posts,
language,
pagination ->
render_date_archive_page(
plan,
"#{year}-#{month}-#{day}",
%{
kind: "day",
year: String.to_integer(year),
month: String.to_integer(month),
day: String.to_integer(day)
},
page_posts,
language,
pagination
)
end)
build_paginated_archive_outputs(
plan,
languages,
[year, month, day],
posts,
fn page_posts, language, pagination ->
render_date_archive_page(
plan,
"#{year}-#{month}-#{day}",
%{
kind: "day",
year: String.to_integer(year),
month: String.to_integer(month),
day: String.to_integer(day)
},
page_posts,
language,
pagination
)
end,
on_output
)
end)
year_outputs ++ month_outputs ++ day_outputs
end
@spec build_core_outputs(map(), [map()], map()) :: [{String.t(), iodata()}]
def build_core_outputs(plan, published_posts, localized_posts_by_language) do
@spec build_core_outputs(map(), [map()], map(), output_callback()) :: [{String.t(), iodata()}]
def build_core_outputs(plan, published_posts, localized_posts_by_language, on_output \\ nil) do
language = plan.language
additional_languages = Enum.reject(plan.blog_languages, &(&1 == language))
main_posts = build_list_posts(plan.base_url, published_posts, nil)
build_root_outputs(plan, language, main_posts) ++
main_static_outputs =
[
{"404.html", render_not_found_output(plan, language)},
{"feed.xml", render_feed(plan, language, published_posts)},
{"atom.xml", render_atom(plan, language, published_posts)},
{"calendar.json", render_calendar(published_posts)}
] ++
]
|> Enum.map(&report_output(&1, on_output))
build_root_outputs(plan, language, main_posts, on_output) ++
main_static_outputs ++
Enum.flat_map(additional_languages, fn localized_language ->
localized_prefix = route_language(plan.language, localized_language)
localized_source_posts = Map.get(localized_posts_by_language, localized_language, [])
@@ -352,19 +386,20 @@ defmodule BDS.Generation.Outputs do
localized_posts =
build_list_posts(plan.base_url, localized_source_posts, localized_prefix)
build_root_outputs(plan, localized_language, localized_posts) ++
[
{Path.join(localized_language, "404.html"),
render_not_found_output(plan, localized_language)},
{Path.join(localized_language, "feed.xml"),
render_feed(plan, localized_language, localized_source_posts)},
{Path.join(localized_language, "atom.xml"),
render_atom(plan, localized_language, localized_source_posts)}
]
(build_root_outputs(plan, localized_language, localized_posts, on_output) ++
[
{Path.join(localized_language, "404.html"),
render_not_found_output(plan, localized_language)},
{Path.join(localized_language, "feed.xml"),
render_feed(plan, localized_language, localized_source_posts)},
{Path.join(localized_language, "atom.xml"),
render_atom(plan, localized_language, localized_source_posts)}
])
|> Enum.map(&report_output(&1, on_output))
end)
end
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map()) :: [
@spec build_page_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) :: [
{String.t(), iodata()}
]
def build_page_outputs(
@@ -372,39 +407,44 @@ defmodule BDS.Generation.Outputs do
main_language,
published_posts,
translations_by_post_language,
localized_posts_by_language
localized_posts_by_language,
on_output \\ nil
) do
page_outputs =
published_posts
|> Enum.filter(&("page" in (&1.categories || [])))
|> Enum.map(fn post ->
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
render_language = effective_render_language(canonical_variant.language, main_language)
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
effective_slug = effective_template_slug(project_id, post)
{page_output_path(post.slug, nil),
render_post_output(
project_id,
effective_slug,
%{
id: canonical_variant.id,
title: canonical_variant.title,
content: body,
slug: post.slug,
language: canonical_variant.language,
excerpt: canonical_variant.excerpt,
_post_record: canonical_variant
},
fn ->
render_post_page(
canonical_variant.title,
body,
post.slug,
canonical_variant.language
)
end
)}
output =
{page_output_path(post.slug, nil),
render_post_output(
project_id,
effective_slug,
%{
id: canonical_variant.id,
title: canonical_variant.title,
content: body,
slug: post.slug,
language: render_language,
excerpt: canonical_variant.excerpt,
_post_record: canonical_variant
},
fn ->
render_post_page(
canonical_variant.title,
body,
post.slug,
render_language
)
end
)}
report_output(output, on_output)
end)
translation_page_outputs =
@@ -413,33 +453,39 @@ defmodule BDS.Generation.Outputs do
posts
|> Enum.filter(&("page" in (&1.categories || [])))
|> Enum.map(fn post ->
render_language = effective_render_language(post.language, language)
body = load_body(project_id, post.file_path, post.content)
effective_slug = effective_template_slug(project_id, post)
{page_output_path(post.slug, language),
render_post_output(
project_id,
effective_slug,
%{
id: post.id,
title: post.title,
content: body,
slug: post.slug,
language: post.language,
excerpt: post.excerpt,
_post_record: post
},
fn -> render_post_page(post.title, body, post.slug, post.language) end
)}
output =
{page_output_path(post.slug, language),
render_post_output(
project_id,
effective_slug,
%{
id: post.id,
title: post.title,
content: body,
slug: post.slug,
language: render_language,
excerpt: post.excerpt,
_post_record: post
},
fn -> render_post_page(post.title, body, post.slug, render_language) end
)}
report_output(output, on_output)
end)
end)
page_outputs ++ translation_page_outputs
end
@spec build_root_outputs(map(), String.t(), [map()]) :: [{String.t(), iodata()}]
def build_root_outputs(plan, language, posts) do
@spec build_root_outputs(map(), String.t(), [map()], output_callback()) :: [
{String.t(), iodata()}
]
def build_root_outputs(plan, language, posts, on_output \\ nil) do
total_items = length(posts)
total_pages = page_count(total_items, plan.max_posts_per_page)
@@ -449,30 +495,46 @@ defmodule BDS.Generation.Outputs do
|> Enum.map(fn {page_posts, page_number} ->
route_language = route_language(plan.language, language)
{root_output_path(route_language, page_number),
render_list_output(
plan,
language,
plan.project_name,
page_posts,
%{kind: "core"},
pagination_for_page(
page_number,
total_pages,
total_items,
plan.max_posts_per_page,
route_language,
[]
),
fn -> render_home(plan, language) end
)}
output =
{root_output_path(route_language, page_number),
render_list_output(
plan,
language,
plan.project_name,
page_posts,
%{kind: "core"},
pagination_for_page(
page_number,
total_pages,
total_items,
plan.max_posts_per_page,
route_language,
[]
),
fn -> render_home(plan, language) end
)}
report_output(output, on_output)
end)
end
@spec build_paginated_archive_outputs(map(), [String.t()], [String.t()], [map()], (... ->
iodata())) ::
@spec build_paginated_archive_outputs(
map(),
[String.t()],
[String.t()],
[map()],
(... -> iodata()),
output_callback()
) ::
[{String.t(), iodata()}]
def build_paginated_archive_outputs(plan, languages, segments, posts, render_fun) do
def build_paginated_archive_outputs(
plan,
languages,
segments,
posts,
render_fun,
on_output \\ nil
) do
total_items = length(posts)
total_pages = page_count(total_items, plan.max_posts_per_page)
@@ -483,87 +545,100 @@ defmodule BDS.Generation.Outputs do
Enum.map(languages, fn language ->
route_language = route_language(plan.language, language)
{archive_path(route_language, segments, page_number),
render_fun.(
page_posts,
language,
pagination_for_page(
page_number,
total_pages,
total_items,
plan.max_posts_per_page,
route_language,
segments
)
)}
output =
{archive_path(route_language, segments, page_number),
render_fun.(
page_posts,
language,
pagination_for_page(
page_number,
total_pages,
total_items,
plan.max_posts_per_page,
route_language,
segments
)
)}
report_output(output, on_output)
end)
end)
end
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map()) :: [
{String.t(), iodata()}
]
@spec build_single_outputs(String.t(), String.t(), [map()], map(), map(), output_callback()) ::
[
{String.t(), iodata()}
]
def build_single_outputs(
project_id,
main_language,
published_posts,
translations_by_post_language,
localized_posts_by_language
localized_posts_by_language,
on_output \\ nil
) do
post_outputs =
Enum.map(published_posts, fn post ->
canonical_variant = Map.get(translations_by_post_language, {post.id, main_language}, post)
render_language = effective_render_language(canonical_variant.language, main_language)
body = load_body(project_id, canonical_variant.file_path, canonical_variant.content)
effective_slug = effective_template_slug(project_id, post)
{post_output_path(post),
render_post_output(
project_id,
effective_slug,
%{
id: canonical_variant.id,
title: canonical_variant.title,
content: body,
slug: post.slug,
language: canonical_variant.language,
excerpt: canonical_variant.excerpt,
_post_record: canonical_variant
},
fn ->
render_post_page(
canonical_variant.title,
body,
post.slug,
canonical_variant.language
)
end
)}
output =
{post_output_path(post),
render_post_output(
project_id,
effective_slug,
%{
id: canonical_variant.id,
title: canonical_variant.title,
content: body,
slug: post.slug,
language: render_language,
excerpt: canonical_variant.excerpt,
_post_record: canonical_variant
},
fn ->
render_post_page(
canonical_variant.title,
body,
post.slug,
render_language
)
end
)}
report_output(output, on_output)
end)
translation_outputs =
localized_posts_by_language
|> Enum.flat_map(fn {language, posts} ->
Enum.map(posts, fn post ->
render_language = effective_render_language(post.language, language)
body = load_body(project_id, post.file_path, post.content)
effective_slug = effective_template_slug(project_id, post)
{post_output_path(post, language),
render_post_output(
project_id,
effective_slug,
%{
id: post.id,
title: post.title,
content: body,
slug: post.slug,
language: post.language,
excerpt: post.excerpt,
_post_record: post
},
fn -> render_post_page(post.title, body, post.slug, post.language) end
)}
output =
{post_output_path(post, language),
render_post_output(
project_id,
effective_slug,
%{
id: post.id,
title: post.title,
content: body,
slug: post.slug,
language: render_language,
excerpt: post.excerpt,
_post_record: post
},
fn -> render_post_page(post.title, body, post.slug, render_language) end
)}
report_output(output, on_output)
end)
end)
@@ -583,4 +658,23 @@ defmodule BDS.Generation.Outputs do
)
end
end
defp effective_render_language(language, _fallback_language)
when is_binary(language) and language != "" do
language
end
defp effective_render_language(_language, fallback_language)
when is_binary(fallback_language) and fallback_language != "" do
fallback_language
end
defp effective_render_language(_language, _fallback_language), do: "en"
defp report_output(output, nil), do: output
defp report_output(output, on_output) when is_function(on_output, 1) do
on_output.(output)
output
end
end

View File

@@ -245,7 +245,9 @@ defmodule BDS.Generation.Validation do
requested_tag_slugs: MapSet.new(),
requested_years: MapSet.new(),
requested_year_months: MapSet.new(),
requested_year_month_days: MapSet.new(),
requested_post_routes: [],
requested_page_slugs: MapSet.new(),
language_plans: %{}
}
end
@@ -277,28 +279,38 @@ defmodule BDS.Generation.Validation do
)
nil ->
case Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) do
[_, year, month] ->
update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}"))
nil ->
case Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path) do
[_, year] ->
update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year)))
nil ->
if path == "/" or Regex.match?(~r|^/page/\d+$|, path) do
%{plan | request_root_routes: true}
else
%{plan | requires_fallback_section_render: true}
end
end
end
classify_archive_or_page_validation_path(path, plan)
end
end
end
end
defp classify_archive_or_page_validation_path(path, plan) do
cond do
Regex.match?(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path) ->
[_, year, month, day] = Regex.run(~r|^/(\d{4})/(\d{2})/(\d{2})(?:/page/\d+)?$|, path)
update_in(plan.requested_year_month_days, &MapSet.put(&1, "#{year}/#{month}/#{day}"))
Regex.match?(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path) ->
[_, year, month] = Regex.run(~r|^/(\d{4})/(\d{2})(?:/page/\d+)?$|, path)
update_in(plan.requested_year_months, &MapSet.put(&1, "#{year}/#{month}"))
Regex.match?(~r|^/(\d{4})(?:/page/\d+)?$|, path) ->
[_, year] = Regex.run(~r|^/(\d{4})(?:/page/\d+)?$|, path)
update_in(plan.requested_years, &MapSet.put(&1, String.to_integer(year)))
path == "/" or Regex.match?(~r|^/page/\d+$|, path) ->
%{plan | request_root_routes: true}
Regex.match?(~r|^/([^/]+)$|, path) ->
[_, slug] = Regex.run(~r|^/([^/]+)$|, path)
update_in(plan.requested_page_slugs, &MapSet.put(&1, slug))
true ->
%{plan | requires_fallback_section_render: true}
end
end
@spec build_targeted_validation_plan(map(), [map()]) :: map()
def build_targeted_validation_plan(initial_plan, published_posts) do
if initial_plan.requires_fallback_section_render do
@@ -334,10 +346,14 @@ defmodule BDS.Generation.Validation do
[:requested_year_months],
&MapSet.put(&1, route_month_key(route.year, route.month))
)
|> update_in(
[:requested_year_month_days],
&MapSet.put(&1, route_day_key(route.year, route.month, route.day))
)
|> Map.put(:request_root_routes, true)
post ->
{year, month, _day} = local_date_parts!(post.created_at)
{year, month, day} = local_date_parts!(post.created_at)
acc
|> update_in([:requested_category_slugs], fn set ->
@@ -355,10 +371,15 @@ defmodule BDS.Generation.Validation do
[:requested_year_months],
&MapSet.put(&1, route_month_key(year, month))
)
|> update_in(
[:requested_year_month_days],
&MapSet.put(&1, route_day_key(year, month, day))
)
|> Map.put(:request_root_routes, true)
end
end
)
|> cascade_requested_date_archives()
language_plans =
initial_plan.language_plans
@@ -384,7 +405,8 @@ defmodule BDS.Generation.Validation do
post.slug == route.slug and year == route.year and month == route.month and day == route.day
end
defp route_key(year, month, day, slug) do
@spec route_key(integer(), integer(), integer(), String.t()) :: String.t()
def route_key(year, month, day, slug) do
"#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}/#{slug}"
end
@@ -392,6 +414,33 @@ defmodule BDS.Generation.Validation do
"#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}"
end
defp route_day_key(year, month, day) do
"#{route_month_key(year, month)}/#{String.pad_leading(Integer.to_string(day), 2, "0")}"
end
defp cascade_requested_date_archives(plan) do
plan
|> cascade_requested_year_month_days()
|> cascade_requested_year_months()
end
defp cascade_requested_year_month_days(plan) do
Enum.reduce(plan.requested_year_month_days, plan, fn year_month_day, acc ->
[year, month, _day] = String.split(year_month_day, "/", parts: 3)
acc
|> update_in([:requested_year_months], &MapSet.put(&1, "#{year}/#{month}"))
|> update_in([:requested_years], &MapSet.put(&1, String.to_integer(year)))
end)
end
defp cascade_requested_year_months(plan) do
Enum.reduce(plan.requested_year_months, plan, fn year_month, acc ->
[year | _rest] = String.split(year_month, "/", parts: 2)
update_in(acc.requested_years, &MapSet.put(&1, String.to_integer(year)))
end)
end
defp extract_language_path(path, additional_languages) do
case Regex.run(~r|^/([a-z]{2,3})(/.*)?$|, path) do
[_, language, suffix] ->
@@ -413,83 +462,6 @@ defmodule BDS.Generation.Validation do
end
end
@spec targeted_output?(String.t(), map(), String.t() | nil, [String.t()]) :: boolean()
def targeted_output?(relative_path, targeted_plan, main_language, additional_languages) do
{language, stripped_path} =
extract_relative_output_language(relative_path, additional_languages)
plan =
case language do
nil -> targeted_plan
value -> Map.get(targeted_plan.language_plans, value, empty_validation_path_plan())
end
targeted_output_for_plan?(stripped_path, plan, main_language == language or is_nil(language))
end
defp extract_relative_output_language(relative_path, additional_languages) do
segments = String.split(relative_path, "/", trim: true)
case segments do
[language | rest] ->
if language in additional_languages do
{language, Path.join(rest)}
else
{nil, relative_path}
end
_other ->
{nil, relative_path}
end
end
defp targeted_output_for_plan?(
_relative_path,
%{requires_fallback_section_render: true},
_main?
),
do: true
defp targeted_output_for_plan?(relative_path, plan, _main?) do
cond do
relative_path in ["index.html", "404.html", "feed.xml", "atom.xml"] ->
plan.request_root_routes
Regex.match?(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path) ->
[_, slug] = Regex.run(~r|^category/([^/]+)(?:/page/\d+)?/index\.html$|, relative_path)
MapSet.member?(plan.requested_category_slugs, slug)
Regex.match?(~r|^tag/([^/]+)/index\.html$|, relative_path) ->
[_, slug] = Regex.run(~r|^tag/([^/]+)/index\.html$|, relative_path)
MapSet.member?(plan.requested_tag_slugs, slug)
Regex.match?(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path) ->
[_, year, month, day, slug] =
Regex.run(~r|^(\d{4})/(\d{2})/(\d{2})/([^/]+)/index\.html$|, relative_path)
MapSet.member?(
plan.requested_post_routes,
route_key(
String.to_integer(year),
String.to_integer(month),
String.to_integer(day),
slug
)
)
Regex.match?(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path) ->
[_, year, month] = Regex.run(~r|^(\d{4})/(\d{2})/index\.html$|, relative_path)
MapSet.member?(plan.requested_year_months, "#{year}/#{month}")
Regex.match?(~r|^(\d{4})/index\.html$|, relative_path) ->
[_, year] = Regex.run(~r|^(\d{4})/index\.html$|, relative_path)
MapSet.member?(plan.requested_years, String.to_integer(year))
true ->
false
end
end
@spec route_html_path?(String.t()) :: boolean()
def route_html_path?(relative_path), do: String.ends_with?(relative_path, "index.html")

View File

@@ -20,7 +20,7 @@ defmodule BDS.Rendering.PostRendering do
metadata = RenderMetadata.project_metadata(project_id)
template_context = TemplateSelection.template_render_context(project_id)
language = MapUtils.attr(assigns, :language, metadata.main_language || "en")
language = MapUtils.attr(assigns, :language) || metadata.main_language || "en"
main_language = metadata.main_language || language
post_record = Map.get(assigns, :_post_record) || load_post_record(assigns)
@@ -58,24 +58,26 @@ defmodule BDS.Rendering.PostRendering do
%{
language: language,
language_prefix: assign_override(
assigns,
:language_prefix,
"language_prefix",
LinksAndLanguages.language_prefix(language, main_language)
),
language_prefix:
assign_override(
assigns,
:language_prefix,
"language_prefix",
LinksAndLanguages.language_prefix(language, main_language)
),
page_title:
Map.get(
assigns,
:page_title,
MapUtils.attr(assigns, :title)
),
pico_stylesheet_href: assign_override(
assigns,
:pico_stylesheet_href,
"pico_stylesheet_href",
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
),
pico_stylesheet_href:
assign_override(
assigns,
:pico_stylesheet_href,
"pico_stylesheet_href",
RenderMetadata.default_pico_stylesheet_href(metadata.pico_theme)
),
html_theme_attribute:
assign_override(assigns, :html_theme_attribute, "html_theme_attribute", nil),
blog_languages: RenderMetadata.blog_languages(metadata, language),

View File

@@ -221,6 +221,86 @@ msgstr "Auswahl anwenden"
msgid "Apply Theme"
msgstr "Theme anwenden"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Website-Validierung anwenden"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Aufgaben zum Anwenden der Website-Validierung eingereiht"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Suchindex aufbauen"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Suchindex aufgebaut"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Überzählige URLs werden gelöscht..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Keine überzähligen URLs zu löschen"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Schritte zum Anwenden der Validierung werden geplant..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Anwenden der Validierung vorbereiten"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Kalender neu erzeugt"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Kategoriearchive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Kategoriearchive gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Datumsarchive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Datumsarchive gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Einzelbeiträge rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Einzelbeiträge gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Website-Kern rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Website-Kern gerendert"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Tag-Archive rendern"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Tag-Archive gerendert"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Anwenden der Validierung vorbereitet"
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr ""
msgid "Apply Theme"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr ""
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr ""
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr ""
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Aplicar seleccionados"
msgid "Apply Theme"
msgstr "Aplicar tema"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Aplicar validación del sitio"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Tareas para aplicar la validación del sitio en cola"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Crear índice de búsqueda"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Índice de búsqueda creado"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Eliminando URLs sobrantes..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "No hay URLs sobrantes para eliminar"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Planificando pasos para aplicar la validación..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Preparar aplicación de validación"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendario regenerado"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Renderizar archivos de categorías"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archivos de categorías renderizados"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Renderizar archivos por fecha"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archivos por fecha renderizados"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Renderizar entradas individuales"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Entradas individuales renderizadas"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Renderizar núcleo del sitio"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Núcleo del sitio renderizado"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Renderizar archivos de etiquetas"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archivos de etiquetas renderizados"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Aplicación de validación preparada"
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Appliquer la selection"
msgid "Apply Theme"
msgstr "Appliquer le thème"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Appliquer la validation du site"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Tâches dapplication de la validation du site mises en file"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Construire lindex de recherche"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Index de recherche construit"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Suppression des URLs en trop..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Aucune URL en trop à supprimer"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Planification de lapplication de la validation..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Préparer lapplication de la validation"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendrier régénéré"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Rendre les archives de catégories"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archives de catégories rendues"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Rendre les archives par date"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archives par date rendues"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Rendre les articles individuels"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Articles individuels rendus"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Rendre le cœur du site"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Cœur du site rendu"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Rendre les archives de tags"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archives de tags rendues"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Application de la validation préparée"
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format

View File

@@ -221,6 +221,86 @@ msgstr "Applica selezionati"
msgid "Apply Theme"
msgstr "Applica tema"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr "Applica validazione sito"
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr "Attività per applicare la validazione sito in coda"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr "Crea indice di ricerca"
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr "Indice di ricerca creato"
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr "Eliminazione URL extra..."
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr "Nessun URL extra da eliminare"
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr "Pianificazione dei passaggi di applicazione della validazione..."
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr "Prepara applicazione validazione"
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr "Calendario rigenerato"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr "Renderizza archivi categorie"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr "Archivi categorie renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr "Renderizza archivi data"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr "Archivi data renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr "Renderizza post singoli"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr "Post singoli renderizzati"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr "Renderizza nucleo sito"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr "Nucleo sito renderizzato"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr "Renderizza archivi tag"
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr "Archivi tag renderizzati"
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr "Applicazione della validazione preparata"
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format

View File

@@ -11,7 +11,7 @@
msgid ""
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:333
#: lib/bds/desktop/shell_live/misc_editor.ex:319
#, elixir-autogen, elixir-format
msgid "%{canonical} = %{translation}"
msgstr ""
@@ -92,7 +92,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:888
#: lib/bds/desktop/shell_live/post_editor.ex:918
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -234,6 +234,86 @@ msgstr ""
msgid "Apply Theme"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Apply Site Validation tasks queued"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Build Search Index complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Deleting extra URLs..."
msgstr ""
#: lib/bds/generation.ex
msgid "No extra URLs to delete"
msgstr ""
#: lib/bds/generation.ex
msgid "Planning validation apply steps..."
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Prepare Validation Apply"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Regenerate Calendar complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Category Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Date Archives complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Single Posts complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Site Core complete"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives"
msgstr ""
#: lib/bds/desktop/shell_commands.ex
msgid "Render Tag Archives complete"
msgstr ""
#: lib/bds/generation.ex
msgid "Validation apply prepared"
msgstr ""
#: lib/bds/desktop/shell_data.ex:182
#: lib/bds/ui/sidebar.ex:331
#, elixir-autogen, elixir-format
@@ -272,12 +352,12 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:422
#: lib/bds/desktop/shell_live/chat_editor.ex:234
#: lib/bds/desktop/shell_live/media_editor.ex:160
#: lib/bds/desktop/shell_live/media_editor.ex:353
#: lib/bds/desktop/shell_live/media_editor.ex:546
#: lib/bds/desktop/shell_live/media_editor.ex:171
#: lib/bds/desktop/shell_live/media_editor.ex:364
#: lib/bds/desktop/shell_live/media_editor.ex:557
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:755
#: lib/bds/desktop/shell_live/post_editor.ex:804
#: lib/bds/desktop/shell_live/post_editor.ex:763
#: lib/bds/desktop/shell_live/post_editor.ex:812
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr ""
@@ -380,8 +460,8 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:1024
#: lib/bds/desktop/shell_live/import_editor.ex:1188
#: lib/bds/desktop/shell_live/index.html.heex:336
#: lib/bds/desktop/shell_live/misc_editor.ex:755
#: lib/bds/desktop/shell_live/misc_editor.ex:756
#: lib/bds/desktop/shell_live/misc_editor.ex:741
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:256
#: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:59
#: lib/bds/desktop/shell_live/settings_editor/managed_categories.ex:75
@@ -439,7 +519,7 @@ msgstr ""
msgid "Check Syntax"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:486
#: lib/bds/desktop/shell_live/misc_editor.ex:472
#, elixir-autogen, elixir-format
msgid "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}"
msgstr ""
@@ -675,7 +755,7 @@ msgstr ""
msgid "Delete Media"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:396
#: lib/bds/desktop/shell_live/media_editor.ex:407
#, elixir-autogen, elixir-format
msgid "Delete Translation"
msgstr ""
@@ -691,7 +771,7 @@ msgstr ""
msgid "Delete this unpublished draft"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:120
#: lib/bds/desktop/shell_live/misc_editor.ex:106
#, elixir-autogen, elixir-format
msgid "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk"
msgstr ""
@@ -716,14 +796,14 @@ msgstr ""
msgid "Detect"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:159
#: lib/bds/desktop/shell_live/media_editor.ex:198
#: lib/bds/desktop/shell_live/media_editor.ex:203
#: lib/bds/desktop/shell_live/media_editor.ex:170
#: lib/bds/desktop/shell_live/media_editor.ex:209
#: lib/bds/desktop/shell_live/media_editor.ex:214
#: lib/bds/desktop/shell_live/media_editor.ex:220
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:754
#: lib/bds/desktop/shell_live/post_editor.ex:783
#: lib/bds/desktop/shell_live/post_editor.ex:789
#: lib/bds/desktop/shell_live/post_editor.ex:762
#: lib/bds/desktop/shell_live/post_editor.ex:791
#: lib/bds/desktop/shell_live/post_editor.ex:797
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr ""
@@ -854,8 +934,8 @@ msgstr ""
msgid "Editor Settings"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:757
#: lib/bds/desktop/shell_live/misc_editor.ex:780
#: lib/bds/desktop/shell_live/misc_editor.ex:743
#: lib/bds/desktop/shell_live/misc_editor.ex:766
#, elixir-autogen, elixir-format
msgid "Embeddings"
msgstr ""
@@ -980,11 +1060,11 @@ msgstr ""
msgid "Find Duplicate Posts"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:176
#: lib/bds/desktop/shell_live/misc_editor.ex:182
#: lib/bds/desktop/shell_live/misc_editor.ex:209
#: lib/bds/desktop/shell_live/misc_editor.ex:216
#: lib/bds/desktop/shell_live/misc_editor.ex:503
#: lib/bds/desktop/shell_live/misc_editor.ex:162
#: lib/bds/desktop/shell_live/misc_editor.ex:168
#: lib/bds/desktop/shell_live/misc_editor.ex:195
#: lib/bds/desktop/shell_live/misc_editor.ex:202
#: lib/bds/desktop/shell_live/misc_editor.ex:489
#: lib/bds/ui/registry.ex:139
#, elixir-autogen, elixir-format
msgid "Find Duplicates"
@@ -1013,7 +1093,7 @@ msgstr ""
msgid "Git"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:560
#: lib/bds/desktop/shell_live/misc_editor.ex:546
#: lib/bds/ui/registry.ex:113
#, elixir-autogen, elixir-format
msgid "Git Diff"
@@ -1048,8 +1128,8 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:711
#: lib/bds/desktop/shell_live/post_editor.ex:1006
#: lib/bds/desktop/shell_live/media_editor.ex:731
#: lib/bds/desktop/shell_live/post_editor.ex:1036
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr ""
@@ -1219,8 +1299,8 @@ msgstr ""
msgid "Language"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:210
#: lib/bds/desktop/shell_live/post_editor.ex:790
#: lib/bds/desktop/shell_live/media_editor.ex:221
#: lib/bds/desktop/shell_live/post_editor.ex:798
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr ""
@@ -1230,7 +1310,7 @@ msgstr ""
msgid "Light"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:256
#: lib/bds/desktop/shell_live/media_editor.ex:267
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:215
#, elixir-autogen, elixir-format
msgid "Link to Post"
@@ -1304,7 +1384,7 @@ msgstr ""
msgid "Mapped"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1009
#: lib/bds/desktop/shell_live/post_editor.ex:1039
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:123
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1316,10 +1396,10 @@ msgid "Max Posts Per Page"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:168
#: lib/bds/desktop/shell_live/media_editor.ex:498
#: lib/bds/desktop/shell_live/media_editor.ex:502
#: lib/bds/desktop/shell_live/misc_editor.ex:749
#: lib/bds/desktop/shell_live/misc_editor.ex:776
#: lib/bds/desktop/shell_live/media_editor.ex:509
#: lib/bds/desktop/shell_live/media_editor.ex:513
#: lib/bds/desktop/shell_live/misc_editor.ex:735
#: lib/bds/desktop/shell_live/misc_editor.ex:762
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
@@ -1334,7 +1414,7 @@ msgstr ""
msgid "Media (%{count})"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:498
#: lib/bds/desktop/shell_live/media_editor.ex:509
#, elixir-autogen, elixir-format
msgid "Media saved"
msgstr ""
@@ -1361,9 +1441,9 @@ msgid "Metadata"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:185
#: lib/bds/desktop/shell_live/misc_editor.ex:228
#: lib/bds/desktop/shell_live/misc_editor.ex:240
#: lib/bds/desktop/shell_live/misc_editor.ex:462
#: lib/bds/desktop/shell_live/misc_editor.ex:214
#: lib/bds/desktop/shell_live/misc_editor.ex:226
#: lib/bds/desktop/shell_live/misc_editor.ex:448
#: lib/bds/ui/registry.ex:111
#, elixir-autogen, elixir-format
msgid "Metadata Diff"
@@ -1465,7 +1545,7 @@ msgstr ""
msgid "No git history yet"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:473
#: lib/bds/desktop/shell_live/misc_editor.ex:459
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:321
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:333
@@ -1505,12 +1585,12 @@ msgstr ""
msgid "No media files"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:592
#: lib/bds/desktop/shell_live/misc_editor.ex:578
#, elixir-autogen, elixir-format
msgid "No metadata diff items selected"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:621
#: lib/bds/desktop/shell_live/misc_editor.ex:607
#, elixir-autogen, elixir-format
msgid "No orphan files selected"
msgstr ""
@@ -1535,7 +1615,7 @@ msgstr ""
msgid "No posts yet"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:589
#: lib/bds/desktop/shell_live/misc_editor.ex:575
#, elixir-autogen, elixir-format
msgid "No repair action available"
msgstr ""
@@ -1560,7 +1640,7 @@ msgstr ""
msgid "No translations"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:567
#: lib/bds/desktop/shell_live/misc_editor.ex:553
#, elixir-autogen, elixir-format
msgid "No unstaged changes"
msgstr ""
@@ -1775,7 +1855,7 @@ msgstr ""
msgid "Pages (%{count})"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:176
#: lib/bds/desktop/shell_live/misc_editor.ex:162
#, elixir-autogen, elixir-format
msgid "Pair dismissed"
msgstr ""
@@ -1801,17 +1881,17 @@ msgstr ""
msgid "Persist the detected language for this media item"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor.ex:529
#: lib/bds/desktop/shell_live/post_editor.ex:533
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:576
#: lib/bds/desktop/shell_live/post_editor.ex:614
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:658
#: lib/bds/desktop/shell_live/post_editor.ex:661
#: lib/bds/desktop/shell_live/post_editor.ex:741
#: lib/bds/desktop/shell_live/post_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:733
#: lib/bds/desktop/shell_live/post_editor.ex:537
#: lib/bds/desktop/shell_live/post_editor.ex:541
#: lib/bds/desktop/shell_live/post_editor.ex:580
#: lib/bds/desktop/shell_live/post_editor.ex:584
#: lib/bds/desktop/shell_live/post_editor.ex:622
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/post_editor.ex:666
#: lib/bds/desktop/shell_live/post_editor.ex:669
#: lib/bds/desktop/shell_live/post_editor.ex:749
#: lib/bds/desktop/shell_live/post_editor.ex:752
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
@@ -1836,24 +1916,24 @@ msgstr ""
msgid "Post Template"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:315
#: lib/bds/desktop/shell_live/misc_editor.ex:301
#, elixir-autogen, elixir-format
msgid "Post is marked as do-not-translate but has translations"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:580
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:529
#: lib/bds/desktop/shell_live/post_editor.ex:537
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:167
#: lib/bds/desktop/shell_live/misc_editor.ex:678
#: lib/bds/desktop/shell_live/misc_editor.ex:775
#: lib/bds/desktop/shell_live/misc_editor.ex:664
#: lib/bds/desktop/shell_live/misc_editor.ex:761
#: lib/bds/ui/registry.ex:14
#: lib/bds/ui/sidebar.ex:271
#, elixir-autogen, elixir-format
@@ -1870,7 +1950,7 @@ msgstr ""
msgid "Preferences"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1010
#: lib/bds/desktop/shell_live/post_editor.ex:1040
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:124
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1892,8 +1972,8 @@ msgid "Preview unavailable"
msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:509
#: lib/bds/desktop/shell_live/misc_editor.ex:753
#: lib/bds/desktop/shell_live/misc_editor.ex:779
#: lib/bds/desktop/shell_live/misc_editor.ex:739
#: lib/bds/desktop/shell_live/misc_editor.ex:765
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:27
#: lib/bds/ui/sidebar.ex:762
#, elixir-autogen, elixir-format
@@ -1939,19 +2019,19 @@ msgid "Publish Selected"
msgstr ""
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:1004
#: lib/bds/desktop/shell_live/post_editor.ex:1034
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:472
#: lib/bds/ui/sidebar.ex:324
#, elixir-autogen, elixir-format
msgid "Published"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:318
#: lib/bds/desktop/shell_live/misc_editor.ex:304
#, elixir-autogen, elixir-format
msgid "Published translation has content stuck in DB instead of filesystem"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:754
#: lib/bds/desktop/shell_live/misc_editor.ex:740
#: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:40
#: lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex:57
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:338
@@ -2038,8 +2118,8 @@ msgstr ""
msgid "Refresh Online Models"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:368
#: lib/bds/desktop/shell_live/media_editor.ex:377
#: lib/bds/desktop/shell_live/media_editor.ex:379
#: lib/bds/desktop/shell_live/media_editor.ex:388
#, elixir-autogen, elixir-format
msgid "Refresh Translation"
msgstr ""
@@ -2095,14 +2175,14 @@ msgstr ""
msgid "Replace"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:142
#: lib/bds/desktop/shell_live/media_editor.ex:150
#: lib/bds/desktop/shell_live/media_editor.ex:153
#: lib/bds/desktop/shell_live/media_editor.ex:161
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:86
#, elixir-autogen, elixir-format
msgid "Replace File"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:116
#: lib/bds/desktop/shell_live/media_editor.ex:127
#, elixir-autogen, elixir-format
msgid "Replace Media File"
msgstr ""
@@ -2137,7 +2217,7 @@ msgstr ""
msgid "Result"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:1005
#: lib/bds/desktop/shell_live/post_editor.ex:1035
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr ""
@@ -2183,13 +2263,13 @@ msgstr ""
msgid "Save"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:328
#: lib/bds/desktop/shell_live/media_editor.ex:339
#, elixir-autogen, elixir-format
msgid "Save Translation"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:710
#: lib/bds/desktop/shell_live/post_editor.ex:1003
#: lib/bds/desktop/shell_live/media_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor.ex:1033
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr ""
@@ -2199,7 +2279,7 @@ msgstr ""
msgid "Scanning entries..."
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:751
#: lib/bds/desktop/shell_live/misc_editor.ex:737
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
@@ -2227,7 +2307,7 @@ msgstr ""
msgid "Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here."
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:777
#: lib/bds/desktop/shell_live/misc_editor.ex:763
#: lib/bds/desktop/shell_live/script_editor.ex:119
#: lib/bds/desktop/shell_live/script_editor.ex:124
#: lib/bds/desktop/shell_live/script_editor.ex:130
@@ -2332,7 +2412,7 @@ msgstr ""
msgid "Select an existing category or press Enter to create a new archive entry"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:210
#: lib/bds/desktop/shell_live/misc_editor.ex:196
#, elixir-autogen, elixir-format
msgid "Selected pairs dismissed"
msgstr ""
@@ -2366,9 +2446,7 @@ msgstr ""
msgid "Site"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:95
#: lib/bds/desktop/shell_live/misc_editor.ex:105
#: lib/bds/desktop/shell_live/misc_editor.ex:426
#: lib/bds/desktop/shell_live/misc_editor.ex:412
#: lib/bds/ui/registry.ex:125
#, elixir-autogen, elixir-format
msgid "Site Validation"
@@ -2510,7 +2588,7 @@ msgstr ""
msgid "Technology"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:752
#: lib/bds/desktop/shell_live/misc_editor.ex:738
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:299
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
@@ -2534,7 +2612,7 @@ msgstr ""
msgid "Template syntax is valid"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:778
#: lib/bds/desktop/shell_live/misc_editor.ex:764
#: lib/bds/desktop/shell_live/template_editor.ex:111
#: lib/bds/desktop/shell_live/template_editor.ex:116
#: lib/bds/desktop/shell_live/template_editor.ex:121
@@ -2658,39 +2736,35 @@ msgstr ""
msgid "Toggle sidebar"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:352
#: lib/bds/desktop/shell_live/media_editor.ex:545
#: lib/bds/desktop/shell_live/media_editor.ex:566
#: lib/bds/desktop/shell_live/media_editor.ex:571
#: lib/bds/desktop/shell_live/media_editor.ex:363
#: lib/bds/desktop/shell_live/media_editor.ex:556
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:803
#: lib/bds/desktop/shell_live/post_editor.ex:832
#: lib/bds/desktop/shell_live/post_editor.ex:837
#: lib/bds/desktop/shell_live/post_editor.ex:811
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:105
#: lib/bds/desktop/shell_live/misc_editor.ex:119
#: lib/bds/desktop/shell_live/misc_editor.ex:133
#: lib/bds/desktop/shell_live/misc_editor.ex:482
#: lib/bds/desktop/shell_live/misc_editor.ex:468
#, elixir-autogen, elixir-format
msgid "Translation Validation"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:312
#: lib/bds/desktop/shell_live/misc_editor.ex:298
#, elixir-autogen, elixir-format
msgid "Translation language matches canonical post language"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:321
#: lib/bds/desktop/shell_live/misc_editor.ex:307
#, elixir-autogen, elixir-format
msgid "Translation points to a missing source post"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:183
#: lib/bds/desktop/shell_live/misc_editor.ex:748
#: lib/bds/desktop/shell_live/misc_editor.ex:750
#: lib/bds/desktop/shell_live/misc_editor.ex:734
#: lib/bds/desktop/shell_live/misc_editor.ex:736
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:129
#: lib/bds/ui/registry.ex:131
#, elixir-autogen, elixir-format
@@ -2735,14 +2809,14 @@ msgstr ""
msgid "Unknown"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:269
#: lib/bds/desktop/shell_live/media_editor.ex:280
#, elixir-autogen, elixir-format
msgid "Unlink from Post"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:709
#: lib/bds/desktop/shell_live/media_editor.ex:729
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:1002
#: lib/bds/desktop/shell_live/post_editor.ex:1032
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2811,11 +2885,6 @@ msgstr ""
msgid "Validate Translations"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:96
#, elixir-autogen, elixir-format
msgid "Validation changes applied"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:146
#, elixir-autogen, elixir-format
msgid "View"
@@ -3243,12 +3312,12 @@ msgstr ""
msgid "Move this post to the archive"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:658
#: lib/bds/desktop/shell_live/post_editor.ex:666
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:741
#: lib/bds/desktop/shell_live/post_editor.ex:749
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr ""
@@ -3427,18 +3496,18 @@ msgstr ""
msgid "Open a project before importing a blogmark."
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:684
#: lib/bds/desktop/shell_live/post_editor.ex:692
#, elixir-autogen, elixir-format
msgid "Added %{name}"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:699
#, elixir-autogen, elixir-format
msgid "Failed to import %{path}: %{reason}"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:683
#: lib/bds/desktop/shell_live/post_editor.ex:690
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:698
#, elixir-autogen, elixir-format
msgid "Insert Image"
msgstr ""

View File

@@ -458,10 +458,11 @@ rule SiteValidationScan {
rule SiteValidationApply {
when: SiteValidationApplyRequested(report)
-- Classifies affected paths into generation sections (core, single, category, tag, date)
-- Renders only affected sections in parallel
-- Renders only affected routes (a post's own single page plus only its own
-- categories, tags, and date archives); unaffected routes are left untouched
-- Deletes extra HTML files, removes empty directories
-- Regenerates calendar if anything changed
-- Rebuilds search index if anything rendered or deleted
-- Regenerates calendar if anything changed (feed/atom/404/sitemap/search
-- index are NOT rebuilt on apply)
ensures: ApplyValidationRequested(report.project_id, report.affected_sections)
}

View File

@@ -368,6 +368,76 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert Map.has_key?(completed.result.payload.summary, :updated_count)
end
test "apply_site_validation queues visible apply subtasks and revalidates the site", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Validation Apply Task Post",
content: "Validation apply task body",
language: "en",
categories: ["notes"],
tags: ["validation-task"]
})
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, _generation} = BDS.Generation.generate_site(project.id, [:core, :single])
post_path = BDS.Generation.post_output_path(published_post)
File.rm!(Path.join([temp_dir, "html", post_path]))
extra_path = Path.join([temp_dir, "html", "obsolete", "index.html"])
File.mkdir_p!(Path.dirname(extra_path))
File.write!(extra_path, "<html>obsolete</html>")
post_url_path = BDS.Generation.Paths.relative_path_to_url_path(post_path)
assert {:ok, report} =
BDS.Generation.validate_site(project.id, [:core, :single, :category, :tag, :date])
assert post_url_path in report.missing_url_paths
assert "/obsolete" in report.extra_url_paths
assert {:ok, result} = ShellCommands.execute("apply_site_validation", %{report: report})
assert result.kind == "task_queued"
assert result.action == "apply_site_validation"
assert result.title == "Apply Site Validation"
assert is_binary(result.task_id)
assert is_binary(result.task_group_id)
tasks =
wait_for_tasks_by_name(
[
"Prepare Validation Apply",
"Render Site Core",
"Render Single Posts",
"Validate Site"
],
&(&1.status == :completed),
5_000
)
assert Enum.all?(tasks, &(&1.group_id == result.task_group_id))
assert Enum.all?(tasks, &(&1.group_name == "Apply Site Validation"))
validation_task = Enum.find(tasks, &(&1.name == "Validate Site"))
assert validation_task.result.kind == "open_editor"
assert validation_task.result.route == "site_validation"
assert validation_task.result.payload.missing_url_paths == []
assert validation_task.result.payload.extra_url_paths == []
assert validation_task.result.payload.updated_post_url_paths == []
end
test "metadata_diff queues a tracked maintenance task and returns the report as an editor payload" do
assert {:ok, result} = ShellCommands.execute("metadata_diff")

View File

@@ -914,6 +914,321 @@ defmodule BDS.GenerationTest do
assert clean_report.updated_post_url_paths == []
end
test "apply_validation renders only targeted routes and leaves feed/atom/404/pagefind untouched",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post_a} =
Posts.create_post(%{
project_id: project.id,
title: "First Post",
content: "First body",
language: "en"
})
assert {:ok, _published_a} = Posts.publish_post(post_a.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
feed_path = Path.join([temp_dir, "html", "feed.xml"])
atom_path = Path.join([temp_dir, "html", "atom.xml"])
not_found_path = Path.join([temp_dir, "html", "404.html"])
pagefind_path = Path.join([temp_dir, "html", "pagefind", "index.json"])
feed_before = File.read!(feed_path)
atom_before = File.read!(atom_path)
not_found_before = File.read!(not_found_path)
pagefind_before = File.read!(pagefind_path)
# Publishing a new post leaves the generated feed/index reflecting only post A
# until validation apply targets the newly missing routes.
assert {:ok, post_b} =
Posts.create_post(%{
project_id: project.id,
title: "Second Post",
content: "Second body",
language: "en"
})
assert {:ok, published_b} = Posts.publish_post(post_b.id)
post_b_path = BDS.Generation.post_output_path(published_b)
post_b_file = Path.join([temp_dir, "html", post_b_path])
refute File.exists?(post_b_file)
assert {:ok, report} = BDS.Generation.validate_site(project.id)
assert relative_path_to_url_path(post_b_path) in report.missing_url_paths
assert {:ok, repair} = BDS.Generation.apply_validation(project.id, report)
assert repair.rendered_url_count > 0
# The targeted single-post route is rendered.
assert File.exists?(post_b_file)
# Old-app parity: feed, atom, 404, and the search index are NOT regenerated on apply.
assert File.read!(feed_path) == feed_before
assert File.read!(atom_path) == atom_before
assert File.read!(not_found_path) == not_found_before
assert File.read!(pagefind_path) == pagefind_before
end
test "apply_validation re-renders only the affected post's categories, tags, and single route",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post_a} =
Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "Alpha body",
language: "en",
categories: ["notes"],
tags: ["elixir"]
})
assert {:ok, published_a} = Posts.publish_post(post_a.id)
assert {:ok, post_b} =
Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "Beta body",
language: "en",
categories: ["photos"],
tags: ["erlang"]
})
assert {:ok, published_b} = Posts.publish_post(post_b.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
post_a_path = BDS.Generation.post_output_path(published_a)
post_b_path = BDS.Generation.post_output_path(published_b)
notes_before = generated_updated_at(project.id, "category/notes/index.html")
elixir_before = generated_updated_at(project.id, "tag/elixir/index.html")
photos_before = generated_updated_at(project.id, "category/photos/index.html")
erlang_before = generated_updated_at(project.id, "tag/erlang/index.html")
post_b_before = generated_updated_at(project.id, post_b_path)
File.rm!(Path.join([temp_dir, "html", post_a_path]))
assert {:ok, report} = BDS.Generation.validate_site(project.id)
assert relative_path_to_url_path(post_a_path) in report.missing_url_paths
# Only post A's route is affected; post B must not be flagged as updated.
assert report.updated_post_url_paths == []
assert {:ok, _repair} = BDS.Generation.apply_validation(project.id, report)
# Post A's own single route plus only its category and tag archives are re-rendered.
assert File.exists?(Path.join([temp_dir, "html", post_a_path]))
assert generated_updated_at(project.id, "category/notes/index.html") > notes_before
assert generated_updated_at(project.id, "tag/elixir/index.html") > elixir_before
# Post B's single route and its unrelated category and tag archives are left untouched.
assert generated_updated_at(project.id, post_b_path) == post_b_before
assert generated_updated_at(project.id, "category/photos/index.html") == photos_before
assert generated_updated_at(project.id, "tag/erlang/index.html") == erlang_before
end
test "apply_validation streams one progress message per rewritten URL with a running count",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Progress Post",
content: "Progress body",
language: "en",
categories: ["notes"],
tags: ["elixir"]
})
assert {:ok, published} = Posts.publish_post(post.id)
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
post_path = BDS.Generation.post_output_path(published)
File.rm!(Path.join([temp_dir, "html", post_path]))
assert {:ok, report} = BDS.Generation.validate_site(project.id)
test_pid = self()
assert {:ok, _repair} =
BDS.Generation.apply_validation(project.id, report,
on_progress: fn progress, message ->
send(test_pid, {:progress, progress, message})
end
)
messages = collect_progress_messages([])
post_url = relative_path_to_url_path(post_path)
# The rewritten single-post URL is named in a progress message (like the old app).
assert Enum.any?(messages, fn {_progress, message} -> String.contains?(message, post_url) end)
# Progress messages carry a running "(rewritten/total)" count of URLs.
assert Enum.any?(messages, fn {_progress, message} -> message =~ ~r/\(\d+\/\d+\)/ end)
# Progress advances incrementally rather than jumping straight from 0.0 to 1.0.
fractions =
messages
|> Enum.map(fn {progress, _message} -> progress end)
|> Enum.filter(fn progress -> progress > 0.0 and progress < 1.0 end)
assert fractions != []
end
test "prepare_validation_apply classifies page and day archive routes without section fallback",
%{project: project} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, page} =
Posts.create_post(%{
project_id: project.id,
title: "About",
content: "About body",
language: "en",
categories: ["page"]
})
assert {:ok, published_page} = Posts.publish_post(page.id)
assert {:ok, dated_post} =
Posts.create_post(%{
project_id: project.id,
title: "Day Archive Source",
content: "Day archive body",
language: "en"
})
assert {:ok, published_dated_post} = Posts.publish_post(dated_post.id)
{year, month, day} =
published_dated_post.created_at
|> DateTime.from_unix!(:millisecond)
|> then(&{&1.year, &1.month, &1.day})
assert {:ok, page_preparation} =
BDS.Generation.prepare_validation_apply(project.id, %{
missing_url_paths: ["/#{published_page.slug}"],
updated_post_url_paths: [],
extra_url_paths: []
})
assert page_preparation.sections_to_render == [:core]
assert {:ok, day_preparation} =
BDS.Generation.prepare_validation_apply(project.id, %{
missing_url_paths: [
"/#{year}/#{String.pad_leading(Integer.to_string(month), 2, "0")}/#{String.pad_leading(Integer.to_string(day), 2, "0")}"
],
updated_post_url_paths: [],
extra_url_paths: []
})
assert day_preparation.sections_to_render == [:date]
end
test "apply_validation_section reports visible progress before section rendering starts", %{
project: project
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Section Progress Template",
kind: :post,
content: "{{ labels.site_search_label }}: {{ post.title }}"
})
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
published_posts =
Enum.map(1..3, fn index ->
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Section Progress Post #{index}",
content: "Section progress body #{index}",
template_slug: published_template.slug
})
assert {:ok, published_post} = Posts.publish_post(post.id)
published_post
end)
post_url_paths =
Enum.map(
published_posts,
&(BDS.Generation.post_output_path(&1) |> relative_path_to_url_path())
)
parent = self()
on_progress = fn value, message ->
send(parent, {:apply_section_progress, value, message})
end
assert {:ok, _result} =
BDS.Generation.apply_validation_section(
project.id,
%{
missing_url_paths: post_url_paths,
updated_post_url_paths: [],
extra_url_paths: []
},
:single,
on_progress: on_progress
)
assert_receive {:apply_section_progress, +0.0, "Render Single Posts"}
assert_receive {:apply_section_progress, progress, message}
when progress > 0.0 and progress < 1.0 and
is_binary(message)
rendered_path =
Path.join([
project.data_path,
"html",
BDS.Generation.post_output_path(List.first(published_posts))
])
assert File.read!(rendered_path) =~ "Site search: Section Progress Post"
end
test "apply_validation returns an error for unreadable generated files", %{
project: project,
temp_dir: temp_dir
@@ -1343,6 +1658,23 @@ defmodule BDS.GenerationTest do
# Pushes generation timestamps and html mtimes far into the past so a
# subsequent source-file write is newer by more than the second-granularity
# mtime tolerance, without sleeping across real second boundaries.
defp collect_progress_messages(acc) do
receive do
{:progress, progress, message} -> collect_progress_messages([{progress, message} | acc])
after
0 -> Enum.reverse(acc)
end
end
defp generated_updated_at(project_id, relative_path) do
BDS.Generation.GeneratedFileHash
|> Repo.get_by(project_id: project_id, relative_path: relative_path)
|> case do
nil -> flunk("expected generated file hash for #{relative_path}")
%{updated_at: updated_at} -> updated_at
end
end
defp backdate_generated_outputs(project_id, temp_dir) do
past_posix = System.os_time(:second) - 120