diff --git a/README.md b/README.md
index 7750366..e371c0f 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,13 @@ Run shell commands from natural-language requests:
jst find all files bigger than 500 mb in ~/downloads
```
+
+
+
+
+The demo above is randomly selected on each fresh image request. It is recorded
+from a real `jst` run; [see every demo](docs/demos/).
+
## Install
### Homebrew
diff --git a/docs/demos/changed-today.gif b/docs/demos/changed-today.gif
new file mode 100644
index 0000000..097eaf0
Binary files /dev/null and b/docs/demos/changed-today.gif differ
diff --git a/docs/demos/changed-today.tape b/docs/demos/changed-today.tape
new file mode 100644
index 0000000..ff29338
--- /dev/null
+++ b/docs/demos/changed-today.tape
@@ -0,0 +1,32 @@
+Output docs/demos/changed-today.gif
+
+Require jst
+
+Set Shell "zsh"
+Set FontFamily "Monaco"
+Set FontSize 18
+Set Width 1120
+Set Height 560
+Set Padding 28
+Set Margin 34
+Set MarginFill "#f3efe6"
+Set BorderRadius 14
+Set WindowBar "Colorful"
+Set WindowBarSize 38
+Set Theme "Catppuccin Mocha"
+Set TypingSpeed 34ms
+Set Framerate 30
+Set PlaybackSpeed 1.15
+
+Hide
+Type `unset NO_COLOR; export COLUMNS=80; export PS1=$'\n❯ '`
+Enter
+Type "mkdir -p /tmp/jst-demo-changed; cd /tmp/jst-demo-changed; touch draft.md deploy.log; touch -t 202501010000 old-notes.txt"
+Enter
+Ctrl+L
+Show
+
+Type "jst find every file changed in the last 24 hours"
+Enter
+Sleep 5s
+Sleep 2s
diff --git a/docs/demos/clear-port-8080.gif b/docs/demos/clear-port-8080.gif
new file mode 100644
index 0000000..842758a
Binary files /dev/null and b/docs/demos/clear-port-8080.gif differ
diff --git a/docs/demos/clear-port-8080.tape b/docs/demos/clear-port-8080.tape
new file mode 100644
index 0000000..9c856c2
--- /dev/null
+++ b/docs/demos/clear-port-8080.tape
@@ -0,0 +1,38 @@
+Output docs/demos/clear-port-8080.gif
+
+Require jst
+
+Set Shell "zsh"
+Set FontFamily "Monaco"
+Set FontSize 18
+Set Width 1120
+Set Height 720
+Set Padding 28
+Set Margin 34
+Set MarginFill "#f3efe6"
+Set BorderRadius 14
+Set WindowBar "Colorful"
+Set WindowBarSize 38
+Set Theme "Catppuccin Mocha"
+Set TypingSpeed 34ms
+Set Framerate 30
+Set PlaybackSpeed 1.15
+
+Hide
+Type `unset NO_COLOR; export COLUMNS=80; export PS1=$'\n❯ '`
+Enter
+Type "python3 -m http.server 8080 >/dev/null 2>&1 &"
+Enter
+Sleep 1s
+Ctrl+L
+Show
+
+Type "jst stop whatever is using port 8080"
+Enter
+Sleep 7s
+Type "y"
+Enter
+Sleep 2s
+Type "lsof -nP -iTCP:8080 -sTCP:LISTEN || echo 'port 8080 is free'"
+Enter
+Sleep 2s
diff --git a/docs/demos/remove-ds-store.gif b/docs/demos/remove-ds-store.gif
new file mode 100644
index 0000000..b348afe
Binary files /dev/null and b/docs/demos/remove-ds-store.gif differ
diff --git a/docs/demos/remove-ds-store.tape b/docs/demos/remove-ds-store.tape
new file mode 100644
index 0000000..f905ae1
--- /dev/null
+++ b/docs/demos/remove-ds-store.tape
@@ -0,0 +1,34 @@
+Output docs/demos/remove-ds-store.gif
+
+Require jst
+
+Set Shell "zsh"
+Set FontFamily "Monaco"
+Set FontSize 18
+Set Width 1120
+Set Height 720
+Set Padding 28
+Set Margin 34
+Set MarginFill "#f3efe6"
+Set BorderRadius 14
+Set WindowBar "Colorful"
+Set WindowBarSize 38
+Set Theme "Catppuccin Mocha"
+Set TypingSpeed 34ms
+Set Framerate 30
+Set PlaybackSpeed 1.15
+
+Hide
+Type `unset NO_COLOR; export COLUMNS=80; export PS1=$'\n❯ '`
+Enter
+Type "mkdir -p /tmp/jst-demo-ds-store/nested; cd /tmp/jst-demo-ds-store; touch .DS_Store nested/.DS_Store"
+Enter
+Ctrl+L
+Show
+
+Type "jst remove all .DS_Store files below the current directory"
+Enter
+Sleep 7s
+Type "n"
+Enter
+Sleep 2s
diff --git a/docs/demos/zip-folder.gif b/docs/demos/zip-folder.gif
new file mode 100644
index 0000000..2ef44c2
Binary files /dev/null and b/docs/demos/zip-folder.gif differ
diff --git a/docs/demos/zip-folder.tape b/docs/demos/zip-folder.tape
new file mode 100644
index 0000000..bdc0c03
--- /dev/null
+++ b/docs/demos/zip-folder.tape
@@ -0,0 +1,35 @@
+Output docs/demos/zip-folder.gif
+
+Require jst
+
+Set Shell "zsh"
+Set FontFamily "Monaco"
+Set FontSize 18
+Set Width 1120
+Set Height 620
+Set Padding 28
+Set Margin 34
+Set MarginFill "#f3efe6"
+Set BorderRadius 14
+Set WindowBar "Colorful"
+Set WindowBarSize 38
+Set Theme "Catppuccin Mocha"
+Set TypingSpeed 34ms
+Set Framerate 30
+Set PlaybackSpeed 1.15
+
+Hide
+Type `unset NO_COLOR; export COLUMNS=80; export PS1=$'\n❯ '`
+Enter
+Type "mkdir -p /tmp/jst-demo-zip/folder/node_modules /tmp/jst-demo-zip/folder/src; cd /tmp/jst-demo-zip; mkfile 1m folder/src/app.js; mkfile 2m folder/node_modules/bundle.js"
+Enter
+Ctrl+L
+Show
+
+Type "jst zip this folder without node_modules"
+Enter
+Sleep 5s
+Sleep 2s
+Type "unzip -l folder.zip"
+Enter
+Sleep 2s
diff --git a/site/demo-gif.test.js b/site/demo-gif.test.js
new file mode 100644
index 0000000..cc4e394
--- /dev/null
+++ b/site/demo-gif.test.js
@@ -0,0 +1,18 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { demoFileAt, demoRedirect } from "../workers/demo-gif.js";
+
+test("the demo endpoint can select every recorded GIF", () => {
+ assert.equal(demoFileAt(0), "changed-today.gif");
+ assert.equal(demoFileAt(1), "clear-port-8080.gif");
+ assert.equal(demoFileAt(2), "remove-ds-store.gif");
+ assert.equal(demoFileAt(3), "zip-folder.gif");
+});
+
+test("the demo endpoint redirects without allowing the selection to be cached", () => {
+ const response = demoRedirect(3);
+
+ assert.equal(response.status, 302);
+ assert.equal(response.headers.get("Location"), "https://jst.sh/demos/zip-folder.gif");
+ assert.equal(response.headers.get("Cache-Control"), "no-store");
+});
diff --git a/workers/demo-gif.js b/workers/demo-gif.js
new file mode 100644
index 0000000..c5c81ca
--- /dev/null
+++ b/workers/demo-gif.js
@@ -0,0 +1,26 @@
+const DEMO_FILES = [
+ "changed-today.gif",
+ "clear-port-8080.gif",
+ "remove-ds-store.gif",
+ "zip-folder.gif",
+];
+
+export function demoFileAt(index) {
+ return DEMO_FILES[index % DEMO_FILES.length];
+}
+
+export function demoRedirect(index) {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ "Cache-Control": "no-store",
+ Location: `https://jst.sh/demos/${demoFileAt(index)}`,
+ },
+ });
+}
+
+export default {
+ fetch() {
+ return demoRedirect(Math.floor(Math.random() * DEMO_FILES.length));
+ },
+};