diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json
index 3ddc39a516..6679b04f5c 100644
--- a/assets/javascripts/news.json
+++ b/assets/javascripts/news.json
@@ -1,4 +1,8 @@
[
+ [
+ "2025-06-04",
+ "New documentation: es-toolkit"
+ ],
[
"2025-05-28",
"New documentation: Vert.x"
diff --git a/docs/file-scrapers.md b/docs/file-scrapers.md
index 7a6e0a5f5f..133b621b9b 100644
--- a/docs/file-scrapers.md
+++ b/docs/file-scrapers.md
@@ -59,6 +59,12 @@ curl -L https://github.com/erlang/otp/releases/download/OTP-$RELEASE/otp_doc_htm
bsdtar --extract --file - --directory=docs/erlang\~$VERSION/
```
+## es-toolkit
+
+```sh
+git clone https://github.com/toss/es-toolkit docs/es_toolkit
+```
+
## Gnu
### Bash
diff --git a/lib/docs/scrapers/es_toolkit.rb b/lib/docs/scrapers/es_toolkit.rb
new file mode 100644
index 0000000000..85079723a0
--- /dev/null
+++ b/lib/docs/scrapers/es_toolkit.rb
@@ -0,0 +1,88 @@
+module Docs
+ class EsToolkit < FileScraper
+ self.name = "es-toolkit"
+ self.slug = "es_toolkit"
+ self.type = "simple"
+ self.links = {
+ code: "https://github.com/toss/es-toolkit",
+ home: "https://es-toolkit.slash.page",
+ }
+
+ options[:attribution] = <<-HTML
+ © 2024-2025, Viva Republica
+ Licensed under the MIT License.
+ HTML
+
+ def get_latest_version(opts)
+ get_github_tags("toss", "es-toolkit", opts).first["name"]
+ end
+
+ def build_pages(&block)
+ internal("docs/intro.md", path: "index", &block)
+ Dir.chdir(source_directory) do
+ Dir["docs/reference/**/*.md"]
+ end.each { internal(_1, &block) }
+ end
+
+ protected
+
+ def internal(filename, path: nil, &block)
+ path ||= filename[%r{docs/reference/(.*/.*).md$}, 1]
+
+ # calculate name/type
+ if path != "index"
+ name = filename[%r{([^/]+).md$}, 1]
+ type = path.split("/")[0..-2]
+ type = type.map(&:capitalize).join(" ")
+ # really bad way to sort
+ type = type.gsub(/^(Compat|Error)\b/, "\u2063\\1") # U+2063 INVISIBLE SEPARATOR
+ else
+ name = type = nil
+ end
+
+ # now yield
+ entries = [Entry.new(name, path, type)]
+ output = render(filename)
+ store_path = "#{path}.html"
+ yield({entries:, output:, path:, store_path:})
+ end
+
+ # render/style HTML
+ def render(filename)
+ s = md.render(request_one(filename).body)
+
+ # kill all links, they don't work
+ s.gsub!(%r{(.*?)}, "\\1")
+
+ # syntax highlighting
+ s.gsub!(%r{
}, "")
+
+ # h3 => h4
+ s.gsub!(%r{(?h)3>}, "\\14>")
+
+ # manually add attribution
+ link = "#{self.class.links[:home]}#{filename.gsub(/^docs/,'').gsub(/md$/,'html')}"
+ s += <<~HTML
+
+
+ #{options[:attribution]}
+
+
+ #{link}
+
+
+
+ HTML
+ s
+ end
+
+ def md
+ @md ||= Redcarpet::Markdown.new(
+ Redcarpet::Render::HTML,
+ autolink: true,
+ fenced_code_blocks: true,
+ tables: true
+ )
+ end
+ end
+end
diff --git a/public/icons/docs/es_toolkit/16.png b/public/icons/docs/es_toolkit/16.png
new file mode 100644
index 0000000000..d63aff1c35
Binary files /dev/null and b/public/icons/docs/es_toolkit/16.png differ
diff --git a/public/icons/docs/es_toolkit/16@2x.png b/public/icons/docs/es_toolkit/16@2x.png
new file mode 100644
index 0000000000..40dff7a352
Binary files /dev/null and b/public/icons/docs/es_toolkit/16@2x.png differ
diff --git a/public/icons/docs/es_toolkit/SOURCE b/public/icons/docs/es_toolkit/SOURCE
new file mode 100644
index 0000000000..90444ebb34
--- /dev/null
+++ b/public/icons/docs/es_toolkit/SOURCE
@@ -0,0 +1 @@
+https://es-toolkit.slash.page/favicon-100x100.png