{title}
+-
+ {lines.map((line) => (
+
- + {line.day} + + {line.who} › + + + {line.text} + + + ))} +
+ {summary} +
+From d38c6e426f54742f1c692184c1cfbe9526312efc Mon Sep 17 00:00:00 2001
From: "mads.thines"
- Your AI agent forgets everything when a session ends. LoreKit gives it persistent memory —
- so it never asks the same question twice.
+
+ An account connects the same memory store to every machine, teammate, and CI
+ run. The CLI itself works offline without one.
- Sign in free · 2-minute setup
-
- Free · Open source
+
+ Free — 5,000 memories, no card
Sign in and get your token GitHub, email + password, or a magic link — takes 30 seconds Run one command Your agent now remembers Memories persist across sessions, repos, and tools — automatically
- {description}
-
- Persistent memory
-
- for your AI agents
+
+ Sign in to LoreKit
-
- Get started in 2 minutes
-
-
-
- {/* Explore docs & guides — links to the public /docs index */}
-
- {title}
-
-
+
+
{command}
);
diff --git a/packages/web/src/components/landing/GetStartedCard.tsx b/packages/web/src/components/landing/GetStartedCard.tsx
new file mode 100644
index 000000000..523446670
--- /dev/null
+++ b/packages/web/src/components/landing/GetStartedCard.tsx
@@ -0,0 +1,77 @@
+import { GetStartedButton } from '@/components/learn/GetStartedButton';
+
+import { CopyCommand } from './CopyCommand';
+
+const STEP_BADGE_CLASS =
+ 'flex size-6 shrink-0 items-center justify-center rounded-full border border-[var(--color-accent)]/40 bg-[var(--color-accent-subtle)] font-mono text-xs font-semibold text-[var(--color-accent)]';
+
+/**
+ * The closing call to action on the home page — the same three steps that used
+ * to sit on `/login`, now placed where they belong: after the page has said
+ * what the product is, rather than above the fold in place of that explanation.
+ *
+ * Step 1 is deliberately the command and not the sign-in, matching the hero and
+ * the actual product: the CLI's offline mode needs no account, so an ordering
+ * that put "create an account" first would describe a requirement that does not
+ * exist.
+ */
+export function GetStartedCard() {
+ return (
+
+
+ Get started in 2 minutes
+
+
+ Free · Open source · No card, because there is nothing to charge for yet
+
+
+
+ -
+ 1
+
+
+ Run one command
+
+
+
+
+ -
+ 2
+
+
+ Your agent starts remembering
+
+
+ Lessons persist across sessions on this machine, stored as markdown you can
+ read and commit
+
+
+
+ -
+ 3
+
+
+ Sign in when you want them shared
+
+
+ A token connects the same store to every machine, teammate, and CI run — and
+ brings your local lessons with it
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/HowItWorks.tsx b/packages/web/src/components/landing/HowItWorks.tsx
new file mode 100644
index 000000000..652ee480a
--- /dev/null
+++ b/packages/web/src/components/landing/HowItWorks.tsx
@@ -0,0 +1,67 @@
+const STEPS = [
+ {
+ title: 'Your agent writes what it learns',
+ body: 'One memory.write call when something is worth keeping — a gotcha, a convention, a fix that took three attempts.',
+ },
+ {
+ title: 'The lesson is stored under a scope',
+ body: 'This branch, this repo, this project, or everywhere. The scope is what keeps one repository’s lore out of another’s.',
+ },
+ {
+ title: 'The next session reads it back',
+ body: 'Narrowest scope first, then wider — so an agent gets this repo’s lessons without drowning in everything else you have ever recorded.',
+ },
+] as const;
+
+/** The ASCII diagram from the repo README, reused so the front door and the
+ * README describe the product in the same shape. Decorative: the three steps
+ * below carry the same content in prose, which is what a screen reader — and a
+ * phone, where a fixed-width block would need horizontal scrolling — gets
+ * instead. */
+const DIAGRAM = ` ┌─ your agent, any tool, any machine ─┐
+learns something │ memory.write { scope, key, value } │
+in a session ───→│ │──→ one store, your call:
+recalls it next │ memory.list { scope } │←── remote (shared) or local
+time it needs it └──────────────────────────────────────┘`;
+
+export function HowItWorks() {
+ return (
+
+
+ One store. Every agent, every machine.
+
+
+ Two MCP tool calls are the whole mechanic.
+
+
+
+ {DIAGRAM}
+
+
+
+ {STEPS.map(({ title, body }, index) => (
+ -
+
+ {index + 1}
+
+
+ {title}
+
+
+ {body}
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/LandingBackdrop.tsx b/packages/web/src/components/landing/LandingBackdrop.tsx
new file mode 100644
index 000000000..3b2c1b9da
--- /dev/null
+++ b/packages/web/src/components/landing/LandingBackdrop.tsx
@@ -0,0 +1,31 @@
+/**
+ * The amber halo + grid wash behind the public pages.
+ *
+ * Purely decorative (`aria-hidden`, `pointer-events-none`) and shared by `/`
+ * and `/login` so the two never drift into two different-looking front doors.
+ * Static by design: a moving gradient behind a whole page is the one background
+ * effect that reliably costs paint frames on a low-end laptop, and it competes
+ * with the entrance motion the content itself carries.
+ */
+export function LandingBackdrop() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/LandingHeader.tsx b/packages/web/src/components/landing/LandingHeader.tsx
new file mode 100644
index 000000000..df02c8bf5
--- /dev/null
+++ b/packages/web/src/components/landing/LandingHeader.tsx
@@ -0,0 +1,71 @@
+import Image from 'next/image';
+import Link from 'next/link';
+import type { ReactNode } from 'react';
+
+/**
+ * The public site header, shared by the marketing home page (`/`) and the sign-in
+ * page (`/login`).
+ *
+ * It takes the sign-in control as a slot rather than rendering one itself,
+ * because the two pages need different things there and the difference is
+ * behavioural, not cosmetic: on `/` the control is a plain link to the sign-in
+ * page (a header button that fires GitHub OAuth on click does something the
+ * label "Sign in" does not say), while `/login` passes the compact
+ * `LoginButton`, which is that OAuth trigger and is correct once the visitor
+ * has already chosen to sign in.
+ */
+export function LandingHeader({ action }: { action?: ReactNode }) {
+ return (
+
+
+
+ LoreKit
+
+
+
+
+ Docs
+
+
+
+
+ {action ?? (
+
+ Sign in
+
+ )}
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/LandingHero.tsx b/packages/web/src/components/landing/LandingHero.tsx
new file mode 100644
index 000000000..b40a5a10e
--- /dev/null
+++ b/packages/web/src/components/landing/LandingHero.tsx
@@ -0,0 +1,102 @@
+import Link from 'next/link';
+
+import { CopyCommand } from './CopyCommand';
+
+/**
+ * The home page's first screen.
+ *
+ * Its primary call to action is the install command, not the sign-in button,
+ * and that ordering is the point: the CLI works with no account at all, so
+ * asking a first-time visitor to authenticate before they can try anything
+ * would be a wall in front of a door that is already open. Sign-in is offered
+ * as the next step for the thing an account actually buys — sharing lore across
+ * machines and teammates — and the copy says so instead of implying the product
+ * needs one.
+ *
+ * The lines are staggered (`--lk-delay`) rather than arriving together, so the
+ * eye lands on the headline and the supporting lines catch up; see the
+ * `lk-rise` block in `globals.css` for the motion contract.
+ */
+export function LandingHero() {
+ return (
+
+ {/* Trust bar. Each claim is checkable: the plan limit is the free-plan
+ memory cap, the licence is in the repo, and the tool list names the
+ integration level rather than implying all three are equal. */}
+
+ -
+
+ Free · 5,000 memories
+
+ -
+
+
+ Open source · MIT
+
+
+ -
+ Claude Code · Cursor · any MCP client
+
+
+
+
+ Shared memory for{' '}
+ your coding agents
+
+
+
+ Your agent works something out — why the build only breaks in CI, which migration
+ needs the flag — and forgets it when the session ends. LoreKit writes it down and
+ reads it back in every session after, on every machine, in every tool.
+
+
+
+
+
+
+
+
+ Works offline with no account. Sign in when you want lessons shared across
+ machines and teammates.
+
+
+
+
+ Sign in
+
+ ·
+
+ Read the docs
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/Limitations.tsx b/packages/web/src/components/landing/Limitations.tsx
new file mode 100644
index 000000000..1e40b4d45
--- /dev/null
+++ b/packages/web/src/components/landing/Limitations.tsx
@@ -0,0 +1,41 @@
+const LIMITS = [
+ 'It doesn’t read your codebase. It stores what your agent writes to it, and nothing else.',
+ 'It doesn’t make an agent smarter on its own. If nothing writes a lesson, there is nothing to read — which is why the plugins wire lifecycle hooks instead of leaving it to the agent’s discretion.',
+ 'It can surface a lesson you didn’t need. Scopes narrow what an agent sees, grooming prunes what has stopped earning its place, and every memory is visible to you in the dashboard.',
+ 'Codex support is experimental. Claude Code and Cursor are the supported paths; anything speaking MCP works too.',
+ 'There is no paid tier yet. If the free limits ever change, your lore exports to plain markdown files you own.',
+] as const;
+
+/**
+ * The limits, in the product's own voice.
+ *
+ * This is the section a sceptical developer screenshots, and it is cheaper to
+ * write it ourselves than to have someone discover each line on day one. Every
+ * entry names something that is true right now — an overstated claim withdrawn
+ * later reads as a retraction, while a limit removed later reads as progress.
+ */
+export function Limitations() {
+ return (
+
+
+ What LoreKit doesn’t do
+
+
+ {LIMITS.map((limit) => (
+ -
+
+ —
+
+ {limit}
+
+ ))}
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/MemoryCost.tsx b/packages/web/src/components/landing/MemoryCost.tsx
new file mode 100644
index 000000000..f2103eae7
--- /dev/null
+++ b/packages/web/src/components/landing/MemoryCost.tsx
@@ -0,0 +1,33 @@
+/**
+ * The section that says memory has a price.
+ *
+ * It is on the page deliberately. Every lesson injected into a session is paid
+ * for in context whether the agent uses it or not, and a memory product that
+ * does not say so is selling a cost as a feature. LoreKit measures the ratio
+ * and shows it, so the honest version of the claim is also the differentiating
+ * one — which is why this sits in the page's own voice rather than in a FAQ.
+ */
+export function MemoryCost() {
+ return (
+
+
+
+ Memory isn’t free, so we measure it
+
+
+ Every lesson delivered into a session costs tokens whether the agent uses it or
+ not. LoreKit tracks how often each lesson is actually chosen versus merely
+ delivered, estimates what your store costs you a month, and shows you the lore
+ that is earning nothing.
+
+
+ Retention policies then archive it — reviewed by you, or swept nightly, and
+ never a hard delete.
+
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/TerminalTheater.tsx b/packages/web/src/components/landing/TerminalTheater.tsx
index 278861689..5c39d7110 100644
--- a/packages/web/src/components/landing/TerminalTheater.tsx
+++ b/packages/web/src/components/landing/TerminalTheater.tsx
@@ -252,11 +252,11 @@ function TypewriterLine({
// layout shift. With the loop restarting forever, the login page accumulated
// thousands of CLS entries per visit (p75 0.21, worst 0.58).
//
- // The theater does NOT sit above the sign-in CTA: `(auth)/login/page.tsx`
- // renders the primary `LoginButton` at L155 and ` ` at
- // L165, so the shifting text is below it — only the header's compact sign-in
- // button (L111) is above. The cost is the accrued shift entries themselves,
- // which CLS sums for the whole session; it is not a CTA moving under a cursor.
+ // The theater does NOT sit above any call to action: `app/page.tsx` renders
+ // the hero — install command, sign-in and docs links — and only reaches this
+ // several sections further down, so the shifting text is always below every
+ // control. The cost is the accrued shift entries themselves, which CLS sums
+ // for the whole session; it is not a CTA moving under a cursor.
//
// So: a full, invisible copy of the text reserves the space, and the typed
// prefix is painted over it. The overlay is out of flow, so the number of
@@ -551,10 +551,10 @@ export function TerminalTheater() {
{/* Heading */}
- Watch memories form and persist
+ Session A writes. Session B already knows.
- Session A writes. Session B loads. No re-explaining.
+ Two sessions, one store, no re-explaining.
diff --git a/packages/web/src/components/landing/WhereLoreLives.tsx b/packages/web/src/components/landing/WhereLoreLives.tsx
new file mode 100644
index 000000000..9290b08c4
--- /dev/null
+++ b/packages/web/src/components/landing/WhereLoreLives.tsx
@@ -0,0 +1,66 @@
+const STORES = [
+ {
+ name: 'Local',
+ summary: 'No account, no network.',
+ body: 'Markdown files under ~/.lorekit and /.lorekit — greppable, diffable, and yours. Commit them to share via git, or gitignore them to keep them private.',
+ },
+ {
+ name: 'Hosted',
+ summary: 'Shared across machines and teammates.',
+ body: 'Every memory is personal by default; org sharing is opt-in. The free plan holds 5,000 active memories at 120 requests a minute.',
+ },
+ {
+ name: 'Self-hosted',
+ summary: 'Your own infrastructure.',
+ body: 'The whole stack — MCP server, dashboard, database — deploys to your own Supabase and Vercel projects. MIT licensed.',
+ },
+] as const;
+
+/**
+ * Where the data physically sits, stated before anyone has to ask.
+ *
+ * "Is this a service that stores my repository's internals?" is the first
+ * question a developer asks about a memory product and the last thing most
+ * landing pages answer. All three answers are true of LoreKit today, and the
+ * local one — which needs no account at all — is listed FIRST rather than as a
+ * footnote, because burying the option that needs nothing from us would be
+ * steering rather than informing.
+ */
+export function WhereLoreLives() {
+ return (
+
+
+ Where your lore lives is your call
+
+
+ The same memory tools run against any of the three.
+
+
+
+ {STORES.map(({ name, summary, body }) => (
+ -
+
+ {name}
+
+ {summary}
+
+ {body}
+
+
+ ))}
+
+
+
+ Start local and move up later —{' '}
+ lorekit migrate{' '}
+ previews the whole plan before it changes anything.
+
+
+ );
+}
diff --git a/packages/web/src/lib/analytics/track.ts b/packages/web/src/lib/analytics/track.ts
index af11bbcae..39711689f 100644
--- a/packages/web/src/lib/analytics/track.ts
+++ b/packages/web/src/lib/analytics/track.ts
@@ -121,8 +121,24 @@ export type CommandSource = 'palette' | 'shortcut';
*/
export type InstallCommandId = 'cli-install';
-/** Where the copy affordance was rendered. Bounded for the same reason. */
-export type CopySurface = 'login-get-started' | 'blog-cta';
+/**
+ * Where the copy affordance was rendered. Bounded for the same reason.
+ *
+ * `landing-hero` and `landing-get-started` are the two positions on the public
+ * home page, and they are kept apart because they answer different questions:
+ * the hero measures whether the command ALONE converts a first-time visitor,
+ * the card measures whether someone who read the whole page converts at the end
+ * of it. Collapsing them would hide which half of the page is doing the work.
+ *
+ * `login-get-started` is retained rather than deleted: the card it named moved
+ * from `/login` to `/`, but the value is already recorded against past events
+ * and dropping it from the union would make that history untypeable.
+ */
+export type CopySurface =
+ | 'landing-hero'
+ | 'landing-get-started'
+ | 'login-get-started'
+ | 'blog-cta';
/** Discriminated union of every tracked event. Add new events here. */
export type AnalyticsEvent =
From d7111d2d4bf944fc7c16a8230ca7636b7f13a1c1 Mon Sep 17 00:00:00 2001
From: "mads.thines"
Date: Sun, 11 Oct 2026 05:06:25 +0000
Subject: [PATCH 2/6] =?UTF-8?q?feat(web):=20make=20the=20landing=20page=20?=
=?UTF-8?q?persuade=20=E2=80=94=20product=20shot,=20problem,=20bento,=20ho?=
=?UTF-8?q?nest=20next=20steps?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Raises the PR's honest copy to a marketing page that sells:
- Hero leads with the problem and resolves it in one fixation
("Your coding agents forget. LoreKit remembers."), the README's own framing.
The install command gets the amber frame and the page's only filled button;
trust points move under it, to the moment of the click.
- The live demo becomes the hero's product shot, directly below it, and tilts
flat as it scrolls in (CSS view() timeline, transform-only, flat at rest).
- New "01 / the problem" before/after, and a "03 / what you get" bento that
absorbs MemoryCost and adds review-comment learning, one store for every
tool, and grooming — each with a small picture of the behaviour.
- One heading system (mono numbered eyebrow, 30-36px title) and two repeated
signature moves (lk-glow-frame, lk-gradient-text) across every section.
- Sticky blurred header; the Blog link from #685 now lives in the shared
LandingHeader (the merge kept the slim /login).
Copy corrected against the CLI: install with a blank token still writes the
hosted MCP entry, so "works offline with no account" directly under the
command promised a setup one command does not produce. The hero and the
closing card now name both real paths — hosted (sign in, paste a token) and
offline (skip the token, switch to local mode) — at equal weight. Webhook
lessons are described as candidates that expire, matching webhook.ts.
Review findings applied: AuthHashCatcher comment no longer claims a move;
LandingBackdrop uses tokens, not hex; StepBadge replaces the duplicated
class; one GitHubMark replaces four inline copies of the SVG path.
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
---
packages/web/src/app/globals.css | 61 +++++
packages/web/src/app/page.tsx | 34 ++-
.../web/src/components/auth/LoginButton.tsx | 20 +-
.../src/components/landing/BeforeAfter.tsx | 162 +++++++++++++
.../src/components/landing/CopyCommand.tsx | 37 ++-
.../src/components/landing/FeatureBento.tsx | 226 ++++++++++++++++++
.../src/components/landing/GetStartedCard.tsx | 153 +++++++-----
.../web/src/components/landing/HowItWorks.tsx | 57 +++--
.../components/landing/LandingBackdrop.tsx | 35 ++-
.../src/components/landing/LandingHeader.tsx | 97 ++++----
.../src/components/landing/LandingHero.tsx | 180 +++++++++-----
.../src/components/landing/Limitations.tsx | 21 +-
.../web/src/components/landing/MemoryCost.tsx | 33 ---
.../src/components/landing/SectionHeading.tsx | 54 +++++
.../web/src/components/landing/StepBadge.tsx | 19 ++
.../components/landing/TerminalTheater.tsx | 44 ++--
.../src/components/landing/WhereLoreLives.tsx | 84 +++++--
.../web/src/components/layout/SiteFooter.tsx | 12 +-
packages/web/src/components/ui/GitHubMark.tsx | 21 ++
19 files changed, 1024 insertions(+), 326 deletions(-)
create mode 100644 packages/web/src/components/landing/BeforeAfter.tsx
create mode 100644 packages/web/src/components/landing/FeatureBento.tsx
delete mode 100644 packages/web/src/components/landing/MemoryCost.tsx
create mode 100644 packages/web/src/components/landing/SectionHeading.tsx
create mode 100644 packages/web/src/components/landing/StepBadge.tsx
create mode 100644 packages/web/src/components/ui/GitHubMark.tsx
diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css
index 6b3fd2f78..6010a08ba 100644
--- a/packages/web/src/app/globals.css
+++ b/packages/web/src/app/globals.css
@@ -16,6 +16,7 @@
--color-content-tertiary: #4f5668;
--color-accent: #f5a623; /* amber — the LoreKit signature colour */
+ --color-accent-bright: #ffd27a; /* the light end of the amber — gradient highlights only, never text on its own */
--color-accent-subtle: #2a2010; /* dark amber tint for backgrounds */
--color-accent-glow: #f5a62340; /* glow rings */
@@ -153,6 +154,66 @@ body:has([data-app-shell]) {
to { opacity: 1; transform: translateY(0); }
}
+/* `lk-tilt` lays the product window back and lifts it flat as it scrolls into
+ view — the demo arrives like a screen being raised to face you. Same contract
+ as above: transform only, and the resting state is FLAT, so Safari/Firefox
+ and reduced-motion visitors get an ordinary upright window.
+
+ The range ends at `cover 35%`, well before the window is centred, so by the
+ time anyone is reading the terminal it has stopped moving and its text
+ renders crisp rather than through a 3D transform. */
+@media (prefers-reduced-motion: no-preference) {
+ @supports (animation-timeline: view()) {
+ .lk-tilt {
+ transform-origin: 50% 0%;
+ animation: lk-tilt-in linear both;
+ animation-timeline: view();
+ animation-range: entry 0% cover 35%;
+ }
+ }
+}
+
+@keyframes lk-tilt-in {
+ from { transform: perspective(1400px) rotateX(18deg) scale(0.92); }
+ to { transform: none; }
+}
+
+/* ── Landing signature ───────────────────────────────────────────────────── */
+/* The public pages repeat exactly two moves, and nothing else decorates:
+
+ 1. `lk-glow-frame` — an amber hairline that is brightest at one corner and
+ fades out, plus the brand's amber glow. Reserved for the surfaces that
+ carry an ACTION or the product itself (the hero command, the demo window,
+ the closing call to action, the one highlighted card per section), so the
+ eye learns that amber edge = "this is the thing to look at".
+ 2. `lk-gradient-text` — the amber ramp on the single phrase per heading that
+ is the payoff. Never on body copy: it is a highlight, not a colour.
+
+ The frame is a two-layer background (padding-box fill over a border-box
+ gradient) rather than a pseudo-element, so it needs no extra DOM and never
+ sits on top of the content. Set `--lk-frame-fill` to change the fill. */
+.lk-glow-frame {
+ border: 1px solid transparent;
+ background:
+ linear-gradient(var(--lk-frame-fill, var(--color-bg-raised)), var(--lk-frame-fill, var(--color-bg-raised))) padding-box,
+ linear-gradient(135deg, var(--color-accent) 0%, var(--color-border) 35%, var(--color-border) 70%, var(--color-accent-glow) 100%) border-box;
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4), 0 24px 80px -24px var(--color-accent-glow);
+}
+
+/* Text fallback first: a browser without `background-clip: text` shows solid
+ amber, never transparent text. */
+.lk-gradient-text {
+ color: var(--color-accent);
+}
+@supports ((-webkit-background-clip: text) or (background-clip: text)) {
+ .lk-gradient-text {
+ background-image: linear-gradient(180deg, var(--color-accent-bright) 0%, var(--color-accent) 70%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+}
+
/* ── Reduced motion ──────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx
index 1f17313dd..a31a4241c 100644
--- a/packages/web/src/app/page.tsx
+++ b/packages/web/src/app/page.tsx
@@ -2,13 +2,14 @@ import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { AuthHashCatcher } from '@/components/auth/AuthHashCatcher';
+import { BeforeAfter } from '@/components/landing/BeforeAfter';
+import { FeatureBento } from '@/components/landing/FeatureBento';
import { GetStartedCard } from '@/components/landing/GetStartedCard';
import { HowItWorks } from '@/components/landing/HowItWorks';
import { LandingBackdrop } from '@/components/landing/LandingBackdrop';
import { LandingHeader } from '@/components/landing/LandingHeader';
import { LandingHero } from '@/components/landing/LandingHero';
import { Limitations } from '@/components/landing/Limitations';
-import { MemoryCost } from '@/components/landing/MemoryCost';
import { TerminalTheater } from '@/components/landing/TerminalTheater';
import { WhereLoreLives } from '@/components/landing/WhereLoreLives';
import { SiteFooter } from '@/components/layout/SiteFooter';
@@ -20,7 +21,7 @@ type SearchParams = Record;
export const metadata: Metadata = {
title: 'Shared memory for your coding agents',
description:
- 'LoreKit gives your AI coding agents a shared, persistent memory. One command to connect — offline with no account, or hosted across every machine, teammate, and CI run.',
+ 'LoreKit gives your AI coding agents a shared, persistent memory. One command to connect — hosted across every machine, teammate, and CI run, or fully offline with no account.',
};
function toURLSearchParams(input: SearchParams): URLSearchParams {
@@ -59,9 +60,11 @@ export default async function RootPage({
//
// Only the query-string shapes can be rescued server-side; an implicit-flow
// `#access_token=…` fragment never reaches the server and is picked up by the
- // AuthHashCatcher mounted below. It has to be mounted HERE now: this page no
- // longer redirects to /login, so the catcher that used to receive those
- // fragments there would never run.
+ // AuthHashCatcher mounted below. It is mounted here IN ADDITION to /login, not
+ // moved from it: this page used to bounce every visitor to /login, so the
+ // /login catcher received the fragments that land on the Site URL. With the
+ // bounce gone those fragments stay here, while a fragment that lands on
+ // /login directly still needs the one there.
if (classifyAuthCallback(params).kind !== 'none') {
if (!params.has('next')) params.set('next', '/welcome');
redirect(`/api/auth/callback?${params.toString()}`);
@@ -76,24 +79,35 @@ export default async function RootPage({
redirect('/lore');
}
+ // Reading order is an argument, not a feature list:
+ // promise (hero) → proof (the live demo, peeking above the fold as the
+ // hero's product shot) → the pain it removes → how → what you get → where
+ // your data sits → what it won't do → the same action again.
+ // The demo sits directly under the hero because a visitor decides within the
+ // first screen whether to keep reading, and "session B already knows" lands
+ // faster as a picture than as any sentence above it.
+ //
+ // `overflow-x-clip`, not `overflow-hidden`: hidden makes a scroll
+ // container, and the sticky header would stick to a box that never scrolls.
return (
-
+
-
-
+
-
+
+
+
-
+
);
}
diff --git a/packages/web/src/components/auth/LoginButton.tsx b/packages/web/src/components/auth/LoginButton.tsx
index a915c0522..1eb964adc 100644
--- a/packages/web/src/components/auth/LoginButton.tsx
+++ b/packages/web/src/components/auth/LoginButton.tsx
@@ -17,21 +17,9 @@ import {
import { validatePassword } from '@/lib/password-policy';
import { authCallbackOrigin, buildAuthCallbackUrl } from '@/lib/auth-callback-url';
import { safeNextPath } from '@/lib/auth-redirect';
-import { FIELD_CLASS } from './field-styles';
+import { GitHubMark } from '@/components/ui/GitHubMark';
-function GitHubIcon({ className }: { className?: string }) {
- return (
-
- );
-}
+import { FIELD_CLASS } from './field-styles';
function MailIcon({ className }: { className?: string }) {
return (
@@ -306,7 +294,7 @@ export function LoginButton({ compact = false }: LoginButtonProps) {
className="flex h-9 items-center gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-elevated)] px-3.5 text-sm font-medium text-[var(--color-content-primary)] transition-all duration-200 hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-subtle)] hover:text-[var(--color-accent)] focus-visible:outline-2 focus-visible:outline-[var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-50"
aria-busy={loading}
>
-
+
{loading ? 'Redirecting...' : 'Sign in'}
@@ -528,7 +516,7 @@ export function LoginButton({ compact = false }: LoginButtonProps) {
className="group relative flex h-12 min-w-[220px] items-center justify-center gap-2.5 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-elevated)] px-6 text-sm font-semibold text-[var(--color-content-primary)] shadow-[0_0_0_1px_transparent] transition-all duration-200 hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-subtle)] hover:text-[var(--color-accent)] hover:shadow-[0_0_20px_var(--color-accent-glow)] focus-visible:outline-2 focus-visible:outline-[var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-50"
aria-busy={loading}
>
-
+
{loading ? 'Redirecting...' : 'Continue with GitHub'}
diff --git a/packages/web/src/components/landing/BeforeAfter.tsx b/packages/web/src/components/landing/BeforeAfter.tsx
new file mode 100644
index 000000000..759a0a47b
--- /dev/null
+++ b/packages/web/src/components/landing/BeforeAfter.tsx
@@ -0,0 +1,162 @@
+import { Check, X } from 'lucide-react';
+
+import { cn } from '@/lib/cn';
+
+import { SectionHeading } from './SectionHeading';
+
+type TranscriptLine = {
+ day: string;
+ who: 'you' | 'agent';
+ text: string;
+ /** The line that lands the point — the relapse, or the remembered answer. */
+ emphasis?: boolean;
+};
+
+const WITHOUT: TranscriptLine[] = [
+ { day: 'mon', who: 'you', text: 'we validate with Zod here, not Yup' },
+ { day: 'tue', who: 'you', text: 'reminder: Zod, not Yup' },
+ { day: 'wed', who: 'you', text: 'again — Zod.' },
+ { day: 'thu', who: 'agent', text: 'pnpm add yup', emphasis: true },
+];
+
+const WITH: TranscriptLine[] = [
+ { day: 'mon', who: 'agent', text: 'memory.write conventions/validation "Zod, not Yup"' },
+ { day: 'tue', who: 'agent', text: 'memory.list ↳ 1 lesson loaded' },
+ { day: 'wed', who: 'agent', text: 'memory.list ↳ 1 lesson loaded' },
+ { day: 'thu', who: 'agent', text: 'import { z } from "zod"', emphasis: true },
+];
+
+/**
+ * "01 / the problem" — the cost of forgetting, shown rather than described.
+ *
+ * The demo above it shows the MECHANISM (session A writes, session B reads).
+ * This section shows what that is worth to the person at the keyboard: the
+ * same sentence typed four days running, and the relapse anyway. It is the one
+ * part of the page that argues with feeling instead of facts, so it stays a
+ * single, recognisable scene — a convention every reader has had to repeat —
+ * rather than an invented statistic about hours lost.
+ *
+ * The "with" column uses the real tool names and the real `memory.list` output
+ * shape, so it promises exactly what the product does: a lesson written once
+ * is read back in later sessions. It does not claim the agent can never
+ * relapse — that depends on the agent reading what it is given.
+ *
+ * Server-rendered and static; the transcripts are presentational, so each
+ * column also carries a plain-language summary line for screen readers and
+ * skimmers alike.
+ */
+export function BeforeAfter() {
+ return (
+
+
+ Every session starts from zero.{' '}
+ Yours don’t have to.
+ >
+ }
+ lede="The agent that untangled your build an hour ago has no memory of it now. So you re-explain — and it still gets it wrong on Thursday."
+ />
+
+
+
+
+
+
+ );
+}
+
+function TranscriptCard({
+ variant,
+ title,
+ summary,
+ lines,
+}: {
+ variant: 'without' | 'with';
+ title: string;
+ summary: string;
+ lines: TranscriptLine[];
+}) {
+ const isWith = variant === 'with';
+ const Icon = isWith ? Check : X;
+
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ {lines.map((line) => (
+ -
+ {line.day}
+
+ {line.who} ›
+
+
+ {line.text}
+
+
+ ))}
+
+
+
+ {summary}
+
+
+ );
+}
diff --git a/packages/web/src/components/landing/CopyCommand.tsx b/packages/web/src/components/landing/CopyCommand.tsx
index 4b14ba4a0..b8ea39c96 100644
--- a/packages/web/src/components/landing/CopyCommand.tsx
+++ b/packages/web/src/components/landing/CopyCommand.tsx
@@ -40,7 +40,9 @@ export function CopyCommand({
* `sm` is the inline form used inside a step list. `lg` is the hero form,
* where this affordance IS the page's primary call to action: it gets a
* 40px-tall copy control (WCAG 2.2 SC 2.5.8 asks for 24px — a primary CTA
- * should clear that comfortably) and body-sized command text.
+ * should clear that comfortably), body-sized command text, the amber
+ * `lk-glow-frame`, and a FILLED copy button — the one solid-amber control on
+ * the page, so nothing else competes with it for the first click.
*/
size?: 'sm' | 'lg';
}) {
@@ -73,31 +75,42 @@ export function CopyCommand({
return (
- {command}
+ {/* The prompt says "this goes in a terminal" at a glance. It sits
+ outside the copied string and is unselectable, so neither the
+ button nor a manual drag-select ever picks up a stray `$`. */}
+
+ $
+
+ {command}