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
213 changes: 128 additions & 85 deletions apps/docs/app/[lang]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,11 @@ import { ArrowRight, Check } from 'lucide-react';
import { Bricolage_Grotesque, IBM_Plex_Mono } from 'next/font/google';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions, gitConfig } from '@/lib/layout.shared';
import { YouTubeEmbed } from '@/components/youtube-embed';

/** The 90-second overview — the same video the README's hero cover links to. */
const OVERVIEW_VIDEO_ID = 'CX_FlOoOtr0';
const YOUTUBE_CHANNEL_URL = 'https://www.youtube.com/@objectstack';

const display = Bricolage_Grotesque({
subsets: ['latin'],
Expand DownExpand Up@@ -105,72 +110,140 @@ export default function HomePage() {
/>

{/* ── hero ─────────────────────────────────────────────── */}
<section className="relative mx-auto grid w-full max-w-6xl items-center gap-12 px-6 pt-20 pb-16 md:grid-cols-[1.05fr_0.95fr] md:pt-28 md:pb-24">
<div>
<p className="text-xs font-medium tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
Open protocol &amp; runtime · Apache-2.0
</p>
<h1
className="mt-5 text-[2.6rem]/[1.06] font-bold tracking-tight text-balance md:text-6xl/[1.04]"
style={{ fontFamily: 'var(--l-display)' }}
<section className="relative mx-auto w-full max-w-5xl px-6 pt-16 text-center md:pt-20">
<p className="text-xs font-medium tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
Open protocol &amp; runtime · Apache-2.0
</p>
<h1
className="mx-auto mt-5 max-w-3xl text-[2.6rem]/[1.06] font-bold tracking-tight text-balance md:text-6xl/[1.04]"
style={{ fontFamily: 'var(--l-display)' }}
>
Apps small enough{' '}
<span className="block bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-purple-400">
for AI to hold whole.
</span>
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg text-fd-muted-foreground text-pretty">
ObjectStack turns the whole app — data model, UI, workflows, permissions — into
typed metadata: a complete CRM in under 150k tokens, one context window. Agents
read it whole, reason it whole, refactor it whole.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Link
href="/docs/getting-started"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
>
Apps small enough{' '}
<span className="block bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-purple-400">
for AI to hold whole.
</span>
</h1>
<p className="mt-6 max-w-xl text-lg text-fd-muted-foreground text-pretty">
ObjectStack turns the whole app — data model, UI, workflows, permissions — into
typed metadata: a complete CRM in under 150k tokens, one context window. Agents
read it whole, reason it whole, refactor it whole.
</p>
<p className="mt-4 max-w-xl text-lg text-fd-muted-foreground text-pretty">
Get started
<ArrowRight className="size-4" />
</Link>
<Link
href="/docs"
className="inline-flex items-center rounded-lg border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
Documentation
</Link>
<a
href={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center rounded-lg border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
GitHub
</a>
<a
href={YOUTUBE_CHANNEL_URL}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center rounded-lg border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
YouTube
</a>
</div>
<div
className="mt-8 flex flex-wrap items-center justify-center gap-x-3 gap-y-2 text-[13px] text-fd-muted-foreground"
style={{ fontFamily: 'var(--l-mono)' }}
>
<span>Fits in an agent&apos;s context</span>
<span aria-hidden className="text-fd-border">|</span>
<span>Typed, validated, governed</span>
<span aria-hidden className="text-fd-border">|</span>
<span>Self-host anywhere</span>
</div>
</section>

{/* ── the 90-second overview — the page's first visual ──── */}
<section className="relative mx-auto w-full max-w-5xl px-6 pt-12 md:pt-14">
<figure className="relative">
<YouTubeEmbed
videoId={OVERVIEW_VIDEO_ID}
title="ObjectStack in 90 Seconds"
poster="/hero-cover-dark.png"
/>
<figcaption
className="mt-3 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[12px] text-fd-muted-foreground"
style={{ fontFamily: 'var(--l-mono)' }}
>
<span>▶ ObjectStack in 90 Seconds</span>
<a
href={YOUTUBE_CHANNEL_URL}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 text-fd-foreground underline underline-offset-4 transition-colors hover:text-fd-primary"
>
More videos on YouTube
<ArrowRight className="size-3" />
</a>
</figcaption>
</figure>
</section>

{/* ── the claim the video makes, in prose ──────────────── */}
<section className="relative mx-auto w-full max-w-5xl px-6 pt-12 md:pt-14">
<div className="grid gap-8 md:grid-cols-2">
<p className="text-lg text-fd-muted-foreground text-pretty">
The business logic alone — every object, workflow and permission — is under 100k
tokens; the UI adds just 50k more.
</p>
<p className="mt-4 max-w-xl text-base text-fd-muted-foreground text-pretty">
<p className="text-base text-fd-muted-foreground text-pretty">
That metadata is your business ontology — an open, versioned definition you own.
Strict TypeScript, Zod schemas, and a validation gate catch the agent's mistakes
Strict TypeScript, Zod schemas, and a validation gate catch the agent&apos;s mistakes
at authoring time, and the runtime derives the database, REST API, UI, and MCP
server — permissions and audit enforced on every call.
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link
href="/docs/getting-started"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-5 py-2.5 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
>
Get started
<ArrowRight className="size-4" />
</Link>
<Link
href="/docs"
className="inline-flex items-center rounded-lg border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
Documentation
</Link>
<a
href={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center rounded-lg border border-fd-border px-5 py-2.5 text-sm font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
GitHub
</a>
</div>
<div
className="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[13px] text-fd-muted-foreground"
style={{ fontFamily: 'var(--l-mono)' }}
>
<span>Fits in an agent's context</span>
<span aria-hidden className="text-fd-border">|</span>
<span>Typed, validated, governed</span>
<span aria-hidden className="text-fd-border">|</span>
<span>Self-host anywhere</span>
</div>
</div>
</section>

{/* ── 01 · the loop ────────────────────────────────────── */}
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
<span className="text-xs tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
01 · The loop
</span>
</div>
<h2 className="mt-4 max-w-2xl text-3xl font-bold tracking-tight md:text-4xl" style={{ fontFamily: 'var(--l-display)' }}>
From requirement to running app
</h2>
<div className="mt-10 grid gap-8 md:grid-cols-3">
{STEPS.map((s) => (
<div key={s.num} className="group relative">
<div className="flex items-center gap-3">
<span className="text-sm text-fd-muted-foreground" style={{ fontFamily: 'var(--l-mono)' }}>{s.num}</span>
<div className="h-px flex-1 bg-fd-border" aria-hidden />
</div>
<h3 className="mt-4 text-lg font-semibold" style={{ fontFamily: 'var(--l-display)' }}>{s.title}</h3>
<p
className="mt-2 inline-block rounded-md border border-fd-border bg-fd-muted/50 px-2.5 py-1 text-[12.5px] text-fd-muted-foreground"
style={{ fontFamily: 'var(--l-mono)' }}
>
{s.cmd}
</p>
<p className="mt-3 text-[15px] text-fd-muted-foreground text-pretty">{s.copy}</p>
</div>
))}
</div>

{/* the artifact card — requirement → metadata → gate → preview */}
<figure className="relative">
<figure className="relative mx-auto mt-12 max-w-2xl">
<div className="overflow-hidden rounded-xl border border-fd-border bg-fd-card shadow-[0_24px_60px_-24px_rgb(0_0_0/0.35)]">
<div className="border-b border-fd-border px-4 py-3" style={{ fontFamily: 'var(--l-mono)' }}>
<p className="text-[11px] tracking-wider text-fd-muted-foreground uppercase">you → claude code</p>
Expand DownExpand Up@@ -217,36 +290,6 @@ export default function HomePage() {
</figure>
</section>

{/* ── 01 · the loop ────────────────────────────────────── */}
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
<span className="text-xs tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
01 · The loop
</span>
</div>
<h2 className="mt-4 max-w-2xl text-3xl font-bold tracking-tight md:text-4xl" style={{ fontFamily: 'var(--l-display)' }}>
From requirement to running app
</h2>
<div className="mt-10 grid gap-8 md:grid-cols-3">
{STEPS.map((s) => (
<div key={s.num} className="group relative">
<div className="flex items-center gap-3">
<span className="text-sm text-fd-muted-foreground" style={{ fontFamily: 'var(--l-mono)' }}>{s.num}</span>
<div className="h-px flex-1 bg-fd-border" aria-hidden />
</div>
<h3 className="mt-4 text-lg font-semibold" style={{ fontFamily: 'var(--l-display)' }}>{s.title}</h3>
<p
className="mt-2 inline-block rounded-md border border-fd-border bg-fd-muted/50 px-2.5 py-1 text-[12.5px] text-fd-muted-foreground"
style={{ fontFamily: 'var(--l-mono)' }}
>
{s.cmd}
</p>
<p className="mt-3 text-[15px] text-fd-muted-foreground text-pretty">{s.copy}</p>
</div>
))}
</div>
</section>

{/* ── 02 · four gates ──────────────────────────────────── */}
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
Expand Down
64 changes: 64 additions & 0 deletions apps/docs/components/youtube-embed.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
'use client';

import { useState } from 'react';
import { Play } from 'lucide-react';

type YouTubeEmbedProps = {
/** The video id — the part after `youtu.be/`. */
videoId: string;
/** Video title. Used for the iframe title and the play button's label. */
title: string;
/** Cover shown until the viewer clicks play. Served from `public/`. */
poster: string;
};

/**
* Click-to-play YouTube embed.
*
* A facade, not a bare `<iframe>`: nothing is requested from YouTube until the
* viewer actually clicks play, so the homepage costs no third-party frame,
* script or cookie on load. The cover is the same one the README links from
* (`docs/screenshots/hero-cover-dark.png`, copied into `public/`), which is 2:1
* rather than the video's 16:9 — hence the fixed 2:1 frame with the player
* centred inside it at its own aspect ratio. Cropping the cover to 16:9 would
* cut through the logo on the left edge, and letting the frame change shape on
* click would shove the rest of the page down mid-interaction.
*/
export function YouTubeEmbed({ videoId, title, poster }: YouTubeEmbedProps) {
const [playing, setPlaying] = useState(false);

return (
<div className="relative aspect-[2/1] w-full overflow-hidden rounded-xl border border-fd-border bg-black shadow-[0_24px_60px_-24px_rgb(0_0_0/0.45)]">
{playing ? (
<iframe
className="absolute inset-0 m-auto aspect-video h-full"
src={`https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`}
title={title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
) : (
<button
type="button"
onClick={() => setPlaying(true)}
aria-label={`Play the video: ${title}`}
className="group absolute inset-0 h-full w-full cursor-pointer"
>
{/* Plain <img>, as everywhere else on this site — see lib/layout.shared.tsx. */}
<img src={poster} alt="" className="h-full w-full object-cover" loading="lazy" decoding="async" />
<span
aria-hidden
className="absolute inset-0 bg-black/10 transition-colors group-hover:bg-black/25"
/>
<span
aria-hidden
className="absolute top-1/2 left-1/2 flex size-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 shadow-lg transition-transform group-hover:scale-110 md:size-20"
>
<Play className="ml-1 size-7 fill-black text-black md:size-8" />
</span>
</button>
)}
</div>
);
}
Binary file addedapps/docs/public/hero-cover-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading