From 2c1a895ea526678df4c2d5e844994bcb2218f2c4 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 17 Sep 2024 14:56:00 -0400 Subject: [PATCH] Add `redirects` config (#1952) --- README.md | 43 +++++++++++++++++++++++++ lib/ex_doc/config.ex | 2 ++ lib/ex_doc/formatter/html.ex | 29 ++++++++++++----- test/ex_doc/formatter/html_test.exs | 50 +++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ad7093234..9fe792a59 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,49 @@ Similarly to the example above, if your Markdown includes Mermaid graph specific For more details and configuration options, see the [Mermaid usage docs](https://mermaid-js.github.io/mermaid/#/usage). +## Changing documentation over time + +As your project grows, your documentation may very likely change, even structurally. There are a few important things to consider in this regard: + +- Links to your *extras* will break if you change or move file names. +- Links to your *modules, and mix tasks* will change if you change their name. +- Links to *functions* are actually links to modules with anchor links. If you change the function name, the link does + not break but will leave users at the top of the module's documentation. + +Because these docs are static files, the behavior of a missing page will depend on where they are hosted. +In particular, [hexdocs.pm](https://hexdocs.pm) will show a 404 page. + +You can improve the developer experience on everything but function names changing +by using the `redirects` configuration. For example, if you changed the module `MyApp.MyModule` +to `MyApp.My.Module` and the extra `get-started.md` to `quickstart.md`, you can +setup the following redirects: + + + +### Elixir + +For this example, we've changed the module `MyApp.MyModule` to `MyApp.My.Module`, and the extra `get-started.md` to `quickstart.md` + +```elixir +redirects: %{ + "MyApp.MyModule" => "MyApp.My.Module", + "get-started" => "quickstart" +} +``` + +### Erlang + +For this example, we've changed the module `:my_module` to `:my_module2`, and the extra `get-started.md` to `quickstart.md` + +```erlang +{redirects, [ + {"my_module", "my_module2"}, + {"get-started", "quickstart"} +]}. +``` + + + ## Contributing The easiest way to test changes to ExDoc is to locally rebuild the app and its own documentation: diff --git a/lib/ex_doc/config.ex b/lib/ex_doc/config.ex index b20debced..1646d8b38 100644 --- a/lib/ex_doc/config.ex +++ b/lib/ex_doc/config.ex @@ -39,6 +39,7 @@ defmodule ExDoc.Config do package: nil, proglang: :elixir, project: nil, + redirects: %{}, retriever: ExDoc.Retriever, skip_undefined_reference_warnings_on: &__MODULE__.skip_undefined_reference_warnings_on/1, @@ -78,6 +79,7 @@ defmodule ExDoc.Config do output: nil | Path.t(), package: :atom | nil, project: nil | String.t(), + redirects: %{optional(String.t()) => String.t()} | [{String.t(), String.t()}], retriever: atom(), skip_undefined_reference_warnings_on: (String.t() -> boolean), skip_code_autolink_to: (String.t() -> boolean), diff --git a/lib/ex_doc/formatter/html.ex b/lib/ex_doc/formatter/html.ex index bbc0226b0..c4fcb818d 100644 --- a/lib/ex_doc/formatter/html.ex +++ b/lib/ex_doc/formatter/html.ex @@ -47,7 +47,8 @@ defmodule ExDoc.Formatter.HTML do generate_search(nodes_map, config) ++ generate_not_found(nodes_map, config) ++ generate_list(nodes_map.modules, nodes_map, config) ++ - generate_list(nodes_map.tasks, nodes_map, config) ++ generate_index(config) + generate_list(nodes_map.tasks, nodes_map, config) ++ + generate_redirects(config, ".html") generate_build(Enum.sort(all_files), build) config.output |> Path.join("index.html") |> Path.relative_to_cwd() @@ -187,13 +188,6 @@ defmodule ExDoc.Formatter.HTML do File.write!(build, entries) end - defp generate_index(config) do - index_file = "index.html" - main_file = "#{config.main}.html" - generate_redirect(index_file, config, main_file) - [index_file] - end - defp generate_not_found(nodes_map, config) do filename = "404.html" config = set_canonical_url(config, filename) @@ -390,6 +384,25 @@ defmodule ExDoc.Formatter.HTML do |> Enum.sort_by(fn extra -> GroupMatcher.group_index(groups, extra.group) end) end + def generate_redirects(config, ext) do + config.redirects + |> Map.new() + |> Map.put_new("index", config.main) + |> Enum.map(fn {from, to} -> + unless is_binary(from), + do: raise("expected a string for the source of a redirect, got: #{inspect(from)}") + + unless is_binary(to), + do: raise("expected a string for the destination of a redirect, got: #{inspect(to)}") + + source = from <> ext + destination = to <> ext + generate_redirect(source, config, destination) + + source + end) + end + defp disambiguate_id(extra, discriminator) do Map.put(extra, :id, "#{extra.id}-#{discriminator}") end diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index 97396c59a..b9a93416f 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -326,6 +326,56 @@ defmodule ExDoc.Formatter.HTMLTest do end end + describe "generates redirects" do + test "redirects are generated based on the configuration", %{tmp_dir: tmp_dir} = context do + generate_docs( + doc_config(context, + extras: ["test/fixtures/LICENSE"], + redirects: %{ + "/old-license" => "license" + } + ) + ) + + assert File.read!(tmp_dir <> "/html/old-license.html") == """ + + + + + Elixir v1.0.1 — Documentation + + + + + + """ + end + + test "redirects accept a list", %{tmp_dir: tmp_dir} = context do + generate_docs( + doc_config(context, + extras: ["test/fixtures/LICENSE"], + redirects: [ + {"/old-license", "license"} + ] + ) + ) + + assert File.read!(tmp_dir <> "/html/old-license.html") == """ + + + + + Elixir v1.0.1 — Documentation + + + + + + """ + end + end + describe "generates extras" do @extras [ "test/fixtures/LICENSE",