mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-24 08:00:37 +00:00
feat: remove roadmap page and ground changelog in real history
Delete the public roadmap page and every reference to it (footer, open-source showcase, about, changelog, trust). The roadmap was built from seeded, fabricated items. Rebuild the changelog from the real git history. The previous entries were self-described seeded fabrications and contradicted the codebase (claiming SAML SSO and BYOK shipped, which they have not). There are no releases, tags, or CHANGELOG.md, so the commit log is the source of truth. Group the work into a handful of milestone entries rather than one per release, mirror them on the homepage changelog showcase, and drop the invented version numbers. Fix the broken links the old page carried: the dead /changelog/feed.xml RSS feed and the non-existent CHANGELOG.md now point at the real commit history. Relabel the trust page's 'roadmap' posture state to 'planned' so SOC 2 and ISO 27001 stay honestly marked as not yet in place.
This commit is contained in:
@@ -5,7 +5,7 @@ import Icon from './Icon.astro';
|
||||
* ChangelogShowcase - a landing-page CHANGELOG section.
|
||||
*
|
||||
* A clean vertical timeline of the latest releases. Content is grounded in the
|
||||
* real entries on /changelog/ (versions, dates, change bullets) so the section
|
||||
* real entries on /changelog/ (dates, change bullets) so the section
|
||||
* never drifts from the actual product history. Self-contained, no props, no
|
||||
* client JS, no app-window frame - this is a content section, not a mock.
|
||||
*
|
||||
@@ -16,56 +16,52 @@ import Icon from './Icon.astro';
|
||||
*/
|
||||
|
||||
interface Release {
|
||||
version: string;
|
||||
date: string;
|
||||
category: 'New' | 'Improved' | 'Fixed';
|
||||
title: string;
|
||||
bullets: string[];
|
||||
}
|
||||
|
||||
// Latest four releases, taken from /changelog/ and split into 2-3 bullet lines.
|
||||
// Latest releases, taken from /changelog/ (which is derived from the real
|
||||
// git history) and split into 2-3 bullet lines.
|
||||
const releases: Release[] = [
|
||||
{
|
||||
version: 'v1.9',
|
||||
date: '2026-05-26',
|
||||
date: '2026-06-09',
|
||||
category: 'New',
|
||||
title: 'BYOK envelope encryption for self-host',
|
||||
title: 'Automation builder',
|
||||
bullets: [
|
||||
'Register your own AWS KMS key as the root of trust for the per-user DEK envelope.',
|
||||
'Existing encrypted material is re-wrapped on first key activation.',
|
||||
'Plaintext DEKs stay only in the Redis cache with a bounded TTL.',
|
||||
'Build branching flows in a visual graph builder.',
|
||||
'Launch automations straight from a campaign.',
|
||||
'Execution safeguards protect every live flow.',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: 'v1.8',
|
||||
date: '2026-05-22',
|
||||
date: '2026-06-08',
|
||||
category: 'New',
|
||||
title: 'SAML SSO for Business plans',
|
||||
title: 'Reply classification and campaign branching',
|
||||
bullets: [
|
||||
'Connect Okta, Azure AD, or any SAML 2.0 identity provider.',
|
||||
'Just-in-time provisioning is on by default.',
|
||||
'Admins can pin a default role for new sign-ins.',
|
||||
'Inbound replies are classified by intent.',
|
||||
'Sequences branch on what a reply actually says.',
|
||||
'New campaigns stop on reply by default.',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: 'v1.7',
|
||||
date: '2026-05-20',
|
||||
date: '2026-06-08',
|
||||
category: 'New',
|
||||
title: 'API keys dashboard',
|
||||
title: 'Two-factor auth and notifications',
|
||||
bullets: [
|
||||
'Per-key scopes, last-used timestamps, and one-click rotation.',
|
||||
'Existing keys auto-migrated to full-access scope.',
|
||||
'Two-factor authentication, required at login once on.',
|
||||
'A realtime notification center with per-member preferences.',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: 'v1.6',
|
||||
date: '2026-05-15',
|
||||
category: 'Improved',
|
||||
title: 'Per-mailbox health bands',
|
||||
date: '2026-06-07',
|
||||
category: 'New',
|
||||
title: 'CRM, unified inbox, and integrations',
|
||||
bullets: [
|
||||
'Mailboxes surface a health band: healthy, watch, quarantined, or blocked.',
|
||||
'Bands are driven by rolling placement, complaint, and bounce signals.',
|
||||
'Pool selection now skips anything not healthy.',
|
||||
'Server-driven CRM with a deals board and team tasks.',
|
||||
'Reworked unified inbox routing and seen-state.',
|
||||
'Integrations and meetings dashboard.',
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -136,9 +132,6 @@ const categoryStyle = (c: Release['category']) => {
|
||||
<span aria-hidden="true" class={`absolute left-0 top-0 bottom-0 w-[3px] ${s.rail}`}></span>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span class="text-[12.5px] font-mono font-semibold text-heading tabular-nums">
|
||||
{r.version}
|
||||
</span>
|
||||
<span class="text-[11.5px] font-mono uppercase tracking-[0.14em] text-muted-foreground tabular-nums">
|
||||
{r.date}
|
||||
</span>
|
||||
|
||||
@@ -14,7 +14,6 @@ const groups: { title: string; links: { label: string; href: string }[] }[] = [
|
||||
{ label: 'API & Webhooks', href: '/developers/' },
|
||||
{ label: 'Integrations', href: '/integrations/' },
|
||||
{ label: 'Changelog', href: '/changelog/' },
|
||||
{ label: 'Roadmap', href: '/roadmap/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ const facts = [
|
||||
{ value: '100%', label: 'Open source' },
|
||||
{ value: 'Apache 2.0', label: 'Fork it, ship it' },
|
||||
{ value: '$0', label: 'To self-host' },
|
||||
{ value: 'Public', label: 'Roadmap and issues' },
|
||||
{ value: 'Public', label: 'Issues and pull requests' },
|
||||
];
|
||||
|
||||
const lines = [
|
||||
@@ -35,7 +35,7 @@ const layers = [
|
||||
const contribute = [
|
||||
{ label: 'Browse good first issues', href: 'https://github.com/warmbly/warmbly/labels/good%20first%20issue' },
|
||||
{ label: 'Read the contributing guide', href: 'https://github.com/warmbly/warmbly/blob/main/CONTRIBUTING.md' },
|
||||
{ label: 'Follow the public roadmap', href: '/roadmap/' },
|
||||
{ label: 'Follow the changelog', href: '/changelog/' },
|
||||
{ label: 'Open a discussion or RFC', href: 'https://github.com/warmbly/warmbly/discussions' },
|
||||
];
|
||||
const selfhost = [
|
||||
@@ -47,7 +47,6 @@ const selfhost = [
|
||||
const links = [
|
||||
{ label: 'Docs', href: '/developers/' },
|
||||
{ label: 'Architecture', href: '/trust/' },
|
||||
{ label: 'Roadmap', href: '/roadmap/' },
|
||||
{ label: 'Changelog', href: '/changelog/' },
|
||||
{ label: 'Apache 2.0 license', href: 'https://github.com/warmbly/warmbly/blob/main/LICENSE' },
|
||||
];
|
||||
|
||||
+14
-14
@@ -85,8 +85,8 @@ const positioning = [
|
||||
closed: 'Undocumented. You find out when your reputation is gone.',
|
||||
},
|
||||
{
|
||||
label: 'Roadmap',
|
||||
warmbly: 'Public board. Shipped, in progress, researching.',
|
||||
label: 'Changelog',
|
||||
warmbly: 'Public, dated changelog. Every entry maps to a commit.',
|
||||
closed: 'Internal. Released on announcement cadence.',
|
||||
},
|
||||
{
|
||||
@@ -117,16 +117,16 @@ const surface = [
|
||||
const involve = [
|
||||
{ label: 'GitHub', href: 'https://github.com/warmbly/warmbly', note: 'Source, issues, pull requests.', icon: 'arrowUpRight' },
|
||||
{ label: 'Developers', href: '/developers/', note: 'API, webhooks, scopes, idempotency.', icon: 'arrowRight' },
|
||||
{ label: 'Roadmap', href: '/roadmap/', note: 'Shipped, in progress, researching.', icon: 'arrowRight' },
|
||||
{ label: 'Changelog', href: '/changelog/', note: 'Every release, dated, plain wording.', icon: 'arrowRight' },
|
||||
{ label: 'Trust', href: '/trust/', note: 'Encryption, subprocessors, security.', icon: 'arrowRight' },
|
||||
];
|
||||
---
|
||||
<Layout
|
||||
title="About - The Open Cold Email Engine | Warmbly"
|
||||
description="Warmbly is an open-source email warmup and cold outreach platform. Control plane and distributed workers. Per-mailbox safety. Public roadmap."
|
||||
description="Warmbly is an open-source email warmup and cold outreach platform. Control plane and distributed workers. Per-mailbox safety. Public changelog."
|
||||
>
|
||||
<!-- ============================================================
|
||||
HERO · HeroAtmosphere (matches sending, warmup, developers, roadmap)
|
||||
HERO · HeroAtmosphere (matches sending, warmup, developers)
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
@@ -136,7 +136,7 @@ const involve = [
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded-full text-[10.5px] font-semibold uppercase tracking-[0.06em] bg-white" style="color:#0369a1;">
|
||||
About
|
||||
</span>
|
||||
Open source. Apache 2.0. Public roadmap.
|
||||
Open source. Apache 2.0. Public changelog.
|
||||
</div>
|
||||
|
||||
<h1 class="mt-8 md:mt-10 text-[44px] sm:text-6xl md:text-[72px] lg:text-[80px] font-semibold tracking-[-0.04em] leading-[0.98] text-white max-w-4xl mx-auto">
|
||||
@@ -191,7 +191,7 @@ const involve = [
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We are a small team with a public roadmap. We do not hide what we are working on. We do not invent metrics. We do not pretend to be larger than we are. The product gets better in public.
|
||||
We are a small team building in public. We do not hide what we are working on. We do not invent metrics. We do not pretend to be larger than we are. The product gets better in public.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +319,7 @@ const involve = [
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-[11.5px] font-mono text-muted-foreground">
|
||||
Comparison reflects how Warmbly is built. We do not speak for any other vendor's roadmap or pricing.
|
||||
Comparison reflects how Warmbly is built. We do not speak for any other vendor's plans or pricing.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -366,7 +366,7 @@ const involve = [
|
||||
<div class="lg:sticky lg:top-24 self-start">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-[#0284c7] mb-3">The team</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Small team. Public roadmap. Open source.
|
||||
Small team. Built in public. Open source.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -378,14 +378,14 @@ const involve = [
|
||||
Because the team is small, the constraints we put on the codebase have to do the heavy lifting. Safe defaults, conservative warmup, layered abuse controls, encryption everywhere we touch user secrets. We would rather ship one fewer feature than ship a feature that puts your sending reputation at risk.
|
||||
</p>
|
||||
<p>
|
||||
You can follow the work in public. Open issues, pull requests, the roadmap board, the changelog. Everything that matters is visible.
|
||||
You can follow the work in public. Open issues, pull requests, and the changelog. Everything that matters is visible.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 grid sm:grid-cols-3 gap-3">
|
||||
<a href="/roadmap/" class="group rounded-[12px] bg-white ring-1 ring-[color:var(--border)] hover:ring-[#0284c7]/40 transition-all p-5 block">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Roadmap</div>
|
||||
<div class="mt-2 text-[14.5px] font-semibold text-heading">What is next</div>
|
||||
<div class="mt-1 text-[12px] text-foreground/60">Shipped, in progress, researched.</div>
|
||||
<a href="/trust/" class="group rounded-[12px] bg-white ring-1 ring-[color:var(--border)] hover:ring-[#0284c7]/40 transition-all p-5 block">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Trust</div>
|
||||
<div class="mt-2 text-[14.5px] font-semibold text-heading">How it is secured</div>
|
||||
<div class="mt-1 text-[12px] text-foreground/60">Encryption, subprocessors, posture.</div>
|
||||
</a>
|
||||
<a href="/changelog/" class="group rounded-[12px] bg-white ring-1 ring-[color:var(--border)] hover:ring-[#0284c7]/40 transition-all p-5 block">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Changelog</div>
|
||||
|
||||
@@ -4,81 +4,41 @@ import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
|
||||
// Seeded entries (from the prior changelog) plus 3 new ones grounded
|
||||
// in the real codebase / CLAUDE.md (SOC 2 in progress, BYOK / KMS,
|
||||
// SSO, worker assignment, tracking edge, warmup token validation).
|
||||
// Entries are grounded in the real git history of warmbly/warmbly. There are
|
||||
// no published releases or tags yet, so the commit log is the source of truth.
|
||||
// We do not log every release: each entry groups the related work that shipped
|
||||
// a milestone, dated when it landed on main. Add an entry here when a feature
|
||||
// area is worth telling customers about, not for every commit.
|
||||
const entries = [
|
||||
{
|
||||
date: '2026-05-26',
|
||||
title: 'BYOK envelope encryption for self-host',
|
||||
date: '2026-06-09',
|
||||
title: 'Automation builder',
|
||||
tag: 'shipped',
|
||||
body: 'Self-hosted deployments can now register their own AWS KMS key as the root of trust for the per-user DEK envelope. Existing encrypted material is re-wrapped on first key activation. Plaintext DEKs continue to live only in the Redis cache with a bounded TTL.',
|
||||
body: 'Build branching automation flows in a visual graph builder, launch them straight from a campaign, and run multi-step graphs with conditional expressions. Live flows are protected by execution safeguards, and delete conflicts are surfaced before they break a running automation.',
|
||||
},
|
||||
{
|
||||
date: '2026-05-22',
|
||||
title: 'SAML SSO for Business plans',
|
||||
date: '2026-06-08',
|
||||
title: 'Reply classification and campaign branching',
|
||||
tag: 'shipped',
|
||||
body: 'Workspace owners on Business can connect Okta, Azure AD, or any SAML 2.0 IdP. Just-in-time provisioning is on by default and admins can pin a default role for new sign-ins.',
|
||||
body: 'Inbound campaign replies are classified by intent and stored against the campaign, so a sequence can branch on what a reply actually says. Reply branches fire instantly off tracking events, new campaigns stop on reply by default, and reporting surfaces the winning A/B variant per step.',
|
||||
},
|
||||
{
|
||||
date: '2026-05-20',
|
||||
title: 'API keys dashboard',
|
||||
date: '2026-06-08',
|
||||
title: 'Two-factor auth and in-app notifications',
|
||||
tag: 'shipped',
|
||||
body: 'New API keys page in Settings. Per-key scopes, last-used timestamps, and one-click rotation. Existing keys auto-migrated to full-access scope. Rotate when you can.',
|
||||
body: 'Members can enable two-factor authentication from security settings, required at login once it is on. A new notification center surfaces account and workflow events in realtime, with per-member preferences and a dedicated channel for security events.',
|
||||
},
|
||||
{
|
||||
date: '2026-05-15',
|
||||
title: 'Per-mailbox health bands',
|
||||
date: '2026-06-08',
|
||||
title: 'Deliverability dashboard',
|
||||
tag: 'shipped',
|
||||
body: 'Mailboxes now surface a health band: healthy, watch, quarantined, or blocked. Bands are driven by rolling placement, complaint, and bounce signals. Pool selection skips anything not healthy.',
|
||||
body: 'A dedicated deliverability dashboard with expanded metrics: placement, complaints, bounces, and warmup health, surfaced per mailbox.',
|
||||
},
|
||||
{
|
||||
date: '2026-05-05',
|
||||
title: 'Contacts power tools',
|
||||
date: '2026-06-07',
|
||||
title: 'CRM, unified inbox, and integrations',
|
||||
tag: 'shipped',
|
||||
body: 'CSV, XLSX, and JSON export with a field picker and a scope toggle. A CSV / XLSX import wizard handles column mapping, dedup, and category assignment. Categories replace Tags in the contact model.',
|
||||
},
|
||||
{
|
||||
date: '2026-04-25',
|
||||
title: 'Recovery pool',
|
||||
tag: 'shipped',
|
||||
body: 'Quarantined mailboxes move to a separate recovery pool, isolated from the main paid pool. 7-day cooldown plus a 20-delivery probation window before any re-entry into premium warmup traffic.',
|
||||
},
|
||||
{
|
||||
date: '2026-04-18',
|
||||
title: 'SOC 2 Type I in progress',
|
||||
tag: 'changed',
|
||||
body: 'We engaged an external auditor for SOC 2 Type I. Trust center now lists subprocessors, the data flow, and the encryption model. Type II observation window starts after the Type I report lands.',
|
||||
},
|
||||
{
|
||||
date: '2026-04-12',
|
||||
title: 'Worker concentration warnings',
|
||||
tag: 'shipped',
|
||||
body: 'Workspace surfaces a warning when a shared worker is carrying too much active cold volume. We add workers and rebalance before forcing any one machine to do more.',
|
||||
},
|
||||
{
|
||||
date: '2026-04-01',
|
||||
title: 'Plain-text default',
|
||||
tag: 'changed',
|
||||
body: 'New sequences default to plain text. HTML is opt-in per step. Existing sequences are untouched, but new templates inherit the plain-text default.',
|
||||
},
|
||||
{
|
||||
date: '2026-03-22',
|
||||
title: 'Microsoft 365 OAuth refresh',
|
||||
tag: 'fixed',
|
||||
body: 'Resolved an intermittent token-refresh failure for Microsoft mailboxes with mandatory MFA. Affected mailboxes auto-reconnect on the next sync window with no manual reauth.',
|
||||
},
|
||||
{
|
||||
date: '2026-03-10',
|
||||
title: 'Sequence A/B variants',
|
||||
tag: 'shipped',
|
||||
body: 'Up to four subject and body variants per step. Per-variant reply-rate reporting, with significance hints once each variant has enough sample.',
|
||||
},
|
||||
{
|
||||
date: '2026-03-01',
|
||||
title: 'Tracking edge upgrade',
|
||||
tag: 'changed',
|
||||
body: 'The tracking edge now dedupes by task and IP at the edge, and again in the consumer. Drops a class of replay-driven noise from analytics without losing real engagement events.',
|
||||
body: 'A server-driven CRM with a paged deals board, deal sequence actions, team task assignment, and searchable task types, wired into the unified inbox. Inbox routing and seen-state handling were reworked, and a new integrations and meetings dashboard can push contacts out to connected systems.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -96,10 +56,9 @@ const counts = {
|
||||
};
|
||||
|
||||
// Hoisted to avoid `<` / `<=` inside JSX expressions (Astro JSX parser is fragile).
|
||||
const monthEntries = entries.filter(e => e.date.startsWith('2026-05'));
|
||||
const monthCount = monthEntries.length;
|
||||
const ytdEntries = entries.filter(e => e.date.startsWith('2026-'));
|
||||
const ytdCount = ytdEntries.length;
|
||||
const recentEntries = entries.slice(0, 3);
|
||||
const recentCount = recentEntries.length;
|
||||
const totalCount = entries.length;
|
||||
const shippedCount = counts.shipped;
|
||||
const changedCount = counts.changed;
|
||||
const fixedCount = counts.fixed;
|
||||
@@ -130,14 +89,14 @@ const fixedCount = counts.fixed;
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="/changelog/feed.xml" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">Subscribe to the feed</span>
|
||||
<a href="https://github.com/warmbly/warmbly/commits/main" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">View the commits</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#075985" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/roadmap/" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
See the roadmap
|
||||
<a href="https://github.com/warmbly/warmbly" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,21 +109,21 @@ const fixedCount = counts.fixed;
|
||||
<div class="container-page">
|
||||
<div class="flex flex-wrap items-end justify-between gap-6 mb-10 md:mb-12">
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Shipped this month</div>
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Latest releases</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
The latest production work.
|
||||
</h2>
|
||||
<p class="mt-3 text-[14.5px] md:text-[15px] text-foreground/70 leading-relaxed max-w-2xl">
|
||||
{monthCount} entries in May 2026. Trust and infrastructure work led the month, with BYOK envelope encryption, SAML SSO, and a new API keys dashboard all landing within two weeks.
|
||||
The {recentCount} most recent milestones. The automation builder, reply classification, and two-factor auth all landed in early June 2026.
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-[11.5px] font-mono uppercase tracking-[0.18em] text-muted-foreground">
|
||||
<span class="text-heading font-semibold">{monthCount}</span> / {ytdCount} year to date
|
||||
<span class="text-heading font-semibold">{recentCount}</span> / {totalCount} total
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{monthEntries.map((e) => {
|
||||
{recentEntries.map((e) => {
|
||||
const t = tagStyle(e.tag);
|
||||
return (
|
||||
<article class="grid grid-cols-1 md:grid-cols-[140px_110px_1fr] gap-x-8 gap-y-3 bg-white ring-1 ring-[color:var(--border)] rounded-[14px] p-6 md:p-7 items-baseline hover:ring-[#0284c7]/40 transition-all">
|
||||
@@ -249,44 +208,44 @@ const fixedCount = counts.fixed;
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center gap-x-4 gap-y-2 text-[11.5px] font-mono text-muted-foreground">
|
||||
<span>Older than 90 days?</span>
|
||||
<a href="https://github.com/warmbly/warmbly/blob/main/CHANGELOG.md" class="text-[#0369a1] hover:text-[#075985]">CHANGELOG.md on GitHub</a>
|
||||
<span>Want the full detail?</span>
|
||||
<a href="https://github.com/warmbly/warmbly/commits/main" class="text-[#0369a1] hover:text-[#075985]">Commit history on GitHub</a>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>Every entry maps to a commit SHA.</span>
|
||||
<span>Every entry maps to commits on main.</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
SUBSCRIBE · editorial split with feed URL + GitHub follow
|
||||
SUBSCRIBE · editorial split with GitHub follow
|
||||
============================================================ -->
|
||||
<section class="border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.4fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Follow along</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Get the changelog<br/>in your reader.
|
||||
Follow every<br/>release as it ships.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
The feed updates within minutes of an entry going live. No email signup, no tracking pixels, no batching. Just a plain RSS feed and the public GitHub repository.
|
||||
Every entry maps to commits on main. Watch or star the repository to hear about the next release, and open a discussion on what we build next. No email signup, no tracking pixels.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<!-- Feed card -->
|
||||
<a href="/changelog/feed.xml" class="group rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6 hover:ring-[#0284c7]/40 hover:shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)] transition-all">
|
||||
<!-- Commit history card -->
|
||||
<a href="https://github.com/warmbly/warmbly/commits/main" class="group rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6 hover:ring-[#0284c7]/40 hover:shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)] transition-all">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 rounded-[10px] bg-sky-50 text-[#0369a1] ring-1 ring-sky-100">
|
||||
<Icon name="bell" size={18} />
|
||||
<Icon name="list" size={18} />
|
||||
</div>
|
||||
<Icon name="arrowUpRight" size={14} class="text-muted-foreground group-hover:text-[#0369a1] transition-colors" />
|
||||
</div>
|
||||
<div class="text-[16px] font-semibold text-heading tracking-[-0.01em]">RSS feed</div>
|
||||
<div class="text-[16px] font-semibold text-heading tracking-[-0.01em]">Commit history</div>
|
||||
<p class="mt-1.5 text-[13px] text-foreground/65 leading-relaxed">
|
||||
Subscribe in any RSS reader. Entries arrive in publication order, with the full body inline.
|
||||
The full record on main, in the order it reached production. Every changelog entry traces back here.
|
||||
</p>
|
||||
<div class="mt-4 inline-flex items-center gap-1.5 text-[11.5px] font-mono text-[#0369a1] bg-sky-50 px-2 py-1 rounded">
|
||||
/changelog/feed.xml
|
||||
warmbly/warmbly · main
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -307,20 +266,20 @@ const fixedCount = counts.fixed;
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Roadmap card -->
|
||||
<a href="/roadmap/" class="group rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6 hover:ring-[#0284c7]/40 hover:shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)] transition-all">
|
||||
<!-- Discussions card -->
|
||||
<a href="https://github.com/warmbly/warmbly/discussions" class="group rounded-[14px] bg-white ring-1 ring-[color:var(--border)] p-6 hover:ring-[#0284c7]/40 hover:shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)] transition-all">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 rounded-[10px] bg-amber-50 text-amber-700 ring-1 ring-amber-100">
|
||||
<Icon name="layers" size={18} />
|
||||
<Icon name="users" size={18} />
|
||||
</div>
|
||||
<Icon name="arrowUpRight" size={14} class="text-muted-foreground group-hover:text-[#0369a1] transition-colors" />
|
||||
</div>
|
||||
<div class="text-[16px] font-semibold text-heading tracking-[-0.01em]">Public roadmap</div>
|
||||
<div class="text-[16px] font-semibold text-heading tracking-[-0.01em]">Discussions</div>
|
||||
<p class="mt-1.5 text-[13px] text-foreground/65 leading-relaxed">
|
||||
What is shipping next, what is in flight, and what we are still thinking about. Open for comment.
|
||||
Request a feature, report a bug, or open an RFC. We read every thread and respond in the open.
|
||||
</p>
|
||||
<div class="mt-4 inline-flex items-center gap-1.5 text-[11.5px] font-mono text-amber-700 bg-amber-50 px-2 py-1 rounded">
|
||||
/roadmap
|
||||
/discussions
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
|
||||
// Roadmap items are seeded from the previous version of this page,
|
||||
// the actual codebase (CLAUDE.md), and the live changelog.
|
||||
// Categories: Warmup · Inbox · API · Security · CRM · Workers · Pricing · Contacts · Deliverability · Platform
|
||||
|
||||
const shipped = [
|
||||
{ cat: 'API', tone: 'sky', title: 'Per-key API scopes and rotation', note: 'Per-key scopes, last-used timestamps, one-click rotation.', link: '/changelog/#api-keys-dashboard' },
|
||||
{ cat: 'Deliverability',tone: 'sky', title: 'Per-mailbox health bands', note: 'Healthy, watch, quarantined, blocked. Pool selection skips anything below healthy.', link: '/changelog/' },
|
||||
{ cat: 'Contacts', tone: 'sky', title: 'CSV, XLSX, and JSON contact export', note: 'Field picker and scope, with import wizard for column mapping and dedup.', link: '/changelog/' },
|
||||
{ cat: 'Contacts', tone: 'sky', title: 'Categories on contacts', note: 'Reuses the existing categories group, deprecates Tags.', link: '/changelog/' },
|
||||
{ cat: 'Warmup', tone: 'sky', title: 'Recovery pool for quarantined mailboxes', note: '7-day cooldown plus a 20-delivery probation before re-entry.', link: '/changelog/' },
|
||||
{ cat: 'Workers', tone: 'sky', title: 'Per-worker concentration warnings', note: 'Surfaces when a shared worker is carrying too much active cold volume.', link: '/changelog/' },
|
||||
{ cat: 'Workers', tone: 'sky', title: 'Worker rebalancing without downtime', note: 'Move accounts between workers as machines come and go.', link: '/changelog/' },
|
||||
];
|
||||
|
||||
const inProgress = [
|
||||
{ cat: 'Security', tone: 'amber', title: 'SOC 2 Type II audit', note: 'Observation window underway. Report targeting Q3 2026.' },
|
||||
{ cat: 'Security', tone: 'amber', title: 'Customer-managed KMS keys', note: 'Bring your own AWS KMS root for envelope encryption on Business plans.' },
|
||||
{ cat: 'Security', tone: 'amber', title: 'SSO and SAML on Business plan', note: 'OIDC and SAML 2.0, with SCIM provisioning to follow.' },
|
||||
{ cat: 'API', tone: 'amber', title: 'Tenant-scoped IP allowlist for the API', note: 'Per-key allowlist on top of scope-based authorisation.' },
|
||||
{ cat: 'Inbox', tone: 'amber', title: 'Multi-workspace switcher in the inbox', note: 'Read every workspace you belong to without re-logging in.' },
|
||||
{ cat: 'CRM', tone: 'amber', title: 'Attio and Folk integrations', note: 'Two-way contact sync with field mapping and change attribution.' },
|
||||
];
|
||||
|
||||
const researching = [
|
||||
{ cat: 'Inbox', tone: 'slate', title: 'AI reply triage suggestions', note: 'Opt-in. Suggest disposition only, never auto-reply on the user\'s behalf.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'Predictive placement scores from warmup', note: 'Use warmup probe history to forecast cold-campaign placement.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'Seed-list inbox placement testing on demand', note: 'Trigger a fresh placement test before kicking off a sequence.' },
|
||||
{ cat: 'Workers', tone: 'slate', title: 'Per-region dedicated workers in APAC', note: 'Mirror the existing US and EU dedicated-worker availability.' },
|
||||
{ cat: 'Deliverability', tone: 'slate', title: 'BIMI and VMC tooling', note: 'Setup checks, status surfacing, and provider guidance.' },
|
||||
];
|
||||
|
||||
// Quarter strip. Counts represent shipped feature items recorded in the
|
||||
// changelog and roadmap for each window. Q2 2026 includes May items.
|
||||
const quarters = [
|
||||
{ label: 'Q1 2026', range: 'Jan to Mar', shipped: 4, theme: 'Sequences, tracking edge, plain-text default' },
|
||||
{ label: 'Q2 2026', range: 'Apr to Jun', shipped: 5, theme: 'Health bands, recovery pool, API keys', current: true },
|
||||
{ label: 'Q3 2026', range: 'Jul to Sep', shipped: 0, theme: 'SOC 2, SSO, BYOK, integrations' },
|
||||
{ label: 'Q4 2026', range: 'Oct to Dec', shipped: 0, theme: 'Researching items move into build' },
|
||||
];
|
||||
|
||||
// Cadence stats, computed from the seed lists above.
|
||||
const specs = [
|
||||
{ k: 'Items shipped in the last 90 days', v: String(shipped.length) },
|
||||
{ k: 'Items currently in progress', v: String(inProgress.length) },
|
||||
{ k: 'Items under active research', v: String(researching.length) },
|
||||
{ k: 'Average weeks between releases', v: '~2 weeks' },
|
||||
{ k: 'Roadmap items per quarter (median)',v: '4 to 6' },
|
||||
{ k: 'Roadmap refresh cadence', v: 'Monthly' },
|
||||
];
|
||||
|
||||
// How we prioritise.
|
||||
const priorities = [
|
||||
{ n: '01', t: 'User-requested features', b: 'Feature requests from /contact, in-app feedback and our community channels are read weekly. Repeat requests rank highest.' },
|
||||
{ n: '02', t: 'Deliverability incidents', b: 'Anything that protects shared pool reputation jumps the line. Quarantine bands and the recovery pool both shipped this way.' },
|
||||
{ n: '03', t: 'Infrastructure debt', b: 'Worker rebalancing, concentration warnings, and tracking edge dedupe are debt items that paid for themselves within a quarter.' },
|
||||
{ n: '04', t: 'Compliance and contracts', b: 'SOC 2 and SSO are gates for the customers we want next. They get sequenced ahead of new surface area while audits are open.' },
|
||||
{ n: '05', t: 'Quality of life', b: 'Smaller items get bundled together. We do not let a quarter pass without at least one quality-of-life batch.' },
|
||||
];
|
||||
|
||||
const faq: Array<[string, string]> = [
|
||||
['Why is the roadmap public?', 'Sales-driven secrecy makes it harder for customers to plan. If we are working on it, you can see it. If it stalls, you can see that too.'],
|
||||
['How often does the roadmap change?', 'Items rotate monthly. The full board is reviewed every quarter. Anything that ships moves to the changelog with the same wording.'],
|
||||
['Can I request a feature?', 'Yes. Use the request link, or open an issue if it is a bug. We read every request weekly and respond to common ones in the next roadmap pass.'],
|
||||
['Will you commit to dates?', 'We commit to quarters, not weeks. Security and audit items have firmer windows because partners gate on them. Everything else slips when reality demands it.'],
|
||||
];
|
||||
|
||||
// Tone class lookup, kept simple to satisfy Astro’s JSX parser.
|
||||
const tone = (t: string) => {
|
||||
if (t === 'sky') return { dot: 'bg-emerald-500', chip: 'bg-emerald-50 text-emerald-700', rail: 'bg-emerald-500' };
|
||||
if (t === 'amber') return { dot: 'bg-amber-500', chip: 'bg-amber-50 text-amber-700', rail: 'bg-amber-500' };
|
||||
return { dot: 'bg-sky-500', chip: 'bg-sky-50 text-sky-700', rail: 'bg-sky-500' };
|
||||
};
|
||||
|
||||
const catChip = (c: string) =>
|
||||
'inline-flex items-center h-5 px-1.5 rounded text-[10.5px] font-medium font-mono uppercase tracking-[0.08em] bg-[color:var(--surface-1)] text-foreground/70 ring-1 ring-[color:var(--border)]';
|
||||
|
||||
// Pre-compute column metadata so the JSX stays simple.
|
||||
const columns = [
|
||||
{ title: 'Shipped recently', sub: 'Past 90 days', items: shipped, dot: 'bg-emerald-500', chipTone: 'bg-emerald-50 text-emerald-700' },
|
||||
{ title: 'In progress', sub: 'Q2 to Q3 2026', items: inProgress, dot: 'bg-amber-500', chipTone: 'bg-amber-50 text-amber-700' },
|
||||
{ title: 'Researching', sub: 'Targeting H2 2026', items: researching, dot: 'bg-sky-500', chipTone: 'bg-sky-50 text-sky-700' },
|
||||
];
|
||||
---
|
||||
<Layout
|
||||
title="Public Roadmap | Warmbly"
|
||||
description="What we just shipped, what we are building, and what we are researching. The Warmbly roadmap is public on purpose."
|
||||
>
|
||||
<!-- ============================================================
|
||||
HERO · HeroAtmosphere (matches warmup, campaigns, pricing)
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
|
||||
<div class="container-page relative pt-16 md:pt-24 pb-20 md:pb-28 text-center">
|
||||
<div class="inline-flex items-center gap-2 h-7 pl-1 pr-3 rounded-full bg-white/15 backdrop-blur ring-1 ring-white/25 text-[12px] text-white/95 shadow-[0_4px_14px_-4px_rgba(0,0,0,0.25)]">
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded-full text-[10.5px] font-semibold uppercase tracking-[0.06em] bg-white" style="color:#0369a1;">Roadmap</span>
|
||||
Public on purpose
|
||||
</div>
|
||||
|
||||
<h1 class="mt-8 md:mt-10 text-[44px] sm:text-6xl md:text-[72px] lg:text-[80px] font-semibold tracking-[-0.04em] leading-[0.98] text-white max-w-4xl mx-auto">
|
||||
What is shipping,<br/>and what is next.
|
||||
</h1>
|
||||
<p class="mt-6 text-[17px] md:text-[19px] text-white/80 max-w-2xl mx-auto leading-relaxed">
|
||||
No sales-driven secrecy. The whole board is here, including what is researched but not committed. If a feature matters to you, tell us, and it gets weighed against the rest.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="/contact/?topic=feature-request" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">Request a feature</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#075985" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/changelog/" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
See the changelog
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
LIVE BOARD · three editorial divide-y lists, no card chrome
|
||||
============================================================ -->
|
||||
<section class="pt-20 md:pt-24 pb-16 md:pb-24">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Live board</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Three columns. Updated monthly.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed">
|
||||
Everything we are working on, grouped by where it sits in the pipeline. Items move left as they ship, and the changelog gets the same wording.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-x-12 gap-y-12">
|
||||
{columns.map((c) => (
|
||||
<div>
|
||||
<div class="flex items-baseline justify-between pb-3 border-b border-[color:var(--border)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`w-2 h-2 rounded-full ${c.dot}`}></span>
|
||||
<div class="text-[14.5px] font-semibold text-heading">{c.title}</div>
|
||||
<span class={`inline-flex items-center h-5 px-1.5 rounded text-[10.5px] font-medium font-mono uppercase tracking-[0.08em] ${c.chipTone}`}>{c.items.length}</span>
|
||||
</div>
|
||||
<span class="text-[11px] font-mono uppercase tracking-[0.14em] text-muted-foreground">{c.sub}</span>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-[color:var(--border)]">
|
||||
{c.items.map((it: any) => (
|
||||
<li class="py-4">
|
||||
<div class="flex items-center justify-between gap-2 mb-1.5">
|
||||
<span class={catChip(it.cat)}>{it.cat}</span>
|
||||
{it.link ? (
|
||||
<a href={it.link} class="text-[11px] font-mono text-muted-foreground hover:text-[#0369a1] inline-flex items-center gap-1">
|
||||
log
|
||||
<Icon name="arrowUpRight" size={10} />
|
||||
</a>
|
||||
) : (
|
||||
<span class="text-[11px] font-mono text-muted-foreground/60">queued</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="text-[14px] font-medium text-heading leading-snug">{it.title}</div>
|
||||
{it.note ? (
|
||||
<p class="mt-1 text-[13px] text-muted-foreground leading-snug">{it.note}</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
QUARTERLY TIMELINE · horizontal strip with progression
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] py-20 md:py-24">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-10">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Quarter by quarter</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
One year, four windows.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed">
|
||||
We size work in quarters. Anything smaller than a quarter is a ship, not a roadmap entry. Anything larger gets broken down before it lands on the board.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4">
|
||||
{quarters.map((q) => (
|
||||
<div class={`relative rounded-[12px] bg-white ring-1 ${q.current ? 'ring-[#0284c7]/40 shadow-[0_18px_36px_-18px_rgba(2,132,199,0.3)]' : 'ring-[color:var(--border)]'} overflow-hidden`}>
|
||||
<div class={`h-1.5 ${q.current ? 'bg-[#0284c7]' : 'bg-slate-200'}`}></div>
|
||||
<div class="p-5">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class={`text-[15px] font-semibold tracking-[-0.01em] ${q.current ? 'text-[#0369a1]' : 'text-heading'}`}>{q.label}</div>
|
||||
{q.current ? (
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded text-[10px] font-semibold uppercase tracking-[0.12em] font-mono bg-[#0284c7] text-white">Now</span>
|
||||
) : (
|
||||
<span class="text-[10.5px] uppercase tracking-[0.16em] font-mono text-muted-foreground">{q.range}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-baseline gap-1.5">
|
||||
<span class={`text-[34px] md:text-[40px] font-semibold tracking-[-0.035em] leading-none font-mono ${q.current ? 'text-[#0369a1]' : 'text-heading'}`}>{q.shipped}</span>
|
||||
<span class="text-[12px] text-foreground/55">shipped</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-[12.5px] text-foreground/65 leading-snug">{q.theme}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-[11.5px] font-mono text-muted-foreground">
|
||||
Counts pull from /changelog. Future quarters fill as items ship, not when they are scheduled.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
CADENCE SPEC · two-column spec sheet, mono numbers
|
||||
============================================================ -->
|
||||
<section class="py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Cadence</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
The shape of how we ship.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
Steady, boring, and visible. Most weeks see something land. Most months see a roadmap update. Most quarters see one larger piece complete end to end.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-x-12 gap-y-px">
|
||||
{specs.map((d, i) => (
|
||||
<div class="group grid grid-cols-[1fr_auto] gap-6 py-6 border-b border-[color:var(--border)] items-baseline transition-colors hover:bg-[color:var(--surface-1)]/40 -mx-3 px-3 rounded-[6px]">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground">
|
||||
{String(i + 1).padStart(2, '0')} · {d.k}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-[22px] md:text-[26px] font-semibold tracking-[-0.02em] text-heading font-mono whitespace-nowrap">{d.v}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-[11.5px] font-mono text-muted-foreground">
|
||||
Source: this roadmap and <span class="text-foreground/75">/changelog</span>. Refreshed each month.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
HOW WE PICK · editorial sticky-left split
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_2fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">How we pick</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Five inputs decide the next quarter.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
We weigh user requests, deliverability incidents, infrastructure debt, compliance commitments, and quality of life. No single input wins by default, and we publish the result on this page.
|
||||
</p>
|
||||
<a href="/contact/?topic=feature-request" class="mt-6 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-[#0369a1] hover:text-[#075985]">
|
||||
Request a feature
|
||||
<Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ol class="divide-y divide-[color:var(--border)] border-y border-[color:var(--border)]">
|
||||
{priorities.map((s) => (
|
||||
<li class="grid grid-cols-[44px_1fr] gap-4 py-5 items-baseline">
|
||||
<span class="font-mono text-[12px] text-[#0284c7] font-semibold">{s.n}</span>
|
||||
<div>
|
||||
<div class="text-[15.5px] font-semibold text-heading">{s.t}</div>
|
||||
<p class="mt-2 text-[14px] text-foreground/70 leading-relaxed">{s.b}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FAQ · sticky title with smooth grid-rows accordion
|
||||
============================================================ -->
|
||||
<section class="py-20 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.8fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Roadmap FAQ</div>
|
||||
<h2 class="text-[28px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
Four questions about how this works.
|
||||
</h2>
|
||||
<p class="mt-4 text-[14.5px] text-foreground/70 leading-relaxed">
|
||||
Want a feature considered? <a class="text-[#0369a1] hover:text-[#075985] font-medium" href="/contact/?topic=feature-request">Tell us</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-[color:var(--border)]" data-faq>
|
||||
{faq.map(([q, a]) => (
|
||||
<div class="faq-row py-2">
|
||||
<button type="button" class="faq-trigger group w-full flex items-start justify-between gap-4 py-3 text-left" aria-expanded="false">
|
||||
<span class="text-[15.5px] font-medium text-heading group-hover:text-[#0369a1] transition-colors">{q}</span>
|
||||
<span class="faq-icon shrink-0 inline-flex items-center justify-center w-7 h-7 rounded-full bg-white ring-1 ring-[color:var(--border)] text-foreground/60 transition-transform duration-300 ease-out">
|
||||
<Icon name="plus" size={12} />
|
||||
</span>
|
||||
</button>
|
||||
<div class="faq-panel grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out">
|
||||
<div class="overflow-hidden">
|
||||
<p class="pb-4 pr-12 text-[14px] text-foreground/75 leading-relaxed max-w-2xl">{a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const faq = document.querySelector('[data-faq]');
|
||||
if (!faq) return;
|
||||
faq.querySelectorAll('.faq-trigger').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const row = btn.closest('.faq-row');
|
||||
const panel = row.querySelector('.faq-panel');
|
||||
const icon = btn.querySelector('.faq-icon');
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!open));
|
||||
panel.style.gridTemplateRows = open ? '0fr' : '1fr';
|
||||
if (icon) icon.style.transform = open ? 'rotate(0deg)' : 'rotate(45deg)';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTA
|
||||
title="Missing something?"
|
||||
description="Tell us what to build."
|
||||
primaryLabel="Request a feature"
|
||||
primaryHref="/contact/?topic=feature-request"
|
||||
secondaryLabel="See the changelog"
|
||||
secondaryHref="/changelog/"
|
||||
/>
|
||||
</Layout>
|
||||
@@ -18,13 +18,13 @@ import CTA from '../components/CTA.astro';
|
||||
- internal/repository/pg_warmup.go
|
||||
|
||||
Tone: factual. No fake compliance badges. No fake metrics.
|
||||
No customer quotes. Roadmap items marked as roadmap.
|
||||
No customer quotes. Planned items are marked as planned, not shipped.
|
||||
|
||||
Astro parser quirk: any `<` or `<=` comparison inside `{}` blocks
|
||||
breaks the parser. Hoist them up here.
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
// Posture badges. Honest about what is shipped vs roadmap.
|
||||
// Posture badges. Honest about what is shipped vs planned.
|
||||
// `state` drives the dot color in the JSX below.
|
||||
const posture = [
|
||||
{
|
||||
@@ -50,12 +50,12 @@ const posture = [
|
||||
{
|
||||
label: 'SOC 2 Type II',
|
||||
note: 'Controls in place. Audit window opens with our auditor.',
|
||||
state: 'roadmap',
|
||||
state: 'planned',
|
||||
},
|
||||
{
|
||||
label: 'ISO 27001',
|
||||
note: 'Under evaluation. No certification claimed today.',
|
||||
state: 'roadmap',
|
||||
state: 'planned',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -221,7 +221,7 @@ const disclosure = [
|
||||
<div class="px-5 md:px-7 py-4 border-b border-slate-200 flex items-center gap-3">
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium">Posture</div>
|
||||
<div class="mt-0.5 text-[13px] text-slate-900 font-medium">What we have shipped, and what is on the roadmap.</div>
|
||||
<div class="mt-0.5 text-[13px] text-slate-900 font-medium">What we have shipped, and what is still planned.</div>
|
||||
</div>
|
||||
<a href="/changelog/" class="ml-auto text-[12px] font-medium text-[color:var(--brand)] hover:text-[color:var(--brand-strong)] inline-flex items-center gap-1">
|
||||
Changelog <Icon name="arrowUpRight" size={11} />
|
||||
@@ -233,7 +233,7 @@ const disclosure = [
|
||||
const shipped = p.state === 'shipped';
|
||||
const dot = shipped ? 'bg-emerald-500' : 'bg-amber-500';
|
||||
const pillBg = shipped ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700';
|
||||
const stateLabel = shipped ? 'Shipped' : 'Roadmap';
|
||||
const stateLabel = shipped ? 'Shipped' : 'Planned';
|
||||
const borderRight = (i % 3) !== 2 ? 'lg:border-r' : '';
|
||||
const borderBottomLg = i < 3 ? 'lg:border-b' : '';
|
||||
return (
|
||||
@@ -253,7 +253,7 @@ const disclosure = [
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-center text-[12px] text-muted-foreground max-w-2xl mx-auto">
|
||||
We do not claim certifications we do not hold. SOC 2 and ISO 27001 are listed as roadmap items because they are honestly roadmap items.
|
||||
We do not claim certifications we do not hold. SOC 2 and ISO 27001 are listed as planned because they are honestly not in place yet.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user