-
Notifications
You must be signed in to change notification settings - Fork 0
[pm-manager/pm-coder][pricing-fallback-landing] add /pricing and /get-started fallback landing page #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[pm-manager/pm-coder][pricing-fallback-landing] add /pricing and /get-started fallback landing page #144
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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’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> | ||
| </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> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', '') | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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,
/pricingreceives no campaign parameters. Its registration CTA then usesutm_source=pricing_pageinstead of the original campaign attribution.Build the pricing href with
withCampaignParamsRelative('/pricing', searchParams, 'get_started_fallback').Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents