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;