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
123 changes: 123 additions & 0 deletions app/app/get-started/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import EditorialShell, { PageHero } from '@/components/editorial/shell'
import { COMPARE_CSS } from '../compare/compare-styles'
import { PRICING_TIERS } from '@/lib/pricing'
import { withCampaignParams } from '@/lib/campaign'

// /get-started — Odoo EPIC 24 (#6071) / [feature] #7670, "Fallback landing page".
//
// Insurance for the 16 Aug paid-ad launch: an outside agency (StratDev) is building
// the real campaign page. If it slips, ads still need somewhere to point. This page
// is deliberately plain — what Knowcap is, who it's for, what it costs, and a
// working signup link that survives the click. It does not replace the agency page;
// both can exist (switching ads later is a destination change, not a delete).
export const metadata: Metadata = {
title: 'Get Started — Knowcap',
description:
'Knowcap turns meetings into confirmed, agent-ready evidence. A named human confirms every claim before any AI agent acts. Free to start.',
robots: { index: false }, // not an SEO surface — an ad destination, kept out of search results like /beta
openGraph: {
title: 'Knowcap — The Trust Layer for AI Agents',
description: 'Capture meetings and messages, confirm every decision with a human, and let agents act only on what was confirmed. Free to start.',
url: 'https://knowcap.ai/get-started',
type: 'website',
images: [{ url: '/og/default.jpg', width: 1376, height: 768, alt: 'Knowcap — The Trust Layer for AI Agents' }],
},
twitter: {
card: 'summary_large_image',
images: ['/og/default.jpg'],
},
}

const GS_CSS = `
.gs-body{padding:24px 0 110px}
.gs-section{padding:clamp(36px,5vw,56px) 0}
.gs-section + .gs-section{border-top:1px solid var(--border)}
.gs-h2{font-family:var(--disp);font-size:clamp(1.35rem,2.6vw,1.75rem);font-weight:460;
letter-spacing:-.02em;font-variation-settings:'SOFT' 55,'WONK' 0;margin-bottom:14px}
.gs-prose{color:var(--sec);font-size:16px;line-height:1.75;max-width:64ch}
.gs-for{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:12px;max-width:64ch}
.gs-for li{display:flex;gap:10px;font-size:15px;line-height:1.55;color:var(--ink-soft)}
.gs-for .gs-dot{flex-shrink:0;color:var(--green);font-weight:700}
.gs-price-strip{display:flex;flex-wrap:wrap;gap:14px;margin-top:8px}
.gs-price-card{flex:1 1 200px;background:var(--white);border:1px solid var(--border);
border-radius:10px;padding:16px 18px}
.gs-price-name{font-family:var(--disp);font-weight:560;font-size:15px;color:var(--ink)}
.gs-price-amt{font-family:var(--disp);font-weight:500;font-size:1.4rem;color:var(--ink);margin-top:4px}
.gs-price-note{font-size:12px;color:var(--sec)}
.gs-price-link{margin-top:16px;font-size:13.5px}
.gs-price-link a{color:var(--green);text-decoration:underline;text-underline-offset:3px}
.gs-cta-row{margin-top:10px}
`

