feat: PLAN step 1 done, supposedly

This commit is contained in:
2026-04-25 21:53:44 +02:00
parent f1957cbab2
commit 2991edf4cf
18 changed files with 704 additions and 133 deletions

View File

@@ -0,0 +1,138 @@
defmodule BDS.CompatibilitySerializerParityTest do
use ExUnit.Case, async: true
test "post frontmatter serialization matches old bDS output" do
rendered =
BDS.Frontmatter.serialize_document(
[
{"id", "post-1"},
{"title", "Published Post"},
{"slug", "published-post"},
{"excerpt", "Summary"},
{"status", :published},
{"author", "Writer"},
{"language", "en"},
{"doNotTranslate", true},
{"templateSlug", "article"},
{"createdAt", 1_711_833_600_000},
{"updatedAt", 1_711_920_000_000},
{"publishedAt", 1_712_006_400_000},
{"tags", ["alpha"]},
{"categories", ["notes"]}
],
"Hello from markdown"
)
assert rendered ==
[
"---",
"id: post-1",
"title: Published Post",
"slug: published-post",
"excerpt: Summary",
"status: published",
"author: Writer",
"language: en",
"doNotTranslate: true",
"templateSlug: article",
"createdAt: '2024-03-30T21:20:00.000Z'",
"updatedAt: '2024-03-31T21:20:00.000Z'",
"publishedAt: '2024-04-01T21:20:00.000Z'",
"tags:",
" - alpha",
"categories:",
" - notes",
"---",
"Hello from markdown",
""
]
|> Enum.join("\n")
end
test "media sidecar serialization matches old bDS output" do
rendered =
BDS.Sidecar.serialize_document([
{"id", "media-from-file"},
{"originalName", "original.jpg"},
{"mimeType", "image/jpeg"},
{"size", 123},
{"width", 3},
{"height", 2},
{"title", "Recovered"},
{"alt", "Recovered alt"},
{"caption", "Recovered caption"},
{"author", "Writer"},
{"language", "en"},
{"createdAt", 1_711_833_600_000},
{"updatedAt", 1_711_920_000_000},
{"tags", ["alpha"]},
{"linkedPostIds", ["post-a"]}
])
assert rendered ==
[
"---",
"id: media-from-file",
"originalName: \"original.jpg\"",
"mimeType: image/jpeg",
"size: 123",
"width: 3",
"height: 2",
"title: \"Recovered\"",
"alt: \"Recovered alt\"",
"caption: \"Recovered caption\"",
"author: \"Writer\"",
"language: en",
"createdAt: 2024-03-30T21:20:00.000Z",
"updatedAt: 2024-03-31T21:20:00.000Z",
"tags: [\"alpha\"]",
"linkedPostIds: [\"post-a\"]",
"---"
]
|> Enum.join("\n")
end
test "media sidecar parsing accepts old bDS inline arrays and document markers" do
contents =
[
"---",
"id: media-from-file",
"originalName: \"original.jpg\"",
"mimeType: image/jpeg",
"size: 123",
"width: 3",
"height: 2",
"title: \"Recovered\"",
"alt: \"Recovered alt\"",
"caption: \"Recovered caption\"",
"author: \"Writer\"",
"language: en",
"createdAt: 2024-03-30T21:20:00.000Z",
"updatedAt: 2024-03-31T21:20:00.000Z",
"tags: [\"alpha\", \"beta\"]",
"linkedPostIds: [\"post-a\", \"post-b\"]",
"---"
]
|> Enum.join("\n")
assert {:ok, fields} = BDS.Sidecar.parse_document(contents)
assert fields == %{
"id" => "media-from-file",
"originalName" => "original.jpg",
"mimeType" => "image/jpeg",
"size" => 123,
"width" => 3,
"height" => 2,
"title" => "Recovered",
"alt" => "Recovered alt",
"caption" => "Recovered caption",
"author" => "Writer",
"language" => "en",
"createdAt" => 1_711_833_600_000,
"updatedAt" => 1_711_920_000_000,
"tags" => ["alpha", "beta"],
"linkedPostIds" => ["post-a", "post-b"]
}
end
end

