Landing pages

The kaihuman marketing site is a static Astro site styled with Tailwind CSS v4 (@tailwindcss/vite). Two versions live in the monorepo: apps/landing-page-nextgen (@lifosy/landing-page-nextgen) is the production site, and apps/landing-page (@lifosy/landing-page) is the older design, kept in the repo but no longer deployed. Both pages link to the app at https://app.kaihuman.com, the live demo at /demo, and the source at github.com/lifosy/monorepo.

Which landing page is in production

apps/landing-page-nextgen is the production landing page since 2026-06-25.

AppPackageStatus
apps/landing-page-nextgen@lifosy/landing-page-nextgenProduction, deployed to the Cloudflare Pages project lifosy-landing
apps/landing-page@lifosy/landing-pageOlder design, not deployed by any workflow

Before that date a separate deploy-landing-nextgen.yml deployed nextgen to a preview project (kh-landingpage-nextgen). That workflow was removed, and deploy-landing.yml was switched from the old app to nextgen. doc/progress.md records the change under “Nextgen is the default landing page”.

What the landing page contains

The nextgen page is a single route, src/pages/index.astro, wrapped by src/layouts/Layout.astro. Its sections, top to bottom:

  • Hero: “EVERY DAY IS AN EDIT.” with the 改 (kai) mark and the CTAs Start Improving (https://app.kaihuman.com) and View Source →.
  • Demo: the looping product screencast in an app-window frame, plus ▶ Try the live demo — no signup, linking to https://app.kaihuman.com/demo.
  • Philosophy: kaizen, getting 1% better every day.
  • Principles: three cards, Track, Own and Know.
  • Ownership: “YOUR DATA. YOUR RULES.” — local-first, plain files, no tracking.
  • Docs (#docs): “DOCUMENTED FOR AGENTS.” — the @lifosy/docspack package, with a terminal block showing pnpm add -D docspack @lifosy/docspack, npx docspack sync and npx docspack ask, and a Read the docs → link to /docs/.
  • CTA: START IMPROVING TODAY and the GitHub link.

The Docs link in the nav pill (src/components/NavPill.astro, shared by every page) opens /docs/. The page title is “kaihuman — The ever-improving human.” The default meta description is set in Layout.astro. The old page in apps/landing-page has the same screencast and demo link right below its hero.

Reading the docs on the website

The nextgen site renders this documentation as web pages at /docs/ (for example https://kaihuman.com/docs/cli/). The pages are built from the same Markdown that ships as @lifosy/docspack, so the site and the agent index never disagree.

  • src/content.config.ts defines the docs content collection: Astro’s glob loader reads ../../packages/docspack/docs/*.md. The id drops the number prefix, so 20-cli.md becomes /docs/cli/.
  • src/lib/docs.ts (getDocPages) sorts the pages by filename and takes each title from its # heading and its summary from the first paragraph.
  • src/pages/docs/index.astro lists every page as a card. src/pages/docs/[id].astro renders one page, with the page list from src/layouts/DocsLayout.astro as a sidebar (below the article on narrow screens).

A change under packages/docspack/docs/ triggers the production deploy too, so the website follows every docs edit.

How the landing page is styled

Both apps load Tailwind through the Vite plugin in astro.config.mjs:

import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ vite: { plugins: [tailwindcss()] } });
  • Nextgen: src/styles/global.css imports ./tokens.css and then tailwindcss. tokens.css defines OKLCH color tokens on :root (--color-paper, --color-ink, --color-accent and others), a near-black background with a neon-green accent. Most layout is plain CSS classes such as hero__declaration and demo__player. Fonts are Tomorrow and JetBrains Mono from Google Fonts.
  • Old page: src/styles/global.css imports tailwindcss and defines --los-* variables (--los-bg, --los-fg, --los-accent). Markup uses Tailwind utility classes. The font is JetBrains Mono.

Static files (favicon.svg, favicon.ico, the screencast) live in each app’s public/ folder.

Running and building the landing page locally

Run pnpm install from the repository root first. Each app has the same Astro scripts:

ScriptCommandResult
devastro devDev server at localhost:4321
buildastro buildStatic site in ./dist/
previewastro previewServes the built dist/
astroastroThe Astro CLI, for example pnpm astro check

From the root:

pnpm --filter @lifosy/landing-page-nextgen dev
pnpm landing-nextgen:build     # turbo run build --filter=@lifosy/landing-page-nextgen
pnpm dev:landing               # old page: turbo run dev --filter=@lifosy/landing-page
pnpm landing:build             # old page build

How the landing page is deployed

.github/workflows/deploy-landing.yml (named [PROD] Deploy Landing Page) deploys the nextgen app. It runs on a push to main that touches apps/landing-page-nextgen/** or packages/docspack/docs/**, or manually via workflow_dispatch.

It calls the reusable template-deploy-to-cloudflare.yaml with these inputs:

InputValue
buildTargetlanding-nextgen:build
rootDir./apps/landing-page-nextgen
distDir./dist
cfPagesNamelifosy-landing
branchNamemain
commitGeneratedFilefalse

The template installs with pnpm on Node 22, runs pnpm run build and pnpm run landing-nextgen:build, then runs wrangler pages deploy ./dist --project-name=lifosy-landing --branch=main. It needs the secrets CF_ACCOUNT_ID and CF_API_TOKEN. A change to apps/landing-page triggers no deploy.

Regenerating the product screencast

The hero video is recorded automatically from the console’s demo mode by apps/landing-page-nextgen/scripts/record-screencast.mjs.

pnpm --filter @lifosy/landing-page-nextgen screencast

What it needs:

  • The console dev server on http://localhost:4300. If none is running, the script starts pnpm --filter @lifosy/app-console dev and stops it afterwards. That needs pnpm install and one pnpm --filter @lifosy/ui build.
  • Chromium for Playwright: pnpm exec playwright install chromium.
  • ffmpeg. The script looks for FFMPEG_PATH, then Playwright’s bundled ffmpeg, then ffmpeg on PATH. Without it the raw clip ships untrimmed and the loop is not seamless.

What it writes, to both apps/landing-page-nextgen/public/ and apps/landing-page/public/:

  • screencast.webm — VP8 WebM, 1440×900, about 51 s.
  • screencast-poster.jpg — the first-frame poster.

Commit both files. A commit under apps/landing-page-nextgen/ triggers the production deploy.

What the screencast shows and how the loop works

The script opens /demo, which seeds the in-memory demo data. It then walks a fixed storyline: Overview dashboard, the Finance and Reading workspaces, back to Overview, /board (kanban), /analytics, /action-log, /wiki, then the files browser and Markdown editor at /, and back to Overview.

  • A full page reload wipes the demo session, so every later route change uses history.pushState plus a popstate event, which preact-router listens to.
  • Recorded video has no mouse pointer, so the script injects a synthetic cursor that glides and pulses on clicks.
  • The clip starts and ends on the same Overview frame with the cursor centred. ffmpeg trims the leading /demo load, so the wrap from last frame to first is invisible.