Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion components/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { track, EVENTS } from 'utils/analytics'
import repositoryStatsView from 'utils/repositoryStatsView'
import repositoryStatsSelection from 'utils/repositoryStatsSelection'
import useLiveRepositoryStats from 'utils/useLiveRepositoryStats'
import NewsletterCapture from './NewsletterCapture'
import styles from './Footer.module.css'

const { sourceLabel } = repositoryStatsView
Expand Down Expand Up @@ -126,12 +127,28 @@ const DEVELOPER_COLUMN = [
})

const CONNECT_COLUMN = [
BLOG_LINK,
byLabel('Discord'),
{ label: 'GitHub', href: OPENADAPT_REPOSITORY_URL },
{ label: 'Contribute workflows', href: '/contribute' },
]

// Blog posts live on the external blog; "All posts" is the canonical
// BLOG_LINK destination shared with the top nav.
const BLOG_COLUMN = [
{
label: 'We ran it on a real EMR',
href: 'https://blog.openadapt.ai/posts/openemr-benchmark/',
},
{
label: 'The silent wrong write',
href: 'https://blog.openadapt.ai/posts/silent-wrong-action/',
},
{
label: 'Compile once, govern every repair',
href: 'https://blog.openadapt.ai/posts/2026-07-20-compile-once-govern-every-repair/',
},
]

