diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-03 21:33:33 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-03 21:39:25 +0300 |
| commit | 22bb476d646ccadec71f584b90c2a135fec92929 (patch) | |
| tree | c6d4b6dffe0b8891c52e7e0cc10c8e6051734c30 /website/src/pages | |
| parent | eacea2daf0b72973173b728936b27478374f2cd2 (diff) | |
feat(website): rebuild marketing pages on new brand chassis
Diffstat (limited to 'website/src/pages')
| -rw-r--r-- | website/src/pages/blog/[slug].astro | 69 | ||||
| -rw-r--r-- | website/src/pages/blog/index.astro | 56 | ||||
| -rw-r--r-- | website/src/pages/compare.astro | 8 | ||||
| -rw-r--r-- | website/src/pages/index.astro | 28 | ||||
| -rw-r--r-- | website/src/pages/og/card-b.html | 60 | ||||
| -rw-r--r-- | website/src/pages/og/card-perf.html | 56 | ||||
| -rw-r--r-- | website/src/pages/plugins.astro | 189 | ||||
| -rw-r--r-- | website/src/pages/stats.astro | 270 |
8 files changed, 369 insertions, 367 deletions
diff --git a/website/src/pages/blog/[slug].astro b/website/src/pages/blog/[slug].astro index 48cba857..3c33a38c 100644 --- a/website/src/pages/blog/[slug].astro +++ b/website/src/pages/blog/[slug].astro @@ -1,18 +1,16 @@ --- import { getCollection, render } from 'astro:content'; -import MarketingLayout from '../../components/MarketingLayout.astro'; +import SiteLayout from '../../components/SiteLayout.astro'; import AuthorCard from '../../components/AuthorCard.astro'; export async function getStaticPaths() { const posts = await getCollection('blog'); + /* dev only, see blog/index.astro */ return posts - .filter((post) => !post.data.draft) + .filter((post) => import.meta.env.DEV || !post.data.draft) .map((post) => { const slug = post.id.replace(/\.(md|mdx)$/i, ''); - return { - params: { slug }, - props: { post, slug }, - }; + return { params: { slug }, props: { post, slug } }; }); } @@ -21,7 +19,7 @@ const { Content, headings } = await render(post); const sectionHeadings = headings.filter((heading) => heading.depth === 2); --- -<MarketingLayout +<SiteLayout title={`${post.data.title} — Herdr blog`} description={post.data.description} canonicalPath={`/blog/${slug}/`} @@ -29,9 +27,33 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2); ogImage={post.data.ogImage} ogImageAlt={post.data.ogImage ? post.data.title : undefined} > - <main class="content-shell"> - <div class="article-layout section"> - <aside class="article-toc" aria-label="Article sections"> + {/* a post with no h2s renders no sidebar, so the grid must stop reserving + a column for it */} + <div class:list={['article', sectionHeadings.length === 0 && 'article-solo']}> + <article class="article-body"> + <p class="article-meta"> + <a href="/blog/">← Blog</a> + <time datetime={post.data.date.toISOString()}> + {post.data.date.toLocaleDateString('en', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + </time> + </p> + <h1>{post.data.title}</h1> + {post.data.lede && post.data.description && ( + <p class="article-lede">{post.data.description}</p> + )} + <hr class="article-rule" /> + <div class="prose"> + <Content /> + </div> + <AuthorCard /> + </article> + + {sectionHeadings.length > 0 && ( + <aside class="toc" aria-label="Article sections"> <p>Sections</p> <nav> {sectionHeadings.map((heading) => ( @@ -39,30 +61,11 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2); ))} </nav> </aside> - - <article class="article-shell"> - <header class="article-header"> - <p class="post-meta"> - <a href="/blog/">Blog</a> - <time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString('en', { - year: 'numeric', - month: 'long', - day: 'numeric', - })}</time> - </p> - <h1>{post.data.title}</h1> - <p>{post.data.description}</p> - </header> - <div class="article-prose"> - <Content /> - </div> - <AuthorCard /> - </article> - </div> - </main> + )} + </div> <script> - const headings = Array.from(document.querySelectorAll('.article-prose h2[id]')); + const headings = Array.from(document.querySelectorAll('.prose h2[id]')); const links = new Map( Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [ link.getAttribute('data-heading-link'), @@ -89,4 +92,4 @@ const sectionHeadings = headings.filter((heading) => heading.depth === 2); for (const heading of headings) observer.observe(heading); } </script> -</MarketingLayout> +</SiteLayout> diff --git a/website/src/pages/blog/index.astro b/website/src/pages/blog/index.astro index 8ca2ca10..de9349ea 100644 --- a/website/src/pages/blog/index.astro +++ b/website/src/pages/blog/index.astro @@ -1,38 +1,42 @@ --- import { getCollection } from 'astro:content'; -import MarketingLayout from '../../components/MarketingLayout.astro'; +import SiteLayout from '../../components/SiteLayout.astro'; +/* Drafts render locally and never in a build, so an unpublished post can be + reviewed on the dev server without being one deploy away from going live. */ const posts = (await getCollection('blog')) - .filter((post) => !post.data.draft) + .filter((post) => import.meta.env.DEV || !post.data.draft) .map((post) => ({ ...post, slug: post.id.replace(/\.(md|mdx)$/i, '') })) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + +const stamp = (date) => + date.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' }); --- -<MarketingLayout +<SiteLayout title="Herdr blog" - description="Notes on terminal-native agent workflows and the engineering behind Herdr." + description="Notes on the agent runtime — product decisions, implementation writeups, and what we learned running agents on machines nobody is watching." canonicalPath="/blog/" > - <main class="content-shell"> - <section class="page-hero page-hero-compact"> - <p class="eyebrow"><span class="status-dot dot-working"></span>Blog</p> - <h1>Notes from Herdr.</h1> - <p class="hero-lede"> - Product notes and implementation writeups for the terminal-native agent multiplexer. - </p> - </section> + <header class="ph"> + <div class="hd-eyebrow"><s></s>the blog</div> + <h1>Notes from the herd.</h1> + <p> + Product decisions, implementation writeups, and what we learn running agents on + machines nobody is watching. + </p> + </header> - <section class="section section-narrow" aria-label="Blog posts"> - <div class="post-list"> - {posts.map((post) => ( - <article class="post-card"> - <a class="post-card-link" href={`/blog/${post.slug}/`}> - <h2>{post.data.title}</h2> - <span aria-hidden="true">→</span> - </a> - </article> - ))} - </div> - </section> - </main> -</MarketingLayout> + <div class="posts"> + {posts.map((post) => ( + <a href={`/blog/${post.slug}/`}> + <time datetime={post.data.date.toISOString()}>{stamp(post.data.date)}</time> + <div> + <h2>{post.data.title}</h2> + {post.data.description && <p>{post.data.description}</p>} + </div> + <span class="go-arrow" aria-hidden="true">→</span> + </a> + ))} + </div> +</SiteLayout> diff --git a/website/src/pages/compare.astro b/website/src/pages/compare.astro index c3ba436d..b9131be6 100644 --- a/website/src/pages/compare.astro +++ b/website/src/pages/compare.astro @@ -19,10 +19,10 @@ content="Herdr combines tmux-style persistence, real terminal panes, agent state, direct attach, SSH workflows, and an API agents can drive." /> <meta property="og:url" content="https://herdr.dev/compare/" /> - <meta property="og:image" content="https://herdr.dev/assets/og-card-v8.png" /> + <meta property="og:image" content="https://herdr.dev/assets/og-card-v9.png" /> <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 — run them anywhere, leave them running." /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Compare Herdr — terminal-native agent multiplexer" /> @@ -30,8 +30,8 @@ name="twitter:description" content="Compare Herdr with tmux, Zellij, cmux, Warp, Solo, Conductor, Emdash, and Superset." /> - <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" content="https://herdr.dev/assets/og-card-v9.png" /> + <meta name="twitter:image:alt" content="Herdr — run them anywhere, leave them running." /> <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon.png?v=14" /> <link rel="apple-touch-icon" href="/assets/logo.png" /> diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index e46ccbe6..26a048d4 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -1,7 +1,33 @@ --- import { readFile } from 'node:fs/promises'; +import { agentCount, fmt, releaseVersion, siteStats } from '../site-data'; +/* The numbers in the nav and hero strip are baked here at build time from the + snapshots and latest.json, so the page ships with real values, renders + correctly without JS, and can never be more than one deploy stale. A small + script on the page refreshes the counts live afterwards. */ const homepage = await readFile(new URL('../../index.html', import.meta.url), 'utf8'); + +const { starCount, installCount, pluginCount } = await siteStats(); +const version = await releaseVersion(); +const facts = version ? `v${version} · Apache 2.0` : 'Apache 2.0'; + +/* the line drops out entirely rather than showing a placeholder if the + manifests can't be read — a missing sentence beats a wrong number */ +const agents = await agentCount(); +const agentsNote = agents ? `${agents} agents detected out of the box` : ''; +/* the strip carried a hardcoded 19; same source as row 04 now */ +const agentsCount = agents ? String(agents) : '—'; + +/* replaceAll, not replace: the star count appears in the nav as well as the + strip, and replace() would fill only the first of them. */ +const page = homepage + .replaceAll('<!--N_STARS-->', fmt(starCount)) + .replaceAll('<!--N_INSTALLS-->', fmt(installCount)) + .replaceAll('<!--N_PLUGINS-->', fmt(pluginCount)) + .replaceAll('<!--FACTS-->', facts) + .replaceAll('<!--AGENTS_NOTE-->', agentsNote) + .replaceAll('<!--N_AGENTS-->', agentsCount); --- -<Fragment set:html={homepage} /> +<Fragment set:html={page} /> diff --git a/website/src/pages/og/card-b.html b/website/src/pages/og/card-b.html new file mode 100644 index 00000000..c508778b --- /dev/null +++ b/website/src/pages/og/card-b.html @@ -0,0 +1,60 @@ +<!doctype html> +<html lang="en" class="og"> +<head> +<meta charset="utf-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>herdr OG card — source</title> +<meta name="robots" content="noindex" /> +<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&display=swap" rel="stylesheet" /> +<link rel="stylesheet" href="/css/og-card.css?v=3" /> +<style> + /* Source of truth for /assets/og-card-v9.png. + Regenerate: open this page, capture the full document, crop to the + leftmost 1200x630 (the document is pinned to exactly the card size, but + the browser viewport may be wider). Bump the filename when the artwork + changes — Slack, X and LinkedIn cache unfurls by URL for weeks. */ + /* centre the headline in the band below the head rather than in the whole + card, so a four-line headline can't ride up into the lockup */ + .in { display: flex; flex-direction: column; justify-content: center; padding-top: 118px; } + h1 { + font-family: var(--disp); + font-weight: 900; + letter-spacing: -0.055em; + line-height: 0.86; + font-size: 104px; + margin: 30px 0 0; + max-width: 15ch; + color: var(--ink); + } + h1 em { font-style: normal; color: var(--spot); } + /* X, LinkedIn and Slack all draw their own attribution over the bottom of an + unfurl, so the lockup and provenance sit at the top and the bottom band is + left deliberately empty. */ + .head { + position: absolute; + left: 62px; + right: 62px; + top: 48px; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + } + .head .sub { color: var(--faint); font-size: 21px; letter-spacing: 0.1em; } + .head .sub b { color: var(--dim); font-weight: 400; } +</style> +</head> +<body class="og"> +<div class="card"> + <span class="ram mass"></span> + <div class="head"> + <div class="lockup"><span class="ram"></span><b>herdr</b></div> + <div class="sub"><b>herdr.dev</b> · Apache 2.0</div> + </div> + <div class="in pad"> + <div class="eyebrow"><s></s>the agent runtime</div> + <h1>Run them <em>anywhere.</em> Leave them running.</h1> + </div> +</div> +</body> +</html> diff --git a/website/src/pages/og/card-perf.html b/website/src/pages/og/card-perf.html new file mode 100644 index 00000000..e2142252 --- /dev/null +++ b/website/src/pages/og/card-perf.html @@ -0,0 +1,56 @@ +<!doctype html> +<html lang="en" class="og"> +<head> +<meta charset="utf-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>herdr performance OG card — source</title> +<meta name="robots" content="noindex" /> +<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&display=swap" rel="stylesheet" /> +<link rel="stylesheet" href="/css/og-card.css?v=3" /> +<style> + /* Source of truth for /assets/og-blog-frames-v1.png. Same capture recipe as + card-b: open the page, capture the full document, it is already 1200x630. + + A share card is read at thumbnail size in a timeline, so it carries the + headline and the one measurement that backs it. The caveats live in the + post, where there is room to read them. */ + .head { + position: absolute; left: 62px; right: 62px; top: 48px; z-index: 1; + display: flex; align-items: center; justify-content: space-between; + } + .head .sub { color: var(--faint); font-size: 21px; letter-spacing: 0.1em; } + .head .sub b { color: var(--dim); font-weight: 400; } + + .in { display: flex; flex-direction: column; justify-content: center; padding: 118px 62px 54px; } + h1 { + font-family: var(--disp); font-weight: 900; letter-spacing: -0.055em; + line-height: 0.9; font-size: 96px; margin: 0; color: var(--ink); + } + h1 em { font-style: normal; color: var(--spot); } + + .meas { + display: flex; align-items: baseline; gap: 22px; + margin-top: 42px; padding-top: 22px; border-top: 1px solid var(--line2); + color: var(--faint); font-size: 22px; letter-spacing: 0.04em; + } + .meas b { color: var(--ink); font-weight: 400; } + .meas .r { margin-left: auto; color: var(--dim); } +</style> +</head> +<body class="og"> +<div class="card"> + <span class="ram mass"></span> + <div class="head"> + <div class="lockup"><span class="ram"></span><b>herdr</b></div> + <div class="sub"><b>herdr.dev</b> · Apache 2.0</div> + </div> + <div class="in"> + <h1>Ten agents,<br />three clients,<br /><em>95% less CPU.</em></h1> + <div class="meas"> + <span>total cpu <b>13.014% → 0.617%</b></span> + <span class="r">v0.7.5 → v0.8.0</span> + </div> + </div> +</div> +</body> +</html> diff --git a/website/src/pages/plugins.astro b/website/src/pages/plugins.astro index 6119f834..f9c4854f 100644 --- a/website/src/pages/plugins.astro +++ b/website/src/pages/plugins.astro @@ -1,5 +1,5 @@ --- -import MarketingLayout from '../components/MarketingLayout.astro'; +import SiteLayout from '../components/SiteLayout.astro'; import fixture from '../data/plugins-fixture.json'; const snapshotUrl = 'https://assets.herdr.dev/plugins/index.json'; @@ -17,29 +17,45 @@ try { } const initialData = liveData ?? (import.meta.env.DEV ? fixture : null); const initialIsFixture = !liveData && import.meta.env.DEV; +// A card is a repository; a plugin is a validated manifest inside it. A monorepo +// with three manifests is one card and three plugins, so the two counts differ +// and both are stated. `source.totalCount` is the raw GitHub topic search and +// still includes blacklisted and manifest-less repositories, so it is not used. +const repos = initialData?.plugins ?? []; +const repositoryCount = initialData?.repositoryCount ?? repos.length; +const pluginCount = + initialData?.pluginCount ?? + repos.reduce((n, repo) => n + (repo.manifests?.length || 1), 0); const apiUrl = import.meta.env.DEV ? null : snapshotUrl; --- -<MarketingLayout +<SiteLayout 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 + <header class="ph ph-split"> + <div> + <div class="hd-eyebrow"><s></s>the marketplace</div> + <h1>Built by the herd.</h1> + <p> + Community plugins published on GitHub, discovered automatically from the + <code>herdr-plugin</code> topic. 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>. + Listings aren't reviewed by Herdr, so install at your own discretion. + <a href="/docs/plugins/#trust-and-security">Trust guidance →</a> </p> - </section> + </div> + <aside class="ph-side"> + <b>{pluginCount.toLocaleString('en-US')}</b> + <span>plugins across {repositoryCount.toLocaleString('en-US')} repositories</span> + <a href="/docs/plugins/">Publish yours →</a> + </aside> + </header> <section class="section plugins-controls"> <div class="plugins-search-wrap"> @@ -48,7 +64,7 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; id="plugins-search" type="search" class="plugins-search" - placeholder="search plugins, owners, languages…" + placeholder="search plugins, repositories, owners, languages…" autocomplete="off" spellcheck="false" aria-label="Search plugins" @@ -63,6 +79,9 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; <option value="name">Name</option> </select> </label> + </section> + + <section class="section plugins-metabar"> <p id="plugins-meta" class="plugins-meta" aria-live="polite"></p> </section> @@ -98,8 +117,8 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; const moreButton = document.getElementById("plugins-more"); // 24 fills complete rows in both the 3-column and 2-column grid layouts. - const SHOW_LIMIT = 24; - let showAll = false; + const PAGE_SIZE = 24; + let visibleLimit = PAGE_SIZE; const LANG_COLORS = { TypeScript: "#3178c6", @@ -113,7 +132,8 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; Ruby: "#701516", }; - let plugins = []; + // One entry per repository. Its `manifests` are the plugins it ships. + let repos = []; let usingFallback = false; function timeAgo(iso) { @@ -188,23 +208,58 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; meta.appendChild(lang); } + // Nearly every repository ships exactly one plugin whose name restates the + // repository name, so only its version earns a place in the meta row. + const manifests = manifestsOf(p); + if (manifests.length === 1 && manifests[0].version) { + meta.appendChild(el("span", "plugin-stat plugin-version", "v" + manifests[0].version)); + } + const updated = timeAgo(p.pushedAt); if (updated) meta.appendChild(el("span", "plugin-stat plugin-updated", "updated " + updated)); card.appendChild(meta); + + // A monorepo is the case the card title can't answer on its own, so it + // gets the extra row: how many plugins, and which. + if (manifests.length > 1) { + const ships = el("div", "plugin-ships"); + ships.appendChild(el("span", "plugin-ship-count", manifests.length + " plugins")); + ships.appendChild( + el("span", "plugin-ship-list", manifests.map(manifestLabel).join(" · ")), + ); + card.appendChild(ships); + } + return card; } + function manifestsOf(p) { + return Array.isArray(p.manifests) ? p.manifests : []; + } + + function manifestLabel(m) { + return m.name || m.id || m.path || "plugin"; + } + function matches(p, q) { if (!q) return true; - const hay = [p.fullName, p.owner, p.name, p.description, p.language, ...(p.topics || [])] + const hay = [ + p.fullName, + p.owner, + p.name, + p.description, + p.language, + ...(p.topics || []), + ...manifestsOf(p).flatMap((m) => [m.name, m.id, m.description]), + ] .filter(Boolean) .join(" ") .toLowerCase(); return q.split(/\s+/).every((term) => hay.includes(term)); } - function sortPlugins(list, mode) { + function sortRepos(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)); @@ -219,16 +274,18 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; function render() { const q = searchInput.value.trim().toLowerCase(); - const visible = sortPlugins(plugins.filter((p) => matches(p, q)), sortSelect.value); - const shown = showAll ? visible : visible.slice(0, SHOW_LIMIT); + const visible = sortRepos(repos.filter((p) => matches(p, q)), sortSelect.value); + const shown = visible.slice(0, visibleLimit); grid.replaceChildren(); for (const p of shown) grid.appendChild(buildCard(p)); - const hiddenCount = visible.length - shown.length; - moreButton.hidden = hiddenCount === 0; - if (hiddenCount > 0) { - moreButton.textContent = "show " + hiddenCount + " more"; + // Grow a page at a time, and name the real remainder on the last step so + // the button never promises 24 and delivers 5. + const remaining = visible.length - shown.length; + moreButton.hidden = remaining === 0; + if (remaining > 0) { + moreButton.textContent = "show " + Math.min(PAGE_SIZE, remaining) + " more"; } if (visible.length === 0) { @@ -241,23 +298,28 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; } } - function setMeta(generatedAt) { + function setMeta(data) { const parts = []; - parts.push(plugins.length + (plugins.length === 1 ? " plugin" : " plugins")); - const ago = timeAgo(generatedAt); + const repoCount = (data && data.repositoryCount) ?? repos.length; + const total = + (data && data.pluginCount) ?? + repos.reduce((n, repo) => n + (manifestsOf(repo).length || 1), 0); + parts.push(total + (total === 1 ? " plugin" : " plugins")); + parts.push(repoCount + (repoCount === 1 ? " repository" : " repositories")); + const ago = timeAgo(data && data.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); + repos = Array.isArray(data && data.plugins) ? data.plugins : []; + setMeta(data); render(); } function showError() { - plugins = []; + repos = []; metaEl.textContent = ""; grid.replaceChildren(); moreButton.hidden = true; @@ -288,10 +350,16 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; } } - searchInput.addEventListener("input", render); - sortSelect.addEventListener("change", render); + // A new query or order is a new list, so paging starts over. + function resetAndRender() { + visibleLimit = PAGE_SIZE; + render(); + } + + searchInput.addEventListener("input", resetAndRender); + sortSelect.addEventListener("change", resetAndRender); moreButton.addEventListener("click", () => { - showAll = true; + visibleLimit += PAGE_SIZE; render(); }); load(); @@ -300,6 +368,7 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; <style is:global> .plugins-page { padding-bottom: 5rem; + --plugins-clear-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.5 1.5l9 9M10.5 1.5l-9 9' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); } /* Reuse .section only for max-width centering, not its 5rem padding + top divider. */ @@ -416,6 +485,23 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; border-color: var(--accent); } + /* Chromium paints the native clear button in the OS accent colour, which + belongs to no ground the site defines. Redraw it from our own tokens. */ + .plugins-search::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; + width: 0.7rem; + height: 0.7rem; + cursor: pointer; + background: var(--muted-2); + -webkit-mask: var(--plugins-clear-icon) center / contain no-repeat; + mask: var(--plugins-clear-icon) center / contain no-repeat; + } + + .plugins-search::-webkit-search-cancel-button:hover { + background: var(--ink); + } + .plugins-sort-wrap { display: inline-flex; align-items: center; @@ -566,6 +652,43 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; color: var(--muted-2); } + /* What the repository ships. Sits below the repo stats, separated by a rule, + so the repository stays the primary identity of the card. */ + .plugin-ships { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.45rem; + padding-top: 0.7rem; + border-top: 1px solid var(--line); + font-family: var(--mono); + font-size: 0.78rem; + line-height: 1.5; + min-width: 0; + } + + .plugin-version { + color: var(--muted-2); + } + + .plugin-ship-count { + flex: 0 0 auto; + padding: 0.05rem 0.4rem; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + color: var(--muted-2); + } + + .plugin-ship-list { + align-self: stretch; + min-width: 0; + color: var(--muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + .plugins-status { padding: 3rem 1rem; text-align: center; @@ -626,4 +749,4 @@ const apiUrl = import.meta.env.DEV ? null : snapshotUrl; } } </style> -</MarketingLayout> +</SiteLayout> diff --git a/website/src/pages/stats.astro b/website/src/pages/stats.astro deleted file mode 100644 index 2287b929..00000000 --- a/website/src/pages/stats.astro +++ /dev/null @@ -1,270 +0,0 @@ ---- -import MarketingLayout from '../components/MarketingLayout.astro'; ---- - -<MarketingLayout - title="Herdr stats: live public numbers" - description="Live public stats for Herdr — GitHub stars, downloads, and website traffic, pulled from publicly verifiable sources." - canonicalPath="/stats/" -> - <main class="stats-page"> - <section class="hero stats-hero"> - <div class="hero-left"> - <div class="hero-copy"> - <p class="eyebrow"> - <span class="status-dot dot-working"></span> - build in public · live numbers - </p> - <div class="hero-stat-value" id="star-count">—</div> - <div class="hero-stat-label"><span class="live-dot" aria-hidden="true"></span> GitHub stars · counting live</div> - <p class="hero-lede"> - Live public stats for herdr: stars, installs, and traffic. Everything on this - page comes from sources you can check yourself, refreshed continuously. - </p> - <div class="hero-actions"> - <a class="button button-primary" href="https://github.com/herdrdev/herdr">★ Star on GitHub</a> - <a class="button button-secondary" href="/docs/install/">Get herdr</a> - </div> - <p class="hero-note" id="hero-note"></p> - </div> - </div> - <div class="hero-right"> - <div class="chart-card"> - <div class="chart-card-title"> - <span>github stars · all time</span> - <span class="when" id="stars-since"></span> - </div> - <div id="chart-stars" class="chart-slot"></div> - </div> - </div> - </section> - - <div class="stats-main"> - <section class="kpi-row" aria-label="Headline numbers"> - <div class="kpi"> - <div class="label">total installs</div> - <div class="value" id="kpi-installs">—</div> - <div class="sub" id="kpi-installs-sub"></div> - </div> - <div class="kpi"> - <div class="label">daily visitors</div> - <div class="value" id="kpi-visitors">—</div> - <div class="sub">herdr.dev uniques · 7-day avg · cloudflare</div> - </div> - <div class="kpi"> - <div class="label">releases shipped</div> - <div class="value" id="kpi-releases">—</div> - <div class="sub">stable + preview channels</div> - </div> - <div class="kpi"> - <div class="label">days old</div> - <div class="value" id="kpi-age">—</div> - <div class="sub" id="kpi-age-sub"></div> - </div> - </section> - - <section class="stats-section"> - <h2>Week by week</h2> - <p class="section-sub">Weekly totals. The current week is still filling up, shown dimmed.</p> - <div class="velocity-grid"> - <div class="chart-card"> - <div class="chart-card-title"><span>stars / week</span></div> - <div id="chart-stars-week" class="chart-slot"></div> - </div> - <div class="chart-card"> - <div class="chart-card-title"><span>downloads / week</span></div> - <div id="chart-downloads-week" class="chart-slot"></div> - </div> - <div class="chart-card"> - <div class="chart-card-title"><span>avg daily visitors / week</span></div> - <div id="chart-visitors-week" class="chart-slot"></div> - </div> - </div> - </section> - - <section class="stats-section"> - <h2>Downloads</h2> - <p class="section-sub" id="downloads-sub"></p> - <div class="chart-card"> - <div class="chart-card-title"> - <span>release downloads · cumulative</span> - <span class="when">github api</span> - </div> - <div id="chart-downloads" class="chart-slot"></div> - </div> - </section> - - <section class="stats-section"> - <h2>Where the herd browses from</h2> - <p class="section-sub" id="traffic-sub"></p> - <div class="traffic-grid"> - <div class="chart-card"> - <div class="chart-card-title"><span>top countries</span><span class="when">visits</span></div> - <div id="rank-countries"></div> - </div> - <div class="chart-card"> - <div class="chart-card-title"><span>top referrers</span><span class="when">visits</span></div> - <div id="rank-referrers"></div> - </div> - <div class="chart-card"> - <div class="chart-card-title"><span>top pages</span><span class="when">page views</span></div> - <div id="rank-paths"></div> - </div> - </div> - </section> - - <section class="stats-section"> - <h2>Timeline</h2> - <p class="section-sub">From first commit to today.</p> - <div id="timeline"></div> - </section> - - <p class="sources-note" id="sources-note"> - Every number on this page comes from publicly verifiable sources: the - <a href="https://api.github.com/repos/herdrdev/herdr">GitHub API</a> for stars and release downloads, and - <a href="https://formulae.brew.sh/api/analytics/install/365d.json">Homebrew analytics</a> for brew installs. - Website traffic is measured by Cloudflare (unique visitors per day, herdr.dev zone). - <span id="updated-note"></span> - </p> - </div> - - <script> - import { initStatsPage } from '../scripts/stats-page'; - initStatsPage(); - </script> - </main> -</MarketingLayout> - -<style is:global> - .stats-hero { min-height: auto; } - .stats-hero .hero-left { padding: 3.5rem 3rem 3.5rem max(2.5rem, calc((100vw - var(--max)) / 2)); } - .stats-hero .hero-right { padding: 3rem max(2.5rem, calc((100vw - var(--max)) / 2)) 3rem 2.5rem; } - .stats-hero .chart-card { width: 100%; position: relative; z-index: 1; } - .hero-stat-value { - font-family: var(--mono); - font-weight: 600; - font-size: clamp(3.2rem, 7vw, 5.2rem); - line-height: 1; - letter-spacing: -0.04em; - color: var(--ink); - min-height: 1em; - } - .hero-stat-label { - display: flex; align-items: center; gap: 0.55rem; - margin-top: 0.55rem; - font-family: var(--mono); font-size: 0.95rem; color: var(--muted); - } - .live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: live-pulse 2.4s ease-in-out infinite; } - @keyframes live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } } - @media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } } - - .stats-main { max-width: var(--max); margin: 0 auto; padding: 0 1.5rem 4rem; } - .stats-section { margin-top: 3.5rem; } - .stats-section > h2 { font-family: var(--mono); font-weight: 400; letter-spacing: -0.02em; font-size: 1.45rem; margin: 0 0 0.35rem; color: var(--ink); } - .stats-section > .section-sub { color: var(--muted); font-size: 0.95rem; margin: 0 0 1.4rem; max-width: 62ch; } - - .chart-card { - border: 1px solid var(--line); border-radius: var(--radius-lg); - background: var(--bg-elevated); padding: 1rem 1.1rem 0.8rem; position: relative; - } - .chart-card-title { - display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; - font-family: var(--mono); font-size: 0.85rem; color: var(--ink-soft); margin-bottom: 0.5rem; - } - .chart-card-title .when { color: var(--muted-2); font-size: 0.78rem; } - .chart { position: relative; } - .chart svg { display: block; width: 100%; height: auto; } - .chart svg:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); } - - .chart .grid { stroke: var(--line); stroke-width: 1; } - .chart .grid.strong { stroke: var(--line-strong); } - .chart .tick { fill: var(--muted-2); font-family: var(--mono); font-size: 11px; } - .chart .line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } - .chart .area { opacity: 0.1; } - .chart .dot { stroke: var(--bg-elevated); stroke-width: 2; } - .chart .event-dot { fill: var(--bg-elevated); stroke-width: 2; } - .chart .milestone-label { fill: var(--muted); font-family: var(--mono); font-size: 11.5px; font-weight: 600; } - .chart .milestone-leader { stroke: var(--line-strong); stroke-width: 1; } - .chart .cap-label { fill: var(--muted); font-family: var(--mono); font-size: 11.5px; font-weight: 600; } - .chart .crosshair { stroke: var(--line-strong); stroke-width: 1; } - .chart .hover-dot { stroke: var(--bg-elevated); stroke-width: 2; } - .chart .bar .hit { fill: transparent; } - .chart .bar.partial .fill { opacity: 0.45; } - .chart .bar:hover .fill, .chart .bar.active .fill { filter: brightness(1.12); } - - .chart-tip { - position: absolute; pointer-events: none; z-index: 4; - background: var(--ink); color: var(--bg); - font-family: var(--mono); font-size: 0.78rem; line-height: 1.4; - padding: 0.35rem 0.55rem; border-radius: var(--radius-md); - white-space: nowrap; transform: translate(-50%, calc(-100% - 10px)); - } - .chart-tip strong { font-size: 0.88rem; } - .chart-empty { color: var(--muted); font-family: var(--mono); font-size: 0.85rem; padding: 2rem 0; text-align: center; } - - .chart-table { margin-top: 0.4rem; font-family: var(--mono); font-size: 0.78rem; } - .chart-table summary { color: var(--muted-2); cursor: pointer; padding: 0.25rem 0; } - .chart-table summary:hover { color: var(--muted); } - .chart-table-scroll { - max-height: 250px; overflow-y: auto; margin-top: 0.4rem; - scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; - } - .chart-table-scroll::-webkit-scrollbar { width: 8px; } - .chart-table-scroll::-webkit-scrollbar-track { background: transparent; } - .chart-table-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; } - /* the site stylesheet styles tables globally (min-width, accent last column) — reset inside cards */ - .chart-table table { width: 100%; min-width: 0; border-collapse: collapse; font-size: 0.78rem; } - .chart-table th, .chart-table td { text-align: left; padding: 0.28rem 0.6rem 0.28rem 0; border-bottom: 1px solid var(--line); color: var(--ink-soft); font-variant-numeric: tabular-nums; font-size: 0.78rem; } - .chart-table th { color: var(--muted); font-weight: 600; letter-spacing: 0.04em; } - .chart-table th:last-child, .chart-table td:last-child { background: none; color: var(--ink-soft); font-weight: 600; text-align: right; padding-right: 0; } - .chart-table tr:last-child td { border-bottom: 0; } - - .kpi-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.9rem; margin-top: 3rem; } - .kpi { - border: 1px solid var(--line); border-radius: var(--radius-lg); - background: var(--bg-elevated); padding: 1rem 1.1rem; - } - .kpi .label { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); } - .kpi .value { font-family: var(--mono); font-weight: 600; font-size: 1.9rem; letter-spacing: -0.02em; color: var(--ink); margin: 0.25rem 0 0.15rem; } - .kpi .sub { font-family: var(--mono); font-size: 0.75rem; color: var(--muted-2); } - .kpi .sub .up { color: var(--green); font-weight: 600; } - - .velocity-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; } - .traffic-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; } - - .rank-list { display: flex; flex-direction: column; gap: 0.15rem; margin-top: 0.3rem; } - .rank-row { - display: grid; grid-template-columns: minmax(0, 45%) 1fr auto; - gap: 0.7rem; align-items: center; padding: 0.28rem 0; - } - .rank-name { font-family: var(--mono); font-size: 0.8rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .rank-bar { height: 8px; border-radius: 2px; background: color-mix(in srgb, var(--yellow) 14%, transparent); overflow: hidden; } - .rank-fill { display: block; height: 100%; border-radius: 2px; background: var(--yellow); } - .rank-value { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; } - - .milestone-term { - background: var(--term-bg); border: 1px solid var(--term-border); border-radius: var(--radius-lg); - font-family: var(--mono); font-size: 0.92rem; padding: 1.1rem 1.3rem; max-width: 560px; - } - .term-line { display: flex; gap: 1.2rem; padding: 0.22rem 0; align-items: baseline; } - .term-cmd-line { margin-bottom: 0.5rem; gap: 0.6rem; } - .term-prompt { color: var(--term-prompt); } - .term-cmd { color: var(--term-text); } - .term-date { color: var(--term-dim); flex: 0 0 4.4em; } - .term-text { color: var(--term-text); flex: 1; } - .term-note { color: var(--term-muted); } - .term-line:last-child .term-text { color: var(--term-green-bright); font-weight: 600; } - .term-line:last-child .term-note { color: var(--term-green-bright); } - - .sources-note { margin-top: 3.5rem; padding-top: 1.4rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.88rem; max-width: 72ch; } - .sources-note a { color: var(--accent); } - - .stats-stale { - margin-top: 1rem; font-family: var(--mono); font-size: 0.8rem; color: var(--yellow); - } - - @media (max-width: 900px) { - .kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .velocity-grid, .traffic-grid { grid-template-columns: minmax(0, 1fr); } - } -</style> |
