Skip to content

Commit eeec62a

Browse files
os-zhuangclaude
andauthored
feat(docs-site): lead the homepage with the 90-second overview, link the YouTube channel (#12218)
The README has carried the overview video since #11941 (a cover linking to https://youtu.be/CX_FlOoOtr0); the site homepage had nothing. It now plays there, in the position a product video is normally given — directly under the headline, before anything else. - `components/youtube-embed.tsx`: a click-to-play facade. Nothing is requested from YouTube until the viewer clicks, so the homepage costs no third-party frame, script or cookie on load; the click swaps in a youtube-nocookie iframe with autoplay. The cover is the README's own `hero-cover-dark.png` (copied into `public/`), which is 2:1 while the video is 16:9 — hence a fixed 2:1 frame with the player centred inside it at its own aspect ratio. Cropping the cover to 16:9 cuts through the logo on its left edge, and re-shaping the frame on click would shove the page down mid-interaction. - The hero is now a single centred column (eyebrow, h1, lead, CTAs, chips) with the video full-width beneath it. Side by side with a three-paragraph text column the video was small and floated in the middle of the row; the two paragraphs it displaced now sit under the video as a two-column band, and the artifact card moved to "01 · The loop", which is what it depicts. - The YouTube channel is linked twice: a `YouTube` button in the hero CTA row next to `GitHub`, and "More videos on YouTube" under the video. Verified in the browser at 1440/1280/500px, light and dark: poster renders, click loads the nocookie iframe at 16:9 and plays, no horizontal overflow. Docs-site only, no published package — no changeset (`skip-changeset`). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent cd25417 commit eeec62a

3 files changed

Lines changed: 192 additions & 85 deletions

File tree

apps/docs/app/[lang]/page.tsx

Lines changed: 128 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { ArrowRight, Check } from 'lucide-react';
44
import { Bricolage_Grotesque, IBM_Plex_Mono } from 'next/font/google';
55
import { HomeLayout } from 'fumadocs-ui/layouts/home';
66
import { baseOptions, gitConfig } from '@/lib/layout.shared';
7+
import { YouTubeEmbed } from '@/components/youtube-embed';
8+
9+
/** The 90-second overview — the same video the README's hero cover links to. */
10+
const OVERVIEW_VIDEO_ID = 'CX_FlOoOtr0';
11+
const YOUTUBE_CHANNEL_URL = 'https://www.youtube.com/@objectstack';
712

813
const display = Bricolage_Grotesque({
914
subsets: ['latin'],
@@ -105,72 +110,140 @@ export default function HomePage() {
105110
/>
106111

107112
{/* ── hero ─────────────────────────────────────────────── */}
108-
<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">
109-
<div>
110-
<p className="text-xs font-medium tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
111-
Open protocol &amp; runtime · Apache-2.0
112-
</p>
113-
<h1
114-
className="mt-5 text-[2.6rem]/[1.06] font-bold tracking-tight text-balance md:text-6xl/[1.04]"
115-
style={{ fontFamily: 'var(--l-display)' }}
113+
<section className="relative mx-auto w-full max-w-5xl px-6 pt-16 text-center md:pt-20">
114+
<p className="text-xs font-medium tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
115+
Open protocol &amp; runtime · Apache-2.0
116+
</p>
117+
<h1
118+
className="mx-auto mt-5 max-w-3xl text-[2.6rem]/[1.06] font-bold tracking-tight text-balance md:text-6xl/[1.04]"
119+
style={{ fontFamily: 'var(--l-display)' }}
120+
>
121+
Apps small enough{' '}
122+
<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">
123+
for AI to hold whole.
124+
</span>
125+
</h1>
126+
<p className="mx-auto mt-6 max-w-2xl text-lg text-fd-muted-foreground text-pretty">
127+
ObjectStack turns the whole app — data model, UI, workflows, permissions — into
128+
typed metadata: a complete CRM in under 150k tokens, one context window. Agents
129+
read it whole, reason it whole, refactor it whole.
130+
</p>
131+
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
132+
<Link
133+
href="/docs/getting-started"
134+
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"
116135
>
117-
Apps small enough{' '}
118-
<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">
119-
for AI to hold whole.
120-
</span>
121-
</h1>
122-
<p className="mt-6 max-w-xl text-lg text-fd-muted-foreground text-pretty">
123-
ObjectStack turns the whole app — data model, UI, workflows, permissions — into
124-
typed metadata: a complete CRM in under 150k tokens, one context window. Agents
125-
read it whole, reason it whole, refactor it whole.
126-
</p>
127-
<p className="mt-4 max-w-xl text-lg text-fd-muted-foreground text-pretty">
136+
Get started
137+
<ArrowRight className="size-4" />
138+
</Link>
139+
<Link
140+
href="/docs"
141+
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"
142+
>
143+
Documentation
144+
</Link>
145+
<a
146+
href={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}
147+
target="_blank"
148+
rel="noreferrer noopener"
149+
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"
150+
>
151+
GitHub
152+
</a>
153+
<a
154+
href={YOUTUBE_CHANNEL_URL}
155+
target="_blank"
156+
rel="noreferrer noopener"
157+
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"
158+
>
159+
YouTube
160+
</a>
161+
</div>
162+
<div
163+
className="mt-8 flex flex-wrap items-center justify-center gap-x-3 gap-y-2 text-[13px] text-fd-muted-foreground"
164+
style={{ fontFamily: 'var(--l-mono)' }}
165+
>
166+
<span>Fits in an agent&apos;s context</span>
167+
<span aria-hidden className="text-fd-border">|</span>
168+
<span>Typed, validated, governed</span>
169+
<span aria-hidden className="text-fd-border">|</span>
170+
<span>Self-host anywhere</span>
171+
</div>
172+
</section>
173+
174+
{/* ── the 90-second overview — the page's first visual ──── */}
175+
<section className="relative mx-auto w-full max-w-5xl px-6 pt-12 md:pt-14">
176+
<figure className="relative">
177+
<YouTubeEmbed
178+
videoId={OVERVIEW_VIDEO_ID}
179+
title="ObjectStack in 90 Seconds"
180+
poster="/hero-cover-dark.png"
181+
/>
182+
<figcaption
183+
className="mt-3 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[12px] text-fd-muted-foreground"
184+
style={{ fontFamily: 'var(--l-mono)' }}
185+
>
186+
<span>▶ ObjectStack in 90 Seconds</span>
187+
<a
188+
href={YOUTUBE_CHANNEL_URL}
189+
target="_blank"
190+
rel="noreferrer noopener"
191+
className="inline-flex items-center gap-1 text-fd-foreground underline underline-offset-4 transition-colors hover:text-fd-primary"
192+
>
193+
More videos on YouTube
194+
<ArrowRight className="size-3" />
195+
</a>
196+
</figcaption>
197+
</figure>
198+
</section>
199+
200+
{/* ── the claim the video makes, in prose ──────────────── */}
201+
<section className="relative mx-auto w-full max-w-5xl px-6 pt-12 md:pt-14">
202+
<div className="grid gap-8 md:grid-cols-2">
203+
<p className="text-lg text-fd-muted-foreground text-pretty">
128204
The business logic alone — every object, workflow and permission — is under 100k
129205
tokens; the UI adds just 50k more.
130206
</p>
131-
<p className="mt-4 max-w-xl text-base text-fd-muted-foreground text-pretty">
207+
<p className="text-base text-fd-muted-foreground text-pretty">
132208
That metadata is your business ontology — an open, versioned definition you own.
133-
Strict TypeScript, Zod schemas, and a validation gate catch the agent's mistakes
209+
Strict TypeScript, Zod schemas, and a validation gate catch the agent&apos;s mistakes
134210
at authoring time, and the runtime derives the database, REST API, UI, and MCP
135211
server — permissions and audit enforced on every call.
136212
</p>
137-
<div className="mt-8 flex flex-wrap items-center gap-3">
138-
<Link
139-
href="/docs/getting-started"
140-
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"
141-
>
142-
Get started
143-
<ArrowRight className="size-4" />
144-
</Link>
145-
<Link
146-
href="/docs"
147-
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"
148-
>
149-
Documentation
150-
</Link>
151-
<a
152-
href={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}
153-
target="_blank"
154-
rel="noreferrer noopener"
155-
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"
156-
>
157-
GitHub
158-
</a>
159-
</div>
160-
<div
161-
className="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[13px] text-fd-muted-foreground"
162-
style={{ fontFamily: 'var(--l-mono)' }}
163-
>
164-
<span>Fits in an agent's context</span>
165-
<span aria-hidden className="text-fd-border">|</span>
166-
<span>Typed, validated, governed</span>
167-
<span aria-hidden className="text-fd-border">|</span>
168-
<span>Self-host anywhere</span>
169-
</div>
213+
</div>
214+
</section>
215+
216+
{/* ── 01 · the loop ────────────────────────────────────── */}
217+
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
218+
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
219+
<span className="text-xs tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
220+
01 · The loop
221+
</span>
222+
</div>
223+
<h2 className="mt-4 max-w-2xl text-3xl font-bold tracking-tight md:text-4xl" style={{ fontFamily: 'var(--l-display)' }}>
224+
From requirement to running app
225+
</h2>
226+
<div className="mt-10 grid gap-8 md:grid-cols-3">
227+
{STEPS.map((s) => (
228+
<div key={s.num} className="group relative">
229+
<div className="flex items-center gap-3">
230+
<span className="text-sm text-fd-muted-foreground" style={{ fontFamily: 'var(--l-mono)' }}>{s.num}</span>
231+
<div className="h-px flex-1 bg-fd-border" aria-hidden />
232+
</div>
233+
<h3 className="mt-4 text-lg font-semibold" style={{ fontFamily: 'var(--l-display)' }}>{s.title}</h3>
234+
<p
235+
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"
236+
style={{ fontFamily: 'var(--l-mono)' }}
237+
>
238+
{s.cmd}
239+
</p>
240+
<p className="mt-3 text-[15px] text-fd-muted-foreground text-pretty">{s.copy}</p>
241+
</div>
242+
))}
170243
</div>
171244

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

220-
{/* ── 01 · the loop ────────────────────────────────────── */}
221-
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
222-
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
223-
<span className="text-xs tracking-[0.18em] text-fd-muted-foreground uppercase" style={{ fontFamily: 'var(--l-mono)' }}>
224-
01 · The loop
225-
</span>
226-
</div>
227-
<h2 className="mt-4 max-w-2xl text-3xl font-bold tracking-tight md:text-4xl" style={{ fontFamily: 'var(--l-display)' }}>
228-
From requirement to running app
229-
</h2>
230-
<div className="mt-10 grid gap-8 md:grid-cols-3">
231-
{STEPS.map((s) => (
232-
<div key={s.num} className="group relative">
233-
<div className="flex items-center gap-3">
234-
<span className="text-sm text-fd-muted-foreground" style={{ fontFamily: 'var(--l-mono)' }}>{s.num}</span>
235-
<div className="h-px flex-1 bg-fd-border" aria-hidden />
236-
</div>
237-
<h3 className="mt-4 text-lg font-semibold" style={{ fontFamily: 'var(--l-display)' }}>{s.title}</h3>
238-
<p
239-
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"
240-
style={{ fontFamily: 'var(--l-mono)' }}
241-
>
242-
{s.cmd}
243-
</p>
244-
<p className="mt-3 text-[15px] text-fd-muted-foreground text-pretty">{s.copy}</p>
245-
</div>
246-
))}
247-
</div>
248-
</section>
249-
250293
{/* ── 02 · four gates ──────────────────────────────────── */}
251294
<section className="relative mx-auto w-full max-w-6xl px-6 py-16 md:py-20">
252295
<div className="flex items-baseline gap-4 border-t border-fd-border pt-8">
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Play } from 'lucide-react';
5+
6+
type YouTubeEmbedProps = {
7+
/** The video id — the part after `youtu.be/`. */
8+
videoId: string;
9+
/** Video title. Used for the iframe title and the play button's label. */
10+
title: string;
11+
/** Cover shown until the viewer clicks play. Served from `public/`. */
12+
poster: string;
13+
};
14+
15+
/**
16+
* Click-to-play YouTube embed.
17+
*
18+
* A facade, not a bare `<iframe>`: nothing is requested from YouTube until the
19+
* viewer actually clicks play, so the homepage costs no third-party frame,
20+
* script or cookie on load. The cover is the same one the README links from
21+
* (`docs/screenshots/hero-cover-dark.png`, copied into `public/`), which is 2:1
22+
* rather than the video's 16:9 — hence the fixed 2:1 frame with the player
23+
* centred inside it at its own aspect ratio. Cropping the cover to 16:9 would
24+
* cut through the logo on the left edge, and letting the frame change shape on
25+
* click would shove the rest of the page down mid-interaction.
26+
*/
27+
export function YouTubeEmbed({ videoId, title, poster }: YouTubeEmbedProps) {
28+
const [playing, setPlaying] = useState(false);
29+
30+
return (
31+
<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)]">
32+
{playing ? (
33+
<iframe
34+
className="absolute inset-0 m-auto aspect-video h-full"
35+
src={`https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`}
36+
title={title}
37+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
38+
referrerPolicy="strict-origin-when-cross-origin"
39+
allowFullScreen
40+
/>
41+
) : (
42+
<button
43+
type="button"
44+
onClick={() => setPlaying(true)}
45+
aria-label={`Play the video: ${title}`}
46+
className="group absolute inset-0 h-full w-full cursor-pointer"
47+
>
48+
{/* Plain <img>, as everywhere else on this site — see lib/layout.shared.tsx. */}
49+
<img src={poster} alt="" className="h-full w-full object-cover" loading="lazy" decoding="async" />
50+
<span
51+
aria-hidden
52+
className="absolute inset-0 bg-black/10 transition-colors group-hover:bg-black/25"
53+
/>
54+
<span
55+
aria-hidden
56+
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"
57+
>
58+
<Play className="ml-1 size-7 fill-black text-black md:size-8" />
59+
</span>
60+
</button>
61+
)}
62+
</div>
63+
);
64+
}
397 KB
Loading

0 commit comments

Comments
 (0)