export default function GetStartedPage({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined }
}) {
const registerUrl = withCampaignParams('https://app.knowcap.ai/register', searchParams, 'get_started_fallback')

return (
<EditorialShell registerHref={registerUrl}>
<style dangerouslySetInnerHTML={{ __html: COMPARE_CSS + GS_CSS }} />
<PageHero
kicker="Start here"
title={<>The trust layer for AI agents — <span style={{ color: 'var(--green)' }}>free to start</span></>}
sub="Knowcap captures your meetings, voice notes, and messages, turns every decision, task, and risk into a claim card, and waits for a named human to confirm it before any agent acts."
/>
<div className="gs-body">
<div className="cl-wrap">
<div className="cm-cta-row gs-cta-row">
<a className="cl-btn cl-btn--solid" href={registerUrl}>Start free →</a>
<Link className="cl-btn cl-btn--ghost" href="/contact-us">Talk to us</Link>
</div>

<div className="gs-section">
<h2 className="gs-h2">What it does</h2>
<p className="gs-prose">
Listen → Extract → Confirm → Act. Knowcap joins your meetings or captures a voice note, chat, or
recording, pulls out every decision, task, and risk as a claim pinned to its exact timestamp and
speaker quote, has a named human confirm or reject it with one tap, and only then lets AI agents
act — opening tickets, drafting PRs, sending follow-ups — on what was actually confirmed.
</p>
</div>

<div className="gs-section">
<h2 className="gs-h2">Who it&rsquo;s for</h2>
<ul className="gs-for">
<li><span className="gs-dot" aria-hidden="true">●</span><span>Teams who need a real record of what was decided, not a paraphrased summary.</span></li>
<li><span className="gs-dot" aria-hidden="true">●</span><span><Link href="/for/odoo-partners">Odoo implementation partners</Link> turning a client call into a ticket before the meeting ends.</span></li>
<li><span className="gs-dot" aria-hidden="true">●</span><span>Agencies capturing scope and client approvals with receipts.</span></li>
<li><span className="gs-dot" aria-hidden="true">●</span><span>Anyone who wants AI to help without letting it act on something nobody confirmed.</span></li>
</ul>
</div>

<div className="gs-section">
<h2 className="gs-h2">What it costs</h2>
<div className="gs-price-strip">
{PRICING_TIERS.map((tier) => (
<div className="gs-price-card" key={tier.slug}>
<div className="gs-price-name">{tier.name}</div>
<div className="gs-price-amt">{tier.price}</div>
<div className="gs-price-note">{tier.priceNote}</div>
</div>
))}
</div>
<p className="gs-price-link">
Full plan details on the <Link href="/pricing">pricing page</Link>.
</p>
Comment on lines +107 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve campaign parameters on the pricing-page link.

When a paid-ad visitor selects this link, /pricing receives no campaign parameters. Its registration CTA then uses utm_source=pricing_page instead of the original campaign attribution.

Build the pricing href with withCampaignParamsRelative('/pricing', searchParams, 'get_started_fallback').

Proposed fix
-import { withCampaignParams } from '`@/lib/campaign`'
+import { withCampaignParams, withCampaignParamsRelative } from '`@/lib/campaign`'
@@
   const registerUrl = withCampaignParams('https://app.knowcap.ai/register', searchParams, 'get_started_fallback')
+  const pricingUrl = withCampaignParamsRelative('/pricing', searchParams, 'get_started_fallback')
@@
-              Full plan details on the <Link href="/pricing">pricing page</Link>.
+              Full plan details on the <Link href={pricingUrl}>pricing page</Link>.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="gs-price-link">
Full plan details on the <Link href="/pricing">pricing page</Link>.
</p>
<p className="gs-price-link">
Full plan details on the <Link href={pricingUrl}>pricing page</Link>.
</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/app/get-started/page.tsx` around lines 107 - 109, Update the pricing Link
in the get-started page to build its href with
withCampaignParamsRelative('/pricing', searchParams, 'get_started_fallback'),
preserving campaign parameters for paid-ad visitors while retaining the existing
link text and destination.

</div>
</div>
</div>

<div className="cm-close">
<div className="cl-wrap">
<h2>Try it on your next meeting.</h2>
<p>Free to start. Upgrade only when you need more than 5 lifetime meetings.</p>
<a className="cl-btn cl-btn--solid" href={registerUrl}>Get Started Free →</a>
</div>
</div>
</EditorialShell>
)
}
108 changes: 108 additions & 0 deletions app/app/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import EditorialShell, { PageHero } from '@/components/editorial/shell'
import { PRICING_TIERS } from '@/lib/pricing'
import { withCampaignParams, withCampaignParamsRelative } from '@/lib/campaign'

// /pricing — Odoo EPIC 24 (#6071) / [feature] #5824.
// The marketing site had no pricing surface at all; this is the first one.
export const metadata: Metadata = {
title: 'Pricing — Knowcap',
description:
'Knowcap pricing: Free for 5 lifetime meetings, Pro at $20/seat, Business at $40/seat with confirmation governance, and Enterprise on a custom plan.',
alternates: { canonical: 'https://knowcap.ai/pricing' },
openGraph: {
title: 'Knowcap Pricing',
description: 'Free to try, $20/seat for individuals, $40/seat for teams that need governance, or a custom Enterprise plan.',
url: 'https://knowcap.ai/pricing',
type: 'website',
images: [{ url: '/og/default.jpg', width: 1376, height: 768, alt: 'Knowcap — The Trust Layer for AI Agents' }],
},
twitter: {
card: 'summary_large_image',
images: ['/og/default.jpg'],
},
}

const PRICING_CSS = `
.pr-body{padding:24px 0 110px}
.pr-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;margin-top:8px;align-items:stretch}
@media(max-width:980px){.pr-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.pr-grid{grid-template-columns:1fr}}
.pr-card{display:flex;flex-direction:column;background:var(--white);border:1px solid var(--border);
border-radius:12px;padding:26px 22px;position:relative}
.pr-card--hl{border-color:var(--green);box-shadow:0 10px 30px rgba(31,107,58,.12)}
.pr-badge{position:absolute;top:-11px;left:22px;background:var(--green);color:#fff;
font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:.08em;
text-transform:uppercase;padding:4px 10px;border-radius:100px}
.pr-name{font-family:var(--disp);font-weight:560;font-size:19px;color:var(--ink)}
.pr-audience{font-size:12.5px;color:var(--sec);margin-top:2px;min-height:32px}
.pr-price-row{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px;margin:18px 0 2px}
.pr-price{font-family:var(--disp);font-weight:500;font-size:clamp(1.7rem,3vw,2.1rem);color:var(--ink);letter-spacing:-.02em}
.pr-price-note{flex-basis:100%;font-size:12.5px;color:var(--sec)}
.pr-features{list-style:none;margin:20px 0 22px;padding:0;display:flex;flex-direction:column;gap:10px;flex:1}
.pr-features li{display:flex;gap:9px;font-size:13.5px;line-height:1.5;color:var(--ink-soft)}
.pr-check{flex-shrink:0;color:var(--green);font-weight:700;margin-top:1px}
.pr-note{margin-top:36px;text-align:center;font-size:13.5px;color:var(--sec)}
.pr-note a{color:var(--green);text-decoration:underline;text-underline-offset:3px}
`

export default function PricingPage({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined }
}) {
const registerUrl = withCampaignParams('https://app.knowcap.ai/register', searchParams, 'pricing_page')
return (
<EditorialShell registerHref={registerUrl}>
<style dangerouslySetInnerHTML={{ __html: PRICING_CSS }} />
<PageHero
kicker="Pricing"
title="Simple pricing that grows with your team"
sub="Start free. Upgrade when a lifetime of 5 meetings isn’t enough, or when your team needs confirmation governance. No annual lock-in offer today — pay monthly, cancel anytime."
/>
<div className="pr-body">
<div className="cl-wrap">
<div className="pr-grid">
{PRICING_TIERS.map((tier) => {
const href = tier.cta === 'book' ? withCampaignParamsRelative('/book', searchParams, 'pricing_page') : registerUrl
const isExternal = tier.cta === 'register'
return (
<div key={tier.slug} className={`pr-card${tier.highlight ? ' pr-card--hl' : ''}`}>
{tier.highlight && <span className="pr-badge">Most popular</span>}
<div className="pr-name">{tier.name}</div>
<div className="pr-audience">{tier.audience}</div>
<div className="pr-price-row">
<span className="pr-price">{tier.price}</span>
<span className="pr-price-note">{tier.priceNote}</span>
</div>
<ul className="pr-features">
{tier.features.map((f) => (
<li key={f}>
<span className="pr-check" aria-hidden="true">✓</span>
<span>{f}</span>
</li>
))}
</ul>
{isExternal ? (
<a className={`cl-btn ${tier.highlight ? 'cl-btn--solid' : 'cl-btn--ghost'}`} href={href} style={{ textAlign: 'center' }}>
{tier.ctaLabel}
</a>
) : (
<Link className="cl-btn cl-btn--ghost" href={href} style={{ textAlign: 'center' }}>
{tier.ctaLabel}
</Link>
)}
</div>
)
})}
</div>
<p className="pr-note">
Every tier includes a named human confirming each claim before any agent acts. Have questions about which plan fits?{' '}
<Link href="/contact-us">Talk to us</Link>.
</p>
</div>
</div>
</EditorialShell>
)
}
4 changes: 4 additions & 0 deletions app/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${base}/`, lastModified: now, changeFrequency: 'weekly', priority: 1.0 },

