Skip to content

improvement: add redirects config, and documentation explaining it #1952

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 17, 2024
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,35 @@ 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, when a user gets to a page that is not found, they will see a generic 404 page.
They will not be redirected to your package's home page. This can potentially be jarring for users.

With this in mind, it is a good idea to preserve links to your old documentation. We do this with the `redirects` configuration.
This can solve for everything but the function names changing.

For this example, we've changed the module `MyApp.MyModule` to `MyApp.My.Module`, and the extra `get-started.md` to `quickstart.md`

```elixir
defp docs do
[
...,
redirects: %{
MyApp.MyModule => MyApp.My.Module,
"get-started" => "quickstart"
}
]
end
```

## Contributing

The easiest way to test changes to ExDoc is to locally rebuild the app and its own documentation:
Expand Down
2 changes: 2 additions & 0 deletions lib/ex_doc/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()},
retriever: atom(),
skip_undefined_reference_warnings_on: (String.t() -> boolean),
skip_code_autolink_to: (String.t() -> boolean),
Expand Down
40 changes: 32 additions & 8 deletions lib/ex_doc/formatter/html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -390,6 +384,36 @@ 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.put_new("index", config.main)
|> Enum.map(fn {from, to} ->
source = stringify_redirect_item(from) <> ext
destination = stringify_redirect_item(to) <> ext
generate_redirect(source, config, destination)

source
end)
end

defp stringify_redirect_item(item) when is_binary(item) do
item
end

defp stringify_redirect_item(item) when is_atom(item) do
inspected = inspect(item)

case to_string(item) do
"Elixir." <> ^inspected -> inspected
other -> other
end
end

defp stringify_redirect_item(item) do
raise ArgumentError,
"Redirect source and destination must be a string or an atom, got: #{inspect(item)}"
end

defp disambiguate_id(extra, discriminator) do
Map.put(extra, :id, "#{extra.id}-#{discriminator}")
end
Expand Down
10 changes: 10 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ defmodule ExDoc.Mixfile do
"Cheatsheet.cheatmd",
"CHANGELOG.md"
] ++ test_dev_examples(Mix.env()),
redirects: redirects(Mix.env()),
source_ref: "v#{@version}",
source_url: @source_url,
groups_for_modules: [
Expand All @@ -107,6 +108,15 @@ defmodule ExDoc.Mixfile do
defp test_dev_examples(:dev), do: Path.wildcard("test/examples/*")
defp test_dev_examples(_), do: []

defp redirects(:dev) do
%{
"old-admonition" => "admonition",
Exdoc.OldMarkdown => ExDoc.Markdown
}
end

defp redirects(_), do: %{}

defp clean_test_fixtures(_args) do
File.rm_rf("test/tmp")
end
Expand Down
Loading