From bb0b040a928ab7a3868fa4aba68898a5890ab8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 11:07:34 +0200 Subject: [PATCH 1/2] Use subdomain URLs for hexpm package docs Switch hexpm-repo docs URLs from `https://hexdocs.pm/PACKAGE/...` to `https://PACKAGE.hexdocs.pm/...`. Org-repo URLs are unchanged. --- lib/hex/scm.ex | 2 +- lib/hex/utils.ex | 8 ++++---- lib/mix/tasks/hex.publish.ex | 4 ++-- lib/mix/tasks/hex.search.ex | 2 +- test/mix/tasks/hex.docs_test.exs | 10 +++++----- test/mix/tasks/hex.publish_test.exs | 6 +++--- test/mix/tasks/hex.search_test.exs | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/hex/scm.ex b/lib/hex/scm.ex index 3fcf921e..2e61ae8d 100644 --- a/lib/hex/scm.ex +++ b/lib/hex/scm.ex @@ -118,7 +118,7 @@ defmodule Hex.SCM do end end - # https://hexdocs.pm/mix/Mix.Tasks.Deps.html#module-dependency-definition-options + # https://mix.hexdocs.pm/Mix.Tasks.Deps.html#module-dependency-definition-options mix_keys = ~w[app env compile optional only targets override manager runtime system_env]a # https://hex.pm/docs/usage#options diff --git a/lib/hex/utils.ex b/lib/hex/utils.ex index 54d5a555..5997a075 100644 --- a/lib/hex/utils.ex +++ b/lib/hex/utils.ex @@ -218,28 +218,28 @@ defmodule Hex.Utils do def hexdocs_url(organization, package) when organization in ["hexpm", nil], - do: "https://hexdocs.pm/#{package}" + do: "https://#{package}.hexdocs.pm" def hexdocs_url(organization, package), do: "https://#{organization}.hexorgs.pm/#{package}" def hexdocs_url(organization, package, version) when organization in ["hexpm", nil], - do: "https://hexdocs.pm/#{package}/#{version}" + do: "https://#{package}.hexdocs.pm/#{version}" def hexdocs_url(organization, package, version), do: "https://#{organization}.hexorgs.pm/#{package}/#{version}" def hexdocs_module_url(organization, package, module) when organization in ["hexpm", nil], - do: "https://hexdocs.pm/#{package}/#{module}.html" + do: "https://#{package}.hexdocs.pm/#{module}.html" def hexdocs_module_url(organization, package, module), do: "https://#{organization}.hexorgs.pm/#{package}/#{module}.html" def hexdocs_module_url(organization, package, version, module) when organization in ["hexpm", nil], - do: "https://hexdocs.pm/#{package}/#{version}/#{module}.html" + do: "https://#{package}.hexdocs.pm/#{version}/#{module}.html" def hexdocs_module_url(organization, package, version, module), do: "https://#{organization}.hexorgs.pm/#{package}/#{version}/#{module}.html" diff --git a/lib/mix/tasks/hex.publish.ex b/lib/mix/tasks/hex.publish.ex index 21a7c01c..47d8254d 100644 --- a/lib/mix/tasks/hex.publish.ex +++ b/lib/mix/tasks/hex.publish.ex @@ -23,8 +23,8 @@ defmodule Mix.Tasks.Hex.Publish do is the generated documentation located in the `doc/` directory with an `index.html` file. - The documentation will be accessible at `https://hexdocs.pm/my_package/1.0.0`, - `https://hexdocs.pm/my_package` will always redirect to the latest published + The documentation will be accessible at `https://my_package.hexdocs.pm/1.0.0`, + `https://my_package.hexdocs.pm` will always redirect to the latest published version. Documentation will be built and published automatically. To publish a package diff --git a/lib/mix/tasks/hex.search.ex b/lib/mix/tasks/hex.search.ex index bdee9166..a02ec80a 100644 --- a/lib/mix/tasks/hex.search.ex +++ b/lib/mix/tasks/hex.search.ex @@ -251,7 +251,7 @@ defmodule Mix.Tasks.Hex.Search do defp document_url(package, ref) do case :binary.split(package, "-") do [name, version] -> - "https://hexdocs.pm/#{Enum.join([name, version, ref], "/")}" + "https://#{name}.hexdocs.pm/#{version}/#{ref}" _ -> Mix.raise("Unexpected package search result format: #{inspect(package)}") diff --git a/test/mix/tasks/hex.docs_test.exs b/test/mix/tasks/hex.docs_test.exs index ee94ad5b..af06d10b 100644 --- a/test/mix/tasks/hex.docs_test.exs +++ b/test/mix/tasks/hex.docs_test.exs @@ -340,7 +340,7 @@ defmodule Mix.Tasks.Hex.DocsTest do test "open docs online" do Mix.Tasks.Hex.Docs.run(["online", "ecto"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://hexdocs.pm/ecto" + assert Enum.fetch!(browser_open_cmd, -1) == "https://ecto.hexdocs.pm" end test "open the version of a package this app uses online" do @@ -350,7 +350,7 @@ defmodule Mix.Tasks.Hex.DocsTest do Mix.Dep.Lock.write(%{docs_package: {:hex, :docs_package, "1.1.1"}}) Mix.Tasks.Hex.Docs.run(["online", "docs_package"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://hexdocs.pm/docs_package/1.1.1" + assert Enum.fetch!(browser_open_cmd, -1) == "https://docs_package.hexdocs.pm/1.1.1" end) end @@ -361,14 +361,14 @@ defmodule Mix.Tasks.Hex.DocsTest do Mix.Dep.Lock.write(%{docs_package: {:hex, :docs_package, "1.1.1"}}) Mix.Tasks.Hex.Docs.run(["online", "docs_package", "--latest"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://hexdocs.pm/docs_package" + assert Enum.fetch!(browser_open_cmd, -1) == "https://docs_package.hexdocs.pm" end) end test "open a specific page online using the page option" do Mix.Tasks.Hex.Docs.run(["online", "ecto", "--page", "Ecto.Repo"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://hexdocs.pm/ecto/Ecto.Repo.html" + assert Enum.fetch!(browser_open_cmd, -1) == "https://ecto.hexdocs.pm/Ecto.Repo.html" end end @@ -377,7 +377,7 @@ defmodule Mix.Tasks.Hex.DocsTest do Mix.Tasks.Hex.Docs.run(["online", "ecto", "--module", "Ecto.Repo"]) assert_received {:mix_shell, :error, ["--module is deprecated, use --page instead"]} assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://hexdocs.pm/ecto/Ecto.Repo.html" + assert Enum.fetch!(browser_open_cmd, -1) == "https://ecto.hexdocs.pm/Ecto.Repo.html" end @tag mirror: true diff --git a/test/mix/tasks/hex.publish_test.exs b/test/mix/tasks/hex.publish_test.exs index 1255f0fb..a6bea7d8 100644 --- a/test/mix/tasks/hex.publish_test.exs +++ b/test/mix/tasks/hex.publish_test.exs @@ -237,7 +237,7 @@ defmodule Mix.Tasks.Hex.PublishTest do refute_received {:mix_shell, :info, [ - "Docs will soon be available at https://hexdocs.pm/invalid_filename/0.1.0" + "Docs will soon be available at https://invalid_filename.hexdocs.pm/0.1.0" ]} end end) @@ -258,7 +258,7 @@ defmodule Mix.Tasks.Hex.PublishTest do refute_received {:mix_shell, :info, [ - "Docs will soon be available at https://hexdocs.pm/invalid_dirname/0.1.0" + "Docs will soon be available at https://invalid_dirname.hexdocs.pm/0.1.0" ]} end end) @@ -278,7 +278,7 @@ defmodule Mix.Tasks.Hex.PublishTest do Mix.Tasks.Hex.Publish.run(["docs", "--no-progress", "--replace"]) refute_received {:mix_shell, :info, - ["Docs will soon be available at https://hexdocs.pm/ex_doc/0.1.0"]} + ["Docs will soon be available at https://ex_doc.hexdocs.pm/0.1.0"]} end end) end diff --git a/test/mix/tasks/hex.search_test.exs b/test/mix/tasks/hex.search_test.exs index 34b816be..a70aa4b0 100644 --- a/test/mix/tasks/hex.search_test.exs +++ b/test/mix/tasks/hex.search_test.exs @@ -118,12 +118,12 @@ defmodule Mix.Tasks.Hex.SearchTest do assert_received {:mix_shell, :info, [ - "# cast/4 (1/2)\nhttps://hexdocs.pm/ecto/3.13.4/Ecto.Changeset.html#cast/4\n\nCast changesets.\n\n" + "# cast/4 (1/2)\nhttps://ecto.hexdocs.pm/3.13.4/Ecto.Changeset.html#cast/4\n\nCast changesets.\n\n" ]} assert_received {:mix_shell, :info, [ - "# Examples - mix deps.tree (2/2)\nhttps://hexdocs.pm/mix/1.20.0-rc.5/Mix.Tasks.Deps.Tree.html#module-examples\n\nTree docs.\n\n" + "# Examples - mix deps.tree (2/2)\nhttps://mix.hexdocs.pm/1.20.0-rc.5/Mix.Tasks.Deps.Tree.html#module-examples\n\nTree docs.\n\n" ]} end) end From 47c4945382fb9ad5b17d73f5f74c1ff357a57e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 15:39:43 +0200 Subject: [PATCH 2/2] Map underscore to hyphen in hexdocs.pm subdomain URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hex package names allow underscores. RFC 1123 hostname labels and RFC 6125 wildcard SAN matching don't, and Fastly enforces strict SAN matching at the HTTP edge — phoenix_live_view.hexdocs.pm returns 421 'Misdirected Request' even though the wildcard cert covers *.hexdocs.pm. The Fastly Compute subdomain handler reverses the mapping before the GCS bucket key lookup so canonical hex names continue to be the storage key. Apply the same mapping in the Hex CLI helpers that emit hexpm-repo subdomain URLs. Org-repo branch is unchanged (hexorgs.pm is GKE and doesn't enforce strict SAN matching). --- lib/hex/utils.ex | 14 ++++++++++---- test/mix/tasks/hex.docs_test.exs | 4 ++-- test/mix/tasks/hex.publish_test.exs | 6 +++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/hex/utils.ex b/lib/hex/utils.ex index 5997a075..5d61b64b 100644 --- a/lib/hex/utils.ex +++ b/lib/hex/utils.ex @@ -218,32 +218,38 @@ defmodule Hex.Utils do def hexdocs_url(organization, package) when organization in ["hexpm", nil], - do: "https://#{package}.hexdocs.pm" + do: "https://#{package_subdomain(package)}.hexdocs.pm" def hexdocs_url(organization, package), do: "https://#{organization}.hexorgs.pm/#{package}" def hexdocs_url(organization, package, version) when organization in ["hexpm", nil], - do: "https://#{package}.hexdocs.pm/#{version}" + do: "https://#{package_subdomain(package)}.hexdocs.pm/#{version}" def hexdocs_url(organization, package, version), do: "https://#{organization}.hexorgs.pm/#{package}/#{version}" def hexdocs_module_url(organization, package, module) when organization in ["hexpm", nil], - do: "https://#{package}.hexdocs.pm/#{module}.html" + do: "https://#{package_subdomain(package)}.hexdocs.pm/#{module}.html" def hexdocs_module_url(organization, package, module), do: "https://#{organization}.hexorgs.pm/#{package}/#{module}.html" def hexdocs_module_url(organization, package, version, module) when organization in ["hexpm", nil], - do: "https://#{package}.hexdocs.pm/#{version}/#{module}.html" + do: "https://#{package_subdomain(package)}.hexdocs.pm/#{version}/#{module}.html" def hexdocs_module_url(organization, package, version, module), do: "https://#{organization}.hexorgs.pm/#{package}/#{version}/#{module}.html" + # Hex package names allow underscores. RFC 1123 hostname labels and + # RFC 6125 wildcard SAN matching don't, so Fastly returns 421 for + # underscore subdomains under *.hexdocs.pm. Map `_` -> `-` here; the + # Fastly Compute subdomain handler reverses it for the bucket lookup. + defp package_subdomain(package), do: String.replace(package, "_", "-") + def package_retirement_reason(:RETIRED_OTHER), do: "other" def package_retirement_reason(:RETIRED_INVALID), do: "invalid" def package_retirement_reason(:RETIRED_SECURITY), do: "security" diff --git a/test/mix/tasks/hex.docs_test.exs b/test/mix/tasks/hex.docs_test.exs index af06d10b..7ceb9c8f 100644 --- a/test/mix/tasks/hex.docs_test.exs +++ b/test/mix/tasks/hex.docs_test.exs @@ -350,7 +350,7 @@ defmodule Mix.Tasks.Hex.DocsTest do Mix.Dep.Lock.write(%{docs_package: {:hex, :docs_package, "1.1.1"}}) Mix.Tasks.Hex.Docs.run(["online", "docs_package"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://docs_package.hexdocs.pm/1.1.1" + assert Enum.fetch!(browser_open_cmd, -1) == "https://docs-package.hexdocs.pm/1.1.1" end) end @@ -361,7 +361,7 @@ defmodule Mix.Tasks.Hex.DocsTest do Mix.Dep.Lock.write(%{docs_package: {:hex, :docs_package, "1.1.1"}}) Mix.Tasks.Hex.Docs.run(["online", "docs_package", "--latest"]) assert_received {:hex_system_cmd, _cmd, browser_open_cmd} - assert Enum.fetch!(browser_open_cmd, -1) == "https://docs_package.hexdocs.pm" + assert Enum.fetch!(browser_open_cmd, -1) == "https://docs-package.hexdocs.pm" end) end diff --git a/test/mix/tasks/hex.publish_test.exs b/test/mix/tasks/hex.publish_test.exs index a6bea7d8..ac873a44 100644 --- a/test/mix/tasks/hex.publish_test.exs +++ b/test/mix/tasks/hex.publish_test.exs @@ -237,7 +237,7 @@ defmodule Mix.Tasks.Hex.PublishTest do refute_received {:mix_shell, :info, [ - "Docs will soon be available at https://invalid_filename.hexdocs.pm/0.1.0" + "Docs will soon be available at https://invalid-filename.hexdocs.pm/0.1.0" ]} end end) @@ -258,7 +258,7 @@ defmodule Mix.Tasks.Hex.PublishTest do refute_received {:mix_shell, :info, [ - "Docs will soon be available at https://invalid_dirname.hexdocs.pm/0.1.0" + "Docs will soon be available at https://invalid-dirname.hexdocs.pm/0.1.0" ]} end end) @@ -278,7 +278,7 @@ defmodule Mix.Tasks.Hex.PublishTest do Mix.Tasks.Hex.Publish.run(["docs", "--no-progress", "--replace"]) refute_received {:mix_shell, :info, - ["Docs will soon be available at https://ex_doc.hexdocs.pm/0.1.0"]} + ["Docs will soon be available at https://ex-doc.hexdocs.pm/0.1.0"]} end end) end