// Conversion sub-pages (paid traffic destinations)
{ url: `${base}/pricing`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
{ url: `${base}/for/odoo-partners`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
{ url: `${base}/for/saudi-arabia`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
{ url: `${base}/developers`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 },

// /get-started intentionally excluded — noindex ad-landing fallback (EPIC 24 #7670),
// same treatment as /beta: an ad destination, not an SEO surface.

// Blog (index + posts)
...blogEntries,

Expand Down
25 changes: 18 additions & 7 deletions app/components/editorial/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const CSS = `
color:rgba(251,250,248,.55)}
`

export function EditorialHeader() {
export function EditorialHeader({ registerHref }: { registerHref?: string } = {}) {
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24)
Expand All @@ -148,20 +148,21 @@ export function EditorialHeader() {
<nav className="cl-navlinks" aria-label="Primary">
<a className="cl-navlink" href="/#loop">How it works</a>
<a className="cl-navlink" href="/#mcp">For your agents</a>
<Link className="cl-navlink" href="/pricing">Pricing</Link>
<a className="cl-navlink" href="/#faq">FAQ</a>
<Link className="cl-navlink" href="/compare">Compare</Link>
<Link className="cl-navlink" href="/contact-us">Contact</Link>
</nav>
<div className="cl-navauth">
<a className="cl-login" href={`${APP_URL}/login`}>Log in</a>
<a className="cl-btn cl-btn--solid cl-btn--sm" href={`${APP_URL}/register`}>Get Started Free</a>
<a className="cl-btn cl-btn--solid cl-btn--sm" href={registerHref || `${APP_URL}/register`}>Get Started Free</a>
</div>
</div>
</header>
)
}

export function EditorialFooter() {
export function EditorialFooter({ registerHref }: { registerHref?: string } = {}) {
return (
<footer className="cl-footer">
<div className="cl-wrap">
Expand All @@ -180,6 +181,7 @@ export function EditorialFooter() {
<div className="cl-fcol-h">Product</div>
<a href="/#loop">How it works</a>
<a href="/#mcp">For your agents</a>
<Link href="/pricing">Pricing</Link>
<a href="/#faq">FAQ</a>
<Link href="/for/odoo-partners">For Odoo partners</Link>
<Link href="/developers">Developers</Link>
Expand All @@ -202,7 +204,7 @@ export function EditorialFooter() {
</div>
<div className="cl-fcol">
<div className="cl-fcol-h">Get started</div>
<a href={`${APP_URL}/register`}>Get Started Free</a>
<a href={registerHref || `${APP_URL}/register`}>Get Started Free</a>
<a href={`${APP_URL}/login`}>Log in</a>
<Link href="/policy">Privacy</Link>
<Link href="/terms">Terms</Link>
Expand Down Expand Up @@ -240,13 +242,22 @@ export function PageHero({
)
}

export default function EditorialShell({ children }: { children: React.ReactNode }) {
export default function EditorialShell({
children,
registerHref,
}: {
children: React.ReactNode
/** Override the shared header/footer "Get Started Free" link — used by
* campaign-landing pages (e.g. /get-started) so a paid-ad tag survives even
* if the visitor taps the generic nav CTA instead of the page's own button. */
registerHref?: string
}) {
return (
<div className="cl-root">
<style dangerouslySetInnerHTML={{ __html: CSS }} />
<EditorialHeader />
<EditorialHeader registerHref={registerHref} />
<main className="cl-page-main">{children}</main>
<EditorialFooter />
<EditorialFooter registerHref={registerHref} />
</div>
)
}
42 changes: 42 additions & 0 deletions app/lib/campaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Carries paid-ad attribution (utm_*, gclid, fbclid) from a marketing-site query
* string into an app.knowcap.ai link, so a campaign tag survives the click into
* signup (EPIC 24 / #7670's own success criterion).
*
* The app itself does not yet persist these params into the account record —
* that would be a knowcap-app (server) change, and #7670 explicitly excludes
* "any change to what signup does" from this slice. This utility only keeps the
* tag alive in the URL through the hop from this site to app.knowcap.ai; a
* malformed or truncated value is carried as-is (URLSearchParams never throws
* on an odd string), so a broken tag degrades attribution without breaking the
* page or the link.
*/

type SearchParams = { [key: string]: string | string[] | undefined } | undefined

const CARRIED_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gclid', 'fbclid']

export function withCampaignParams(baseUrl: string, searchParams: SearchParams, fallbackUtmSource: string): string {
const url = new URL(baseUrl)
let carried = false
if (searchParams) {
for (const key of CARRIED_KEYS) {
const raw = searchParams[key]
const value = Array.isArray(raw) ? raw[0] : raw
if (value) {
url.searchParams.set(key, value)
carried = true
}
}
}
if (!carried) url.searchParams.set('utm_source', fallbackUtmSource)
return url.toString()
}

/** Same param-carrying logic, returned as an internal relative href (e.g. for
* <Link href="/book">) instead of a full URL — avoids round-tripping through a
* fake absolute URL just to strip the origin back off. */
export function withCampaignParamsRelative(path: string, searchParams: SearchParams, fallbackUtmSource: string): string {
const full = withCampaignParams(`https://knowcap.ai${path}`, searchParams, fallbackUtmSource)
return full.replace('https://knowcap.ai', '')
}
Loading