aboutsummaryrefslogtreecommitdiff
path: root/website/src/pages
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-27 20:50:16 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-27 20:51:48 +0300
commit7ebc72f3f5de24a293db144cd1745cea52c3e717 (patch)
tree355a1c7c04a69af7023cc8afa7ee675596687253 /website/src/pages
parent4ec4d56fcf985e4464bce6b76e3578e9c941ad50 (diff)
feat: add website release notes
Diffstat (limited to 'website/src/pages')
-rw-r--r--website/src/pages/blog/[slug].astro88
-rw-r--r--website/src/pages/blog/index.astro38
-rw-r--r--website/src/pages/compare.astro5
-rw-r--r--website/src/pages/releases/[...slug].astro89
-rw-r--r--website/src/pages/releases/index.astro42
5 files changed, 261 insertions, 1 deletions
diff --git a/website/src/pages/blog/[slug].astro b/website/src/pages/blog/[slug].astro
new file mode 100644
index 00000000..10939fd0
--- /dev/null
+++ b/website/src/pages/blog/[slug].astro
@@ -0,0 +1,88 @@
+---
+import { getCollection, render } from 'astro:content';
+import MarketingLayout from '../../components/MarketingLayout.astro';
+
+export async function getStaticPaths() {
+ const posts = await getCollection('blog');
+ return posts
+ .filter((post) => !post.data.draft)
+ .map((post) => {
+ const slug = post.id.replace(/\.(md|mdx)$/i, '');
+ return {
+ params: { slug },
+ props: { post, slug },
+ };
+ });
+}
+
+const { post, slug } = Astro.props;
+const { Content, headings } = await render(post);
+const sectionHeadings = headings.filter((heading) => heading.depth === 2);
+---
+
+<MarketingLayout
+ title={`${post.data.title} — Herdr blog`}
+ description={post.data.description}
+ canonicalPath={`/blog/${slug}/`}
+ ogType="article"
+>
+ <main class="content-shell">
+ <div class="article-layout section">
+ <aside class="article-toc" aria-label="Article 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">
+ <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>
+ </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/blog/index.astro b/website/src/pages/blog/index.astro
new file mode 100644
index 00000000..8ca2ca10
--- /dev/null
+++ b/website/src/pages/blog/index.astro
@@ -0,0 +1,38 @@
+---
+import { getCollection } from 'astro:content';
+import MarketingLayout from '../../components/MarketingLayout.astro';
+
+const posts = (await getCollection('blog'))
+ .filter((post) => !post.data.draft)
+ .map((post) => ({ ...post, slug: post.id.replace(/\.(md|mdx)$/i, '') }))
+ .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
+---
+
+<MarketingLayout
+ title="Herdr blog"
+ description="Notes on terminal-native agent workflows and the engineering behind Herdr."
+ 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>
+
+ <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>
diff --git a/website/src/pages/compare.astro b/website/src/pages/compare.astro
index 14f98a9c..0c173f6f 100644
--- a/website/src/pages/compare.astro
+++ b/website/src/pages/compare.astro
@@ -55,9 +55,12 @@
<span>herdr</span>
</a>
<div class="nav-links">
+ <a href="/">Home</a>
<a href="/compare/" aria-current="page">Compare</a>
- <a href="/docs/">Docs</a>
+ <a href="/releases/">Releases</a>
+ <a href="/blog/">Blog</a>
<a href="/docs/quick-start/">Quick start</a>
+ <a href="/docs/">Docs</a>
<a href="/docs/socket-api/">API</a>
</div>
<div class="nav-tools">
diff --git a/website/src/pages/releases/[...slug].astro b/website/src/pages/releases/[...slug].astro
new file mode 100644
index 00000000..420d4b9e
--- /dev/null
+++ b/website/src/pages/releases/[...slug].astro
@@ -0,0 +1,89 @@
+---
+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
new file mode 100644
index 00000000..48263ab9
--- /dev/null
+++ b/website/src/pages/releases/index.astro
@@ -0,0 +1,42 @@
+---
+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>