Skip to content
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, 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
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