diff --git a/assets/javascripts/news.json b/assets/javascripts/news.json
index 6679b04f5c..1d049a723d 100644
--- a/assets/javascripts/news.json
+++ b/assets/javascripts/news.json
@@ -1,4 +1,8 @@
[
+ [
+ "2025-06-27",
+ "New documentation: Zsh"
+ ],
[
"2025-06-04",
"New documentation: es-toolkit"
diff --git a/lib/docs/filters/zsh/clean_html.rb b/lib/docs/filters/zsh/clean_html.rb
new file mode 100644
index 0000000000..43d65e045c
--- /dev/null
+++ b/lib/docs/filters/zsh/clean_html.rb
@@ -0,0 +1,20 @@
+module Docs
+ class Zsh
+ class CleanHtmlFilter < Filter
+ def call
+ css('table.header', 'table.menu', 'hr').remove
+
+ # Remove indices from headers.
+ css('h1', 'h2', 'h3').each do |node|
+ node.content = node.content.match(/^[\d\.]* (.*)$/)&.captures&.first
+ end
+
+ css('h2.section ~ a').each do |node|
+ node.next_element['id'] = node['name']
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/docs/filters/zsh/entries.rb b/lib/docs/filters/zsh/entries.rb
new file mode 100644
index 0000000000..02e7dc9602
--- /dev/null
+++ b/lib/docs/filters/zsh/entries.rb
@@ -0,0 +1,74 @@
+module Docs
+ class Zsh
+ class EntriesFilter < Docs::EntriesFilter
+ def get_name
+ extract_header_text(at_css('h1.chapter').content)
+ end
+
+ def additional_entries
+ entries = []
+ used_fns = []
+
+ css('h2.section').each do |node|
+ type = get_type
+ # Linkable anchor sits above
.
+ a = node.xpath('preceding-sibling::a').last
+ header_text = extract_header_text(node.content)
+
+ case type
+ when 'Zsh Modules'
+ module_name = header_text.match(/The (zsh\/.* Module)/)&.captures&.first
+ header_text = module_name if module_name.present?
+ when 'Calendar Function System'
+ header_text << ' (Calendar)'
+ end
+
+ entries << [header_text, a['name'], type] unless header_text.start_with?('Description')
+ end
+
+ # Functions are documented within elements.
+ # Names are wrapped in - , details within
- .
+ #
- can also contain anchors for the next function.
+ doc.css('> dl').each do |node|
+ type = get_type
+ fn_names = node.css('> dt')
+ node.css('dd a[name]').each_with_index do |anchor, i|
+ if fn_names[i].present? && anchor['name'].present?
+ fn_names[i]['id'] = anchor['name']
+
+ # Groups of functions are sometimes comma-delimited.
+ # Strip arguments, flags, etc. from function name.
+ # Skip flag-only headers.
+ fn_names[i].inner_html.split(', ').each do |fn|
+ fn.gsub!(/<(?:tt|var)>(.+?)<\/(?:tt|var)>/, '\1')
+ fn = fn.split(' ').first
+ fn.gsub!(/(?:[\[\(]).*(?:[\]\)]).*$/, '')
+
+ # Add context for operators.
+ fn << " (#{type})" if fn.length == 1
+
+ if fn.present? && !fn.match?(/^[\-\[]/) && !used_fns.include?(fn)
+ used_fns << fn
+ entries << [fn, anchor['name'], type]
+ end
+ end
+ end
+ end
+ end
+
+ entries
+ end
+
+ def get_type
+ extract_header_text(at_css('h1.chapter').content)
+ end
+
+ private
+
+ # Extracts text from a string, dropping indices preceding it.
+ def extract_header_text(str)
+ str.match(/^[\d\.]* (.*)$/)&.captures&.first
+ end
+ end
+ end
+end
diff --git a/lib/docs/scrapers/zsh.rb b/lib/docs/scrapers/zsh.rb
new file mode 100644
index 0000000000..b4705960b3
--- /dev/null
+++ b/lib/docs/scrapers/zsh.rb
@@ -0,0 +1,33 @@
+module Docs
+ class Zsh < UrlScraper
+ self.type = 'zsh'
+ self.release = '5.9.0'
+ self.base_url = 'https://zsh.sourceforge.io/Doc/Release/'
+ self.root_path = 'index.html'
+ self.links = {
+ home: 'https://zsh.sourceforge.io/',
+ code: 'https://sourceforge.net/p/zsh/web/ci/master/tree/',
+ }
+
+ options[:skip] = %w(
+ zsh_toc.html
+ zsh_abt.html
+ The-Z-Shell-Manual.html
+ Introduction.html
+ )
+ options[:skip_patterns] = [/-Index.html/]
+
+ html_filters.push 'zsh/entries', 'zsh/clean_html'
+
+ options[:attribution] = <<-HTML
+ The Z Shell is copyright © 1992–2017 Paul Falstad, Richard Coleman,
+ Zoltán Hidvégi, Andrew Main, Peter Stephenson, Sven Wischnowsky, and others.
+ Licensed under the MIT License.
+ HTML
+
+ def get_latest_version(opts)
+ body = fetch('https://zsh.sourceforge.io/Doc/Release', opts)
+ body.scan(/Zsh version ([0-9.]+)/)[0][0]
+ end
+ end
+end
diff --git a/public/icons/docs/zsh/16.png b/public/icons/docs/zsh/16.png
new file mode 100644
index 0000000000..05dc56e07e
Binary files /dev/null and b/public/icons/docs/zsh/16.png differ
diff --git a/public/icons/docs/zsh/16@2x.png b/public/icons/docs/zsh/16@2x.png
new file mode 100644
index 0000000000..014d7ab78a
Binary files /dev/null and b/public/icons/docs/zsh/16@2x.png differ
diff --git a/public/icons/docs/zsh/SOURCE b/public/icons/docs/zsh/SOURCE
new file mode 100644
index 0000000000..70cc4aeed8
--- /dev/null
+++ b/public/icons/docs/zsh/SOURCE
@@ -0,0 +1,2 @@
+https://sourceforge.net/p/zsh/web/ci/master/tree/favicon.png
+