aboutsummaryrefslogtreecommitdiff
path: root/website
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-21 17:24:17 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-21 17:27:10 +0300
commit2eeea9a0cd8665bbe0e6c80f08603425498757e5 (patch)
tree83f3f572fafbadd1d40593415ccb335d36373eb6 /website
parent734bf3c09d292012b45b93a4813ccdd0cf26d08a (diff)
feat: add plugin marketplace
Diffstat (limited to 'website')
-rw-r--r--website/css/style.css392
-rw-r--r--website/index.html272
-rw-r--r--website/src/components/MarketingLayout.astro6
-rw-r--r--website/src/components/SiteTitle.astro2
-rw-r--r--website/src/content.config.ts11
-rw-r--r--website/src/content/blog/herdr-0-6-3.md (renamed from website/src/content/releases/v0-6-3.md)5
-rw-r--r--website/src/content/docs/marketplace.mdx54
-rw-r--r--website/src/content/docs/plugins.mdx15
-rw-r--r--website/src/data/plugins-fixture.json139
-rw-r--r--website/src/pages/compare.astro6
-rw-r--r--website/src/pages/plugins.astro601
-rw-r--r--website/src/pages/releases/[...slug].astro89
-rw-r--r--website/src/pages/releases/index.astro42
13 files changed, 1031 insertions, 603 deletions
diff --git a/website/css/style.css b/website/css/style.css
index 82a57a17..8a1cc3cb 100644
--- a/website/css/style.css
+++ b/website/css/style.css
@@ -1467,7 +1467,7 @@ samp {
.logo-cloud {
max-width: var(--max);
margin: 0 auto;
- padding: 0 1.25rem 4rem;
+ padding: 2.5rem 1.25rem 3rem;
overflow: hidden;
}
@@ -1571,21 +1571,12 @@ samp {
}
}
-.section,
-.awareness-section {
+.section {
padding-top: 5rem;
padding-bottom: 5rem;
border-top: 1px solid var(--line);
}
-.intro-section {
- padding-top: 2.5rem;
-}
-
-.intro-section .section-heading-wide h2 {
- max-width: 760px;
-}
-
.contrast-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1625,8 +1616,6 @@ samp {
.section-heading h2,
.section-heading-wide h2,
-.feature-copy h2,
-.api-copy h2,
.final-cta h2 {
font-size: clamp(1.8rem, 4vw, 3.2rem);
max-width: 800px;
@@ -1641,9 +1630,15 @@ samp {
align-items: end;
}
+.why-section .section-heading-wide {
+ align-items: start;
+}
+
+.why-section .section-heading-wide p {
+ margin-top: 0.5rem;
+}
+
.section-heading-wide p,
-.feature-copy p,
-.api-copy p,
.final-cta p {
margin: 0;
color: var(--muted);
@@ -1658,8 +1653,7 @@ samp {
}
.pillar-card,
-.access-card,
-.awareness-card {
+.access-card {
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: transparent;
@@ -1684,8 +1678,7 @@ samp {
}
.pillar-card h3,
-.access-card h3,
-.awareness-card h3 {
+.access-card h3 {
margin: 1rem 0 0;
color: var(--ink);
letter-spacing: -0.02em;
@@ -1696,145 +1689,13 @@ samp {
}
.pillar-card p,
-.access-card p,
-.awareness-card p {
+.access-card p {
margin: 0.75rem 0 0;
color: var(--muted);
line-height: 1.6;
font-size: 0.95rem;
}
-.feature-split,
-.api-section {
- display: grid;
- grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
- gap: 2rem;
- align-items: center;
-}
-
-.feature-copy,
-.api-copy {
- min-width: 0;
-}
-
-.feature-copy .section-kicker,
-.api-copy .section-kicker {
- margin-bottom: 1rem;
-}
-
-.feature-copy p,
-.api-copy p {
- margin-top: 1.1rem;
-}
-
-.check-list {
- list-style: none;
- margin: 1.2rem 0 0;
- padding: 0;
- display: grid;
- gap: 0.6rem;
- color: var(--muted);
- font-size: 0.95rem;
-}
-
-.check-list li {
- position: relative;
- padding-left: 1.4rem;
-}
-
-.check-list li::before {
- content: "→";
- position: absolute;
- left: 0;
- color: var(--accent);
- font-family: var(--mono);
- font-weight: 700;
-}
-
-.feature-mock {
- margin: 0;
- overflow: hidden;
- border-radius: var(--radius-lg);
- background: var(--term-bg);
- border: 1px solid var(--term-border);
- font-family: var(--mono);
- font-size: 0.72rem;
- line-height: 1.5;
- color: var(--term-muted);
- pointer-events: none;
- user-select: none;
- min-height: 380px;
- display: flex;
- flex-direction: column;
-}
-
-.feature-mock-panes {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
- gap: 0.45rem;
- padding: 0.55rem;
- flex: 1;
-}
-
-.feature-mock .mock-pane {
- min-width: 0;
- overflow: hidden;
- border: 1px solid var(--term-border);
- background: var(--term-bg);
- padding: 0.75rem;
-}
-
-.feature-mock .mock-pane.focused {
- border: 2px solid var(--term-tab-active-bg);
- padding: calc(0.75rem - 1px);
-}
-
-.feature-mock .mock-prompt {
- margin-bottom: 0.35rem;
- color: var(--term-prompt);
- font-size: 1.05em;
- font-weight: 700;
-}
-
-.feature-mock .mock-prompt em {
- color: var(--term-dim);
- font-style: normal;
- font-weight: 500;
-}
-
-.feature-mock b {
- color: var(--accent);
-}
-
-.feature-mock .cmd {
- color: var(--term-cmd);
-}
-
-.feature-mock .str {
- color: var(--term-str);
-}
-
-.feature-mock .dim,
-.feature-mock .time {
- color: var(--term-dim);
-}
-
-.feature-mock .green {
- color: var(--term-green-bright);
-}
-
-.feature-mock .red {
- color: var(--red);
-}
-
-.feature-mock .cursor {
- display: inline-block;
- width: 0.4rem;
- height: 0.85rem;
- background: var(--term-text);
- vertical-align: -0.14rem;
-}
-
.access-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1973,10 +1834,6 @@ samp {
border-color: var(--accent);
}
-.api-section {
- align-items: stretch;
-}
-
.inline-links {
display: flex;
flex-wrap: wrap;
@@ -2034,96 +1891,6 @@ samp {
color: var(--term-cmd);
}
-.awareness-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 0.75rem;
-}
-
-.awareness-card {
- display: flex;
- flex-direction: column;
- padding: 1.1rem;
-}
-
-.awareness-mock {
- width: 100%;
- height: 240px;
- margin: 0;
- overflow: hidden;
- border-radius: var(--radius-sm);
- background: var(--term-bg);
- border: 1px solid var(--term-border);
- font-family: var(--mono);
- font-size: 0.68rem;
- line-height: 1.4;
- color: var(--term-muted);
- pointer-events: none;
- user-select: none;
-}
-
-.awareness-mock .mock-sidebar {
- height: 100%;
- border-right: none;
- background: var(--term-sidebar-bg);
-}
-
-.awareness-mock .mock-sidebar-title {
- padding: 0.6rem 0.85rem;
- font-size: 0.72rem;
-}
-
-.awareness-mock .mock-space {
- padding: 0.5rem 0.85rem;
- gap: 0.5rem;
- font-size: 0.72rem;
-}
-
-.awareness-mock .mock-agent {
- padding: 0.4rem 0.85rem;
- gap: 0.45rem;
- font-size: 0.72rem;
-}
-
-.awareness-mock .mock-agent-title {
- padding: 0.5rem 0.85rem;
- font-size: 0.72rem;
-}
-
-.awareness-mock .mock-dot {
- width: 0.42rem;
- height: 0.42rem;
- margin-top: 0.2rem;
-}
-
-.awareness-mock .mock-sidebar-spacer {
- min-height: 2rem;
-}
-
-.awareness-card p {
- margin-top: 0.75rem;
-}
-
-.state-legend {
- display: flex;
- flex-wrap: wrap;
- gap: 0.6rem;
- margin-top: 1rem;
-}
-
-.state-legend span {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
- border: 1px solid var(--line);
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--muted);
- padding: 0.4rem 0.65rem;
- font-size: 0.82rem;
- font-family: var(--mono);
-}
-
.table-wrap {
overflow: auto;
border: 1px solid var(--line);
@@ -2554,8 +2321,6 @@ td:last-child {
max-width: 720px;
}
- .feature-split,
- .api-section,
.section-heading-wide {
grid-template-columns: 1fr;
}
@@ -2610,7 +2375,6 @@ td:last-child {
}
.section,
- .awareness-section,
.demo-section,
.logo-cloud,
.footer,
@@ -2670,16 +2434,13 @@ td:last-child {
max-width: 92px;
}
- .section,
- .awareness-section {
+ .section {
padding-top: 3rem;
padding-bottom: 3rem;
}
.section-heading h2,
.section-heading-wide h2,
- .feature-copy h2,
- .api-copy h2,
.final-cta h2 {
font-size: clamp(1.6rem, 10vw, 2.4rem);
}
@@ -2687,7 +2448,6 @@ td:last-child {
.pillar-grid,
.contrast-grid,
.access-grid,
- .awareness-grid,
.differ-list {
grid-template-columns: 1fr;
}
@@ -3526,3 +3286,127 @@ td:last-child {
animation: none;
}
}
+
+/* Homepage marketplace teaser */
+.marketplace-teaser {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 3rem;
+ align-items: center;
+ padding-block: 4.5rem;
+ border-bottom: 1px solid var(--line);
+}
+
+.marketplace-teaser-copy h2 {
+ margin: 0;
+ color: var(--ink);
+ font-family: var(--mono);
+ font-weight: 400;
+ letter-spacing: -0.04em;
+ line-height: 1.05;
+ font-size: clamp(1.9rem, 3.4vw, 2.8rem);
+}
+
+.marketplace-teaser-lede {
+ max-width: 30rem;
+ margin: 1.1rem 0 0;
+ color: var(--ink-soft);
+ font-size: clamp(1rem, 1.6vw, 1.12rem);
+ line-height: 1.6;
+}
+
+.marketplace-teaser-grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.mp-chip {
+ display: grid;
+ gap: 0.3rem;
+ padding: 1rem 1.1rem;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ text-decoration: none;
+ transition: border-color 0.12s ease, transform 0.12s ease;
+}
+
+.mp-chip:hover {
+ border-color: var(--accent);
+ transform: translateX(2px);
+}
+
+.mp-chip-name {
+ color: var(--ink);
+ font-family: var(--mono);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.mp-chip-desc {
+ color: var(--ink-soft);
+ font-size: 0.85rem;
+ line-height: 1.45;
+}
+
+.mp-chip-meta {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 0.75rem;
+}
+
+@media (max-width: 50rem) {
+ .marketplace-teaser {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ padding-block: 3.5rem;
+ }
+}
+
+/* What you get: balanced capability grid + code card */
+.capability-section .section-heading-wide {
+ align-items: end;
+}
+
+.capability-body {
+ display: grid;
+ grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
+ gap: 2rem;
+ align-items: start;
+ margin-top: 2.25rem;
+}
+
+.capability-cards {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem;
+}
+
+.capability-cards article {
+ padding: 1.1rem 1.2rem;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ background: var(--bg-elevated);
+}
+
+.capability-cards h3 {
+ margin: 0;
+ color: var(--ink);
+ font-family: var(--mono);
+ font-weight: 600;
+ font-size: 0.92rem;
+}
+
+.capability-cards p {
+ margin: 0.5rem 0 0;
+ color: var(--ink-soft);
+ font-size: 0.85rem;
+ line-height: 1.5;
+}
+
+@media (max-width: 50rem) {
+ .capability-body {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
+}
diff --git a/website/index.html b/website/index.html
index bc06ca3e..f75f3c77 100644
--- a/website/index.html
+++ b/website/index.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Herdr — one terminal for the whole herd</title>
+ <title>Herdr: one terminal for the whole herd</title>
<meta
name="description"
content="Herdr is to coding agents what tmux is to terminals: an agent multiplexer that runs where your agents run. Real panes, agent state at a glance, ssh from anywhere, no app."
@@ -13,10 +13,10 @@
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Herdr" />
- <meta property="og:title" content="Herdr — one terminal for the whole herd" />
+ <meta property="og:title" content="Herdr: one terminal for the whole herd" />
<meta
property="og:description"
- content="To coding agents what tmux is to terminals. Runs where your agents run — close the laptop, ssh from anywhere, nothing dies."
+ content="To coding agents what tmux is to terminals. Runs where your agents run: close the laptop, ssh from anywhere, nothing dies."
/>
<meta property="og:url" content="https://herdr.dev/" />
<meta
@@ -25,19 +25,19 @@
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
- <meta property="og:image:alt" content="Herdr — One terminal. The whole herd." />
+ <meta property="og:image:alt" content="Herdr. One terminal. The whole herd." />
<meta name="twitter:card" content="summary_large_image" />
- <meta name="twitter:title" content="Herdr — one terminal for the whole herd" />
+ <meta name="twitter:title" content="Herdr: one terminal for the whole herd" />
<meta
name="twitter:description"
- content="To coding agents what tmux is to terminals. Runs where your agents run — close the laptop, ssh from anywhere, nothing dies."
+ content="To coding agents what tmux is to terminals. Runs where your agents run: close the laptop, ssh from anywhere, nothing dies."
/>
<meta
name="twitter:image"
content="https://herdr.dev/assets/og-card-v8.png"
/>
- <meta name="twitter:image:alt" content="Herdr — One terminal. The whole herd." />
+ <meta name="twitter:image:alt" content="Herdr. One terminal. The whole herd." />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon.png?v=14" />
<link rel="apple-touch-icon" href="/assets/logo.png" />
@@ -57,7 +57,7 @@
document.documentElement.setAttribute("data-palette", saved || system);
})();
</script>
- <link rel="stylesheet" href="/css/style.css?v=33" />
+ <link rel="stylesheet" href="/css/style.css?v=39" />
</head>
<body>
<header class="site-header">
@@ -75,7 +75,7 @@
<div class="nav-menu-panel">
<a href="/">Home</a>
<a href="/compare/">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
@@ -87,7 +87,7 @@
<div class="nav-links">
<a href="/">Home</a>
<a href="/compare/">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
@@ -120,7 +120,7 @@
<h1>One terminal. The whole herd.</h1>
<p class="hero-lede">
Herdr is to coding agents what tmux is to terminals:
- a multiplexer that runs where your agents run — your
+ a multiplexer that runs where your agents run, on your
machine, your server, anywhere you can ssh. See
<span class="state blocked">blocked</span>,
<span class="state working">working</span>, and
@@ -369,7 +369,7 @@
</section>
</div>
<figcaption>
- The whole herd in one terminal. Click the sidebar — it's a real layout.
+ The whole herd in one terminal. Click the sidebar, it's a real layout.
</figcaption>
</figure>
</div>
@@ -422,17 +422,18 @@
</div>
</section>
- <section class="section access-section">
+ <section class="section access-section why-section" id="why">
<div class="section-heading-wide">
<div>
- <p class="section-kicker"># not an app</p>
- <h2>Runs where your agents run.</h2>
+ <p class="section-kicker"># why herdr</p>
+ <h2>Run agents where the work is. Attach from anywhere.</h2>
</div>
<p>
- Desktop agent managers are stuck on the machine with the
- GUI. Herdr is a client–server multiplexer: agents live on
- whichever box does the work, and you attach from anywhere
- — your laptop, any SSH client, even a phone.
+ Agents run wherever the work is: a server, a Mac Mini,
+ a sandbox VM, anywhere you can ssh. You attach from any
+ terminal, even a phone. Desktop agent managers can't
+ leave the machine with the GUI. Herdr can.
+ <a href="/compare/">Why switch from an app →</a>
</p>
</div>
<div class="access-grid">
@@ -440,28 +441,28 @@
<span class="access-label">local</span>
<h3>Your machine</h3>
<p>
- Start a local session, split panes, create tabs, and
- keep agents running while your terminal comes and
- goes.
+ Split panes, create tabs, and keep agents running
+ while your terminal comes and goes.
</p>
<pre><code>$ herdr</code></pre>
</article>
<article class="access-card">
- <span class="access-label">ssh</span>
- <h3>Any SSH client</h3>
+ <span class="access-label">over ssh</span>
+ <h3>Like tmux, on the server</h3>
<p>
- Use Herdr like tmux on a remote box. The session
- stays on the server after you detach.
+ SSH in and run Herdr on the remote shell. The session
+ stays after you detach, ideal from a phone SSH client.
</p>
<pre><code>$ ssh you@server
$ herdr</code></pre>
</article>
<article class="access-card">
<span class="access-label">thin client</span>
- <h3>Remote attach</h3>
+ <h3>Native remote attach</h3>
<p>
- Point your local Herdr client at an SSH config host
- and stream the remote TUI back to your terminal.
+ A local client to a remote session. Installs Herdr on
+ the host for you, bridges your local clipboard
+ (including image paste), and keeps your keybindings.
</p>
<pre><code>$ herdr --remote workbox</code></pre>
</article>
@@ -502,24 +503,21 @@ $ herdr</code></pre>
</div>
</section>
- <section class="section intro-section">
- <p class="section-kicker"># the missing layer</p>
+ <section class="section compare-section">
<div class="section-heading-wide">
- <h2>
- Keep your terminal. Add the control surface.
- </h2>
+ <div>
+ <p class="section-kicker"># why different</p>
+ <h2>Most agent managers are apps. Herdr is a multiplexer.</h2>
+ </div>
<p>
- Desktop agent terminals give you app UI. tmux gives you
- persistence. Herdr sits between them: a lightweight TUI
- process with real terminal panes, clickable layout,
- agent state, detach/reattach, remote attach, and an API
- your agents can drive.
+ tmux and Zellij own persistent terminal sessions but don't understand agents. Desktop agent apps understand agents but live on one machine. Herdr keeps the multiplexer in your terminal and makes it agent-aware.
+ <a href="/compare/">See the full comparison →</a>
</p>
</div>
<div class="contrast-grid" aria-label="What Herdr is not">
<article>
<span>not a terminal emulator</span>
- <p>Ghostty, Kitty, iTerm, Alacritty — your terminal stays.</p>
+ <p>Ghostty, Kitty, iTerm, Alacritty: your terminal stays.</p>
</article>
<article>
<span>not a browser dashboard</span>
@@ -530,19 +528,6 @@ $ herdr</code></pre>
<p>Persistence plus clickable panes, agent state, an API.</p>
</article>
</div>
- </section>
-
- <section class="section compare-section">
- <div class="section-heading-wide">
- <div>
- <p class="section-kicker"># positioning</p>
- <h2>Most agent managers are apps. Herdr is a multiplexer.</h2>
- </div>
- <p>
- tmux and Zellij own persistent terminal sessions but don't understand agents. Desktop agent apps understand agents but live on one machine. Herdr keeps the multiplexer in your terminal and makes it agent-aware.
- <a href="/compare/">See the full comparison →</a>
- </p>
- </div>
<div class="table-wrap">
<table>
<thead>
@@ -646,67 +631,39 @@ $ herdr</code></pre>
</div>
</section>
- <section class="section feature-split">
- <div class="feature-copy">
- <p class="section-kicker"># your terminal</p>
- <h2>Same shell, same keybinds, real panes.</h2>
+ <section class="section capability-section">
+ <div class="section-heading-wide">
+ <div>
+ <p class="section-kicker"># what you get</p>
+ <h2>Real panes, agent state, and an API to drive them.</h2>
+ </div>
<p>
No rebuilt chat view. Real processes in real PTYs, with
- layout, clicks, state, and persistence added around them.
+ clickable layout, persistent sessions, and a control
+ surface your agents can use themselves.
</p>
- <ul class="check-list">
- <li>Use the mouse for panes, tabs, workspaces, and menus.</li>
- <li>Keep your terminal fonts, shortcuts, SSH, and shell setup.</li>
- <li>Stay keyboard-first, or disable mouse input entirely.</li>
- </ul>
</div>
- <figure class="feature-mock" role="img" aria-label="Herdr showing split terminal panes">
- <div class="mock-panes feature-mock-panes">
- <div class="mock-pane focused">
- <div class="mock-prompt"><span>~/Projects/herdr</span> <em>master</em></div>
- <div><b>❯</b> <span class="cmd">just</span> <span class="str">test</span></div>
- <div class="dim">cargo nextest run</div>
- <div><span class="time">02:14:22</span> <span class="green">PASS</span> app::state::tests</div>
- <div><span class="time">02:14:23</span> <span class="green">PASS</span> platform::pty::tests</div>
- <div><span class="time">02:14:24</span> <span class="green">PASS</span> protocol::wire::tests</div>
- <br />
- <div><span class="green">3 passed</span> · 0 failed · 0.8s</div>
- <div class="mock-prompt"><span>~/Projects/herdr</span> <em>master</em></div>
- <div><b>❯</b> <span class="cursor"></span></div>
- </div>
- <div class="mock-pane">
- <div class="mock-prompt"><span>~/Projects/herdr</span> <em>master</em></div>
- <div><b>❯</b> <span class="cmd">git</span> <span class="str">status</span></div>
- <div class="dim">On branch master</div>
- <div>Your branch is up to date.</div>
- <br />
- <div class="dim">Changes not staged:</div>
- <div><span class="red">M</span> src/app/state.rs</div>
- <div><span class="green">??</span> docs/next/</div>
- <br />
- <div class="dim">2 files changed, 14 insertions</div>
- </div>
- </div>
- </figure>
- </section>
-
- <section class="section api-section">
- <div class="api-copy">
- <p class="section-kicker"># agents can drive it</p>
- <h2>The UI is also a control surface.</h2>
- <p>
- A CLI and JSON socket API expose workspaces, panes,
- output, and waits. Your agents can run their own
- workspace.
- </p>
- <div class="inline-links">
- <a href="/docs/cli-reference/">CLI reference →</a>
- <a href="/docs/socket-api/">Socket API →</a>
- <a href="/docs/agent-skill/">Agent skill →</a>
+ <div class="capability-body">
+ <div class="capability-cards">
+ <article>
+ <h3>Real panes</h3>
+ <p>Mouse-first panes, tabs, and workspaces. Keep your shell, fonts, and keybinds.</p>
+ </article>
+ <article>
+ <h3>Agent state</h3>
+ <p>Blocked, working, done, and idle at a glance across the whole session.</p>
+ </article>
+ <article>
+ <h3>Persistence</h3>
+ <p>Detach and reattach. Sessions and agents survive the terminal closing.</p>
+ </article>
+ <article>
+ <h3>Control surface</h3>
+ <p>A CLI and JSON socket API expose workspaces, panes, output, and waits.</p>
+ </article>
</div>
- </div>
- <div class="code-card" aria-label="Herdr CLI orchestration example">
- <pre><code><span class="comment"># create workspace structure</span>
+ <div class="code-card" aria-label="Herdr CLI orchestration example">
+ <pre><code><span class="comment"># create workspace structure</span>
<span class="cmd">herdr</span> workspace create <span class="flag">--cwd</span> <span class="str">~/project</span> <span class="flag">--label</span> <span class="str">api</span>
<span class="cmd">herdr</span> tab create <span class="flag">--label</span> <span class="str">logs</span>
@@ -717,71 +674,15 @@ $ herdr</code></pre>
<span class="comment"># wait, inspect, and continue</span>
<span class="cmd">herdr</span> wait agent-status <span class="str">1-1</span> <span class="flag">--status</span> <span class="str">done</span>
<span class="cmd">herdr</span> pane read <span class="str">1-2</span> <span class="flag">--source</span> <span class="str">recent-unwrapped</span></code></pre>
+ </div>
</div>
- </section>
-
- <section class="section awareness-section">
- <div class="section-heading">
- <p class="section-kicker"># agent awareness</p>
- <h2>Keep the herd in sight.</h2>
- </div>
- <div class="awareness-grid">
- <article class="awareness-card">
- <figure class="awareness-mock" role="img" aria-label="Herdr workspace sidebar showing state rollups">
- <div class="mock-sidebar">
- <div class="mock-sidebar-title">spaces</div>
- <div class="mock-space active">
- <span class="mock-dot dot-done"></span>
- <div><strong>herdr</strong><small>master</small></div>
- </div>
- <div class="mock-space">
- <span class="mock-dot dot-working"></span>
- <div><strong>llm-proxy</strong><small>master</small></div>
- </div>
- <div class="mock-space">
- <span class="mock-dot dot-blocked"></span>
- <div><strong>docs-site</strong><small>docs/palette</small></div>
- </div>
- <div class="mock-space">
- <span class="mock-dot dot-idle"></span>
- <div><strong>qmp</strong><small>autoresearch/…</small></div>
- </div>
- <div class="mock-sidebar-spacer"></div>
- </div>
- </figure>
- <h3>Workspace rollups</h3>
- <p>
- Each workspace rolls up to its most urgent agent
- state, so the whole session is scannable.
- </p>
- </article>
- <article class="awareness-card">
- <figure class="awareness-mock" role="img" aria-label="Herdr agent list showing states">
- <div class="mock-sidebar">
- <div class="mock-agent-title"><span>agents</span><span>all</span></div>
- <div class="mock-agent"><span>✓</span><div><strong>explore</strong><small>idle · opencode</small></div></div>
- <div class="mock-agent working"><span class="braille-spinner"></span><div><strong>herdr</strong><small>working · claude</small></div></div>
- <div class="mock-agent blocked"><span>◉</span><div><strong>llm-proxy</strong><small>blocked · claude</small></div></div>
- <div class="mock-agent done"><span>●</span><div><strong>qmp</strong><small>done · codex</small></div></div>
- <div class="mock-sidebar-spacer"></div>
- </div>
- </figure>
- <h3>Agent list</h3>
- <p>
- See which agents are blocked, working, done, idle, or
- unknown without opening every pane.
- </p>
- </article>
- </div>
- <div class="state-legend">
- <span><i class="status-dot dot-blocked"></i>blocked</span>
- <span><i class="status-dot dot-working"></i>working</span>
- <span><i class="status-dot dot-done"></i>done</span>
- <span><i class="status-dot dot-idle"></i>idle</span>
+ <div class="inline-links">
+ <a href="/docs/cli-reference/">CLI reference →</a>
+ <a href="/docs/socket-api/">Socket API →</a>
+ <a href="/docs/agent-skill/">Agent skill →</a>
</div>
</section>
-
<section class="section integrations-section">
<div class="section-heading">
<p class="section-kicker"># integrations</p>
@@ -810,6 +711,37 @@ $ herdr</code></pre>
</p>
</section>
+ <section class="section marketplace-teaser" id="marketplace">
+ <div class="marketplace-teaser-copy">
+ <p class="section-kicker"># marketplace</p>
+ <h2>Built on, not just with.</h2>
+ <p class="marketplace-teaser-lede">
+ Herdr's CLI and socket API are the plugin surface, so
+ community plugins drive it like an agent does: notifiers,
+ layout presets, link handlers, release checks. Browse what
+ people built, or publish your own with one GitHub topic.
+ </p>
+ <div class="hero-actions">
+ <a class="button button-primary" href="/plugins/">Browse plugins</a>
+ <a class="button button-secondary" href="/docs/plugins/">Build a plugin</a>
+ </div>
+ </div>
+ <div class="marketplace-teaser-grid">
+ <a class="mp-chip" href="https://github.com/ogulcancelik/herdr-plugin-examples/tree/main/agent-telegram-notify" target="_blank" rel="noopener">
+ <span class="mp-chip-name">Agent Telegram Notify</span>
+ <span class="mp-chip-desc">Telegram message when an agent finishes.</span>
+ </a>
+ <a class="mp-chip" href="https://github.com/ogulcancelik/herdr-plugin-examples/tree/main/github-link-preview" target="_blank" rel="noopener">
+ <span class="mp-chip-name">GitHub Link Preview</span>
+ <span class="mp-chip-desc">Open clicked issue and PR links in a side pane.</span>
+ </a>
+ <a class="mp-chip" href="https://github.com/ogulcancelik/herdr-plugin-examples/tree/main/dev-layout-bootstrap" target="_blank" rel="noopener">
+ <span class="mp-chip-name">Dev Layout Bootstrap</span>
+ <span class="mp-chip-desc">A three-pane dev layout around the current pane.</span>
+ </a>
+ </div>
+ </section>
+
<section class="final-cta">
<p class="section-kicker"># start the herd</p>
<h2>Install Herdr, run two agents, detach once.</h2>
diff --git a/website/src/components/MarketingLayout.astro b/website/src/components/MarketingLayout.astro
index a4df022f..381daf03 100644
--- a/website/src/components/MarketingLayout.astro
+++ b/website/src/components/MarketingLayout.astro
@@ -58,7 +58,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
document.documentElement.setAttribute("data-palette", saved || system);
})();
</script>
- <link rel="stylesheet" href="/css/style.css?v=21" />
+ <link rel="stylesheet" href="/css/style.css?v=39" />
</head>
<body>
<header class="site-header">
@@ -76,7 +76,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
<div class="nav-menu-panel">
<a href="/">Home</a>
<a href="/compare/">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
@@ -88,7 +88,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString();
<div class="nav-links">
<a href="/">Home</a>
<a href="/compare/">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro
index d35da5d6..f6a3b7d0 100644
--- a/website/src/components/SiteTitle.astro
+++ b/website/src/components/SiteTitle.astro
@@ -11,7 +11,7 @@ const docsHref = isPreview ? '/docs/preview/' : '/docs/';
</a>
<nav class="title-nav" aria-label="Primary">
<a href={docsHref}>Docs</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
</nav>
</div>
diff --git a/website/src/content.config.ts b/website/src/content.config.ts
index ba0c83f8..bb0f2d8a 100644
--- a/website/src/content.config.ts
+++ b/website/src/content.config.ts
@@ -21,15 +21,4 @@ export const collections = {
ogImage: z.string().optional(),
}),
}),
- releases: defineCollection({
- loader: glob({ pattern: '*.md', base: './src/content/releases' }),
- schema: z.object({
- title: z.string(),
- version: z.string(),
- path: z.string(),
- description: z.string(),
- date: z.coerce.date(),
- draft: z.boolean().default(false),
- }),
- }),
};
diff --git a/website/src/content/releases/v0-6-3.md b/website/src/content/blog/herdr-0-6-3.md
index 23514cc6..97bd2b28 100644
--- a/website/src/content/releases/v0-6-3.md
+++ b/website/src/content/blog/herdr-0-6-3.md
@@ -1,9 +1,8 @@
---
-title: "session persistence, safer updates, and workspace navigation"
-version: "0.6.3"
-path: "0.6.3/session-persistence-safer-updates-and-workspace-navigation"
+title: "Herdr 0.6.3: session persistence, safer updates, and workspace navigation"
description: "This one adds new persistence paths for updates, server restarts, and larger sessions."
date: 2026-05-27
+draft: false
---
## Persistence for the hard parts
diff --git a/website/src/content/docs/marketplace.mdx b/website/src/content/docs/marketplace.mdx
index f07b8f87..09a27b2d 100644
--- a/website/src/content/docs/marketplace.mdx
+++ b/website/src/content/docs/marketplace.mdx
@@ -1,39 +1,53 @@
---
title: Marketplace
-description: Publish and share Herdr plugins, and get listed when the marketplace launches.
+description: Discover community Herdr plugins on GitHub, and get your own plugin listed.
---
-The Herdr plugin marketplace is a discoverable index of installable plugins. It
-is not live yet. This page is how to be ready for it.
+The Herdr plugin marketplace is a discoverable index of community plugins.
+Browse it at [herdr.dev/plugins](/plugins/). It is an automatic index of public
+GitHub repositories, not a reviewed catalog.
-## Share a plugin today
+## Browse plugins
+
+The [marketplace](/plugins/) lists every public repository tagged with the
+GitHub topic `herdr-plugin`. Search by name, owner, description, or language,
+and sort by popularity, recent activity, or newest. Each listing links straight
+to its source repository on GitHub.
+
+Discovery is automatic and unreviewed. A listing means a repository tagged
+itself, not that Herdr vetted it, so the
+[trust guidance](/docs/plugins/#trust-and-security) applies before you install
+anything.
+
+## Install a plugin
The marketplace adds discovery on top of installation; it does not replace it.
-Anyone can already install your plugin straight from GitHub:
+Install any plugin straight from GitHub:
```bash
herdr plugin install owner/repo[/subdir...]
```
Publish a normal public GitHub repository with a `herdr-plugin.toml` manifest at
-its root, or in a subdirectory, and that command works now. See
+its root, or in a subdirectory, and that command works. See
[Plugins](/docs/plugins/) for the manifest and authoring reference.
-## Get listed when it launches
+## Get your plugin listed
-If you want your plugin to appear in the website marketplace when discovery
-launches, add the GitHub topic `herdr-plugin` to your repository now. That
-topic is the signal the future index will use for discovery, so tagging a
-public plugin today means it can be listed when the marketplace goes live.
+Add the GitHub topic `herdr-plugin` to a public repository. That topic is the
+only signal the index uses, so tagging a public plugin is all it takes. The
+index refreshes automatically every 30 minutes, so a newly tagged repository
+shows up shortly after, and one that drops the topic disappears on the next
+refresh.
-## What a listing will show
+## What a listing shows
-A marketplace card will surface your plugin's `id`, `name`, `description`,
-declared `platforms`, and a link back to the source repository. Getting that
-manifest metadata right today is what makes your listing accurate later. Declare
-`platforms` honestly, because the index will use it to show where a plugin runs.
+Each card shows GitHub repository metadata: the repository name and owner, its
+description, star count, primary language, and the time it was last pushed, with
+a link back to the source. The index reads this from GitHub's repository search,
+so keeping your repository description and topics accurate is what makes your
+listing useful.
-Plugins stay ordinary GitHub repositories. The marketplace is a discovery layer
-on top of the same `herdr plugin install` path you can use now. A listing makes a
-plugin easier to find; it does not mean Herdr has reviewed it, so the
-[trust guidance](/docs/plugins/#trust-and-security) still applies.
+The index does not parse `herdr-plugin.toml` yet, so manifest fields like the
+plugin `id`, declared `platforms`, and `min_herdr_version` are not shown in v1.
+Forks and archived repositories are excluded from the list.
diff --git a/website/src/content/docs/plugins.mdx b/website/src/content/docs/plugins.mdx
index 9f069fdf..9f57ff47 100644
--- a/website/src/content/docs/plugins.mdx
+++ b/website/src/content/docs/plugins.mdx
@@ -304,10 +304,11 @@ state should own their files or database.
## Marketplace
-A plugin marketplace is coming. Plugins are already shareable today: publish a
-GitHub repository with `herdr-plugin.toml`, then share
-`herdr plugin install owner/repo[/subdir]`.
-
-If you want the plugin to appear in the website marketplace when discovery
-launches, add the GitHub topic `herdr-plugin` to the repository now. See
-[Marketplace](/docs/marketplace/) for how publishing and discovery will work.
+Community plugins are discoverable in the [marketplace](/plugins/), an automatic
+index of public GitHub repositories tagged with the topic `herdr-plugin`.
+Plugins stay ordinary GitHub repositories: publish one with `herdr-plugin.toml`,
+then share `herdr plugin install owner/repo[/subdir]`.
+
+To get a plugin listed, add the GitHub topic `herdr-plugin` to its public
+repository. The index refreshes every 30 minutes. See
+[Marketplace](/docs/marketplace/) for how discovery works.
diff --git a/website/src/data/plugins-fixture.json b/website/src/data/plugins-fixture.json
new file mode 100644
index 00000000..27a9c265
--- /dev/null
+++ b/website/src/data/plugins-fixture.json
@@ -0,0 +1,139 @@
+{
+ "schemaVersion": 1,
+ "generatedAt": "2026-06-20T12:00:00.000Z",
+ "source": {
+ "provider": "github",
+ "query": "topic:herdr-plugin",
+ "totalCount": 7,
+ "collectedCount": 7,
+ "truncated": false
+ },
+ "plugins": [
+ {
+ "id": 123456789,
+ "fullName": "ogulcancelik/herdr-plugin-github-start",
+ "owner": "ogulcancelik",
+ "name": "herdr-plugin-github-start",
+ "description": "Start Codex or Claude from a GitHub issue, PR, or discussion without leaving the terminal.",
+ "url": "https://github.com/ogulcancelik/herdr-plugin-github-start",
+ "stars": 42,
+ "forks": 3,
+ "openIssues": 1,
+ "language": "TypeScript",
+ "topics": ["herdr-plugin", "github", "agents"],
+ "createdAt": "2026-05-01T00:00:00Z",
+ "updatedAt": "2026-06-19T19:58:26Z",
+ "pushedAt": "2026-06-19T19:58:26Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 223456789,
+ "fullName": "ogulcancelik/herdr-plugin-examples",
+ "owner": "ogulcancelik",
+ "name": "herdr-plugin-examples",
+ "description": "Cookbook of example Herdr plugins: telegram notify, link preview, dev layout bootstrap.",
+ "url": "https://github.com/ogulcancelik/herdr-plugin-examples",
+ "stars": 28,
+ "forks": 6,
+ "openIssues": 0,
+ "language": "JavaScript",
+ "topics": ["herdr-plugin", "examples"],
+ "createdAt": "2026-04-12T00:00:00Z",
+ "updatedAt": "2026-06-18T08:10:00Z",
+ "pushedAt": "2026-06-18T08:10:00Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 323456789,
+ "fullName": "lindqvist/herdr-board",
+ "owner": "lindqvist",
+ "name": "herdr-board",
+ "description": "A live project board pane for Herdr that tracks agent tasks across worktrees.",
+ "url": "https://github.com/lindqvist/herdr-board",
+ "stars": 19,
+ "forks": 1,
+ "openIssues": 4,
+ "language": "Rust",
+ "topics": ["herdr-plugin", "pane", "kanban"],
+ "createdAt": "2026-05-20T00:00:00Z",
+ "updatedAt": "2026-06-17T14:02:00Z",
+ "pushedAt": "2026-06-17T14:02:00Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 423456789,
+ "fullName": "mira-dev/herdr-layout-presets",
+ "owner": "mira-dev",
+ "name": "herdr-layout-presets",
+ "description": "Apply opinionated split-pane layouts per project with one keybinding.",
+ "url": "https://github.com/mira-dev/herdr-layout-presets",
+ "stars": 11,
+ "forks": 0,
+ "openIssues": 2,
+ "language": "Lua",
+ "topics": ["herdr-plugin", "layout"],
+ "createdAt": "2026-06-01T00:00:00Z",
+ "updatedAt": "2026-06-15T09:30:00Z",
+ "pushedAt": "2026-06-15T09:30:00Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 523456789,
+ "fullName": "okabe/herdr-jira-bridge",
+ "owner": "okabe",
+ "name": "herdr-jira-bridge",
+ "description": "Open Jira tickets as Herdr workspaces and push agent summaries back as comments.",
+ "url": "https://github.com/okabe/herdr-jira-bridge",
+ "stars": 7,
+ "forks": 2,
+ "openIssues": 0,
+ "language": "Python",
+ "topics": ["herdr-plugin", "jira", "integration"],
+ "createdAt": "2026-05-28T00:00:00Z",
+ "updatedAt": "2026-06-10T11:45:00Z",
+ "pushedAt": "2026-06-10T11:45:00Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 623456789,
+ "fullName": "noor/herdr-notify",
+ "owner": "noor",
+ "name": "herdr-notify",
+ "description": "Desktop and mobile notifications when an agent finishes, blocks, or needs input.",
+ "url": "https://github.com/noor/herdr-notify",
+ "stars": 4,
+ "forks": 0,
+ "openIssues": 1,
+ "language": "Go",
+ "topics": ["herdr-plugin", "notifications"],
+ "createdAt": "2026-06-05T00:00:00Z",
+ "updatedAt": "2026-06-08T16:20:00Z",
+ "pushedAt": "2026-06-08T16:20:00Z",
+ "archived": false,
+ "fork": false
+ },
+ {
+ "id": 723456789,
+ "fullName": "small-tools/herdr-snippet-runner",
+ "owner": "small-tools",
+ "name": "herdr-snippet-runner",
+ "description": null,
+ "url": "https://github.com/small-tools/herdr-snippet-runner",
+ "stars": 0,
+ "forks": 0,
+ "openIssues": 0,
+ "language": null,
+ "topics": ["herdr-plugin"],
+ "createdAt": "2026-06-14T00:00:00Z",
+ "updatedAt": "2026-06-14T10:00:00Z",
+ "pushedAt": "2026-06-14T10:00:00Z",
+ "archived": false,
+ "fork": false
+ }
+ ]
+}
diff --git a/website/src/pages/compare.astro b/website/src/pages/compare.astro
index 0825ea68..f120ac59 100644
--- a/website/src/pages/compare.astro
+++ b/website/src/pages/compare.astro
@@ -45,7 +45,7 @@
document.documentElement.setAttribute("data-palette", saved || system);
})();
</script>
- <link rel="stylesheet" href="/css/style.css?v=21" />
+ <link rel="stylesheet" href="/css/style.css?v=39" />
</head>
<body>
<header class="site-header">
@@ -63,7 +63,7 @@
<div class="nav-menu-panel">
<a href="/">Home</a>
<a href="/compare/" aria-current="page">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
@@ -75,7 +75,7 @@
<div class="nav-links">
<a href="/">Home</a>
<a href="/compare/" aria-current="page">Compare</a>
- <a href="/releases/">Releases</a>
+ <a href="/plugins/">Plugins</a>
<a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
<a href="/docs/">Docs</a>
diff --git a/website/src/pages/plugins.astro b/website/src/pages/plugins.astro
new file mode 100644
index 00000000..6e96a132
--- /dev/null
+++ b/website/src/pages/plugins.astro
@@ -0,0 +1,601 @@
+---
+import MarketingLayout from '../components/MarketingLayout.astro';
+import fixture from '../data/plugins-fixture.json';
+
+const snapshotUrl = 'https://assets.herdr.dev/plugins/index.json';
+
+// Seed the page server-side from the public R2 snapshot so `bun run dev` and
+// the production build both render real data immediately. If the live fetch
+// fails, dev falls back to a local fixture for offline design; production seeds
+// empty and lets the client fetch fill it (never fake data).
+let liveData = null;
+try {
+ const res = await fetch(snapshotUrl);
+ if (res.ok) liveData = await res.json();
+} catch {
+ // ignore: handled by the fallbacks below
+}
+const initialData = liveData ?? (import.meta.env.DEV ? fixture : null);
+const initialIsFixture = !liveData && import.meta.env.DEV;
+const apiUrl = import.meta.env.DEV ? null : snapshotUrl;
+---
+
+<MarketingLayout
+ title="Herdr plugins: community plugin marketplace"
+ description="Discover community Herdr plugins published on GitHub. An automatic index of public repositories tagged with the herdr-plugin topic."
+ canonicalPath="/plugins/"
+>
+ <main class="plugins-page">
+ <section class="section plugins-hero">
+ <p class="section-kicker"># marketplace</p>
+ <h2>Plugins built on Herdr.</h2>
+ <p class="plugins-lede">
+ Community plugins published on GitHub. Install any of them with
+ <code>herdr plugin install</code>, or publish your own and get listed.
+ </p>
+ <p class="plugins-trust">
+ <span class="status-dot dot-idle"></span>
+ Listings are discovered automatically from the GitHub topic
+ <code>herdr-plugin</code>. They are not reviewed by Herdr, so install at
+ your own discretion. <a href="/docs/plugins/#trust-and-security">Trust guidance</a>.
+ </p>
+ </section>
+
+ <section class="section plugins-controls">
+ <div class="plugins-search-wrap">
+ <span class="plugins-search-icon" aria-hidden="true">/</span>
+ <input
+ id="plugins-search"
+ type="search"
+ class="plugins-search"
+ placeholder="search plugins, owners, languages…"
+ autocomplete="off"
+ spellcheck="false"
+ aria-label="Search plugins"
+ />
+ </div>
+ <label class="plugins-sort-wrap">
+ <span class="plugins-sort-label">sort</span>
+ <select id="plugins-sort" class="plugins-sort" aria-label="Sort plugins">
+ <option value="popular">Popular</option>
+ <option value="recent">Recently updated</option>
+ <option value="newest">Newest</option>
+ <option value="name">Name</option>
+ </select>
+ </label>
+ <p id="plugins-meta" class="plugins-meta" aria-live="polite"></p>
+ </section>
+
+ <section class="section plugins-results">
+ <div id="plugins-grid" class="plugins-grid" role="list"></div>
+ <div id="plugins-status" class="plugins-status">Loading plugins…</div>
+ </section>
+
+ <section class="section plugins-publish">
+ <div class="plugins-publish-card">
+ <p class="section-kicker"># publish</p>
+ <h3>Want your plugin listed?</h3>
+ <p>
+ Add the GitHub topic <code>herdr-plugin</code> to a public repository
+ with a <code>herdr-plugin.toml</code> manifest. The index picks it up
+ on the next refresh. No submission, no review queue.
+ </p>
+ <div class="plugins-publish-actions">
+ <a class="button button-primary" href="/docs/marketplace/">How publishing works</a>
+ <a class="button button-secondary" href="/docs/plugins/">Author a plugin</a>
+ </div>
+ </div>
+ </section>
+ </main>
+
+ <script define:vars={{ initialData, initialIsFixture, apiUrl }}>
+ const grid = document.getElementById("plugins-grid");
+ const statusEl = document.getElementById("plugins-status");
+ const searchInput = document.getElementById("plugins-search");
+ const sortSelect = document.getElementById("plugins-sort");
+ const metaEl = document.getElementById("plugins-meta");
+
+ const LANG_COLORS = {
+ TypeScript: "#3178c6",
+ JavaScript: "#f1e05a",
+ Rust: "#dea584",
+ Python: "#3572A5",
+ Go: "#00ADD8",
+ Lua: "#000080",
+ Shell: "#89e051",
+ "C++": "#f34b7d",
+ Ruby: "#701516",
+ };
+
+ let plugins = [];
+ let usingFallback = false;
+
+ function timeAgo(iso) {
+ if (!iso) return null;
+ const then = new Date(iso).getTime();
+ if (Number.isNaN(then)) return null;
+ const secs = Math.max(0, Math.round((Date.now() - then) / 1000));
+ const units = [
+ ["y", 31536000],
+ ["mo", 2592000],
+ ["w", 604800],
+ ["d", 86400],
+ ["h", 3600],
+ ["m", 60],
+ ];
+ for (const [label, size] of units) {
+ const v = Math.floor(secs / size);
+ if (v >= 1) return v + label + " ago";
+ }
+ return "just now";
+ }
+
+ function isGithubUrl(url) {
+ return typeof url === "string" && /^https:\/\/github\.com\/[^/]+\/[^/]+/.test(url);
+ }
+
+ function el(tag, className, text) {
+ const node = document.createElement(tag);
+ if (className) node.className = className;
+ if (text != null) node.textContent = text;
+ return node;
+ }
+
+ function buildCard(p) {
+ const url = isGithubUrl(p.url) ? p.url : null;
+ const card = el(url ? "a" : "div", "plugin-card");
+ card.setAttribute("role", "listitem");
+ if (url) {
+ card.href = url;
+ card.target = "_blank";
+ card.rel = "noopener noreferrer";
+ }
+
+ const head = el("div", "plugin-card-head");
+ const title = el("div", "plugin-title");
+ title.appendChild(el("span", "plugin-owner", (p.owner || "?") + "/"));
+ title.appendChild(el("span", "plugin-name", p.name || "unknown"));
+ head.appendChild(title);
+ head.appendChild(el("span", "plugin-go", "↗"));
+ card.appendChild(head);
+
+ const desc = el(
+ "p",
+ "plugin-desc" + (p.description ? "" : " is-empty"),
+ p.description || "No description provided.",
+ );
+ card.appendChild(desc);
+
+ const meta = el("div", "plugin-meta-row");
+
+ const stars = el("span", "plugin-stat");
+ stars.appendChild(el("span", "plugin-star", "★"));
+ stars.appendChild(el("span", null, String(p.stars ?? 0)));
+ meta.appendChild(stars);
+
+ if (p.language) {
+ const lang = el("span", "plugin-stat");
+ const dot = el("span", "plugin-lang-dot");
+ dot.style.background = LANG_COLORS[p.language] || "var(--muted)";
+ lang.appendChild(dot);
+ lang.appendChild(el("span", null, p.language));
+ meta.appendChild(lang);
+ }
+
+ const updated = timeAgo(p.pushedAt);
+ if (updated) meta.appendChild(el("span", "plugin-stat plugin-updated", "updated " + updated));
+
+ card.appendChild(meta);
+ return card;
+ }
+
+ function matches(p, q) {
+ if (!q) return true;
+ const hay = [p.fullName, p.owner, p.name, p.description, p.language, ...(p.topics || [])]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+ return q.split(/\s+/).every((term) => hay.includes(term));
+ }
+
+ function sortPlugins(list, mode) {
+ const by = [...list];
+ const t = (v) => new Date(v || 0).getTime() || 0;
+ if (mode === "recent") by.sort((a, b) => t(b.pushedAt) - t(a.pushedAt));
+ else if (mode === "newest") by.sort((a, b) => t(b.createdAt) - t(a.createdAt));
+ else if (mode === "name") by.sort((a, b) => (a.fullName || "").localeCompare(b.fullName || ""));
+ else
+ by.sort(
+ (a, b) => (b.stars ?? 0) - (a.stars ?? 0) || t(b.pushedAt) - t(a.pushedAt),
+ );
+ return by;
+ }
+
+ function render() {
+ const q = searchInput.value.trim().toLowerCase();
+ const visible = sortPlugins(plugins.filter((p) => matches(p, q)), sortSelect.value);
+
+ grid.replaceChildren();
+ for (const p of visible) grid.appendChild(buildCard(p));
+
+ if (visible.length === 0) {
+ statusEl.textContent = q
+ ? "No plugins match your search."
+ : "No plugins are listed yet.";
+ statusEl.hidden = false;
+ } else {
+ statusEl.hidden = true;
+ }
+ }
+
+ function setMeta(generatedAt) {
+ const parts = [];
+ parts.push(plugins.length + (plugins.length === 1 ? " plugin" : " plugins"));
+ const ago = timeAgo(generatedAt);
+ if (ago) parts.push("updated " + ago);
+ if (usingFallback) parts.push("preview data");
+ metaEl.textContent = parts.join(" · ");
+ }
+
+ function apply(data) {
+ plugins = Array.isArray(data && data.plugins) ? data.plugins : [];
+ setMeta(data && data.generatedAt);
+ render();
+ }
+
+ function showError() {
+ plugins = [];
+ metaEl.textContent = "";
+ grid.replaceChildren();
+ statusEl.textContent =
+ "The plugin marketplace is warming up. Check back in a few minutes.";
+ statusEl.hidden = false;
+ }
+
+ async function load() {
+ // Render the server-seeded data immediately (live in prod and dev; fixture
+ // only as a dev offline fallback).
+ if (initialData) {
+ usingFallback = initialIsFixture;
+ apply(initialData);
+ }
+ // Refresh from the same-origin function in production for the freshest snapshot.
+ if (apiUrl) {
+ try {
+ const res = await fetch(apiUrl, { headers: { accept: "application/json" } });
+ if (!res.ok) throw new Error("status " + res.status);
+ usingFallback = false;
+ apply(await res.json());
+ } catch (err) {
+ if (!initialData) showError();
+ }
+ } else if (!initialData) {
+ showError();
+ }
+ }
+
+ searchInput.addEventListener("input", render);
+ sortSelect.addEventListener("change", render);
+ load();
+ </script>
+
+ <style is:global>
+ .plugins-page {
+ padding-bottom: 5rem;
+ }
+
+ /* Reuse .section only for max-width centering, not its 5rem padding + top divider. */
+ .plugins-page .section {
+ padding-block: 0;
+ border-top: none;
+ }
+
+ .plugins-page .plugins-hero {
+ padding-top: 3rem;
+ }
+
+ .plugins-hero h2 {
+ margin: 0;
+ color: var(--ink);
+ font-family: var(--mono);
+ font-weight: 400;
+ letter-spacing: -0.04em;
+ line-height: 1.05;
+ font-size: clamp(2rem, 4vw, 3rem);
+ }
+
+ .plugins-lede {
+ max-width: 640px;
+ margin: 1.1rem 0 0;
+ color: var(--ink-soft);
+ font-size: clamp(1rem, 1.6vw, 1.15rem);
+ line-height: 1.6;
+ }
+
+ .plugins-lede code,
+ .plugins-trust code,
+ .plugins-publish-card code {
+ font-family: var(--mono);
+ font-size: 0.85em;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ padding: 0.1em 0.35em;
+ white-space: nowrap;
+ }
+
+ .plugins-trust {
+ max-width: 640px;
+ margin: 1.1rem 0 0;
+ color: var(--muted);
+ font-size: 0.85rem;
+ line-height: 1.55;
+ }
+
+ .plugins-trust .status-dot {
+ display: inline-block;
+ margin-right: 0.4rem;
+ vertical-align: middle;
+ }
+
+ .plugins-trust a {
+ color: var(--accent);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ }
+
+ /* Controls */
+ .plugins-page .plugins-controls {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 2.5rem;
+ padding-bottom: 1.75rem;
+ }
+
+ /* Divider bound to the content box (padding box), so it aligns with the
+ search row and the cards instead of bleeding past them. */
+ .plugins-controls::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-top: 1px solid var(--line);
+ }
+
+ .plugins-search-wrap {
+ position: relative;
+ flex: 1 1 18rem;
+ display: flex;
+ align-items: center;
+ }
+
+ .plugins-search-icon {
+ position: absolute;
+ left: 0.7rem;
+ color: var(--muted-2);
+ font-family: var(--mono);
+ font-size: 0.9rem;
+ pointer-events: none;
+ }
+
+ .plugins-search {
+ width: 100%;
+ padding: 0.6rem 0.8rem 0.6rem 1.7rem;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ color: var(--ink);
+ font-family: var(--mono);
+ font-size: 0.85rem;
+ }
+
+ .plugins-search:focus {
+ outline: none;
+ border-color: var(--accent);
+ }
+
+ .plugins-sort-wrap {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .plugins-sort-label {
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ text-transform: lowercase;
+ letter-spacing: 0.06em;
+ }
+
+ .plugins-sort {
+ padding: 0.55rem 0.7rem;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ color: var(--ink);
+ font-family: var(--mono);
+ font-size: 0.82rem;
+ cursor: pointer;
+ }
+
+ .plugins-sort:focus {
+ outline: none;
+ border-color: var(--accent);
+ }
+
+ .plugins-meta {
+ margin: 0;
+ margin-left: auto;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ letter-spacing: 0.04em;
+ }
+
+ /* Grid */
+ .plugins-results {
+ margin-top: 2rem;
+ }
+
+ .plugins-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
+ gap: 0.85rem;
+ }
+
+ .plugin-card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.7rem;
+ padding: 1.25rem;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ transition: border-color 0.12s ease, transform 0.12s ease;
+ min-height: 8.5rem;
+ }
+
+ a.plugin-card:hover {
+ border-color: var(--accent);
+ transform: translateY(-2px);
+ }
+
+ .plugin-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.5rem;
+ }
+
+ .plugin-title {
+ font-family: var(--mono);
+ font-size: 0.95rem;
+ line-height: 1.3;
+ word-break: break-word;
+ }
+
+ .plugin-owner {
+ color: var(--muted);
+ }
+
+ .plugin-name {
+ color: var(--ink);
+ font-weight: 600;
+ }
+
+ .plugin-go {
+ color: var(--muted-2);
+ font-family: var(--mono);
+ flex: 0 0 auto;
+ transition: color 0.12s ease;
+ }
+
+ a.plugin-card:hover .plugin-go {
+ color: var(--accent);
+ }
+
+ .plugin-desc {
+ margin: 0;
+ color: var(--ink-soft);
+ font-size: 0.88rem;
+ line-height: 1.5;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .plugin-desc.is-empty {
+ color: var(--muted-2);
+ font-style: italic;
+ }
+
+ .plugin-meta-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.9rem;
+ margin-top: auto;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ }
+
+ .plugin-stat {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ }
+
+ .plugin-star {
+ color: var(--yellow, #d4a72c);
+ }
+
+ .plugin-lang-dot {
+ width: 0.6rem;
+ height: 0.6rem;
+ border-radius: 50%;
+ flex: 0 0 auto;
+ }
+
+ .plugin-updated {
+ margin-left: auto;
+ color: var(--muted-2);
+ }
+
+ .plugins-status {
+ padding: 3rem 1rem;
+ text-align: center;
+ color: var(--muted);
+ font-family: var(--mono);
+ font-size: 0.85rem;
+ }
+
+ /* Publish */
+ .plugins-publish {
+ margin-top: 3.5rem;
+ }
+
+ .plugins-publish-card {
+ padding: 2rem;
+ background: var(--bg-elevated);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-lg);
+ }
+
+ .plugins-publish-card h3 {
+ margin: 0.25rem 0 0;
+ color: var(--ink);
+ font-family: var(--mono);
+ font-weight: 400;
+ font-size: 1.4rem;
+ letter-spacing: -0.02em;
+ }
+
+ .plugins-publish-card p {
+ max-width: 620px;
+ margin: 0.8rem 0 0;
+ color: var(--ink-soft);
+ line-height: 1.6;
+ }
+
+ .plugins-publish-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6rem;
+ margin-top: 1.5rem;
+ }
+
+ @media (max-width: 38rem) {
+ .plugins-meta {
+ margin-left: 0;
+ width: 100%;
+ }
+ }
+ </style>
+</MarketingLayout>
diff --git a/website/src/pages/releases/[...slug].astro b/website/src/pages/releases/[...slug].astro
deleted file mode 100644
index 420d4b9e..00000000
--- a/website/src/pages/releases/[...slug].astro
+++ /dev/null
@@ -1,89 +0,0 @@
----
-import { getCollection, render } from 'astro:content';
-import MarketingLayout from '../../components/MarketingLayout.astro';
-
-export async function getStaticPaths() {
- const releases = await getCollection('releases');
- return releases
- .filter((release) => !release.data.draft)
- .map((release) => {
- const slug = release.data.path;
- return {
- params: { slug },
- props: { release, slug },
- };
- });
-}
-
-const { release, slug } = Astro.props;
-const { Content, headings } = await render(release);
-const sectionHeadings = headings.filter((heading) => heading.depth === 2);
----
-
-<MarketingLayout
- title={`${release.data.version}: ${release.data.title} — Herdr releases`}
- description={release.data.description}
- canonicalPath={`/releases/${slug}/`}
- ogType="article"
->
- <main class="content-shell">
- <div class="article-layout release-layout section">
- <aside class="article-toc" aria-label="Release sections">
- <p>Sections</p>
- <nav>
- {sectionHeadings.map((heading) => (
- <a href={`#${heading.slug}`} data-heading-link={heading.slug}>{heading.text}</a>
- ))}
- </nav>
- </aside>
-
- <article class="article-shell release-shell">
- <header class="article-header release-header">
- <p class="post-meta">
- <a href="/releases/">Releases</a>
- <span>{release.data.version}</span>
- <time datetime={release.data.date.toISOString()}>{release.data.date.toLocaleDateString('en', {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })}</time>
- </p>
- <h1>{release.data.title}</h1>
- <p>{release.data.description}</p>
- </header>
- <div class="article-prose release-prose">
- <Content />
- </div>
- </article>
- </div>
- </main>
-
- <script>
- const headings = Array.from(document.querySelectorAll('.article-prose h2[id]'));
- const links = new Map(
- Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [
- link.getAttribute('data-heading-link'),
- link,
- ]),
- );
-
- function setActive(id) {
- for (const link of links.values()) link.removeAttribute('aria-current');
- links.get(id)?.setAttribute('aria-current', 'true');
- }
-
- if (headings.length > 0 && links.size > 0) {
- setActive(headings[0].id);
- const observer = new IntersectionObserver(
- (entries) => {
- const visible = entries
- .filter((entry) => entry.isIntersecting)
- .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
- if (visible?.target instanceof HTMLElement) setActive(visible.target.id);
- },
- { rootMargin: '-18% 0px -68% 0px', threshold: 0 },
- );
- for (const heading of headings) observer.observe(heading);
- }
- </script>
-</MarketingLayout>
diff --git a/website/src/pages/releases/index.astro b/website/src/pages/releases/index.astro
deleted file mode 100644
index 48263ab9..00000000
--- a/website/src/pages/releases/index.astro
+++ /dev/null
@@ -1,42 +0,0 @@
----
-import { getCollection } from 'astro:content';
-import MarketingLayout from '../../components/MarketingLayout.astro';
-
-const releases = (await getCollection('releases'))
- .filter((release) => !release.data.draft)
- .map((release) => ({ ...release, slug: release.data.path }))
- .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
----
-
-<MarketingLayout
- title="Herdr releases"
- description="Product-focused release notes for Herdr."
- canonicalPath="/releases/"
->
- <main class="content-shell">
- <section class="page-hero page-hero-compact">
- <p class="eyebrow"><span class="status-dot dot-working"></span>Releases</p>
- <h1>What changed in Herdr.</h1>
- <p class="hero-lede">
- See what the herd has been working on.
- </p>
- </section>
-
- <section class="section section-narrow" aria-label="Releases">
- <div class="post-list">
- {releases.map((release) => (
- <article class="post-card">
- <a class="post-card-link" href={`/releases/${release.slug}/`}>
- <div>
- <p class="release-card-version">{release.data.version}</p>
- <h2>{release.data.title}</h2>
- <p>{release.data.description}</p>
- </div>
- <span aria-hidden="true">-></span>
- </a>
- </article>
- ))}
- </div>
- </section>
- </main>
-</MarketingLayout>