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.
| App | Package | Status |
|---|---|---|
apps/landing-page-nextgen | @lifosy/landing-page-nextgen | Production, deployed to the Cloudflare Pages project lifosy-landing |
apps/landing-page | @lifosy/landing-page | Older 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/docspackpackage, with a terminal block showingpnpm add -D docspack @lifosy/docspack,npx docspack syncandnpx 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.tsdefines thedocscontent collection: Astro’sglobloader reads../../packages/docspack/docs/*.md. The id drops the number prefix, so20-cli.mdbecomes/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.astrolists every page as a card.src/pages/docs/[id].astrorenders one page, with the page list fromsrc/layouts/DocsLayout.astroas 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.cssimports./tokens.cssand thentailwindcss.tokens.cssdefines OKLCH color tokens on:root(--color-paper,--color-ink,--color-accentand others), a near-black background with a neon-green accent. Most layout is plain CSS classes such ashero__declarationanddemo__player. Fonts are Tomorrow and JetBrains Mono from Google Fonts. - Old page:
src/styles/global.cssimportstailwindcssand 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:
| Script | Command | Result |
|---|---|---|
dev | astro dev | Dev server at localhost:4321 |
build | astro build | Static site in ./dist/ |
preview | astro preview | Serves the built dist/ |
astro | astro | The 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:
| Input | Value |
|---|---|
buildTarget | landing-nextgen:build |
rootDir | ./apps/landing-page-nextgen |
distDir | ./dist |
cfPagesName | lifosy-landing |
branchName | main |
commitGeneratedFile | false |
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 startspnpm --filter @lifosy/app-console devand stops it afterwards. That needspnpm installand onepnpm --filter @lifosy/ui build. - Chromium for Playwright:
pnpm exec playwright install chromium. - ffmpeg. The script looks for
FFMPEG_PATH, then Playwright’s bundled ffmpeg, thenffmpegonPATH. 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.pushStateplus apopstateevent, 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
/demoload, so the wrap from last frame to first is invisible.