View File

@@ -125,6 +125,48 @@ defmodule BDS.GenerationTest do
assert File.read!(Path.join([temp_dir, "html", "sitemap.xml"])) =~ "https://example.com/blog/"
end
test "generation writes feed and atom entries with canonical URLs for published posts", %{
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: "Feed Entry",
content: "Feed body",
language: "en"
})
created_at = DateTime.to_unix(~U[2026-04-15 12:00:00Z])
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^post.id),
set: [created_at: created_at, updated_at: created_at]
)
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core, :single])
canonical_url =
"https://example.com/blog" <>
"/" <> String.trim_trailing(BDS.Generation.post_output_path(published_post), "index.html")
feed_xml = File.read!(Path.join([temp_dir, "html", "feed.xml"]))
atom_xml = File.read!(Path.join([temp_dir, "html", "atom.xml"]))
assert feed_xml =~ "<rss>"
assert feed_xml =~ "<item><title>Feed Entry</title><link>#{canonical_url}</link></item>"
assert atom_xml =~ "<feed>"
assert atom_xml =~ "<entry><title>Feed Entry</title><id>#{canonical_url}</id></entry>"
end
test "generation renders published list and post templates for core and single pages", %{
project: project,
temp_dir: temp_dir
@@ -377,6 +419,97 @@ defmodule BDS.GenerationTest do
assert not_found_html =~ "Back to preview home"
end
test "generation starter templates render localized archive headings in each output language", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "fr",
blog_languages: ["fr", "en"],
max_posts_per_page: 10
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Archive Heading",
content: "Archive body",
language: "fr"
})
created_at = DateTime.to_unix(~U[2026-02-15 12:00:00Z])
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^post.id),
set: [created_at: created_at, updated_at: created_at]
)
assert {:ok, _published_post} = Posts.publish_post(post.id)
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:date])
french_html = File.read!(Path.join([temp_dir, "html", "2026", "02", "index.html"]))
english_html = File.read!(Path.join([temp_dir, "html", "en", "2026", "02", "index.html"]))
assert french_html =~ ~s(<html lang="fr")
assert french_html =~ "Archives février 2026"
assert english_html =~ ~s(<html lang="en")
assert english_html =~ "Archive February 2026"
end
test "generation rewrites media aliases with suffixes and renders slugged taxonomy links with tag colors",
%{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"]
})
source_path = Path.join(temp_dir, "sample.txt")
File.write!(source_path, "media body")
assert {:ok, media} =
Media.import_media(%{
project_id: project.id,
source_path: source_path,
title: "Sample"
})
media_source_reference =
"/" <> Path.join(Path.dirname(media.file_path), media.original_name) <> "?download=1#preview"
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Taxonomy Colors",
content: "![Asset](#{media_source_reference})",
language: "en",
categories: ["Release Notes"],
tags: ["Elixir"]
})
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, [tag]} = BDS.Tags.sync_tags_from_posts(project.id)
assert {:ok, _updated_tag} = BDS.Tags.update_tag(tag.id, %{color: "#112233"})
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:single])
post_html =
File.read!(Path.join([temp_dir, "html", BDS.Generation.post_output_path(published_post)]))
category_link = ~s(href="/category/release-notes/")
tag_link = ~s(href="/tag/elixir/" style="--bubble-accent: #112233;")
assert post_html =~ category_link
assert post_html =~ tag_link
assert elem(:binary.match(post_html, category_link), 0) <
elem(:binary.match(post_html, tag_link), 0)
assert post_html =~ ~s(src="/#{media.file_path}?download=1#preview")
end
test "single generation writes canonical post pages and language-prefixed translation pages", %{
project: project,
temp_dir: temp_dir
@@ -417,6 +550,52 @@ defmodule BDS.GenerationTest do
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ ~s(data-pagefind-body)
end
test "single generation renders the canonical route in the project main language when a translation exists",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "fr",
blog_languages: ["fr", "en"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Hello World",
content: "Canonical body",
language: "en"
})
assert {:ok, _translation} =
Posts.upsert_post_translation(post.id, "fr", %{
title: "Bonjour le monde",
content: "Corps FR"
})
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, result} = BDS.Generation.generate_site(project.id, [:single])
post_path = BDS.Generation.post_output_path(published_post)
translation_path = BDS.Generation.post_output_path(published_post, "en")
assert Enum.map(result.generated_files, & &1.relative_path) |> Enum.sort() ==
Enum.sort([post_path, translation_path])
canonical_html = File.read!(Path.join([temp_dir, "html", post_path]))
english_html = File.read!(Path.join([temp_dir, "html", translation_path]))
assert canonical_html =~ ~s(<html lang="fr")
assert canonical_html =~ "Bonjour le monde"
assert canonical_html =~ "Corps FR"
refute canonical_html =~ "Canonical body"
assert english_html =~ ~s(<html lang="en")
assert english_html =~ "Hello World"
assert english_html =~ "Canonical body"
end
test "archive generation writes paginated category, tag, and date pages", %{
project: project,
temp_dir: temp_dir

View File

@@ -700,6 +700,55 @@ defmodule BDS.MaintenanceTest do
assert "templates/orphan-view.liquid" in orphan_paths
end
test "metadata_diff ignores tag and category order like old bDS", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Ordered Post",
content: "Body",
tags: ["alpha", "beta"],
categories: ["article", "notes"]
})
assert {:ok, published_post} = BDS.Posts.publish_post(post.id)
post_path = Path.join(temp_dir, published_post.file_path)
File.write!(
post_path,
[
"---",
"id: #{published_post.id}",
"title: #{published_post.title}",
"slug: #{published_post.slug}",
"status: published",
"createdAt: '#{BDS.Persistence.timestamp_to_iso8601(published_post.created_at)}'",
"updatedAt: '#{BDS.Persistence.timestamp_to_iso8601(published_post.updated_at)}'",
"publishedAt: '#{BDS.Persistence.timestamp_to_iso8601(published_post.published_at)}'",
"tags:",
" - beta",
" - alpha",
"categories:",
" - notes",
" - article",
"---",
"Body",
""
]
|> Enum.join("\n")
)
assert {:ok, %{diff_reports: diff_reports}} = BDS.Maintenance.metadata_diff(project.id)
refute Enum.any?(diff_reports, fn report ->
report.entity_type == "post" and report.entity_id == published_post.id and
Enum.any?(report.differences, &(&1.name in ["tags", "categories"]))
end)
end
defp collect_progress_events(acc \\ []) do
receive do
{:rebuild_progress, value, message} -> collect_progress_events([{value, message} | acc])

