diff --git a/exampleSite/content/test-product/stats.md b/exampleSite/content/test-product/stats.md
new file mode 100644
index 00000000..4641d700
--- /dev/null
+++ b/exampleSite/content/test-product/stats.md
@@ -0,0 +1,17 @@
+---
+title: "Shortcode usage"
+layout: shortcode-usage
+---
+This page demonstrates the `shortcode-usage` shortcode.
+
+The usage is as follows:
+``` go-html-template
+{{* shortcode-usage name="include" */>}}
+{{* shortcode-usage name="icon" */>}}
+{{* shortcode-usage name="call-out" */>}}
+```
+`shortcode-usage` uses regex to parse the shortcode name and attributes. The output shows usages grouped by shortcode name, and sorted by the number of usages.
+
+{{< shortcode-usage name="include" >}}
+{{< shortcode-usage name="icon" >}}
+{{< shortcode-usage name="call-out" >}}
diff --git a/layouts/partials/shortcode-usage.html b/layouts/partials/shortcode-usage.html
new file mode 100644
index 00000000..ae3c2857
--- /dev/null
+++ b/layouts/partials/shortcode-usage.html
@@ -0,0 +1,62 @@
+{{/* Group single-tag includes like:
+ {{< include "acm/how-to/policies-intro" >}}
+ {{< include "acm/how-to/policies-proxy-intro.md" >}}
+ */}}
+
+{{ $short := .Shortcode }}
+{{ $site := .Site }}
+
+
Usages of {{ $short }}
+
+{{ $patQuoted := print `(?s)\{\{\s*(?:<|%)\s*` $short `\b[^}]*?(?:"[^"]+"|'[^']+')[^}]*?(?:%|>)\}\}` }}
+
+{{/* Accumulate: key -> dict(path -> Page) */}}
+{{ $byKey := dict }}
+
+{{ range $pg := $site.RegularPages }}
+ {{ $src := readFile $pg.File.Path }}
+ {{ $matches := findRE $patQuoted $src }}
+ {{ if gt (len $matches) 0 }}
+ {{ range $m := $matches }}
+ {{/* first quoted string inside the tag */}}
+ {{ $qs := findRE `(?s)"[^"]+"|'[^']+'` $m }}
+ {{ if gt (len $qs) 0 }}
+ {{ $raw := index $qs 0 }}
+ {{ $key := replaceRE `^['"]|['"]$` "" $raw }}
+ {{ if ne $key "" }}
+ {{ $per := default (dict) (index $byKey $key) }}
+ {{ if not (isset $per $pg.File.Path) }}
+ {{ $per = merge $per (dict $pg.File.Path $pg) }}
+ {{ $byKey = merge $byKey (dict $key $per) }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+{{ end }}
+
+{{ $scratch := newScratch }}
+{{ $scratch.Set "__groups" (slice) }}
+{{ range $k, $per := $byKey }}
+ {{ $scratch.Add "__groups" (dict "k" $k "per" $per "count" (len $per)) }}
+{{ end }}
+
+{{ $groups := default (slice) ($scratch.Get "__groups") }}
+{{ $groupsSorted := sort $groups "count" "desc" }}
+
+{{ range $g := $groupsSorted }}
+ {{ $k := index $g "k" }}
+ {{ $per := index $g "per" }}
+ {{ $count := index $g "count" }}
+
+ {{ $k }}
— used on {{ $count }} {{ if eq $count 1 }}page{{ else }}pages{{ end }}
+
+ {{ if gt $count 0 }}
+ {{ range $path, $p := $per }}
+ - {{ $p.Title }} — {{ $p.File.Path }}
+ {{ end }}
+ {{ else }}
+ - No pages found
+ {{ end }}
+
+{{ end }}
diff --git a/layouts/shortcodes/shortcode-usage.html b/layouts/shortcodes/shortcode-usage.html
new file mode 100644
index 00000000..7a732b28
--- /dev/null
+++ b/layouts/shortcodes/shortcode-usage.html
@@ -0,0 +1,7 @@
+{{/*
+Usage: {{< shortcode-usage "include" >}} or {{< shortcode name="include" >}}.
+*/}}
+
+{{ $name := .Get 0 | default (.Get "name") | default "include" }}
+{{ $ctx := dict "Site" .Page.Site "Shortcode" $name }}
+{{ partial "shortcode-usage.html" $ctx }}