diff --git a/apps/website/src/components/landing/Hero.spec.tsx b/apps/website/src/components/landing/Hero.spec.tsx
index edd0fd3e7..4b3f2c316 100644
--- a/apps/website/src/components/landing/Hero.spec.tsx
+++ b/apps/website/src/components/landing/Hero.spec.tsx
@@ -7,6 +7,7 @@ import {
HERO_H1,
HERO_SECONDARY_HREF,
HERO_SUBHEAD,
+ HERO_SUBHEAD_SEGMENTS,
} from '../../lib/positioning';
const trackMock = vi.hoisted(() => vi.fn());
@@ -57,10 +58,13 @@ describe('Hero', () => {
expect(screen.getByRole('heading', { level: 1 }).textContent).toBe(HERO_H1);
expect(screen.getByText(HERO_EYEBROW)).toBeTruthy();
expect(document.querySelector('.hero-subhead')?.textContent).toBe(HERO_SUBHEAD);
- expect(document.querySelector('.hero-subhead .marker-highlight')?.textContent).toBe(
- 'Your backend stays where it is.',
- );
- expect(document.querySelectorAll('.hero-subhead .marker-highlight')).toHaveLength(1);
+ expect(document.querySelectorAll('.hero-subhead a.marker-highlight')).toHaveLength(5);
+ for (const segment of HERO_SUBHEAD_SEGMENTS.filter((s) => s.href)) {
+ const link = screen.getByRole('link', { name: segment.text, exact: true });
+ expect(link.getAttribute('href')).toBe(segment.href);
+ expect(link.getAttribute('target')).toBe('_blank');
+ expect(link.getAttribute('rel')).toBe('noopener noreferrer');
+ }
expect(document.querySelector('.hero-chip-row')).toBeNull();
expect(screen.queryByText(/six months/)).toBeNull();
expect(screen.queryByRole('link', { name: /Talk to our engineers/ })).toBeNull();
diff --git a/apps/website/src/components/landing/Hero.tsx b/apps/website/src/components/landing/Hero.tsx
index 7e42c471a..abb9aba07 100644
--- a/apps/website/src/components/landing/Hero.tsx
+++ b/apps/website/src/components/landing/Hero.tsx
@@ -39,11 +39,11 @@ export function Hero() {
))}
- {HERO_SUBHEAD_SEGMENTS.map((segment) =>
- segment.highlight ? (
- {segment.text}
+ {HERO_SUBHEAD_SEGMENTS.map((segment, index) =>
+ segment.href ? (
+ {segment.text}
) : (
- {segment.text}
+ {segment.text}
),
)}
diff --git a/apps/website/src/lib/positioning.spec.ts b/apps/website/src/lib/positioning.spec.ts
index b2b9fa395..4aef28bc1 100644
--- a/apps/website/src/lib/positioning.spec.ts
+++ b/apps/website/src/lib/positioning.spec.ts
@@ -50,7 +50,7 @@ describe('positioning: hero copy', () => {
expect(HERO_EYEBROW).toBe('Angular ยท LangGraph & AG-UI');
expect(HERO_H1).toBe('The open-source thread-plane for agents.');
expect(HERO_SUBHEAD).toBe(
- 'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.',
+ 'Make agent work persistent, durable, visible, reviewable, and resumable.',
);
expect(HOME_TITLE).toBe('Threadplane โ The open-source thread-plane for agents');
expect(HOME_DESCRIPTION).toBe(
@@ -64,15 +64,15 @@ describe('positioning: hero copy', () => {
expect(HERO_H1_LINES.join(' ')).toBe(HERO_H1);
});
- it('subhead segments join back to HERO_SUBHEAD, with exactly one highlight', () => {
- // The segments exist only so Hero.tsx can marker-highlight one phrase.
- // If they ever stop reassembling the source-of-truth string, the rendered
- // subhead silently diverges from the copy every other surface quotes.
+ it('subhead segments preserve the copy and link each capability to its docs', () => {
expect(HERO_SUBHEAD_SEGMENTS.map((s) => s.text).join('')).toBe(HERO_SUBHEAD);
- expect(HERO_SUBHEAD_SEGMENTS.filter((s) => s.highlight)).toHaveLength(1);
- expect(HERO_SUBHEAD_SEGMENTS.find((s) => s.highlight)?.text).toBe(
- 'Your backend stays where it is.',
- );
+ expect(HERO_SUBHEAD_SEGMENTS.filter((s) => s.href)).toEqual([
+ { text: 'persistent', href: '/docs/langgraph/guides/persistence' },
+ { text: 'durable', href: '/docs/langgraph/guides/persistence#choosing-a-checkpointer' },
+ { text: 'visible', href: '/docs/chat/components/chat-tool-calls' },
+ { text: 'reviewable', href: '/docs/langgraph/guides/interrupts' },
+ { text: 'resumable', href: '/docs/langgraph/guides/persistence#checkpoint-recovery' },
+ ]);
});
it('pins the hero action labels and the secondary destination', () => {
diff --git a/apps/website/src/lib/positioning.ts b/apps/website/src/lib/positioning.ts
index 0328c061e..467038058 100644
--- a/apps/website/src/lib/positioning.ts
+++ b/apps/website/src/lib/positioning.ts
@@ -12,22 +12,30 @@ export const HERO_H1 = 'The open-source thread-plane for agents.';
*/
export const HERO_H1_LINES: readonly string[] = ['The open-source', 'thread-plane', 'for agents.'];
export const HERO_SUBHEAD =
- 'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.';
+ 'Make agent work persistent, durable, visible, reviewable, and resumable.';
/**
- * The subhead, split so Hero.tsx can marker-highlight the boundary claim.
+ * The subhead, split so each workflow capability links to its documentation.
* HERO_SUBHEAD stays the single source of truth: positioning.spec.ts asserts
* these segments join back to it character for character, so the two cannot
- * drift. Exactly one segment is highlighted โ a second one in a sentence this
- * short reads as decoration and cancels the emphasis.
+ * drift. Linked terms retain the hero's ink underline treatment.
*/
export interface HeroSubheadSegment {
text: string;
- highlight?: boolean;
+ href?: string;
}
export const HERO_SUBHEAD_SEGMENTS: readonly HeroSubheadSegment[] = [
- { text: 'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. ' },
- { text: 'Your backend stays where it is.', highlight: true },
+ { text: 'Make agent work ' },
+ { text: 'persistent', href: '/docs/langgraph/guides/persistence' },
+ { text: ', ' },
+ { text: 'durable', href: '/docs/langgraph/guides/persistence#choosing-a-checkpointer' },
+ { text: ', ' },
+ { text: 'visible', href: '/docs/chat/components/chat-tool-calls' },
+ { text: ', ' },
+ { text: 'reviewable', href: '/docs/langgraph/guides/interrupts' },
+ { text: ', and ' },
+ { text: 'resumable', href: '/docs/langgraph/guides/persistence#checkpoint-recovery' },
+ { text: '.' },
];
export const HERO_PRIMARY_LABEL = 'Install Threadplane';
diff --git a/apps/website/src/lib/site-metadata.spec.ts b/apps/website/src/lib/site-metadata.spec.ts
index 33b897638..e6da55eea 100644
--- a/apps/website/src/lib/site-metadata.spec.ts
+++ b/apps/website/src/lib/site-metadata.spec.ts
@@ -22,7 +22,7 @@ describe('site positioning copy', () => {
expect(LONG_SUBHEAD).toContain('open-source thread-plane for agents');
expect(LONG_SUBHEAD).toContain('LangGraph and AG-UI');
expect(HERO_SUBHEAD).toBe(
- 'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.',
+ 'Make agent work persistent, durable, visible, reviewable, and resumable.',
);
expect(POSITIONING_PROOF_POINTS.map((p) => p.label)).toEqual([
'LangGraph + AG-UI',
diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css
index 67d321c00..df54913be 100644
--- a/apps/website/src/styles/landing.css
+++ b/apps/website/src/styles/landing.css
@@ -84,16 +84,21 @@
}
.hero-stack .hero-subhead {
max-width: 40em;
+ word-spacing: 0.16em;
}
-/* The centered hero wraps the highlighted boundary claim mid-phrase โ measured
- * at 768px it orphaned a lone "Your" in its own pill at the end of line 1, and
- * at 1440px it split into two staggered boxes. inline-block makes the phrase
- * wrap as one unit, so it drops whole onto its own line instead. max-width is
- * the safety valve: below ~360px the phrase is wider than the measure, and
- * without it a shrink-to-fit box would overflow the page rather than wrap. */
+/* Keep each documentation link and its ink underline together when wrapping. */
.hero-stack .hero-subhead .marker-highlight {
display: inline-block;
max-width: 100%;
+ text-decoration: none;
+ word-spacing: normal;
+}
+.hero-stack .hero-subhead a.marker-highlight:hover {
+ box-shadow: inset 0 -5px 0 var(--color-ink);
+}
+.hero-stack .hero-subhead a.marker-highlight:focus-visible {
+ outline: 2px solid var(--color-ink);
+ outline-offset: 5px;
}
.hero-stack .hero-cta-row {
justify-content: center;