View File

@@ -42,18 +42,20 @@ defmodule BDS.MediaTest do
assert File.read!(Path.join(temp_dir, media.file_path)) == "hello media"
sidecar = File.read!(Path.join(temp_dir, media.sidecar_path))
assert sidecar =~ "---\n"
assert sidecar =~ "id: #{media.id}\n"
assert sidecar =~ "originalName: sample.txt\n"
assert sidecar =~ "originalName: \"sample.txt\"\n"
assert sidecar =~ "mimeType: text/plain\n"
assert sidecar =~ "title: Sample\n"
assert sidecar =~ "alt: Alt text\n"
assert sidecar =~ "caption: Caption\n"
assert sidecar =~ "author: Writer\n"
assert sidecar =~ "title: \"Sample\"\n"
assert sidecar =~ "alt: \"Alt text\"\n"
assert sidecar =~ "caption: \"Caption\"\n"
assert sidecar =~ "author: \"Writer\"\n"
assert sidecar =~ "language: en\n"
assert sidecar =~ "tags:\n - alpha\n"
assert sidecar =~ "linkedPostIds:\n"
assert sidecar =~ "tags: [\"alpha\"]\n"
assert sidecar =~ "linkedPostIds: []\n"
assert sidecar =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert sidecar =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert String.ends_with?(sidecar, "\n---")
refute File.exists?(Path.join(temp_dir, media.sidecar_path <> ".tmp"))
end
@@ -78,10 +80,10 @@ defmodule BDS.MediaTest do
assert updated.language == "de"
sidecar = File.read!(Path.join(temp_dir, updated.sidecar_path))
assert sidecar =~ "title: Updated\n"
assert sidecar =~ "alt: Updated alt\n"
assert sidecar =~ "title: \"Updated\"\n"
assert sidecar =~ "alt: \"Updated alt\"\n"
assert sidecar =~ "language: de\n"
assert sidecar =~ "tags:\n - beta\n"
assert sidecar =~ "tags: [\"beta\"]\n"
end
test "delete_media removes the binary, sidecar, and database row", %{
@@ -452,11 +454,12 @@ defmodule BDS.MediaTest do
translated_sidecar_path = Path.join(temp_dir, media.file_path <> ".de.meta")
contents = File.read!(translated_sidecar_path)
assert contents =~ "---\n"
assert contents =~ "translationFor: #{media.id}\n"
assert contents =~ "language: de\n"
assert contents =~ "title: Titel\n"
assert contents =~ "alt: Alt text\n"
assert contents =~ "caption: Bildunterschrift\n"
assert contents =~ "title: \"Titel\"\n"
assert contents =~ "alt: \"Alt text\"\n"
assert contents =~ "caption: \"Bildunterschrift\"\n---"
end
defp tiny_jpeg_binary do

View File

@@ -151,9 +151,9 @@ defmodule BDS.PostsTest do
assert file_contents =~ "templateSlug: article\n"
assert file_contents =~ "tags:\n - alpha\n"
assert file_contents =~ "categories:\n - notes\n"
assert file_contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert file_contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert file_contents =~ ~r/publishedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert file_contents =~ ~r/createdAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert file_contents =~ ~r/updatedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert file_contents =~ ~r/publishedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert file_contents =~ "\n---\nHello from markdown\n"
refute File.exists?(full_path <> ".tmp")

View File

@@ -158,6 +158,50 @@ defmodule BDS.PreviewTest do
assert :ok = BDS.Preview.stop_preview(project.id)
end
test "draft preview honors the lang query parameter and falls back to the canonical draft", %{
project: project
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en", "de"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Canonical Draft",
content: "Canonical body",
language: "en"
})
assert {:ok, _translation} =
Posts.upsert_post_translation(post.id, "de", %{
title: "Deutscher Entwurf",
content: "Deutscher Inhalt"
})
assert {:ok, _server} = BDS.Preview.start_preview(project.id)
assert {:ok, %{body: german_html, content_type: "text/html"}} =
BDS.Preview.preview_draft(project.id, "/draft/canonical-draft?lang=de", post.id)
assert german_html =~ ~s(<html lang="de")
assert german_html =~ "Deutscher Entwurf"
assert german_html =~ "Deutscher Inhalt"
refute german_html =~ "Canonical body"
assert {:ok, %{body: fallback_html, content_type: "text/html"}} =
BDS.Preview.preview_draft(project.id, "/draft/canonical-draft?lang=fr", post.id)
assert fallback_html =~ ~s(<html lang="en")
assert fallback_html =~ "Canonical Draft"
assert fallback_html =~ "Canonical body"
assert :ok = BDS.Preview.stop_preview(project.id)
end
test "preview renders not-found template for missing routes and rewrites markdown macros and canonical URLs",
%{project: project, temp_dir: temp_dir} do
:inets.start()

View File

@@ -72,8 +72,8 @@ defmodule BDS.ScriptsTest do
assert contents =~ "entrypoint: main\n"
assert contents =~ "enabled: true\n"
assert contents =~ "version: 1\n"
assert contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert contents =~ ~r/createdAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ ~r/updatedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
refute File.exists?(full_path <> ".tmp")
end

View File

@@ -72,8 +72,8 @@ defmodule BDS.TemplatesTest do
assert contents =~ "kind: list\n"
assert contents =~ "enabled: true\n"
assert contents =~ "version: 1\n"
assert contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
assert contents =~ ~r/createdAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ ~r/updatedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ "\n---\n<section>{{ page_title }}</section>\n"
refute File.exists?(full_path <> ".tmp")
end