- {/* First image on the page and above the fold, so it is not lazy-loaded. */}
+ } className='lg:pt-[209px]'>
+
+
+

- {GO_HERO.badge}
-
-
- {GO_HERO.headline}
-
-
-
-
-
+
)
}
diff --git a/src/components/landing/go/GoPage.tsx b/src/components/landing/go/GoPage.tsx
index 9724f8d1..d5faf62e 100644
--- a/src/components/landing/go/GoPage.tsx
+++ b/src/components/landing/go/GoPage.tsx
@@ -1,37 +1,23 @@
import React from 'react'
-import LakesidePage from '../ui/LakesidePage'
+import ProductPage from '../product/ProductPage'
import GoHero from './GoHero'
-import GoArchitecture from './GoArchitecture'
import GoProblem from './GoProblem'
import GoFeatures from './GoFeatures'
import GoBenchmark from './GoBenchmark'
-import GoFaq from './GoFaq'
import GoCta from './GoCta'
+import GoFaq from './GoFaq'
import './go.css'
-/** The OLake Go product page: hero, diagram, problem, features, benchmarks, a closing CTA and the FAQ. */
+/** The OLake Go product page: hero, problem, features, benchmarks, a closing CTA and the FAQ. */
export default function GoPage({ title, description }: { title: string; description: string }) {
return (
-
}
- >
- {/* Infima gives headings, paragraphs and lists bottom margins and lists a left padding;
- the lakeside reset is zero-specificity and loses, so it is undone here for the whole page. */}
-
-
+
+
+
+
+
+
+
+
)
}
diff --git a/src/components/landing/go/GoProblem.tsx b/src/components/landing/go/GoProblem.tsx
index fe1148b3..c8d9a4a3 100644
--- a/src/components/landing/go/GoProblem.tsx
+++ b/src/components/landing/go/GoProblem.tsx
@@ -1,79 +1,138 @@
import React from 'react'
-import Section from '../ui/Section'
-import SectionHeading from '../ui/SectionHeading'
-import { cn } from '@site/src/lib/utils'
-import { GO_PROBLEM } from '@site/src/data/landing/go/problem'
+import { GO_PROBLEMS, GO_PROBLEM_INTRO, type GoProblemArt } from '@site/src/data/landing/go/problem'
-/** The cloud outline, stretched to the box it sits in. */
-const CLOUD_PATH =
- 'M205,278 C150,278 108,246 100,206 C60,200 34,168 34,132 C34,96 62,66 100,60 C108,26 146,2 192,2 C224,2 253,15 272,37 C296,16 331,4 370,4 C424,4 470,32 486,72 C505,58 530,50 557,50 C607,50 650,80 662,120 C712,120 752,152 752,192 C752,228 720,258 678,262 C670,290 636,310 596,310 C572,310 550,303 533,290 C512,306 481,316 447,316 C420,316 396,310 377,299 C356,314 328,323 297,323 C258,323 224,308 205,278 Z'
+const IMG = '/img/landing/go'
-/**
- * "The Problem": four statements tilted inside a cloud, the key words larger and in the brand blue,
- * then the answer headline under the cloud. Each statement drifts up and down slowly (go.css).
- */
+/** Each problem's line drawing, at its own size and where the design places it in the card's 203px art column. */
+const ART: Record
= {
+ 'slow-syncs': (
+
+ ),
+ 'legacy-costs': (
+
+ ),
+ 'cdc-breaks': (
+ <>
+
+ {/* The stale-source mark stands upright in the design: rotated a quarter turn in its 48 × 74 slot. */}
+
+
+
+ >
+ ),
+ 'schema-drift': (
+
+ )
+}
+
+/** The frame's dashed outline (22px dashes, as in the design), drawn on the pixel grid so it stays crisp. */
+const DashedFrame = () => (
+
+)
+
+/** One of the frame's two vertical dashed guides. */
+const Guide = ({ className }: { className: string }) => (
+
+)
+
+/** "Problem Statement": the four pain points as cards inside a dashed frame with two vertical guides. */
export default function GoProblem() {
return (
-
-
- {GO_PROBLEM.eyebrow}
-
-
-
-
- {GO_PROBLEM.sentences.map((sentence) => {
- const { top, left, rot, delay, size } = sentence.layout
- return (
- -
+
+
+
+
+
+
+ {GO_PROBLEM_INTRO.eyebrow}
+
+
+ {GO_PROBLEM_INTRO.title}
+
+
+
+ {GO_PROBLEMS.map((problem) => (
+ -
+ {/* The art column: the design's 203 × 128 slot, shown at 5/8 size on phones. */}
+
-
- {sentence.words.map((word, i) => (
-
- {i > 0 && ' '}
-
- {word}
-
-
- ))}
-
-
- )
- })}
+
+ {ART[problem.art]}
+
+
+
+ {problem.label}
+
+ {problem.title}
+
+
+
+ ))}
-
-
+
)
}
diff --git a/src/components/landing/go/go.css b/src/components/landing/go/go.css
index 8484885c..0aa55adb 100644
--- a/src/components/landing/go/go.css
+++ b/src/components/landing/go/go.css
@@ -1,241 +1,27 @@
/* =============================================================================================
- * OLake Go page: the few pieces Tailwind utilities do not express well (the cloud, the tinted
- * features background, keyframes). Every colour is a token or a color-mix of tokens. Motion only
- * runs for visitors without a reduced-motion preference and moves opacity or transform.
+ * OLake Go page: the sources marquee under the hero. The page's palette, grain and mock-up scaling
+ * are shared with the other product pages (../product/product.css). Motion only runs for visitors
+ * without a reduced-motion preference.
* ========================================================================================== */
-.go-page {
- --go-cloud-fill: color-mix(in srgb, var(--olake-blue) 3%, var(--olake-surface));
- --go-cloud-line: color-mix(in srgb, var(--olake-blue) 18%, var(--olake-line));
- --go-tint: color-mix(in srgb, var(--olake-blue) 4%, var(--olake-surface));
+/* ---- The "Available Sources" marquee -------------------------------------------------------- */
+.go-marquee-track {
+ display: flex;
+ width: max-content;
}
-/* ---- The cloud --------------------------------------------------------------------------- */
-.go-cloud {
- position: relative;
- width: 100%;
- max-width: 920px;
- height: 300px;
- margin-inline: auto;
-}
-
-.go-cloud-shape {
- position: absolute;
- inset: -6% -4%;
- width: 108%;
- height: 112%;
-}
-
-.go-cloud-line {
- position: absolute;
- white-space: nowrap;
- transform: translate(-50%, -50%) rotate(var(--go-rot, 0deg));
-}
-
-.go-cloud-line p {
- font-size: 13px;
- line-height: 1.1;
- letter-spacing: -0.005em;
-}
-
-.go-cloud-line .go-key {
- font-size: 16px;
-}
-
-@media (min-width: 640px) {
- .go-cloud {
- height: 400px;
- }
-
- .go-cloud-line p {
- font-size: calc(var(--go-size, 24) * 1px);
- }
-
- .go-cloud-line .go-key {
- font-size: calc((var(--go-size, 24) + 7) * 1px);
- }
-}
-
-/* ---- Features: a very light brand tint that fades in and out ------------------------------- */
-.go-features-tint {
- background: linear-gradient(
- 180deg,
- transparent 0,
- var(--go-tint) 96px,
- var(--go-tint) calc(100% - 96px),
- transparent 100%
- );
-}
-
-.go-feature-tile {
- background: linear-gradient(
- 135deg,
- var(--olake-blue),
- color-mix(in srgb, var(--olake-blue) 72%, var(--olake-surface))
- );
-}
-
-/* ---- Motion ------------------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
- @keyframes go-word-float {
- 0%,
- 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-5px);
- }
- }
-
- @keyframes go-fade-slide-in {
- from {
- opacity: 0;
- transform: translateX(14px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- @keyframes go-fade-in {
- from {
- opacity: 0;
- }
+ @keyframes go-marquee {
to {
- opacity: 1;
+ transform: translateX(-50%);
}
}
- @keyframes go-pulse-ring {
- 0%,
- 100% {
- opacity: 0.4;
- transform: scale(1);
- }
- 50% {
- opacity: 0;
- transform: scale(1.22);
- }
+ .go-marquee-track {
+ animation: go-marquee 36s linear infinite;
}
- /* each sync mode lights up in turn: Full Load, then CDC, then Incremental */
- @keyframes go-light-1 {
- 0%,
- 8% {
- opacity: 0;
- }
- 16%,
- 88% {
- opacity: 1;
- }
- 96%,
- 100% {
- opacity: 0;
- }
- }
-
- @keyframes go-light-2 {
- 0%,
- 32% {
- opacity: 0;
- }
- 40%,
- 88% {
- opacity: 1;
- }
- 96%,
- 100% {
- opacity: 0;
- }
- }
-
- @keyframes go-light-3 {
- 0%,
- 56% {
- opacity: 0;
- }
- 64%,
- 88% {
- opacity: 1;
- }
- 96%,
- 100% {
- opacity: 0;
- }
- }
-
- /* each cloud statement drifts up and down, offset from the others */
- .go-cloud-float {
- animation: go-word-float 5s ease-in-out infinite;
- animation-delay: var(--go-delay, 0s);
- }
-
- /* the ring around the OLake Go node */
- .go-node {
- position: relative;
- }
-
- .go-node::after {
- content: '';
- position: absolute;
- inset: -2px;
- border-radius: inherit;
- border: 2px solid var(--olake-blue);
- pointer-events: none;
- animation: go-pulse-ring 2.4s ease-in-out infinite;
- }
-
- .go-chip {
- position: relative;
- isolation: isolate;
- overflow: hidden;
- }
-
- .go-chip::before {
- content: '';
- position: absolute;
- z-index: -1;
- inset: 0;
- opacity: 0;
- background: var(--olake-blue-tint);
- }
-
- .go-chip-1::before {
- animation: go-light-1 4s ease-in-out infinite;
- }
-
- .go-chip-2::before {
- animation: go-light-2 4s ease-in-out infinite;
- }
-
- .go-chip-3::before {
- animation: go-light-3 4s ease-in-out infinite;
- }
-
- /* the feature card slides in when the active feature changes */
- .go-card-in {
- animation: go-fade-slide-in 500ms var(--olake-ease-out) both;
- }
-
- /* opened FAQ answers and the "how" disclosure fade in */
- .go-reveal {
- animation: go-fade-in 220ms var(--olake-ease-out) both;
- }
-
- /* the live CDC dot in the first feature preview */
- .go-live-dot {
- animation: go-word-float 1.6s ease-in-out infinite;
- }
-
- .go-tab-fill {
- transition: transform 100ms linear;
- }
-}
-
-/* With reduced motion nothing cycles, so the selected feature shows a full underline. */
-@media (prefers-reduced-motion: reduce) {
- .go-tab[aria-selected='true'] .go-tab-fill {
- transform: scaleX(1) !important;
+ .go-marquee:hover .go-marquee-track {
+ animation-play-state: paused;
}
}
diff --git a/src/components/landing/go/logos.ts b/src/components/landing/go/logos.ts
new file mode 100644
index 00000000..38fcc13b
--- /dev/null
+++ b/src/components/landing/go/logos.ts
@@ -0,0 +1,23 @@
+import type { GoSourceId } from '@site/src/data/landing/go/hero'
+
+/** A logo as a plain image: its file and the size the design shows it at. */
+export interface GoLogo {
+ src: string
+ width: number
+ height: number
+}
+
+/**
+ * The logo of each source OLake Go reads from, for the hero's sources strip. Most reuse the site's
+ * existing logo files; MSSQL and DB2 are the design's own artwork, cropped to the mark.
+ */
+export const GO_LOGOS: Record = {
+ kafka: { src: '/img/site/kafka.png', width: 10.44, height: 17.31 },
+ postgres: { src: '/img/site/postgres.svg', width: 20.81, height: 20.81 },
+ mysql: { src: '/img/site/mysql.svg', width: 20.81, height: 20.81 },
+ mongodb: { src: '/img/site/mongodb.svg', width: 20.81, height: 20.81 },
+ s3: { src: '/img/landing/v2/connector-s3.webp', width: 15.55, height: 18.6 },
+ mssql: { src: '/img/landing/go/source-mssql.webp', width: 18.51, height: 20 },
+ db2: { src: '/img/landing/go/source-db2.webp', width: 17.74, height: 17.74 },
+ oracle: { src: '/img/site/oracle.png', width: 21, height: 21 }
+}
diff --git a/src/components/landing/go/useDiagramLinks.ts b/src/components/landing/go/useDiagramLinks.ts
deleted file mode 100644
index b5f01a85..00000000
--- a/src/components/landing/go/useDiagramLinks.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useEffect, useState, type RefObject } from 'react'
-
-export interface DiagramLink {
- d: string
- /** Seconds before the travelling dot starts, so the dots do not move in lockstep. */
- begin: number
- side: 'in' | 'out'
-}
-
-export interface DiagramLinks {
- width: number
- height: number
- links: DiagramLink[]
- /** False when the visitor prefers reduced motion: the lines stay, the dots do not travel. */
- animate: boolean
-}
-
-/**
- * Measures the source chips, the OLake Go node and the destination chips (found by `data-src` and
- * `data-dest`) and returns the bezier paths that join them. It runs after mount only, so the first
- * render matches the server's. On narrow screens the diagram stacks and no links are returned.
- */
-export function useDiagramLinks(
- containerRef: RefObject,
- nodeRef: RefObject
-): DiagramLinks | null {
- const [state, setState] = useState(null)
-
- useEffect(() => {
- const container = containerRef.current
- const node = nodeRef.current
- if (!container || !node) return
-
- const measure = () => {
- const srcs = Array.from(container.querySelectorAll('[data-src]'))
- const dests = Array.from(container.querySelectorAll('[data-dest]'))
- const c = container.getBoundingClientRect()
- const n = node.getBoundingClientRect()
- // Stacked layout (mobile): the first source is not to the left of the node.
- if (!srcs.length || !dests.length || srcs[0].getBoundingClientRect().right > n.left + 4) {
- setState(null)
- return
- }
- const nodeL = { x: n.left - c.left + 10, y: n.top - c.top + n.height / 2 }
- const nodeR = { x: n.right - c.left - 10, y: n.top - c.top + n.height / 2 }
- const links: DiagramLink[] = []
- srcs.forEach((el, i) => {
- const r = el.getBoundingClientRect()
- const p = { x: r.right - c.left - 8, y: r.top - c.top + r.height / 2 }
- const dx = Math.max(28, (nodeL.x - p.x) * 0.5)
- links.push({
- side: 'in',
- begin: i * 0.28,
- d: `M${p.x},${p.y} C${p.x + dx},${p.y} ${nodeL.x - dx},${nodeL.y} ${nodeL.x},${nodeL.y}`
- })
- })
- dests.forEach((el, i) => {
- const r = el.getBoundingClientRect()
- const p = { x: r.left - c.left + 8, y: r.top - c.top + r.height / 2 }
- const dx = Math.max(28, (p.x - nodeR.x) * 0.5)
- links.push({
- side: 'out',
- begin: 0.6 + i * 0.6,
- d: `M${nodeR.x},${nodeR.y} C${nodeR.x + dx},${nodeR.y} ${p.x - dx},${p.y} ${p.x},${p.y}`
- })
- })
- setState({
- width: c.width,
- height: c.height,
- links,
- animate: !window.matchMedia('(prefers-reduced-motion: reduce)').matches
- })
- }
-
- measure()
- const ro = new ResizeObserver(measure)
- ro.observe(container)
- return () => ro.disconnect()
- }, [containerRef, nodeRef])
-
- return state
-}
diff --git a/src/components/landing/go/useFeatureCycle.ts b/src/components/landing/go/useFeatureCycle.ts
deleted file mode 100644
index 71733976..00000000
--- a/src/components/landing/go/useFeatureCycle.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
-
-const TICK_MS = 100
-/** 100 ticks of 100ms: each feature is on screen for ten seconds before the next one. */
-const TICKS_PER_FEATURE = 100
-
-/**
- * Drives the feature selector: once the section scrolls into view the active feature advances every
- * ten seconds and `progress` (0 to 100) fills its underline. Picking a feature by hand stops the
- * cycle for good, and it never starts for visitors who prefer reduced motion.
- */
-export function useFeatureCycle(count: number) {
- const ref = useRef(null)
- const [active, setActive] = useState(0)
- const [progress, setProgress] = useState(0)
- const [running, setRunning] = useState(false)
- const [manual, setManual] = useState(false)
-
- useEffect(() => {
- const el = ref.current
- if (!el || manual) return
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
- if (!('IntersectionObserver' in window)) {
- setRunning(true)
- return
- }
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting) {
- setRunning(true)
- observer.disconnect()
- }
- },
- { threshold: 0.3 }
- )
- observer.observe(el)
- return () => observer.disconnect()
- }, [manual])
-
- useEffect(() => {
- if (!running || manual) return
- const id = window.setInterval(() => {
- setProgress((p) => {
- if (p + 1 < TICKS_PER_FEATURE) return p + 1
- setActive((a) => (a + 1) % count)
- return 0
- })
- }, TICK_MS)
- return () => window.clearInterval(id)
- }, [running, manual, count])
-
- const select = useCallback((index: number) => {
- setActive(index)
- setProgress(TICKS_PER_FEATURE)
- setManual(true)
- }, [])
-
- return { ref, active, progress, select, manual }
-}
diff --git a/src/components/landing/go/useGoBenchmark.ts b/src/components/landing/go/useGoBenchmark.ts
index 33a53926..468042fa 100644
--- a/src/components/landing/go/useGoBenchmark.ts
+++ b/src/components/landing/go/useGoBenchmark.ts
@@ -3,12 +3,16 @@ import {
CONNECTORS,
CONNECTOR_BENCHMARKS,
CONNECTOR_CDC_BENCHMARKS,
- CONNECTOR_METRIC_LABELS,
TOOLS,
type BenchmarkMode,
type ConnectorId
} from '@site/src/data/benchmarkData'
-import { GO_BENCHMARK_COST_NOTE } from '@site/src/data/landing/go/benchmark'
+import {
+ GO_BENCHMARK_CONNECTORS,
+ GO_BENCHMARK_COST_NOTE,
+ GO_BENCHMARK_METRIC_LABELS,
+ goBenchmarkCaption
+} from '@site/src/data/landing/go/benchmark'
/** Row order of the table, top to bottom. */
const METRICS = ['rowsSynced', 'elapsedTime', 'speed', 'comparison', 'cost'] as const
@@ -27,21 +31,26 @@ export interface GoBenchmarkRow {
note?: string
olake: string
competitors: string[]
- /** The comparison row is tinted. */
+ /** The comparison row is set in blue. */
highlight: boolean
}
/**
- * State and table data of the OLake Go benchmark block. Unlike the home page's table, every
- * connector keeps its tab: the ones without published numbers show a "coming soon" panel.
+ * State and table data of the OLake Go benchmark block. Like the home page's table, a source only
+ * gets a tab in the modes it has published numbers for (Kafka and MSSQL have full-load numbers but
+ * no CDC ones), and switching to a mode the selected source lacks falls back to the first source
+ * that has them.
*/
export function useGoBenchmark() {
- const [activeConnector, setActiveConnector] = useState(CONNECTORS[0].id)
+ const [selectedConnector, setActiveConnector] = useState(GO_BENCHMARK_CONNECTORS[0])
const [mode, setMode] = useState('full_load')
return useMemo(() => {
const dataset = mode === 'cdc' ? CONNECTOR_CDC_BENCHMARKS : CONNECTOR_BENCHMARKS
- const connector = CONNECTORS.find((c) => c.id === activeConnector) ?? CONNECTORS[0]
+ const all = GO_BENCHMARK_CONNECTORS.map((id) => CONNECTORS.find((c) => c.id === id)!)
+ const withData = all.filter((c) => dataset[c.id]?.hasData)
+ const connectors = withData.length ? withData : all
+ const connector = connectors.find((c) => c.id === selectedConnector) ?? connectors[0]
const bench = dataset[connector.id]
const keys = competitorKeys(bench.rowsSynced.flink !== undefined)
@@ -49,27 +58,25 @@ export function useGoBenchmark() {
const row = bench[metric] as unknown as Record
const isComparison = metric === 'comparison'
return {
- label: CONNECTOR_METRIC_LABELS[metric],
+ label: GO_BENCHMARK_METRIC_LABELS[metric],
note: metric === 'cost' ? GO_BENCHMARK_COST_NOTE : undefined,
// OLake is the baseline of the comparison row, so it shows a dash instead of a multiplier.
- olake: isComparison ? '–' : (row.olake ?? '-'),
+ olake: isComparison ? '-' : (row.olake ?? '-'),
competitors: keys.map((key) => row[key] ?? '-'),
highlight: isComparison
}
})
return {
- connectors: CONNECTORS.map(({ id, name }) => ({ id, name })),
+ connectors: connectors.map(({ id, name }) => ({ id, name })),
activeConnector: connector.id,
setActiveConnector,
mode,
setMode,
competitors: keys.map((key) => TOOLS[key].name),
- olakeLabel: 'OLake Go',
olakeSub: TOOLS.olake.description ?? '',
- rows,
- hasData: bench.hasData,
- sourceName: connector.name
+ caption: goBenchmarkCaption(mode, bench.rowsSynced.olake, connector.name),
+ rows
}
- }, [activeConnector, mode])
+ }, [selectedConnector, mode])
}
diff --git a/src/components/landing/product/BenchmarkShell.tsx b/src/components/landing/product/BenchmarkShell.tsx
new file mode 100644
index 00000000..d8e61918
--- /dev/null
+++ b/src/components/landing/product/BenchmarkShell.tsx
@@ -0,0 +1,116 @@
+import React from 'react'
+import Link from '@docusaurus/Link'
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
+import { cn } from '@site/src/lib/utils'
+import { PiArrowSquareOut } from 'react-icons/pi'
+
+/** Rules on the dark section. It goes first in cn(): a later border-0 would drop the side classes. */
+export const RULE = 'border-0 border-solid border-olake-line-dark'
+
+/** Focus ring for controls on the dark section. */
+export const FOCUS_ON_DARK =
+ 'focus-visible:rounded-[4px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-olake-blue-on-dark'
+
+/** The latest-release strip; the version comes from docusaurus.config.js. */
+function ReleaseStrip() {
+ const { siteConfig } = useDocusaurusContext()
+ const label = siteConfig.customFields?.latestOlakeReleaseLabel as string | undefined
+ const path =
+ (siteConfig.customFields?.latestOlakeReleasePath as string | undefined) ||
+ '/docs/release/ingestion'
+ if (!label) return null
+ return (
+
+ Latest Release
+ We just released our {label}
+
+ Read More
+
+
+ )
+}
+
+/**
+ * The dark, full-bleed benchmark section of a product page: a ruled column with (optionally) the
+ * latest-release strip, the eyebrow, title and summary, the page's comparison (`children`), the
+ * "how it does this" notes set in Geist Mono, and a bar linking to the detailed benchmarks.
+ */
+export default function BenchmarkShell({
+ release,
+ eyebrow,
+ title,
+ summary,
+ how,
+ link,
+ children
+}: {
+ /** Show the latest-release strip on top. */
+ release?: boolean
+ eyebrow: string
+ title: string
+ summary: React.ReactNode
+ how: { title: string; body: React.ReactNode; className?: string }
+ link: { label: string; href: string }
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {release &&
}
+
+
+
+ {eyebrow}
+
+
+ {title}
+
+
+ {summary}
+
+
+
+ {children}
+
+
+
{how.title}
+
+ {how.body}
+
+
+
+
+ {link.label}
+
+
+
+
+ )
+}
diff --git a/src/components/landing/product/Glow.tsx b/src/components/landing/product/Glow.tsx
new file mode 100644
index 00000000..39755355
--- /dev/null
+++ b/src/components/landing/product/Glow.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import { cn } from '@site/src/lib/utils'
+
+/**
+ * A soft glow drawn in CSS rather than loaded as an image: the design's blurred ellipse layer, an
+ * `rx` × `ry` ellipse in `color` blurred by `blur` px (the Gaussian's standard deviation, as the
+ * design's layer blur). `className` places the layer's `width` × `height` frame the way the design
+ * does; the ellipse sits at its centre.
+ */
+export default function Glow({
+ width,
+ height,
+ rx,
+ ry,
+ blur,
+ color,
+ className
+}: {
+ width: number
+ height: number
+ rx: number
+ ry: number
+ blur: number
+ color: string
+ className?: string
+}) {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/landing/product/ProductCta.tsx b/src/components/landing/product/ProductCta.tsx
new file mode 100644
index 00000000..37524159
--- /dev/null
+++ b/src/components/landing/product/ProductCta.tsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import Button from '../ui/Button'
+import { cn } from '@site/src/lib/utils'
+
+const BUTTON = 'h-[36px] rounded-[8px] px-[10px] text-[16px] leading-[1.3]'
+
+/**
+ * Closing call to action of a product page: a dark-blue card with the page's art rising behind the
+ * two buttons. `className` sets the card's background colour.
+ */
+export default function ProductCta({
+ eyebrow,
+ title,
+ primary,
+ secondary,
+ art,
+ className
+}: {
+ eyebrow: string
+ title: string
+ primary: { label: string; href: string }
+ secondary: { label: string; href: string }
+ art: React.ReactNode
+ className?: string
+}) {
+ return (
+
+
+ {art}
+
{eyebrow}
+
+ {title}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/landing/product/ProductFaq.tsx b/src/components/landing/product/ProductFaq.tsx
new file mode 100644
index 00000000..c3d7695a
--- /dev/null
+++ b/src/components/landing/product/ProductFaq.tsx
@@ -0,0 +1,91 @@
+import React, { useState } from 'react'
+import { cn } from '@site/src/lib/utils'
+import { PiArrowDownBold } from 'react-icons/pi'
+
+export interface ProductFaqItem {
+ q: string
+ a: string
+}
+
+/**
+ * The FAQ card of a product page: accordion, one answer open at a time (the first one to start
+ * with). Answers stay in the DOM (hidden) so the text the page's FAQPage JSON-LD describes is
+ * always on the page.
+ */
+export default function ProductFaq({
+ idPrefix,
+ eyebrow,
+ title,
+ faqs
+}: {
+ /** Prefix of the question and answer ids, e.g. 'go-faq'. */
+ idPrefix: string
+ eyebrow: string
+ title: string
+ faqs: ProductFaqItem[]
+}) {
+ const [open, setOpen] = useState(0)
+
+ return (
+
+
+
+
{eyebrow}
+
+ {title}
+
+
+
+
+ {faqs.map((faq, i) => {
+ const isOpen = open === i
+ return (
+ -
+
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/src/components/landing/product/ProductFeatures.tsx b/src/components/landing/product/ProductFeatures.tsx
new file mode 100644
index 00000000..6d3f482b
--- /dev/null
+++ b/src/components/landing/product/ProductFeatures.tsx
@@ -0,0 +1,166 @@
+import React, { useRef, useState } from 'react'
+import { cn } from '@site/src/lib/utils'
+
+/** A capability behind the tab strip. `tab` is the pill label, `title` the card heading. */
+export interface ProductFeature {
+ id: string
+ tab: string
+ title: string
+ /** Line breaks ("\n") are kept. */
+ body: string
+}
+
+/**
+ * A tab's picture (one design frame): the background field, the glow at the bottom left, where the
+ * copy sits from lg up (its top, and the body's width) and the product screenshot (`image`, at its
+ * display size) with its lg position.
+ */
+export interface ProductFeatureArt {
+ background: React.ReactNode
+ glow: React.ReactNode
+ copyTop: number
+ bodyWidth: number
+ image: { src: string; alt: string; width: number; height: number }
+ mockClassName: string
+}
+
+/**
+ * "Features": a pill tab strip over the blue feature card. Each tab shows its own picture of the
+ * product; the heading and copy follow the selected capability. Every capability's heading and copy
+ * stay in the DOM (the inactive ones hidden), as ProductFaq keeps its answers; only the selected
+ * tab's picture is rendered, since the text is cheap and the picture is not. Arrow keys, Home and
+ * End move between tabs.
+ */
+export default function ProductFeatures({
+ idPrefix,
+ intro,
+ features,
+ art,
+ className
+}: {
+ /** Prefix of the tab and panel ids, e.g. 'go-feature'. */
+ idPrefix: string
+ intro: { eyebrow: string; title: string }
+ features: ProductFeature[]
+ art: Record
+ /** Section overrides, e.g. the gap to the next section. */
+ className?: string
+}) {
+ const [active, setActive] = useState(0)
+ const tabRefs = useRef<(HTMLButtonElement | null)[]>([])
+
+ const select = (index: number) => {
+ setActive(index)
+ tabRefs.current[index]?.focus()
+ }
+
+ const onKeyDown = (event: React.KeyboardEvent, index: number) => {
+ const last = features.length - 1
+ if (event.key === 'ArrowRight') select(index === last ? 0 : index + 1)
+ else if (event.key === 'ArrowLeft') select(index === 0 ? last : index - 1)
+ else if (event.key === 'Home') select(0)
+ else if (event.key === 'End') select(last)
+ else return
+ event.preventDefault()
+ }
+
+ const frame = art[features[active].id]
+
+ return (
+
+
+
+
+ {intro.eyebrow}
+
+
+ {intro.title}
+
+
+
+
+ {features.map((item, i) => {
+ const selected = i === active
+ return (
+
+ )
+ })}
+
+
+
+
{frame.background}
+ {frame.glow}
+
+ {features.map((item, i) => {
+ const selected = i === active
+ return (
+
+
+ {item.title}
+
+
+ {item.body}
+
+
+ )
+ })}
+
+

+
+
+
+ )
+}
diff --git a/src/components/landing/product/ProductHero.tsx b/src/components/landing/product/ProductHero.tsx
new file mode 100644
index 00000000..05fdc5c6
--- /dev/null
+++ b/src/components/landing/product/ProductHero.tsx
@@ -0,0 +1,82 @@
+import React from 'react'
+import Button from '../ui/Button'
+import { cn } from '@site/src/lib/utils'
+import { PiGithubLogo } from 'react-icons/pi'
+
+export interface ProductHeroCopy {
+ eyebrow: string
+ headline: string
+ body: string
+ /** The GitHub repository (opens in a new tab) and the docs. */
+ primary: { label: string; href: string }
+ secondary: { label: string; href: string }
+}
+
+const BUTTON = 'h-[36px] px-[10px] text-[16px] leading-[1.3]'
+
+/**
+ * Hero of a product page: the page's background art, the eyebrow, headline and copy, the GitHub
+ * and docs buttons, then `children` (the product shot). `className` sets the top padding from lg
+ * up, which differs per design.
+ */
+export default function ProductHero({
+ copy,
+ background,
+ className,
+ children
+}: {
+ copy: ProductHeroCopy
+ background: React.ReactNode
+ className?: string
+ children?: React.ReactNode
+}) {
+ return (
+
+ {background}
+
+
+
+
+ {copy.eyebrow}
+
+
+ {copy.headline}
+
+
+ {copy.body}
+
+
+
+
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/landing/product/ProductPage.tsx b/src/components/landing/product/ProductPage.tsx
new file mode 100644
index 00000000..48f5b839
--- /dev/null
+++ b/src/components/landing/product/ProductPage.tsx
@@ -0,0 +1,31 @@
+import React from 'react'
+import LakesidePage from '../ui/LakesidePage'
+import './product.css'
+
+/**
+ * Shell of a product page (OLake Go, OLake Fusion): the Lakeside page with its floating navbar and
+ * footer, and the product palette and paper grain over everything between them.
+ */
+export default function ProductPage({
+ title,
+ description,
+ activePath,
+ children
+}: {
+ title: string
+ description: string
+ activePath: string
+ children: React.ReactNode
+}) {
+ return (
+
+ {/* The negative top margin slides the page (and its grain) under the floating navbar, as the
+ other landing heroes do. Infima gives headings, paragraphs and lists bottom margins and
+ lists a left padding; the lakeside reset is zero-specificity and loses, so it is undone
+ here for the whole page. */}
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/landing/product/product.css b/src/components/landing/product/product.css
new file mode 100644
index 00000000..d74693b0
--- /dev/null
+++ b/src/components/landing/product/product.css
@@ -0,0 +1,40 @@
+/* =============================================================================================
+ * The product pages (OLake Go, OLake Fusion): their palette and the pieces Tailwind utilities do
+ * not express well (the paper grain, the product mock-ups scaled to fit, the fade under the hero
+ * mock-up). Motion only runs for visitors without a reduced-motion preference.
+ * ========================================================================================== */
+
+.product-page {
+ /* The product designs' blues, which the --olake-* tokens do not have. Their near-black, greys and
+ rules use the closest tokens; text greys that fail WCAG AA in the designs (ACCESSIBILITY.md) use
+ --olake-muted on light surfaces and --olake-muted-on-dark / --olake-blue-on-dark on dark ones. */
+ --product-blue: #0029ce; /* primary buttons */
+ --product-blue-hover: #0022ad;
+ --product-blue-deep: #0c36bf; /* the feature cards and OLake Go's closing card */
+ --product-mono: 'Geist Mono', var(--olake-font-mono);
+
+ /* The designs lay a faint monotone grain over the page (Figma noise, #ececE6 at up to 17%). */
+ background-color: var(--olake-surface);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.925 0 0 0 0 0.925 0 0 0 0 0.902 0 0 0 0.425 -0.1275'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
+ background-size: 160px 160px;
+}
+
+.product-mono {
+ font-family: var(--product-mono);
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ @keyframes product-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+
+ /* the feature card copy and picture, and opened FAQ answers, fade in */
+ .product-fade-in {
+ animation: product-fade-in 240ms var(--olake-ease-out) both;
+ }
+}
diff --git a/src/css/fonts.css b/src/css/fonts.css
index 152b25b9..28311520 100644
--- a/src/css/fonts.css
+++ b/src/css/fonts.css
@@ -2,8 +2,9 @@
* Generated from the @fontsource-variable packages; only the latin and latin-ext subsets are
* shipped, and the browser downloads a subset only when a page uses characters from its
* unicode-range. font-display: swap keeps text visible while a font loads.
- * Weight ranges are limited to what the site uses: Geist 400-600 (the same weights the old Google
- * request provided, so marketing pages render exactly as before), JetBrains Mono 400-700. */
+ * Weight ranges are limited to what the site uses: Geist 300-600 (400-600 are the weights the old
+ * Google request provided; 300 is the light weight of the OLake Go benchmark table), JetBrains Mono
+ * 400-700, and Geist Mono 300-400 (the "How OLake Go does this" notes on /olake-go). */
@font-face {
font-family: 'JetBrains Mono';
@@ -27,7 +28,7 @@
font-family: 'Geist';
font-style: normal;
font-display: swap;
- font-weight: 400 600;
+ font-weight: 300 600;
src: url('/fonts/geist-latin-ext-wght-normal.woff2') format('woff2');
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@@ -36,7 +37,25 @@
font-family: 'Geist';
font-style: normal;
font-display: swap;
- font-weight: 400 600;
+ font-weight: 300 600;
src: url('/fonts/geist-latin-wght-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
+
+@font-face {
+ font-family: 'Geist Mono';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 300 400;
+ src: url('/fonts/geist-mono-latin-ext-wght-normal.woff2') format('woff2');
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Geist Mono';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 300 400;
+ src: url('/fonts/geist-mono-latin-wght-normal.woff2') format('woff2');
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
+}
diff --git a/src/data/landing/fusion/benchmark.ts b/src/data/landing/fusion/benchmark.ts
index f03128cf..6fbd4087 100644
--- a/src/data/landing/fusion/benchmark.ts
+++ b/src/data/landing/fusion/benchmark.ts
@@ -1,43 +1,49 @@
/** Fusion vs Spark compaction benchmark as published on the OLake Fusion page. */
export interface BenchmarkRow {
metric: string
- spark: string
fusion: string
- /** The green badge next to the Fusion value. */
+ /** The badge next to the Fusion value. */
delta: string
+ spark: string
}
export const BENCHMARK = {
eyebrow: 'Benchmarks',
- title: 'Engineered for Performance',
- /** Rendered with `2.06× ` and `half the cost` in bold. */
- lead: {
- before: 'OLake Fusion compacts Apache Iceberg tables ',
- boldSpeed: '2.06× ',
- middle: 'faster than Apache Spark and at roughly ',
- boldCost: 'half the cost',
- after: '.'
+ title: 'Engineered for performance',
+ /** The design sets the prose grey and the claims white: those runs are marked `bright: true`. */
+ summary: [
+ { text: 'OLake Fusion compacts ' },
+ { text: 'Apache Iceberg tables 2.06×', bright: true },
+ { text: ' faster than Apache Spark and at roughly ' },
+ { text: 'half the cost.', bright: true }
+ ] as { text: string; bright?: boolean }[],
+ caption: '1 Parameter, 1.8 billion rows compacted in 27 minutes.',
+ headers: {
+ metric: 'Metrics',
+ fusionBrand: 'OLake',
+ fusionProduct: 'Fusion',
+ fusionSub: 'Open Source',
+ spark: 'Spark Compaction'
},
- headers: { metric: 'Metrics', spark: 'Spark compaction', fusionBrand: 'OLake', fusionProduct: 'Fusion' },
rows: [
- { metric: 'Total compaction time', spark: '55m 47s', fusion: '27m 02s', delta: '2.06X Faster' },
- { metric: 'Compaction cost / job', spark: '$2.19', fusion: '$1.06', delta: '52% Less Cost' },
- { metric: 'Config parameters', spark: '10+', fusion: '1', delta: '10X Simpler' }
+ { metric: 'Total Compaction Time', fusion: '27m 2s', delta: '2.06x faster', spark: '55m 47s' },
+ { metric: 'Compaction Cost/ Job', fusion: '$1.06', delta: '52% less cost', spark: '$2.19' },
+ { metric: 'Config Parameters', fusion: '1', delta: '10x simpler', spark: '10+' }
] as BenchmarkRow[],
link: { label: 'View detailed benchmarks', href: '/docs/fusion/getting-started/compaction/' }
}
-/** The expandable "How Fusion does this" panel under the table. */
-export const BENCHMARK_INFO = {
- title: 'How Fusion does this',
- items: [
- {
- term: 'Per-table scheduling',
- text: 'compaction schedules run in sets; Lite, Medium, or Full, instead of one-size-fits-all.'
- },
- {
- term: 'Smarter deletes',
- text: 'equality deletes convert to position deletes before merge, so queries do less work.'
- }
+/** The notes under the table. */
+export const BENCHMARK_HOW = {
+ title: 'How OLake Fusion does this',
+ lead: 'In order to outperform Spark, Fusion runs compaction on a per-table schedule you define. Rather than treating every table the same, it offers three tiers;',
+ tiers: [
+ { name: 'Lite', text: 'for frequent lightweight cleanup' },
+ { name: 'Medium', text: 'for regular small-file and delete resolution' },
+ { name: 'Full', text: 'for occasional deep rewrites' }
+ ],
+ outro: [
+ 'Each tier is scheduled independently, with higher tiers automatically overriding lower ones when schedules overlap.',
+ 'To improve read efficiency, Fusion converts expensive equality deletes into position deletes, reducing the work query engines do at read time.'
]
}
diff --git a/src/data/landing/fusion/cta.ts b/src/data/landing/fusion/cta.ts
index 1c6c1336..b35a0333 100644
--- a/src/data/landing/fusion/cta.ts
+++ b/src/data/landing/fusion/cta.ts
@@ -1,6 +1,7 @@
-/** Closing call to action on the OLake Fusion page. */
+/** The dark "Get in touch" card above the FAQ on the OLake Fusion page. */
export const FUSION_CTA = {
- title: 'Optimize your Iceberg tables today!',
- cta: { label: "Try, it's Free", href: '/docs/fusion/getting-started/quickstart/' },
- image: { src: '/img/landing/shared/iceberg-backdrop.webp', width: 1280, height: 720 }
+ eyebrow: 'Get in touch',
+ title: 'Optimize your Iceberg Table Maintenance',
+ primary: { label: 'Get Started', href: '/docs/fusion/getting-started/quickstart/' },
+ secondary: { label: 'Request Callback', href: '/contact/' }
}
diff --git a/src/data/landing/fusion/faqs.ts b/src/data/landing/fusion/faqs.ts
index 92dd4fd0..15e78580 100644
--- a/src/data/landing/fusion/faqs.ts
+++ b/src/data/landing/fusion/faqs.ts
@@ -1,12 +1,11 @@
-/** OLake Fusion FAQ, in display order. Answers are plain text. */
-export interface Faq {
- q: string
- a: string
-}
+import type { ProductFaqItem } from '@site/src/components/landing/product/ProductFaq'
-export const FAQ_TITLE = 'Frequently Asked Questions'
+export const FAQ_EYEBROW = 'FAQs'
-export const FAQS: Faq[] = [
+export const FAQ_TITLE = 'You might have some questions'
+
+/** OLake Fusion FAQ, in display order. Answers are plain text; the page's FAQPage JSON-LD (seo.ts) is built from it. */
+export const FAQS: ProductFaqItem[] = [
{
q: 'What is OLake Fusion?',
a: 'OLake Fusion is a self-hosted open-source software that keeps your lakehouse tables efficient, compact, and query-ready as your data continuously grows. As Iceberg tables evolve through ingestion, updates, and deletes, they accumulate small files, delete files, and excess metadata, all of which degrade query performance and increase storage overhead over time. Fusion manages that for you automatically.'
diff --git a/src/data/landing/fusion/features.ts b/src/data/landing/fusion/features.ts
index b9bf761d..de56234e 100644
--- a/src/data/landing/fusion/features.ts
+++ b/src/data/landing/fusion/features.ts
@@ -1,57 +1,34 @@
-/** The four Fusion features shown in the selector on the OLake Fusion page. */
-export type FeatureKind = 'tiered' | 'decay' | 'config' | 'hosted'
+import type { ProductFeature } from '@site/src/components/landing/product/ProductFeatures'
-export interface Feature {
- kind: FeatureKind
- title: string
- body: string
+/** "Features": the four capabilities behind the tab strip on the OLake Fusion page. */
+export const FEATURES_INTRO = {
+ eyebrow: 'Features',
+ title: 'Built to keep your lakehouse performant'
}
-export const FEATURES_EYEBROW = 'Features'
-export const FEATURES_TITLE = 'Built to keep your data lake performant'
-
-export const FEATURES: Feature[] = [
+export const FEATURES: ProductFeature[] = [
{
- kind: 'tiered',
- title: 'Tiered compaction',
- body: 'Trigger-based tiers instead of one blunt job, so lightweight compaction can run constantly and deep rewrites only run when they’re actually needed.'
+ id: 'tiered',
+ tab: 'Tiered Compaction',
+ title: 'Tiered Compaction',
+ body: 'Run compaction on trigger tiers instead of one whole job.\nLite every 20 minutes, Medium every 40 minutes, and Full as a periodic deep-clean for much larger datasets like terabyte-scale tables.'
},
{
- kind: 'decay',
- title: 'Reduced decay',
- body: 'Small files, delete files, and excess metadata pile up as tables evolve. Fusion resolves them periodically so query performance never degrades.'
+ id: 'decay',
+ tab: 'Omit Decay',
+ title: 'Omit Decay',
+ body: 'Small files, delete files, and excess metadata pile up as tables evolve.\nFusion resolves them periodically so query performance never degrades.'
},
{
- kind: 'config',
- title: 'Easy configuration',
+ id: 'config',
+ tab: 'Easy Configuration',
+ title: 'Easy Configuration',
body: 'One target-size parameter replaces the seven Spark rewrite_data_files needs. Same result, far less to manage.'
},
{
- kind: 'hosted',
- title: 'Self-hosted',
+ id: 'hosted',
+ tab: 'Self-Hosted',
+ title: 'Self-Hosted',
body: 'Open-source and deployable on Docker or Kubernetes so you pay only for the compute and storage you provision.'
}
]
-
-/** Tiered compaction visual: schedule per tier. `fill` is the bar width in percent. */
-export const TIERS = [
- { label: 'Lite', fill: 30, note: 'every 20 min' },
- { label: 'Medium', fill: 60, note: 'every 40 min' },
- { label: 'Full', fill: 100, note: 'deep-clean' }
-]
-
-/** Config visual: parameter counts. */
-export const CONFIG_COMPARE = {
- fusion: { value: '1', label: 'Fusion parameter' },
- spark: { value: '10+', label: 'Spark parameters' }
-}
-
-/** Self-hosted visual: where Fusion runs. */
-export const HOSTS = ['Docker', 'Kubernetes']
-
-/** Decay visual captions. */
-export const DECAY_LABELS = { before: 'fragmented files', after: 'compacted' }
-
-/** Auto-advance: one step every `FEATURE_TICK_MS`, `FEATURE_STEPS` steps per feature (10 s). */
-export const FEATURE_TICK_MS = 100
-export const FEATURE_STEPS = 100
diff --git a/src/data/landing/fusion/hero.ts b/src/data/landing/fusion/hero.ts
index 846de638..7740edb4 100644
--- a/src/data/landing/fusion/hero.ts
+++ b/src/data/landing/fusion/hero.ts
@@ -1,4 +1,6 @@
-/** Hero copy, calls to action and the "how it fits" diagram on the OLake Fusion page. */
+import { FUSION_GITHUB_REPO_URL } from '@site/src/components/landing/chrome/navItems'
+
+/** Hero copy and calls to action of the OLake Fusion page. */
/** PostHog multivariate flag that A/B tests the hero headline. */
export const HERO_HEADLINE_FLAG = 'fusion-hero-headline'
@@ -9,20 +11,10 @@ export const HERO_HEADLINES = {
simplicity: 'Simplify your Iceberg table maintenance'
}
+/** The headline comes from the A/B test above. */
export const HERO = {
eyebrow: 'OLake Fusion',
- primary: { label: 'Get Started', href: '/docs/fusion/getting-started/quickstart/' },
+ body: 'OLake Fusion helps keep your Iceberg tables performant, by automating compaction and reducing manual overhead.',
+ primary: { label: 'Github', href: FUSION_GITHUB_REPO_URL },
secondary: { label: 'Visit Docs', href: '/docs/fusion/getting-started/overview/' }
}
-
-/** Iceberg tables in, OLake Fusion in the middle, optimized Iceberg tables out. */
-export const FLOW = {
- inputLabel: 'Iceberg Tables',
- engineName: { brand: 'OLake', product: 'Fusion' },
- outputLabel: 'Optimized Iceberg Tables',
- capabilities: [
- { label: 'Compaction' },
- { label: 'Cleanup', badge: 'Soon' },
- { label: 'Logs & Metrics' }
- ] as { label: string; badge?: string }[]
-}
diff --git a/src/data/landing/fusion/problems.ts b/src/data/landing/fusion/problems.ts
index b26a4685..d2dc0fa3 100644
--- a/src/data/landing/fusion/problems.ts
+++ b/src/data/landing/fusion/problems.ts
@@ -1,38 +1,24 @@
-/**
- * "The silent tax": four short statements about what happens to Iceberg tables without maintenance.
- * Words listed in `emphasis` render large, bold and in the ink colour. The other words cycle through
- * the size, weight and colour recipe in SilentTax.tsx, so each sentence reads as a cloud of words.
- * `big` words get the emphasis size without the emphasis style; `recipeAt` makes a word use the
- * recipe slot of another position (word index -> slot).
- */
-export interface ProblemCard {
- words: string[]
- emphasis: string[]
- big?: string[]
- recipeAt?: Record
-}
+/** "Problem Statement": the four pain points OLake Fusion removes, each with its line illustration. */
+export type FusionProblemArt = 'small-files' | 'slower-queries' | 'invisible-costs' | 'debugging'
-export const PROBLEMS_EYEBROW = 'The silent tax'
+export interface FusionProblem {
+ label: string
+ title: string
+ art: FusionProblemArt
+}
-export const PROBLEMS_CLOSING = 'Set a schedule, Fusion will handle the rest.'
+export const PROBLEM_INTRO = {
+ eyebrow: 'Problem Statement',
+ title: 'Small files distracting from the bigger objective?'
+}
-export const PROBLEMS: ProblemCard[] = [
- {
- words: ['Small', 'files', 'PILE UP', 'faster', 'than', 'you', 'notice'],
- emphasis: ['Small', 'files', 'PILE UP']
- },
- {
- words: ['Queries', 'get', 'SLOWER', 'every', 'day'],
- emphasis: ['Queries', 'get', 'SLOWER']
- },
+export const PROBLEMS: FusionProblem[] = [
+ { label: 'Problem I', title: 'Small files pile up faster than you notice', art: 'small-files' },
+ { label: 'Problem II', title: 'Queries get slower every month', art: 'slower-queries' },
{
- words: ['Problems', "aren't", 'visible', 'until', "they're", 'expensive'],
- emphasis: ['Problems', 'expensive'],
- recipeAt: { 4: 2 }
+ label: 'Problem III',
+ title: 'Problems aren’t visible until they’re expensive',
+ art: 'invisible-costs'
},
- {
- words: ['Compaction', 'becomes', 'a', 'debugging', 'issue'],
- emphasis: ['Compaction'],
- big: ['debugging']
- }
+ { label: 'Problem IV', title: 'Compaction jobs become debugging issues', art: 'debugging' }
]
diff --git a/src/data/landing/go/architecture.ts b/src/data/landing/go/architecture.ts
deleted file mode 100644
index 4d2a42fd..00000000
--- a/src/data/landing/go/architecture.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/** The sources → OLake Go → destinations diagram under the hero. */
-export const GO_SOURCES = ['Postgres', 'MongoDB', 'MySQL', 'Kafka', 'S3', 'Oracle', 'MSSQL', 'DB2 LUW']
-
-/** Sync modes shown under the OLake Go node. */
-export const GO_SYNC_MODES = ['Full Load', 'CDC', 'Incremental']
-
-export interface GoDestination {
- label: string
- icon: { src: string; alt: string; width: number; height: number }
- /** The first destination is the highlighted one in the diagram. */
- primary?: boolean
-}
-
-export const GO_DESTINATIONS: GoDestination[] = [
- {
- label: 'Iceberg Tables',
- icon: { src: '/img/landing/shared/iceberg-icon.webp', alt: 'Iceberg', width: 256, height: 70 },
- primary: true
- },
- {
- label: 'Parquet Files',
- icon: { src: '/img/landing/shared/parquet-icon.webp', alt: 'Parquet', width: 128, height: 128 }
- }
-]
-
-export const GO_ARCHITECTURE_LABELS = {
- sources: 'Sources',
- destinations: 'Destinations',
- node: 'OLake Go'
-}
diff --git a/src/data/landing/go/benchmark.ts b/src/data/landing/go/benchmark.ts
index ff1dc8a4..fc9d1135 100644
--- a/src/data/landing/go/benchmark.ts
+++ b/src/data/landing/go/benchmark.ts
@@ -1,19 +1,19 @@
-import type { BenchmarkMode } from '@site/src/data/benchmarkData'
+import type { BenchmarkMode, ConnectorId } from '@site/src/data/benchmarkData'
-/** Copy of the "OLake Advantage" benchmark block. The numbers themselves live in src/data/benchmarkData.ts. */
+/** Copy of the dark "Benchmarks" section. The numbers themselves live in src/data/benchmarkData.ts. */
export const GO_BENCHMARK_INTRO = {
- eyebrow: 'The OLake Advantage',
+ eyebrow: 'Benchmarks',
title: 'Get the OLake Go Advantage'
}
-/** Bold runs are marked with `strong: true`. */
-export const GO_BENCHMARK_SUMMARY: { text: string; strong?: boolean }[] = [
+/** The design greys the figures and keeps the prose white: those runs are marked `muted: true`. */
+export const GO_BENCHMARK_SUMMARY: { text: string; muted?: boolean }[] = [
{ text: 'OLake Go replicates up to ' },
- { text: '12.5× faster', strong: true },
- { text: ' than Fivetran and ' },
- { text: '35–1000× faster', strong: true },
- { text: ' than other open-source tools, syncing ' },
- { text: '50M CDC changes in 15 minutes', strong: true },
+ { text: '12.5× faster than Fivetran', muted: true },
+ { text: ' and ' },
+ { text: '35–1000× faster than other open-source tools, ', muted: true },
+ { text: 'syncing ' },
+ { text: '50M CDC changes in 15 minutes', muted: true },
{ text: '.' }
]
@@ -22,25 +22,58 @@ export const GO_BENCHMARK_MODES: { id: BenchmarkMode; label: string }[] = [
{ id: 'cdc', label: 'CDC' }
]
+/**
+ * Two-line caption above the table, e.g. "Time to move 4.01 billion rows," / "Postgres to Apache
+ * Iceberg, full load". `rows` is the OLake row count, '-' while a source has no published numbers.
+ */
+export function goBenchmarkCaption(
+ mode: BenchmarkMode,
+ rows: string,
+ source: string
+): [string, string] {
+ const amount = rows && rows !== '-' ? `${rows.toLowerCase()} ` : ''
+ return mode === 'cdc'
+ ? [`Time to sync ${amount}CDC changes,`, `${source} to Apache Iceberg`]
+ : [`Time to move ${amount}rows,`, `${source} to Apache Iceberg, full load`]
+}
+
+/** Source tabs of the table, in the design's order. */
+export const GO_BENCHMARK_CONNECTORS: ConnectorId[] = [
+ 'postgres',
+ 'mysql',
+ 'kafka',
+ 'mssql',
+ 'mongodb'
+]
+
+/** Row labels, in the design's title case. */
+export const GO_BENCHMARK_METRIC_LABELS = {
+ rowsSynced: 'Rows Synced',
+ elapsedTime: 'Elapsed Time',
+ speed: 'Speed (Rows/s)',
+ comparison: 'Comparison',
+ cost: 'Cost'
+}
+
export const GO_BENCHMARK_LINK = {
- label: 'View all performance benchmarks',
+ label: 'View detailed benchmarks',
href: '/docs/benchmarks/ingestion'
}
-export const GO_BENCHMARK_COST_NOTE = 'OLake is OSS and self-hosted — only pay for your infrastructure.'
+export const GO_BENCHMARK_COST_NOTE =
+ 'OLake is OSS and self-hosted — only pay for your infrastructure.'
-/** The disclosure under the table. */
-export const GO_BENCHMARK_INFO = {
+/** The notes under the table. */
+export const GO_BENCHMARK_HOW = {
title: 'How OLake Go does this',
- icon: { src: '/img/landing/shared/benchmark-info-icon.webp', width: 96, height: 96 },
items: [
{
term: 'Parallel chunking',
- body: '— OLake Go splits large tables into chunks and loads them concurrently for maximum throughput.'
+ body: 'OLake Go splits large tables into chunks and loads them concurrently for maximum throughput.'
},
{
term: 'Native Iceberg writes',
- body: '— data lands directly in open Apache Iceberg tables with no proprietary staging layer.'
+ body: 'data lands directly in open Apache Iceberg tables with no proprietary staging layer.'
}
]
}
diff --git a/src/data/landing/go/cta.ts b/src/data/landing/go/cta.ts
index 155cc0f3..b9a7d054 100644
--- a/src/data/landing/go/cta.ts
+++ b/src/data/landing/go/cta.ts
@@ -1,5 +1,7 @@
+/** The blue "Get in touch" card above the FAQ. */
export const GO_CTA = {
- title: 'Start replicating to Iceberg today!',
- cta: { label: "Try, it's free!", href: '/docs/getting-started/quickstart/' },
- image: { src: '/img/landing/shared/iceberg-backdrop.webp', width: 1280, height: 720 }
+ eyebrow: 'Get in touch',
+ title: 'Achieve Sub-Second Data Ingestion',
+ primary: { label: 'Get Started', href: '/docs/getting-started/quickstart/' },
+ secondary: { label: 'Request Callback', href: '/contact/' }
}
diff --git a/src/data/landing/go/faq.ts b/src/data/landing/go/faq.ts
index 3560253a..75312ee7 100644
--- a/src/data/landing/go/faq.ts
+++ b/src/data/landing/go/faq.ts
@@ -1,15 +1,11 @@
-/**
- * The visible FAQ. The FAQPage JSON-LD in src/data/landing/seo.ts (GO_SEO) mirrors these questions
- * and answers word for word: keep the two in sync when either changes.
- */
-export interface GoFaq {
- q: string
- a: string
-}
+import type { ProductFaqItem } from '@site/src/components/landing/product/ProductFaq'
-export const GO_FAQ_TITLE = 'Frequently Asked Questions'
+export const GO_FAQ_EYEBROW = 'FAQs'
-export const GO_FAQS: GoFaq[] = [
+export const GO_FAQ_TITLE = 'You might have some questions'
+
+/** The visible FAQ, in display order. The page's FAQPage JSON-LD (seo.ts) is built from it. */
+export const GO_FAQS: ProductFaqItem[] = [
{
q: 'How to get started?',
a: 'Check the Quickstart Guide. With a single Docker command you can spin up OLake Go and access the UI.'
@@ -32,6 +28,6 @@ export const GO_FAQS: GoFaq[] = [
},
{
q: 'What data platforms and tools does OLake Go integrate with?',
- a: "As of now, we integrate with Apache Iceberg and S3 as destinations. You can query it using popular query engines like Spark, AWS Athena, Snowflake, Databricks, and BigQuery."
+ a: 'As of now, we integrate with Apache Iceberg and S3 as destinations. You can query it using popular query engines like Spark, AWS Athena, Snowflake, Databricks, and BigQuery.'
}
]
diff --git a/src/data/landing/go/features.ts b/src/data/landing/go/features.ts
index 7e82e5df..ebb3c2d5 100644
--- a/src/data/landing/go/features.ts
+++ b/src/data/landing/go/features.ts
@@ -1,41 +1,34 @@
-/** "The Fundamental": the four capabilities in the feature selector. */
-export type GoFeatureArt = 'tiered' | 'decay' | 'chunk' | 'resume'
-
-export interface GoFeature {
- id: string
- title: string
- body: string
- art: GoFeatureArt
-}
+import type { ProductFeature } from '@site/src/components/landing/product/ProductFeatures'
+/** "Features": the four capabilities behind the tab strip on the OLake Go page. */
export const GO_FEATURES_INTRO = {
- eyebrow: 'The Fundamental',
+ eyebrow: 'Features',
title: 'Experience the most seamless workflow'
}
-export const GO_FEATURES: GoFeature[] = [
+export const GO_FEATURES: ProductFeature[] = [
{
- id: 'syncs',
- title: 'Full, incremental & CDC syncs',
- body: 'Run full loads, incremental pulls, or real-time change data capture, whatever each table needs, all from a single tool.',
- art: 'tiered'
+ id: 'cdc',
+ tab: 'CDC',
+ title: 'Full, Incremental & CDC Syncs',
+ body: 'Run full loads, incremental pulls, or real-time change data capture, whatever each table needs, all from a single tool.'
},
{
- id: 'evolution',
- title: 'Schema & partition evolution',
- body: 'Source schemas change and partitions grow. OLake Go evolves your Iceberg tables automatically so pipelines never break.',
- art: 'decay'
+ id: 'schema-evolution',
+ tab: 'Schema Evolution',
+ title: 'Schema & Partition Evolution',
+ body: 'Source schemas change and partitions grow. OLake Go evolves your Iceberg tables automatically so pipelines never break.'
},
{
- id: 'chunking',
- title: 'Parallelised chunking',
- body: 'Large collections are split into virtual chunks read in parallel, dramatically cutting the time for full snapshots of big datasets.',
- art: 'chunk'
+ id: 'parallel-chunking',
+ tab: 'Parallel Chunking',
+ title: 'Parallelised Chunking',
+ body: 'Large collections are split into virtual chunks read in parallel, dramatically cutting the time for full snapshots of big datasets.'
},
{
- id: 'resumable',
- title: 'Stateful, resumable syncs',
- body: 'Syncs checkpoint their progress and resume automatically after crashes or network failures; never from scratch.',
- art: 'resume'
+ id: 'resumable-syncs',
+ tab: 'Resumable Syncs',
+ title: 'Stateful Resumable Syncs',
+ body: 'Syncs checkpoint their progress and resume automatically after crashes or network failures; never from scratch.'
}
]
diff --git a/src/data/landing/go/hero.ts b/src/data/landing/go/hero.ts
index 82855f37..adbc002e 100644
--- a/src/data/landing/go/hero.ts
+++ b/src/data/landing/go/hero.ts
@@ -1,7 +1,27 @@
+import { GITHUB_REPO_URL } from '@site/src/components/landing/chrome/navItems'
+
/** Hero copy and calls to action of the OLake Go page. */
export const GO_HERO = {
- badge: 'OLake Go',
- headline: 'Fastest Replication to Iceberg & S3',
- primary: { label: 'Get Started', href: '/docs/getting-started/quickstart/' },
- secondary: { label: 'Talk to us', href: '/contact/' }
+ eyebrow: 'OLake Go',
+ headline: 'Fastest Replication into Apache Iceberg & S3',
+ body: 'OLake Go helps you ingest data from 8 different sources into Apache Iceberg and S3',
+ primary: { label: 'Github', href: GITHUB_REPO_URL },
+ secondary: { label: 'Visit Docs', href: '/docs' }
}
+
+export type GoSourceId =
+ 'kafka' | 'postgres' | 'mysql' | 'mongodb' | 's3' | 'mssql' | 'db2' | 'oracle'
+
+/** The "Available Sources" strip under the hero buttons, in the order it scrolls. */
+export const GO_SOURCES_LABEL = 'Available Sources'
+
+export const GO_SOURCES: { id: GoSourceId; name: string }[] = [
+ { id: 'kafka', name: 'Kafka' },
+ { id: 'postgres', name: 'Postgres' },
+ { id: 'mysql', name: 'MySQL' },
+ { id: 'mongodb', name: 'MongoDB' },
+ { id: 's3', name: 'S3' },
+ { id: 'mssql', name: 'MSSQL' },
+ { id: 'db2', name: 'DB2' },
+ { id: 'oracle', name: 'Oracle' }
+]
diff --git a/src/data/landing/go/problem.ts b/src/data/landing/go/problem.ts
index 15a46633..5712e429 100644
--- a/src/data/landing/go/problem.ts
+++ b/src/data/landing/go/problem.ts
@@ -1,34 +1,20 @@
-/** "The Problem": four pain points and the answer headline. Emphasised words render larger and in the brand blue. */
-export interface ProblemSentence {
- words: string[]
- emphasis: string[]
- /** Where the statement sits in the cloud: percent from the top and left, tilt in degrees, float delay in seconds, base font size in px. */
- layout: { top: number; left: number; rot: number; delay: number; size: number }
+/** "Problem Statement": the four pain points OLake Go removes, each with its line illustration. */
+export type GoProblemArt = 'slow-syncs' | 'legacy-costs' | 'cdc-breaks' | 'schema-drift'
+
+export interface GoProblem {
+ label: string
+ title: string
+ art: GoProblemArt
}
-export const GO_PROBLEM = {
- eyebrow: 'The Problem',
- headline: 'Replicate any source into your lakehouse, seamlessly',
- sentences: [
- {
- words: ['Slow', 'syncs', 'BLOCK', 'your', 'analytics'],
- emphasis: ['Slow', 'syncs', 'BLOCK'],
- layout: { top: 22, left: 42, rot: -3, delay: 0, size: 28 }
- },
- {
- words: ['Legacy', 'ETL', 'costs', 'PILE UP', 'fast'],
- emphasis: ['costs', 'PILE UP'],
- layout: { top: 43, left: 57, rot: 2, delay: 0.6, size: 24 }
- },
- {
- words: ['CDC', 'pipelines', 'BREAK', 'silently', 'in', 'production'],
- emphasis: ['CDC', 'BREAK'],
- layout: { top: 64, left: 46, rot: -2, delay: 1.2, size: 25 }
- },
- {
- words: ['Schema', 'drift', 'stalls', 'ingestion'],
- emphasis: ['Schema', 'drift'],
- layout: { top: 85, left: 55, rot: 3, delay: 0.3, size: 23 }
- }
- ] as ProblemSentence[]
+export const GO_PROBLEM_INTRO = {
+ eyebrow: 'Problem Statement',
+ title: 'Still doubling back on your ingestion jobs?'
}
+
+export const GO_PROBLEMS: GoProblem[] = [
+ { label: 'Problem I', title: 'Slow syncs block your analytics', art: 'slow-syncs' },
+ { label: 'Problem II', title: 'Legacy ETL costs pile up fast', art: 'legacy-costs' },
+ { label: 'Problem III', title: 'CDC pipelines break silently in production', art: 'cdc-breaks' },
+ { label: 'Problem IV', title: 'Schema drift stalls ingestion', art: 'schema-drift' }
+]
diff --git a/src/data/landing/seo.ts b/src/data/landing/seo.ts
index 12830a96..ff4f80cd 100644
--- a/src/data/landing/seo.ts
+++ b/src/data/landing/seo.ts
@@ -1,4 +1,7 @@
import type { JsonLdSchema } from '@site/src/components/landing/seo/LandingSeo'
+import type { ProductFaqItem } from '@site/src/components/landing/product/ProductFaq'
+import { GO_FAQS } from './go/faq'
+import { FAQS as FUSION_FAQS } from './fusion/faqs'
const SITE_URL = 'https://olake.io'
// The design's helmet references https://olake.io/olake-go-og.png / olake-fusion-og.png,
@@ -6,6 +9,21 @@ const SITE_URL = 'https://olake.io'
// existing site OG image rather than a broken reference.
const FALLBACK_OG_IMAGE = `${SITE_URL}/img/logo/olake-og-card.png`
+// FAQPage JSON-LD built from the FAQ a page shows, so the markup always matches the visible
+// questions and answers word for word, as Google requires.
+const faqPageSchema = (faqs: ProductFaqItem[]): JsonLdSchema => ({
+ id: 'faq-page',
+ data: {
+ '@context': 'https://schema.org',
+ '@type': 'FAQPage',
+ mainEntity: faqs.map(({ q, a }) => ({
+ '@type': 'Question',
+ name: q,
+ acceptedAnswer: { '@type': 'Answer', text: a }
+ }))
+ }
+})
+
export const GO_SEO = {
title: 'OLake Go — Fastest Open-Source Data Replication to Apache Iceberg & S3',
description:
@@ -29,63 +47,7 @@ export const GO_SEO = {
publisher: { '@type': 'Organization', name: 'Datazip', url: SITE_URL }
}
},
- {
- // Must mirror the visible FAQ on /olake-go (the `faqs` array in
- // src/data/landing/go/faq.ts) word for word. Google requires FAQ markup to
- // match what the page shows; keep the two in sync when either changes.
- id: 'faq-page',
- data: {
- '@context': 'https://schema.org',
- '@type': 'FAQPage',
- mainEntity: [
- {
- '@type': 'Question',
- name: 'How to get started?',
- acceptedAnswer: { '@type': 'Answer', text: 'Check the Quickstart Guide. With a single Docker command you can spin up OLake Go and access the UI.' }
- },
- {
- '@type': 'Question',
- name: 'Is OLake Go really open source?',
- acceptedAnswer: {
- '@type': 'Answer',
- text: 'Yes. OLake Go is fully open source under the Apache 2.0 license. You can explore the GitHub repository (already starred by 1K+ developers) and use it freely without hidden costs.'
- }
- },
- {
- '@type': 'Question',
- name: 'Is there any enterprise plan?',
- acceptedAnswer: {
- '@type': 'Answer',
- text: "We're actively working on providing enterprise support, from professional assistance and pilot programs to helping teams scale OLake Go in production. You can reach out at hello@olake.io to learn more."
- }
- },
- {
- '@type': 'Question',
- name: 'How can I contribute?',
- acceptedAnswer: {
- '@type': 'Answer',
- text: 'Join our Slack community, review the Contribution Guide, and explore "Good First Issues" on GitHub. Contributors can get their pull requests merged and be part of building the fastest open-source Iceberg-native ingestion tool.'
- }
- },
- {
- '@type': 'Question',
- name: 'Why should I use OLake Go?',
- acceptedAnswer: {
- '@type': 'Answer',
- text: 'OLake Go makes data replication into Apache Iceberg seamless, faster, and cost-efficient. It handles real-time CDC, schema and partition evolution, full and incremental syncs, and compaction, all without vendor lock-in, so your Iceberg tables stay open, scalable, and ready for analytics.'
- }
- },
- {
- '@type': 'Question',
- name: 'What data platforms and tools does OLake Go integrate with?',
- acceptedAnswer: {
- '@type': 'Answer',
- text: "As of now, we integrate with Apache Iceberg and S3 as destinations. You can query it using popular query engines like Spark, AWS Athena, Snowflake, Databricks, and BigQuery."
- }
- }
- ]
- }
- }
+ faqPageSchema(GO_FAQS)
] as JsonLdSchema[]
}
@@ -171,6 +133,7 @@ export const FUSION_SEO = {
url: `${SITE_URL}/olake-fusion/`,
publisher: { '@type': 'Organization', name: 'Datazip', url: SITE_URL }
}
- }
+ },
+ faqPageSchema(FUSION_FAQS)
] as JsonLdSchema[]
}
diff --git a/src/pages/olake-fusion.tsx b/src/pages/olake-fusion.tsx
index f2897958..2d8fd957 100644
--- a/src/pages/olake-fusion.tsx
+++ b/src/pages/olake-fusion.tsx
@@ -1,16 +1,8 @@
import React from 'react'
-import LakesidePage from '@site/src/components/landing/ui/LakesidePage'
import LandingSeo from '@site/src/components/landing/seo/LandingSeo'
-import Hero from '@site/src/components/landing/fusion/Hero'
-import ProductFlow from '@site/src/components/landing/fusion/ProductFlow'
-import SilentTax from '@site/src/components/landing/fusion/SilentTax'
-import Features from '@site/src/components/landing/fusion/Features'
-import Benchmark from '@site/src/components/landing/fusion/Benchmark'
-import FusionCta from '@site/src/components/landing/fusion/FusionCta'
-import Faq from '@site/src/components/landing/fusion/Faq'
+import FusionPage from '@site/src/components/landing/fusion/FusionPage'
import { FUSION_SEO } from '@site/src/data/landing/seo'
import { HERO_HEADLINE_FLAG, HERO_HEADLINES } from '@site/src/data/landing/fusion/hero'
-import '@site/src/components/landing/fusion/fusion.css'
import { useFeatureFlagVariant } from '@site/src/lib/posthog/useFeatureFlagVariant'
export default function OLakeFusionPage() {
@@ -18,12 +10,7 @@ export default function OLakeFusionPage() {
const headline = useFeatureFlagVariant(HERO_HEADLINE_FLAG, HERO_HEADLINES, 'performance')
return (
- }
- >
+ <>
-
-
-
-
-
-
-
+
+ >
)
}
diff --git a/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx
index d776d5ef..4e97f36d 100644
--- a/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx
+++ b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx
@@ -1,8 +1,9 @@
import React, { useState } from 'react'
import Link from '@docusaurus/Link'
+import { useLocation } from '@docusaurus/router'
import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'
import { cn } from '@site/src/lib/utils'
-import { LAKESIDE_NAV, LAKESIDE_CTA } from '@site/src/components/landing/chrome/navItems'
+import { LAKESIDE_NAV, lakesideCta } from '@site/src/components/landing/chrome/navItems'
// Group rows expand in place with Infima's collapsible classes, so they pick up dark mode.
// Upstream renders themeConfig.navbar.items; the real menu lives in chrome/navItems.ts.
@@ -11,6 +12,7 @@ export default function NavbarMobilePrimaryMenu() {
const mobileSidebar = useNavbarMobileSidebar()
const [openGroup, setOpenGroup] = useState(null)
const close = () => mobileSidebar.toggle()
+ const cta = lakesideCta(useLocation().pathname)
return (
diff --git a/static/fonts/LICENSE-geist-mono.txt b/static/fonts/LICENSE-geist-mono.txt
new file mode 100644
index 00000000..2e4aef1e
--- /dev/null
+++ b/static/fonts/LICENSE-geist-mono.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) GeistMono-Italic[wght].ttf: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/static/fonts/geist-mono-latin-ext-wght-normal.woff2 b/static/fonts/geist-mono-latin-ext-wght-normal.woff2
new file mode 100644
index 00000000..ea22fd94
Binary files /dev/null and b/static/fonts/geist-mono-latin-ext-wght-normal.woff2 differ
diff --git a/static/fonts/geist-mono-latin-wght-normal.woff2 b/static/fonts/geist-mono-latin-wght-normal.woff2
new file mode 100644
index 00000000..0750682c
Binary files /dev/null and b/static/fonts/geist-mono-latin-wght-normal.woff2 differ
diff --git a/static/img/landing/fusion/cta-quantity-skew.webp b/static/img/landing/fusion/cta-quantity-skew.webp
new file mode 100644
index 00000000..5e126d69
Binary files /dev/null and b/static/img/landing/fusion/cta-quantity-skew.webp differ
diff --git a/static/img/landing/fusion/feature-background.webp b/static/img/landing/fusion/feature-background.webp
new file mode 100644
index 00000000..5933776a
Binary files /dev/null and b/static/img/landing/fusion/feature-background.webp differ
diff --git a/static/img/landing/fusion/feature-config.webp b/static/img/landing/fusion/feature-config.webp
new file mode 100644
index 00000000..f822313b
Binary files /dev/null and b/static/img/landing/fusion/feature-config.webp differ
diff --git a/static/img/landing/fusion/feature-decay.webp b/static/img/landing/fusion/feature-decay.webp
new file mode 100644
index 00000000..04c242fb
Binary files /dev/null and b/static/img/landing/fusion/feature-decay.webp differ
diff --git a/static/img/landing/fusion/feature-hosted.webp b/static/img/landing/fusion/feature-hosted.webp
new file mode 100644
index 00000000..78e54365
Binary files /dev/null and b/static/img/landing/fusion/feature-hosted.webp differ
diff --git a/static/img/landing/fusion/feature-tiered.webp b/static/img/landing/fusion/feature-tiered.webp
new file mode 100644
index 00000000..fc1d5bc1
Binary files /dev/null and b/static/img/landing/fusion/feature-tiered.webp differ
diff --git a/static/img/landing/fusion/hero-contours.webp b/static/img/landing/fusion/hero-contours.webp
new file mode 100644
index 00000000..6a7f0bdf
Binary files /dev/null and b/static/img/landing/fusion/hero-contours.webp differ
diff --git a/static/img/landing/fusion/hero-dashboard.webp b/static/img/landing/fusion/hero-dashboard.webp
new file mode 100644
index 00000000..39a037e8
Binary files /dev/null and b/static/img/landing/fusion/hero-dashboard.webp differ
diff --git a/static/img/landing/fusion/problem-compaction-debugging.webp b/static/img/landing/fusion/problem-compaction-debugging.webp
new file mode 100644
index 00000000..416d5ef2
Binary files /dev/null and b/static/img/landing/fusion/problem-compaction-debugging.webp differ
diff --git a/static/img/landing/fusion/problem-invisible-costs.webp b/static/img/landing/fusion/problem-invisible-costs.webp
new file mode 100644
index 00000000..c2b43ca7
Binary files /dev/null and b/static/img/landing/fusion/problem-invisible-costs.webp differ
diff --git a/static/img/landing/fusion/problem-slower-queries.webp b/static/img/landing/fusion/problem-slower-queries.webp
new file mode 100644
index 00000000..f00065a9
Binary files /dev/null and b/static/img/landing/fusion/problem-slower-queries.webp differ
diff --git a/static/img/landing/fusion/problem-small-files.webp b/static/img/landing/fusion/problem-small-files.webp
new file mode 100644
index 00000000..9f8e985f
Binary files /dev/null and b/static/img/landing/fusion/problem-small-files.webp differ
diff --git a/static/img/landing/go/cta-dot-wave.webp b/static/img/landing/go/cta-dot-wave.webp
new file mode 100644
index 00000000..d86cadb6
Binary files /dev/null and b/static/img/landing/go/cta-dot-wave.webp differ
diff --git a/static/img/landing/go/feature-background.webp b/static/img/landing/go/feature-background.webp
new file mode 100644
index 00000000..db1477e0
Binary files /dev/null and b/static/img/landing/go/feature-background.webp differ
diff --git a/static/img/landing/go/feature-cdc-background.webp b/static/img/landing/go/feature-cdc-background.webp
new file mode 100644
index 00000000..d2a2bc55
Binary files /dev/null and b/static/img/landing/go/feature-cdc-background.webp differ
diff --git a/static/img/landing/go/feature-cdc.webp b/static/img/landing/go/feature-cdc.webp
new file mode 100644
index 00000000..bbd5ae54
Binary files /dev/null and b/static/img/landing/go/feature-cdc.webp differ
diff --git a/static/img/landing/go/feature-parallel-chunking.webp b/static/img/landing/go/feature-parallel-chunking.webp
new file mode 100644
index 00000000..bdd21d06
Binary files /dev/null and b/static/img/landing/go/feature-parallel-chunking.webp differ
diff --git a/static/img/landing/go/feature-resumable-syncs.webp b/static/img/landing/go/feature-resumable-syncs.webp
new file mode 100644
index 00000000..73c8a813
Binary files /dev/null and b/static/img/landing/go/feature-resumable-syncs.webp differ
diff --git a/static/img/landing/go/feature-schema-evolution.webp b/static/img/landing/go/feature-schema-evolution.webp
new file mode 100644
index 00000000..a681158a
Binary files /dev/null and b/static/img/landing/go/feature-schema-evolution.webp differ
diff --git a/static/img/landing/go/hero-dashboard.webp b/static/img/landing/go/hero-dashboard.webp
new file mode 100644
index 00000000..6381609d
Binary files /dev/null and b/static/img/landing/go/hero-dashboard.webp differ
diff --git a/static/img/landing/go/hero-strata-lines.webp b/static/img/landing/go/hero-strata-lines.webp
new file mode 100644
index 00000000..efabb09c
Binary files /dev/null and b/static/img/landing/go/hero-strata-lines.webp differ
diff --git a/static/img/landing/go/problem-cdc-pipeline.webp b/static/img/landing/go/problem-cdc-pipeline.webp
new file mode 100644
index 00000000..49969494
Binary files /dev/null and b/static/img/landing/go/problem-cdc-pipeline.webp differ
diff --git a/static/img/landing/go/problem-cdc-stale.webp b/static/img/landing/go/problem-cdc-stale.webp
new file mode 100644
index 00000000..84bbc450
Binary files /dev/null and b/static/img/landing/go/problem-cdc-stale.webp differ
diff --git a/static/img/landing/go/problem-legacy-etl-costs.webp b/static/img/landing/go/problem-legacy-etl-costs.webp
new file mode 100644
index 00000000..587294b6
Binary files /dev/null and b/static/img/landing/go/problem-legacy-etl-costs.webp differ
diff --git a/static/img/landing/go/problem-schema-drift.webp b/static/img/landing/go/problem-schema-drift.webp
new file mode 100644
index 00000000..0ece95f3
Binary files /dev/null and b/static/img/landing/go/problem-schema-drift.webp differ
diff --git a/static/img/landing/go/problem-slow-syncs.webp b/static/img/landing/go/problem-slow-syncs.webp
new file mode 100644
index 00000000..484f4eff
Binary files /dev/null and b/static/img/landing/go/problem-slow-syncs.webp differ
diff --git a/static/img/landing/go/source-db2.webp b/static/img/landing/go/source-db2.webp
new file mode 100644
index 00000000..73a27ea0
Binary files /dev/null and b/static/img/landing/go/source-db2.webp differ
diff --git a/static/img/landing/go/source-mssql.webp b/static/img/landing/go/source-mssql.webp
new file mode 100644
index 00000000..fa03db99
Binary files /dev/null and b/static/img/landing/go/source-mssql.webp differ
diff --git a/static/img/landing/shared/benchmark-info-icon.webp b/static/img/landing/shared/benchmark-info-icon.webp
deleted file mode 100644
index 61786732..00000000
Binary files a/static/img/landing/shared/benchmark-info-icon.webp and /dev/null differ
diff --git a/static/img/landing/shared/iceberg-backdrop.webp b/static/img/landing/shared/iceberg-backdrop.webp
deleted file mode 100644
index 71516c9e..00000000
Binary files a/static/img/landing/shared/iceberg-backdrop.webp and /dev/null differ
diff --git a/static/img/landing/shared/iceberg-icon.webp b/static/img/landing/shared/iceberg-icon.webp
deleted file mode 100644
index 5aa90f96..00000000
Binary files a/static/img/landing/shared/iceberg-icon.webp and /dev/null differ
diff --git a/static/img/landing/shared/parquet-icon.webp b/static/img/landing/shared/parquet-icon.webp
deleted file mode 100644
index 33ab9205..00000000
Binary files a/static/img/landing/shared/parquet-icon.webp and /dev/null differ