diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-27 20:50:16 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-27 20:51:48 +0300 |
| commit | 7ebc72f3f5de24a293db144cd1745cea52c3e717 (patch) | |
| tree | 355a1c7c04a69af7023cc8afa7ee675596687253 /website/src/pages | |
| parent | 4ec4d56fcf985e4464bce6b76e3578e9c941ad50 (diff) | |
feat: add website release notes
Diffstat (limited to 'website/src/pages')
| -rw-r--r-- | website/src/pages/blog/[slug].astro | 88 | ||||
| -rw-r--r-- | website/src/pages/blog/index.astro | 38 | ||||
| -rw-r--r-- | website/src/pages/compare.astro | 5 | ||||
| -rw-r--r-- | website/src/pages/releases/[...slug].astro | 89 | ||||
| -rw-r--r-- | website/src/pages/releases/index.astro | 42 |
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> |