export default function Footer({
repositoryStats = OPENADAPT_STATS_SNAPSHOT,
}) {
Expand All @@ -144,6 +161,7 @@ export default function Footer({
return (
<div className={styles.footerContainer}>
<footer className={styles.footer}>
<NewsletterCapture />
<div className={styles.top}>
<div className={styles.brand}>
<a href="/" className={styles.wordmark}>
Expand Down Expand Up @@ -309,6 +327,24 @@ export default function Footer({
</ul>
</div>

<div className={styles.column}>
<h2 className={styles.columnTitle}>Blog</h2>
<ul className={styles.columnList}>
{BLOG_COLUMN.map((item) => (
<li key={item.label}>
<FooterLink href={item.href}>
{item.label}
</FooterLink>
</li>
))}
<li>
<FooterLink href={BLOG_LINK.href}>
All posts
</FooterLink>
</li>
</ul>
</div>

<div className={styles.column}>
<h2 className={styles.columnTitle}>Connect</h2>
<ul className={styles.columnList}>
Expand Down
23 changes: 14 additions & 9 deletions components/MastHead.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import Link from 'next/link'

import { siteCopy } from 'lib/siteCopy'
import { track, EVENTS } from 'utils/analytics'
import useLiveRepositoryStats from 'utils/useLiveRepositoryStats'

Expand Down Expand Up @@ -51,7 +52,7 @@ export default function Home({ githubStats: initialGithubStats }) {
)}
</div>
<h1 className="font-display text-2xl md:text-3xl mt-0 mb-4 font-semibold tracking-tight text-ink">
Automate the work your systems still make people do.
{siteCopy.buyerLine}
</h1>
<p className="mt-0 mb-4 mx-auto max-w-2xl font-sans font-normal text-base md:text-lg text-ink-2">
Show OpenAdapt a repeated task. It compiles the
Expand All @@ -67,29 +68,33 @@ export default function Home({ githubStats: initialGithubStats }) {
<div className="flex flex-wrap items-center justify-center gap-3 mt-0 mb-4">
<Link
className={styles.heroCloud}
href="/qualify"
data-testid="workflow-fit-cta"
href="#demo"
data-testid="demo-cta"
onClick={() =>
track(EVENTS.HERO_CTA_CLICK, {
cta: 'qualify_one_workflow',
cta: 'watch_demo',
})
}
>
See if your workflow fits
Watch it prove a result (65s)
</Link>
<Link
className="btn-ghost-ink"
href="#demo"
data-testid="demo-cta"
href="/qualify"
data-testid="workflow-fit-cta"
onClick={() =>
track(EVENTS.HERO_CTA_CLICK, {
cta: 'watch_demo',
cta: 'qualify_one_workflow',
})
}
>
Watch the 65-second demo
See if your workflow fits
</Link>
</div>
<p className="mt-0 mb-6 text-sm leading-relaxed text-ink-3">
Qualification means we test your workflow
against real failures before it runs.
</p>
<p className="mb-8 text-sm leading-relaxed text-ink-3">
<code className="rounded border border-hairline bg-panel px-2 py-1 text-ink">
pip install openadapt
Expand Down
2 changes: 1 addition & 1 deletion components/NavHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export default function NavHeader() {
})
}
>
Qualify one workflow
Start your first workflow
</Link>
</div>
<button
Expand Down
102 changes: 102 additions & 0 deletions components/NewsletterCapture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useState } from 'react'

import { trackEmailCapture } from 'utils/conversion'
import styles from './NewsletterCapture.module.css'

export default function NewsletterCapture({
location = 'newsletter_footer',
}) {
const [email, setEmail] = useState('')
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [failed, setFailed] = useState(false)

const handleSubmit = async (event) => {
event.preventDefault()
setSubmitting(true)
setFailed(false)

const body = new URLSearchParams(
new FormData(event.target)
).toString()

// Netlify Forms AJAX submission; mirrors the other lead forms.
try {
const response = await fetch('/form.html', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
if (!response.ok) throw new Error('newsletter submission failed')
setSubmitted(true)
trackEmailCapture({ location })
} catch (error) {
setFailed(true)
} finally {
setSubmitting(false)
}
}

return (
<div className={styles.wrap} data-testid={location}>
{submitted ? (
<p className={styles.success} role="status">
Thanks — you are on the list. Product updates only, no
spam.
</p>
) : (
<>
<p className={styles.leader}>
Get product updates by email.
</p>
<form
name="newsletter"
method="post"
data-netlify="true"
netlify-honeypot="bot-field"
onSubmit={handleSubmit}
className={styles.form}
>
<input
type="hidden"
name="form-name"
value="newsletter"
/>
<p className="hidden">
<label>
Do not fill this out if you are human:{' '}
<input
name="bot-field"
tabIndex={-1}
autoComplete="off"
/>
</label>
</p>
<input
className={styles.input}
type="email"
name="email"
placeholder="you@company.com"
aria-label="Email address"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
<button
type="submit"
className={styles.submit}
disabled={submitting}
>
{submitting ? 'Sending…' : 'Subscribe'}
</button>
</form>
{failed && (
<p className={styles.error} role="alert">
That did not go through. Please try again.
</p>
)}
</>
)}
</div>
)
}
75 changes: 75 additions & 0 deletions components/NewsletterCapture.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.wrap {
width: 100%;
min-width: 0;
max-width: 26rem;
margin: 0 auto 2.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.leader {
margin: 0;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-3);
}

.form {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.input {
flex: 1 1 13rem;
min-width: 0;
padding: 9px 16px;
border-radius: 9999px;
border: 1px solid var(--hairline);
background: var(--panel);
color: var(--ink);
font-size: 14px;
}

.input:focus {
outline: none;
border-color: var(--accent);
}

.submit {
padding: 9px 20px;
border-radius: 9999px;
border: 1.5px solid var(--accent);
background: var(--accent);
color: var(--ground);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color var(--dur-2, 180ms) var(--ease-out, ease);
}

.submit:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
}

.submit:disabled {
opacity: 0.6;
cursor: default;
}

.success {
margin: 0;
font-size: 14px;
color: var(--accent);
}

.error {
margin: 0;
font-size: 13px;
color: #b42318;
}

2 changes: 1 addition & 1 deletion cypress/e2e/basic.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('public product truth', () => {
cy.get('header')
.first()
.within(() => {
cy.contains('a', 'Qualify one workflow').should(
cy.contains('a', 'Start your first workflow').should(
'have.attr',
'href',
'/qualify'
Expand Down
7 changes: 7 additions & 0 deletions lib/siteCopy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Canonical marketing copy. Surfaces import from here so a wording change
// can never leave two pages telling different stories about the product.
export const siteCopy = {
buyerLine: 'Automate the work your systems still make people do.',
engineLine:
'OpenAdapt compiles demonstrated GUI workflows into deterministic, locally executable programs. Healthy runs make no model calls. When interfaces drift, OpenAdapt re-resolves from retained evidence or proposes a governed repair and halts when verification fails.',
}
13 changes: 13 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,16 @@
for = "/reference/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"

# Short marketing paths for the hosted demo and the documentation site.
[[redirects]]
from = "/demo"
to = "https://app.openadapt.ai/demo"
status = 301
force = true

[[redirects]]
from = "/docs"
to = "https://docs.openadapt.ai"
status = 301
force = true
12 changes: 7 additions & 5 deletions pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import FinalQualificationCta from '@components/FinalQualificationCta'
import Footer from '@components/Footer'
import HowItWorksCondensed from '@components/HowItWorksCondensed'
import MastHead from '@components/MastHead'
import NewsletterCapture from '@components/NewsletterCapture'
import ProductStatus from '@components/ProductStatus'
import Qualification from '@components/Qualification'
import ReplayHero from '@components/ReplayHero'
Expand Down Expand Up @@ -157,11 +158,6 @@ export default function Home({ githubStats, hostedOffer }) {
/>
</Head>
<MastHead githubStats={currentGithubStats} />
<div id="customer-result">
<Reveal>
<CustomerCaseStudy />
</Reveal>
</div>
<section
id="demo"
className="border-b border-hairline bg-panel px-5 py-20 md:py-28"
Expand All @@ -182,13 +178,19 @@ export default function Home({ githubStats, hostedOffer }) {
<BusinessDecisionPreview />
</div>
</section>
<div id="customer-result">
<Reveal>
<CustomerCaseStudy />
</Reveal>
</div>
<Reveal><HowItWorksCondensed /></Reveal>
<Reveal><Qualification /></Reveal>
<Reveal><ProductStatus /></Reveal>
<Reveal><CommercialOffer hostedOffer={hostedOffer} /></Reveal>
<Reveal><DashboardShowcase /></Reveal>
<Reveal><TrustSummary /></Reveal>
<Reveal><FinalQualificationCta /></Reveal>
<NewsletterCapture location="newsletter_home" />
<Footer repositoryStats={currentGithubStats} />
</div>
)
Expand Down
Loading