From 0b6092e0e13de8af59d9aada3121cda387c7ab30 Mon Sep 17 00:00:00 2001
From: dbjpanda
Date: Fri, 25 Sep 2026 03:49:22 +0530
Subject: [PATCH] perf(www): the landing gets its own root layout and
scroll-mounted islands
Two root layouts. app/(site) keeps the fumadocs shell for docs, blog,
compare, brand and the API routes: provider, global stylesheet, search.
app/(home) is now a root layout of its own with a next-themes provider,
Tailwind's preflight and a mirror of the design tokens (landing-base.css),
and nothing else. The landing used two utilities from the global sheet
and paid for all of it, ~95 KB of CSS before compression plus the search
bundle, on every visit. Same theme storage key, so the toggle on either
side is seen by the other.
The showcases that carry no copy (the dashboard scene, the beam diagram,
the particle wordmark) mount when the reader scrolls within a screen of
them, through a dynamic import that nothing preloads. Each reserves its
measured height first, so nothing moves. Every section with text stays
server-rendered and hydrates as before.
Mobile Lighthouse (software GL, simulated slow 4G): 88 -> 91-92, LCP
3.9 s -> 3.2-3.5 s, CLS 0. Desktop stays 100. Metadata files at app/
(sitemap, robots, icons, share images) apply to both layouts unchanged.
---
www/app/(home)/ThemeToggle.tsx | 8 +-
www/app/(home)/Veil.tsx | 5 +-
www/app/(home)/Wordmark.tsx | 16 +-
www/app/(home)/dashboard-scene.tsx | 17 ++
www/app/(home)/home.css | 11 +
www/app/(home)/islands.tsx | 75 ++++++
www/app/(home)/landing-base.css | 225 ++++++++++++++++++
www/app/(home)/layout.tsx | 88 ++++---
www/app/(home)/page.tsx | 13 +-
www/app/{ => (site)}/(content)/brand/page.tsx | 0
www/app/{ => (site)}/(content)/layout.tsx | 0
www/app/{ => (site)}/api/search/route.ts | 0
www/app/{ => (site)}/blog/[slug]/page.tsx | 0
www/app/{ => (site)}/blog/layout.tsx | 0
www/app/{ => (site)}/blog/page.tsx | 0
www/app/{ => (site)}/compare/[slug]/page.tsx | 0
www/app/{ => (site)}/compare/page.tsx | 0
.../{ => (site)}/docs/[[...slug]]/page.tsx | 0
www/app/{ => (site)}/docs/layout.tsx | 0
www/app/{ => (site)}/layout.tsx | 2 +-
www/app/{ => (site)}/llms-full.txt/route.ts | 0
.../llms.mdx/docs/[[...slug]]/route.ts | 0
www/app/{ => (site)}/llms.txt/route.ts | 0
.../{ => (site)}/og/docs/[...slug]/route.tsx | 0
www/bun.lock | 1 +
www/package.json | 1 +
26 files changed, 405 insertions(+), 57 deletions(-)
create mode 100644 www/app/(home)/dashboard-scene.tsx
create mode 100644 www/app/(home)/islands.tsx
create mode 100644 www/app/(home)/landing-base.css
rename www/app/{ => (site)}/(content)/brand/page.tsx (100%)
rename www/app/{ => (site)}/(content)/layout.tsx (100%)
rename www/app/{ => (site)}/api/search/route.ts (100%)
rename www/app/{ => (site)}/blog/[slug]/page.tsx (100%)
rename www/app/{ => (site)}/blog/layout.tsx (100%)
rename www/app/{ => (site)}/blog/page.tsx (100%)
rename www/app/{ => (site)}/compare/[slug]/page.tsx (100%)
rename www/app/{ => (site)}/compare/page.tsx (100%)
rename www/app/{ => (site)}/docs/[[...slug]]/page.tsx (100%)
rename www/app/{ => (site)}/docs/layout.tsx (100%)
rename www/app/{ => (site)}/layout.tsx (99%)
rename www/app/{ => (site)}/llms-full.txt/route.ts (100%)
rename www/app/{ => (site)}/llms.mdx/docs/[[...slug]]/route.ts (100%)
rename www/app/{ => (site)}/llms.txt/route.ts (100%)
rename www/app/{ => (site)}/og/docs/[...slug]/route.tsx (100%)
diff --git a/www/app/(home)/ThemeToggle.tsx b/www/app/(home)/ThemeToggle.tsx
index 2dd97157..0b59ebc8 100644
--- a/www/app/(home)/ThemeToggle.tsx
+++ b/www/app/(home)/ThemeToggle.tsx
@@ -1,11 +1,9 @@
'use client';
import { useEffect, useState } from 'react';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty, so setTheme()
-// silently no-ops. fumadocs re-exports the matching hook for this reason.
-import { useTheme } from 'fumadocs-ui/provider/base';
+// The landing has its own next-themes provider (see layout.tsx), so the hook
+// comes from next-themes directly.
+import { useTheme } from 'next-themes';
// The landing's own theme control.
//
diff --git a/www/app/(home)/Veil.tsx b/www/app/(home)/Veil.tsx
index b944aaaf..5688a1dd 100644
--- a/www/app/(home)/Veil.tsx
+++ b/www/app/(home)/Veil.tsx
@@ -1,9 +1,6 @@
'use client';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty.
-import { useTheme } from 'fumadocs-ui/provider/base';
+import { useTheme } from 'next-themes';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
diff --git a/www/app/(home)/Wordmark.tsx b/www/app/(home)/Wordmark.tsx
index a551c228..0c75b5f4 100644
--- a/www/app/(home)/Wordmark.tsx
+++ b/www/app/(home)/Wordmark.tsx
@@ -1,11 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty.
-import { useTheme } from 'fumadocs-ui/provider/base';
-import { ParticleText } from './ParticleText';
+import { useTheme } from 'next-themes';
+import dynamic from 'next/dynamic';
+import { LazyIsland } from './islands';
+
+// The particle field is the last thing on a page twelve thousand pixels tall.
+// Its code loads when the footer comes within a screen, not before.
+const ParticleText = dynamic(() => import('./ParticleText').then((m) => m.ParticleText), { ssr: false });
// The wordmark that closes the page.
//
@@ -39,7 +41,7 @@ export function Wordmark() {
const tuning = mounted && resolvedTheme === 'light' ? LIGHT : DARK;
return (
-
+
);
}
diff --git a/www/app/(home)/dashboard-scene.tsx b/www/app/(home)/dashboard-scene.tsx
new file mode 100644
index 00000000..c433026c
--- /dev/null
+++ b/www/app/(home)/dashboard-scene.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { BrowserFrame } from './BrowserFrame';
+import { DashboardShowcase } from './DashboardShowcase';
+import { MacbookScroll } from './MacbookScroll';
+
+// The dashboard showcase and its frame, in one module so the island's single
+// dynamic import brings all of it and none of it lands in the page bundle.
+export function DashboardScene() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/www/app/(home)/home.css b/www/app/(home)/home.css
index c72cb1b1..a765d9eb 100644
--- a/www/app/(home)/home.css
+++ b/www/app/(home)/home.css
@@ -828,3 +828,14 @@ html:not(.dark) .hp-veil canvas {
}
/* No light .hp-veil-scrim override. The scrim derives from --background, which
is already white here. */
+
+/* Scroll-mounted islands (islands.tsx). Each reserves the height its content
+ takes at that breakpoint, measured on the built page, so mounting later
+ moves nothing below it. The dashboard scene is a landscape mock that
+ stacks on phones; the beam diagram is the reverse. */
+.island--dashboard { min-height: 550px; }
+.island--beam { min-height: 556px; }
+@media (min-width: 1024px) {
+ .island--dashboard { min-height: 689px; }
+ .island--beam { min-height: 258px; }
+}
diff --git a/www/app/(home)/islands.tsx b/www/app/(home)/islands.tsx
new file mode 100644
index 00000000..4635b476
--- /dev/null
+++ b/www/app/(home)/islands.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
+
+// Scroll-mounted islands for the landing page's visual showcases.
+//
+// These components carry no copy, only a mock UI or a diagram, so nothing is
+// lost by leaving them out of the server-rendered HTML. What is gained: their
+// JavaScript (framer-style animation, the dashboard mock, the beam diagram,
+// the particle wordmark) is neither downloaded nor hydrated until the reader
+// scrolls within a screen of them. Everything with text on this page stays
+// server-rendered and hydrates as before.
+//
+// Each island reserves its height up front so mounting later moves nothing
+// (CLS). The sizes are the components' natural heights at the widths the
+// page is laid out for; see the notes on each.
+
+export function LazyIsland({
+ children,
+ style,
+ className,
+ rootMargin = '600px 0px',
+}: {
+ children: ReactNode;
+ style?: CSSProperties;
+ className?: string;
+ rootMargin?: string;
+}) {
+ const ref = useRef(null);
+ const [near, setNear] = useState(false);
+ useEffect(() => {
+ const el = ref.current;
+ if (!el || near) return;
+ if (typeof IntersectionObserver === 'undefined') {
+ setNear(true);
+ return;
+ }
+ const io = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setNear(true);
+ io.disconnect();
+ }
+ },
+ { rootMargin },
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, [near, rootMargin]);
+ return (
+
+ {near ? children : null}
+
+ );
+}
+
+const DashboardScene = dynamic(() => import('./dashboard-scene').then((m) => m.DashboardScene), { ssr: false });
+const BeamDiagram = dynamic(() => import('./BeamDiagram').then((m) => m.BeamDiagram), { ssr: false });
+
+export function DashboardIsland() {
+ return (
+
+
+
+ );
+}
+
+export function BeamIsland() {
+ return (
+
+
+
+ );
+}
diff --git a/www/app/(home)/landing-base.css b/www/app/(home)/landing-base.css
new file mode 100644
index 00000000..65c38c32
--- /dev/null
+++ b/www/app/(home)/landing-base.css
@@ -0,0 +1,225 @@
+/* The landing page's base sheet.
+ *
+ * The landing has its own root layout and does not load global.css (Tailwind,
+ * the fumadocs theme and the docs chrome, ~95 KB before compression, of which
+ * the landing used two utilities). What it does need from there: Tailwind's
+ * preflight resets, which home.css and landing.css were written on top of,
+ * and the design tokens, which are copied below so both sheets resolve the
+ * same values. Edit the tokens in global.css first, then mirror here. */
+
+@import 'tailwindcss/preflight.css';
+
+:root {
+ --radius: 0.625rem;
+
+ /* ------------------------------------------------------------------
+ Neutral greyscale, matching payloadcms.com.
+
+ Their whole palette is achromatic: 51 colour tokens, every base step
+ R=G=B, no brand hue anywhere. The only chromatic values on the site are
+ semantic status colours. All the warmth in their hero comes from the
+ background video, not from the theme.
+
+ We keep the neutral surfaces and spend the colour on --primary instead,
+ since we have no hero video to carry it. The tokens that used to hold
+ that video's measured palette (--wave-*, --field-*, and the three
+ opacity values beside them) are gone: nothing referenced them, in CSS
+ or in TSX, after the hero they belonged to was replaced.
+
+ Lightness steps below are read off their base scale:
+ 255 -> 100% 221 -> 88.4% 154 -> 66% 47 -> 25%
+ 245 -> 96.5% 208 -> 84% 101 -> 47.3% 20 -> 11.5%
+ 235 -> 93% 181 -> 75% 74 -> 36.8% 7 -> 4.5%
+ ------------------------------------------------------------------ */
+
+ /* shadcn layer — light */
+ --background: oklch(100% 0 0);
+ --foreground: oklch(18.7% 0 0);
+ --card: oklch(100% 0 0);
+ --card-foreground: oklch(18.7% 0 0);
+ --popover: oklch(100% 0 0);
+ --popover-foreground: oklch(18.7% 0 0);
+ /* Ink, not a hue. --primary drives buttons, active states, the marked phrase
+ and the focus ring, and here it is simply the darkest neutral, inverting to
+ the lightest one on the dark theme. The interface stays monochrome.
+
+ Swapping this for a chromatic value is the single edit that colours the
+ whole site, because everything downstream reads it. */
+ --primary: oklch(18.7% 0 0);
+ --primary-foreground: oklch(100% 0 0);
+ --secondary: oklch(96.5% 0 0);
+ --secondary-foreground: oklch(25% 0 0);
+ --muted: oklch(96.5% 0 0);
+ --muted-foreground: oklch(47.3% 0 0);
+ --accent: oklch(93% 0 0);
+ --accent-foreground: oklch(31% 0 0);
+ /* Destructive keeps its hue. Payload keeps chroma for status too: a neutral
+ palette is a brand decision, not a reason to make errors unreadable. */
+ --destructive: oklch(58% 0.21 25);
+ --border: oklch(88.4% 0 0);
+ --input: oklch(88.4% 0 0);
+ --ring: oklch(47.3% 0 0);
+
+ /* fumadocs layer.
+ *
+ * Aliases, not copies. These were the same sixteen values typed a second
+ * time, with nothing tying the two sets together, so editing one and not the
+ * other was silent and the docs drifted away from everything else.
+ *
+ * Declared once, here, and NOT repeated under .dark. Each one resolves
+ * through a shadcn token that .dark already redefines, so the docs follow
+ * the theme with no second block. A second block is the copy that drifts. */
+ --color-fd-background: var(--background);
+ --color-fd-foreground: var(--foreground);
+ --color-fd-card: var(--card);
+ --color-fd-card-foreground: var(--card-foreground);
+ --color-fd-popover: var(--popover);
+ --color-fd-popover-foreground: var(--popover-foreground);
+ --color-fd-muted: var(--muted);
+ --color-fd-muted-foreground: var(--muted-foreground);
+ --color-fd-secondary: var(--secondary);
+ --color-fd-secondary-foreground: var(--secondary-foreground);
+ --color-fd-border: var(--border);
+ --color-fd-primary: var(--primary);
+ --color-fd-primary-foreground: var(--primary-foreground);
+ --color-fd-ring: var(--ring);
+ --color-fd-accent: var(--accent);
+ --color-fd-accent-foreground: var(--accent-foreground);
+
+ /* ------------------------------------------------------------------
+ The semantic steps shadcn does not provide.
+
+ shadcn gives one --border and one --card. The landing uses four edge
+ weights and three translucent panel washes, so those extra steps are
+ derived here instead of being hardcoded in home.css. That file used to
+ hold them as literals, which is why it needed a whole second copy of
+ itself for light mode.
+ ------------------------------------------------------------------ */
+
+ /* Edge weights. These were rgba(black, a) on white and rgba(white, a) on
+ black, which is exactly a foreground mix, so they carry over without loss.
+ The alphas differ per theme on purpose: a black hairline on white reads
+ heavier than a white one on black at the same alpha. */
+ --rule-1: color-mix(in srgb, var(--foreground) 8%, transparent);
+ --rule-2: color-mix(in srgb, var(--foreground) 13%, transparent);
+ --rule-3: color-mix(in srgb, var(--foreground) 30%, transparent);
+ --rule-frame: color-mix(in srgb, var(--foreground) 14%, transparent);
+ --rule-grid: color-mix(in srgb, var(--foreground) 9%, transparent);
+
+ /* Panel washes. Translucent because they sit over the animated canvas. */
+ --wash-1: color-mix(in srgb, var(--card) 72%, transparent);
+ --wash-2: color-mix(in srgb, var(--card) 86%, transparent);
+ --wash-3: color-mix(in srgb, var(--card) 92%, transparent);
+
+ /* A shadow is an absence of light, not a colour from the palette, so this
+ stays black in both themes and only its strength changes. Deriving it from
+ --foreground would make it white on the dark theme, which is a glow. */
+ --shadow-page: 0 30px 70px rgb(0 0 0 / 0.1);
+
+ /* Syntax colours. Code cannot go monochrome the way the interface can: a
+ single-colour listing reads as a quote block rather than as code. So this
+ is the one part of the theme that stays chromatic while --primary is ink.
+
+ Six hues at one lightness and one chroma, so only the hue varies. Holding L
+ and C is what keeps them distinguishable from each other and readable on
+ the page at the same time.
+
+ --chroma-h is a real anchor rather than a reading of --primary's hue, and
+ that is deliberate. An achromatic --primary has no meaningful hue: the
+ browser serialises it with a floating-point crumb in the b channel, which
+ came out as roughly 112 degrees rather than 0. Anchoring a palette on that
+ is anchoring it on rounding noise. Two knobs are worth the honesty.
+
+ At 25 the ramp runs red, orange, yellow-green, green, blue, magenta, which
+ is close to how most editors colour code already. Move this one number to
+ rotate the whole palette; if --primary is ever given a hue and the code
+ should follow it, set this to the same value. */
+ --chroma-h: 25;
+ /* The one fixed colour in the brand mark: the reconciled cell. See components/brand-mark.tsx. */
+ --brand-violet: #6d4bd4;
+ --chroma-l: 0.48;
+ --chroma-c: 0.15;
+ --chroma-1: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 0));
+ --chroma-2: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 40));
+ --chroma-3: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 80));
+ --chroma-4: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 130));
+ --chroma-5: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 200));
+ --chroma-6: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 300));
+}
+
+.dark {
+ /* shadcn layer — dark */
+ --background: oklch(4.5% 0 0);
+ --foreground: oklch(100% 0 0);
+ --card: oklch(11.5% 0 0);
+ --card-foreground: oklch(100% 0 0);
+ --popover: oklch(11.5% 0 0);
+ --popover-foreground: oklch(100% 0 0);
+ /* The same ink, inverted. Black primary on a black page would be invisible,
+ so the dark theme takes the lightest neutral and puts the dark one on top. */
+ --primary: oklch(100% 0 0);
+ --primary-foreground: oklch(4.5% 0 0);
+ --secondary: oklch(18.7% 0 0);
+ --secondary-foreground: oklch(93% 0 0);
+ --muted: oklch(18.7% 0 0);
+ --muted-foreground: oklch(66% 0 0);
+ --accent: oklch(25% 0 0);
+ --brand-violet: #a78bfa;
+ --accent-foreground: oklch(84% 0 0);
+ --destructive: oklch(70% 0.19 22);
+ --border: oklch(25% 0 0);
+ --input: oklch(25% 0 0);
+ --ring: oklch(66% 0 0);
+
+ /* No --color-fd-* here on purpose. They alias the shadcn tokens in :root,
+ and every one of those is redefined above, so the docs are already dark.
+ Repeating them here is what made the two sets drift.
+
+ The --rule-*, --wash-* and --chroma-* steps are not repeated either: they
+ are built from --foreground, --card and --primary, all redefined above.
+ Only the values that genuinely differ by theme are listed below. */
+
+ /* Below about 7% a white hairline composites to #0e0e0e on black and reads
+ as nothing, so the dark ground needs its own alphas rather than the ones
+ tuned for paper. */
+ --rule-1: color-mix(in srgb, var(--foreground) 7.5%, transparent);
+ --rule-2: color-mix(in srgb, var(--foreground) 12%, transparent);
+ --rule-3: color-mix(in srgb, var(--foreground) 28%, transparent);
+ --rule-frame: color-mix(in srgb, var(--foreground) 16%, transparent);
+ --rule-grid: color-mix(in srgb, var(--foreground) 10%, transparent);
+
+ --wash-1: color-mix(in srgb, var(--card) 66%, transparent);
+ --wash-2: color-mix(in srgb, var(--card) 72%, transparent);
+ --wash-3: color-mix(in srgb, var(--card) 86%, transparent);
+
+ /* A black page can carry a heavy shadow because it has nowhere to go. White
+ cannot: at 0.8 the screenshots look like they are floating off the sheet. */
+ --shadow-page: 0 30px 70px rgb(0 0 0 / 0.8);
+
+ /* Lifted for the dark ground. Same hues, read against black instead of white. */
+ --chroma-l: 0.78;
+ --chroma-c: 0.13;
+}
+
+html {
+ scrollbar-gutter: stable;
+}
+
+/* No page-level horizontal scroll (clip, never hidden — preserves sticky). */
+html,
+body {
+ overflow-x: clip;
+}
+
+/* The one utility the landing used from Tailwind. */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
diff --git a/www/app/(home)/layout.tsx b/www/app/(home)/layout.tsx
index 7955f1e7..7f1c3614 100644
--- a/www/app/(home)/layout.tsx
+++ b/www/app/(home)/layout.tsx
@@ -1,35 +1,37 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
-import { Inter, JetBrains_Mono } from 'next/font/google';
+import { Inter, JetBrains_Mono, Outfit } from 'next/font/google';
+import Script from 'next/script';
+import { ThemeProvider } from 'next-themes';
+import './landing-base.css';
import './home.css';
import { Veil } from './Veil';
import { GridLines } from './Grid';
import { Header } from './Header';
import { Footer } from './Footer';
-// The landing.
+// The landing, as its own root layout.
+//
+// The rest of the site sits under app/(site) with the fumadocs shell: its
+// provider, its global stylesheet (Tailwind plus the docs theme), its search
+// dialog. The landing used two utilities from that sheet and none of the
+// rest, but paid for all of it on every visit: ~95 KB of CSS before
+// compression and the search bundle in the critical path. Two root layouts
+// let the landing ship only what it draws. Same design tokens (mirrored in
+// landing-base.css), same theme storage key, so the toggle here and the one
+// in the docs stay in step.
//
// Marketing chrome of its own rather than the shared Navbar and Footer: a
// compact hairline header, a four-column footer under a giant wordmark, and
-// three grid lines on the quarter points of the content column. The docs keep
-// the shared shell, so the two deliberately differ.
+// three grid lines on the quarter points of the content column.
//
// Behind all of it, PixelBlast: an fbm field thresholded against an 8x8 Bayer
// matrix, so the backdrop is a grid of hard on/off cells rather than a
// gradient. It has a texture, which suits a page whose own structure is three
-// hairlines. Clicking anywhere sends a ring through it.
-//
-// The canvas is fixed to the viewport rather than sized to the document. One
-// screen of GL covers a page twelve thousand pixels tall, and the field holds
-// still while the page scrolls over it.
-//
-// Everything above it is translucent by a few points in home.css, which lets
-// the violet through the sections instead of only between them.
-//
-// Follows the site light/dark toggle, shared with the docs through the single
-// RootProvider in the root layout. Dark is the set this was drawn in; light is
-// the same composition on white, with the backdrop dropped back and its scrim
-// inverted. See the light block in home.css.
+// hairlines. Clicking anywhere sends a ring through it. The canvas is fixed to
+// the viewport rather than sized to the document, and everything above it is
+// translucent by a few points in home.css, which lets the violet through the
+// sections instead of only between them.
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
@@ -40,8 +42,17 @@ const mono = JetBrains_Mono({
weight: ['400', '500'],
variable: '--font-jetbrains-mono',
});
+// Display headings below the fold (landing.css --font-display). Outfit stands
+// in for Gilroy; see the note in app/(site)/layout.tsx.
+const outfit = Outfit({
+ subsets: ['latin'],
+ variable: '--font-outfit',
+});
export const metadata: Metadata = {
+ metadataBase: new URL('https://concile.dev'),
+ alternates: { canonical: './' },
+ verification: { google: 'kiKhCzpdz32bLp_YjGbYMjPcRlpWIlyHkIpXq_zGP9I' },
/* Title and description carry the same words as the H1, because a visitor who
searched one of them should land on a page that repeats it back. "Realtime"
rather than "reactive": reactive is Convex's brand term, realtime is what
@@ -53,18 +64,35 @@ export const metadata: Metadata = {
export default function HomeLayout({ children }: { children: ReactNode }) {
return (
-
-
- {/* Tuned per theme inside Veil rather than filtered here. See Veil.tsx. */}
-
-
-
-
-
-
- {children}
-
-
-
+
+
+ {/* Same settings as fumadocs' RootProvider uses in the (site) layout, so
+ the class on and the "theme" key in storage mean the same
+ thing on both sides. */}
+
+
+
+ {/* Tuned per theme inside Veil rather than filtered here. See Veil.tsx. */}
+
+
+
+
+
+
+ {children}
+
+
+
+
+ {/* Microsoft Clarity, production only, loaded after hydration. The
+ element id must not be "clarity": browsers expose every id as a
+ window global and the loader reads window.clarity. */}
+ {process.env.NODE_ENV === 'production' ? (
+
+ ) : null}
+
+
);
}
diff --git a/www/app/(home)/page.tsx b/www/app/(home)/page.tsx
index 7780797a..2b460512 100644
--- a/www/app/(home)/page.tsx
+++ b/www/app/(home)/page.tsx
@@ -2,10 +2,10 @@ import Link from 'next/link';
import './landing.css';
import { STACK_LOGOS } from './stack-logos';
import { Hero } from './Hero';
+import { BeamIsland, DashboardIsland } from './islands';
import { Reveal } from './Reveal';
import { SpotlightGrid } from './SpotlightGrid';
import { StatNumber } from './StatNumber';
-import { BeamDiagram } from './BeamDiagram';
import { Steps } from './Steps';
import { FeatureCards } from './FeatureCards';
import { Components } from './Components';
@@ -13,9 +13,6 @@ import { Pillars } from './Pillars';
import { ComparisonTable } from './ComparisonTable';
import { Faq } from './Faq';
import { CopyCommand } from './CopyCommand';
-import { DashboardShowcase } from './DashboardShowcase';
-import { BrowserFrame } from './BrowserFrame';
-import { MacbookScroll } from './MacbookScroll';
export default function HomePage() {
return (
@@ -81,11 +78,7 @@ export default function HomePage() {
We include a built-in dashboard that runs on the exact same origin as your app. It uses live subscriptions, meaning you see writes land in the database immediately. We didn't even build a refresh button, because you'll never need it.
-
-
-
-
-
+
{/* ---------------- REACTIVITY DIAGRAM ---------------- */}
@@ -102,7 +95,7 @@ export default function HomePage() {
Read about reactivity
-
+
{/* ---------------- CAPABILITIES (live mini-visuals) ---------------- */}
diff --git a/www/app/(content)/brand/page.tsx b/www/app/(site)/(content)/brand/page.tsx
similarity index 100%
rename from www/app/(content)/brand/page.tsx
rename to www/app/(site)/(content)/brand/page.tsx
diff --git a/www/app/(content)/layout.tsx b/www/app/(site)/(content)/layout.tsx
similarity index 100%
rename from www/app/(content)/layout.tsx
rename to www/app/(site)/(content)/layout.tsx
diff --git a/www/app/api/search/route.ts b/www/app/(site)/api/search/route.ts
similarity index 100%
rename from www/app/api/search/route.ts
rename to www/app/(site)/api/search/route.ts
diff --git a/www/app/blog/[slug]/page.tsx b/www/app/(site)/blog/[slug]/page.tsx
similarity index 100%
rename from www/app/blog/[slug]/page.tsx
rename to www/app/(site)/blog/[slug]/page.tsx
diff --git a/www/app/blog/layout.tsx b/www/app/(site)/blog/layout.tsx
similarity index 100%
rename from www/app/blog/layout.tsx
rename to www/app/(site)/blog/layout.tsx
diff --git a/www/app/blog/page.tsx b/www/app/(site)/blog/page.tsx
similarity index 100%
rename from www/app/blog/page.tsx
rename to www/app/(site)/blog/page.tsx
diff --git a/www/app/compare/[slug]/page.tsx b/www/app/(site)/compare/[slug]/page.tsx
similarity index 100%
rename from www/app/compare/[slug]/page.tsx
rename to www/app/(site)/compare/[slug]/page.tsx
diff --git a/www/app/compare/page.tsx b/www/app/(site)/compare/page.tsx
similarity index 100%
rename from www/app/compare/page.tsx
rename to www/app/(site)/compare/page.tsx
diff --git a/www/app/docs/[[...slug]]/page.tsx b/www/app/(site)/docs/[[...slug]]/page.tsx
similarity index 100%
rename from www/app/docs/[[...slug]]/page.tsx
rename to www/app/(site)/docs/[[...slug]]/page.tsx
diff --git a/www/app/docs/layout.tsx b/www/app/(site)/docs/layout.tsx
similarity index 100%
rename from www/app/docs/layout.tsx
rename to www/app/(site)/docs/layout.tsx
diff --git a/www/app/layout.tsx b/www/app/(site)/layout.tsx
similarity index 99%
rename from www/app/layout.tsx
rename to www/app/(site)/layout.tsx
index 67d5b556..a04a787c 100644
--- a/www/app/layout.tsx
+++ b/www/app/(site)/layout.tsx
@@ -1,5 +1,5 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
-import './global.css';
+import '../global.css';
import type { Metadata } from 'next';
import { Inter, Outfit } from 'next/font/google';
import Script from 'next/script';
diff --git a/www/app/llms-full.txt/route.ts b/www/app/(site)/llms-full.txt/route.ts
similarity index 100%
rename from www/app/llms-full.txt/route.ts
rename to www/app/(site)/llms-full.txt/route.ts
diff --git a/www/app/llms.mdx/docs/[[...slug]]/route.ts b/www/app/(site)/llms.mdx/docs/[[...slug]]/route.ts
similarity index 100%
rename from www/app/llms.mdx/docs/[[...slug]]/route.ts
rename to www/app/(site)/llms.mdx/docs/[[...slug]]/route.ts
diff --git a/www/app/llms.txt/route.ts b/www/app/(site)/llms.txt/route.ts
similarity index 100%
rename from www/app/llms.txt/route.ts
rename to www/app/(site)/llms.txt/route.ts
diff --git a/www/app/og/docs/[...slug]/route.tsx b/www/app/(site)/og/docs/[...slug]/route.tsx
similarity index 100%
rename from www/app/og/docs/[...slug]/route.tsx
rename to www/app/(site)/og/docs/[...slug]/route.tsx
diff --git a/www/bun.lock b/www/bun.lock
index a9db44c9..005555f2 100644
--- a/www/bun.lock
+++ b/www/bun.lock
@@ -15,6 +15,7 @@
"lucide-react": "^1.24.0",
"motion": "^13.3.0",
"next": "16.2.10",
+ "next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.2.7",
"react-dom": "^19.2.7",
diff --git a/www/package.json b/www/package.json
index f6357727..efda72c9 100644
--- a/www/package.json
+++ b/www/package.json
@@ -21,6 +21,7 @@
"lucide-react": "^1.24.0",
"motion": "^13.3.0",
"next": "16.2.10",
+ "next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.2.7",
"react-dom": "^19.2.7",