Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions apps/marketing/src/app/components/CTASection/CTASection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { motion } from "framer-motion";
import { useState } from "react";
import { DownloadButton } from "../DownloadButton";
import { WaitlistModal } from "../WaitlistModal";
Expand All @@ -12,24 +11,15 @@ export function CTASection() {
<>
<section className="relative py-32 px-8 lg:px-[30px]">
<div className="max-w-7xl mx-auto flex flex-col items-center text-center">
<motion.h2
<h2
className="text-[32px] lg:text-[40px] font-normal tracking-normal leading-[1.3em] text-foreground mb-8"
style={{ fontFamily: "var(--font-ibm-plex-mono)" }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
Get Superset Today
</motion.h2>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
>
</h2>
<div>
<DownloadButton onJoinWaitlist={() => setIsWaitlistOpen(true)} />
</motion.div>
</div>
</div>
</section>
<WaitlistModal
Expand Down
19 changes: 4 additions & 15 deletions apps/marketing/src/app/components/FAQSection/FAQSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,16 @@ export function FAQSection() {
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 xl:grid-cols-[1fr_1.5fr] gap-12 xl:gap-20">
{/* Left Column - Title */}
<motion.div
className="xl:sticky xl:top-24 xl:self-start"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<div className="xl:sticky xl:top-24 xl:self-start">
<h2 className="text-3xl sm:text-4xl xl:text-5xl font-medium tracking-tight text-foreground leading-[1.1]">
Frequently
<br />
asked questions
</h2>
</motion.div>
</div>

{/* Right Column - Accordion */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<div>
<div className="w-full">
{FAQ_ITEMS.map((item, index) => (
<FAQAccordionItem
Expand All @@ -124,7 +113,7 @@ export function FAQSection() {
/>
))}
</div>
</motion.div>
</div>
</div>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { motion } from "framer-motion";
import { FeatureDemo } from "./components/FeatureDemo";
import { IsolationDemo } from "./components/IsolationDemo";
import { OpenInDemo } from "./components/OpenInDemo";
Expand All @@ -25,15 +24,11 @@ export function FeaturesSection() {
const isReversed = index % 2 === 1;
const DemoComponent = DEMO_COMPONENTS[index];
return (
<motion.div
<div
key={feature.title}
className={`grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center ${
isReversed ? "lg:direction-rtl" : ""
}`}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
{/* Text Content */}
<div
Expand All @@ -58,7 +53,7 @@ export function FeaturesSection() {
{DemoComponent && <DemoComponent />}
</FeatureDemo>
</div>
</motion.div>
</div>
);
})}
</div>
Expand Down
29 changes: 13 additions & 16 deletions apps/marketing/src/app/components/HeroSection/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
"use client";

import { COMPANY } from "@superset/shared/constants";
import { motion } from "framer-motion";
import dynamic from "next/dynamic";
import { useState } from "react";
import { FaGithub } from "react-icons/fa";
import { ShaderAnimation } from "../../../components/ui/shader-animation";
import { DownloadButton } from "../DownloadButton";
import { WaitlistModal } from "../WaitlistModal";
import { ProductDemo } from "./components/ProductDemo";
import { TypewriterText } from "./components/TypewriterText";

const ShaderAnimation = dynamic(
() =>
import("../../../components/ui/shader-animation").then(
(mod) => mod.ShaderAnimation,
),
{ ssr: false },
);
Comment on lines +4 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Next.js 15 next/dynamic ssr false client component CLS layout shift guidance

💡 Result:

Using next/dynamic with { ssr: false } in Next.js 15 (App Router)

  • ssr: false only works in Client Components. If you try to use dynamic(..., { ssr: false }) in a Server Component, Next.js will error—move the dynamic import into a Client Component wrapper and render that wrapper from your Server Component. [1]
  • Don’t combine suspense: true with ssr: false. With suspense: true, React will try to resolve Suspense on the server and Next.js documents that ssr: false is effectively ignored / invalid in that configuration. [2]

Preventing CLS (layout shift) when the client-only component loads

When you render a client-only dynamic component, the initial HTML won’t contain it—so you must reserve space for whatever will appear after hydration:

  • Provide a stable placeholder while loading (skeleton) that matches the final component’s size. With next/dynamic, use the loading option to render a placeholder. [1]
  • Reserve space via CSS: wrap the dynamic component in a container with a predictable min-height, or use aspect-ratio for responsive boxes so the page layout doesn’t jump when the component appears. [3]
  • If the dynamic content is near the top of the viewport, CLS impact is typically worse—either reserve space carefully or consider placing late-loading UI lower on the page when possible. [3]

Minimal pattern (App Router)

// app/components/ClientOnlyWidget.tsx
'use client'

import dynamic from 'next/dynamic'

const Widget = dynamic(() => import('./Widget'), {
  ssr: false,
  loading: () => <div style={{ minHeight: 240 }} />, // reserve space to prevent CLS
})

export function ClientOnlyWidget() {
  return <Widget />
}

Key idea: the placeholder (or wrapper) should occupy roughly the same dimensions as the final widget to avoid CLS. [3]

Sources: Next.js Lazy Loading guide (updated Oct 15, 2025) [1], Next.js error note on suspense usage with ssr: false [2], web.dev CLS guidance on reserving space / placeholders / aspect-ratio [3].


🏁 Script executed:

# Find and examine the ShaderAnimation component
fd -e tsx -e ts | xargs rg -l "ShaderAnimation" | head -20

Repository: superset-sh/superset

Length of output: 221


🏁 Script executed:

# Check the full HeroSection component for loading state/Suspense boundaries
cat -n apps/marketing/src/app/components/HeroSection/HeroSection.tsx | head -100

Repository: superset-sh/superset

Length of output: 3340


Add loading placeholder to prevent CLS when ShaderAnimation hydrates.

ssr: false is appropriate for this client-only component, but it lacks a loading option with a placeholder that reserves space during client-side hydration. Since ShaderAnimation renders at the top of the hero section, the layout will shift when it loads. Provide a placeholder matching the component's final dimensions:

Suggested fix
const ShaderAnimation = dynamic(
	() =>
		import("../../../components/ui/shader-animation").then(
			(mod) => mod.ShaderAnimation,
		),
	{ 
		ssr: false,
		loading: () => <div style={{ minHeight: 400 }} /> // adjust height to match ShaderAnimation's actual height
	},
);
🤖 Prompt for AI Agents
In `@apps/marketing/src/app/components/HeroSection/HeroSection.tsx` around lines 4
- 18, The dynamic ShaderAnimation import lacks a loading placeholder which
causes CLS when it hydrates; update the dynamic call for ShaderAnimation (the
dynamic(...) that returns mod.ShaderAnimation) to include a loading option that
returns a placeholder element sized to the final animation (e.g., a div with an
appropriate minHeight or fixed height) and keep ssr: false so the layout space
is reserved during client-side hydration.


export function HeroSection() {
const [isWaitlistOpen, setIsWaitlistOpen] = useState(false);

Expand All @@ -20,12 +27,7 @@ export function HeroSection() {

<div className="relative w-full max-w-[1600px] mx-auto px-4 sm:px-8 lg:px-[30px] py-8 sm:py-12 lg:py-16">
<div className="grid grid-cols-1 lg:grid-cols-[2fr_3fr] gap-8 lg:gap-16 items-center">
<motion.div
className="space-y-6 sm:space-y-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="space-y-6 sm:space-y-8">
<div className="space-y-2 sm:space-y-6">
<h1
className="text-2xl sm:text-3xl lg:text-4xl font-normal tracking-normal leading-[1.3em] text-foreground relative"
Expand Down Expand Up @@ -62,16 +64,11 @@ export function HeroSection() {
<FaGithub className="size-4" />
</button>
</div>
</motion.div>
</div>

<motion.div
className="relative w-full min-w-0"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="relative w-full min-w-0">
<ProductDemo />
</motion.div>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,9 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
// - "Open in Any IDE": Highlight top bar / open actions

return (
<motion.div
<div
className="relative w-full min-w-[600px] rounded-xl overflow-hidden bg-black/40 backdrop-blur-xl border border-white/[0.08] shadow-2xl"
style={{ aspectRatio: "16/10" }}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
{/* Window chrome */}
<div className="flex items-center justify-between px-3 py-2 bg-white/[0.03] backdrop-blur-md border-b border-white/[0.06]">
Expand Down Expand Up @@ -917,6 +914,6 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
</div>
</div>
</motion.div>
</motion.div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"use client";

import { MeshGradient } from "@superset/ui/mesh-gradient";
import { motion } from "framer-motion";
import dynamic from "next/dynamic";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx | head -25

Repository: superset-sh/superset

Length of output: 1025


🌐 Web query:

Next.js 15 next/dynamic ssr: false best practices client-only components

💡 Result:

Best practices for client‑only components in Next.js 15 using next/dynamic({ ssr: false })

  1. Only use ssr: false inside a Client Component

    • In the App Router, ssr: false is not supported in Server Components—move the dynamic(..., { ssr: false }) call into a file with 'use client'. This is explicitly required by the Next.js lazy-loading docs. [1]
  2. Create a small client “wrapper” and keep the rest as Server Components

    • Pattern: Server Component page/layout → imports a small Client Component wrapper → wrapper dynamically imports the heavy/browser-only component with ssr:false.
    • This preserves the benefits of Server Components while isolating true browser-only code. [1]
  3. Provide a loading fallback (and reserve layout space)

    • Since the component won’t be prerendered, users will see the fallback until the JS chunk loads. Use loading: () => <Skeleton /> and ensure the skeleton has stable dimensions to reduce layout shift. (Supported directly by next/dynamic patterns in Next.js docs.) [1]
  4. Use ssr: false for truly browser-only dependencies

    • Typical cases: components that touch window/document, depend on browser-only libraries, or trigger hydration mismatches if prerendered.
    • Next.js explicitly recommends disabling prerendering for specific components as a hydration-mismatch fix. [2]
  5. Avoid ssr: false for above-the-fold / SEO-critical content

    • Because it removes server-rendered HTML for that subtree, it can hurt perceived performance and crawlability. Prefer normal Client Components (which can still be prerendered) when possible. [1]
  6. Know the current limitation around code splitting when importing from Server Components

    • Next.js notes that when a Server Component dynamically imports a Client Component, automatic code splitting is currently not supported—another reason to keep the dynamic() usage inside a Client wrapper. [1]

Recommended pattern (App Router)

// app/components/MapClient.tsx
'use client'

import dynamic from 'next/dynamic'

const Map = dynamic(() => import('./Map'), {
  ssr: false,
  loading: () => <div style={{ height: 320 }}>Loading map…</div>,
})

export default function MapClient() {
  return <Map />
}
// app/page.tsx (Server Component by default)
import MapClient from './components/MapClient'

export default function Page() {
  return (
    <main>
      <MapClient />
    </main>
  )
}

Sources: Next.js Lazy Loading guide (App Router) [1], Next.js hydration error guidance (disabling SSR on specific components) [2].

Citations:
[1] Next.js Docs – Lazy Loading (App Router) (nextjs.org)
[2] Next.js Docs – React hydration error message & fixes (nextjs.org)

Citations:


Add a loading fallback to the MeshGradient dynamic import.

The ssr: false configuration is correctly placed within a Client Component ("use client"), which is the proper pattern for Next.js 15. However, Next.js best practices recommend providing a loading fallback when using { ssr: false } to improve UX while the component chunk loads:

const MeshGradient = dynamic(
  () =>
    import("@superset/ui/mesh-gradient").then((mod) => mod.MeshGradient),
  { 
    ssr: false,
    loading: () => <div className="w-full h-full bg-slate-900" /> // or a skeleton with stable dimensions
  },
);

This prevents a blank space during initial load and reduces layout shift.

🤖 Prompt for AI Agents
In
`@apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx`
at line 4, The dynamic import for MeshGradient currently uses { ssr: false }
without a loading fallback; update the dynamic(...) call that creates
MeshGradient (the import("@superset/ui/mesh-gradient").then(mod =>
mod.MeshGradient) expression) to include a loading option that returns a simple
placeholder element (e.g., a div or skeleton with stable width/height and
appropriate classes such as "w-full h-full bg-slate-900") so the component shows
a non-empty fallback while the chunk loads.

import { useState } from "react";
import { type ActiveDemo, AppMockup } from "../AppMockup";
import { SelectorPill } from "./components/SelectorPill";
import { DEMO_OPTIONS } from "./constants";

const MeshGradient = dynamic(
() => import("@superset/ui/mesh-gradient").then((mod) => mod.MeshGradient),
{ ssr: false },
);

export function ProductDemo() {
const [activeOption, setActiveOption] =
useState<ActiveDemo>("Use Any Agents");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import Marquee from "react-fast-marquee";

Expand Down Expand Up @@ -44,23 +43,12 @@ export function TrustedBySection() {
return (
<section className="py-6 sm:py-12 md:py-18 bg-background overflow-hidden">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<div>
<h2 className="text-lg sm:text-xl font-mono font-normal text-center mb-4 sm:mb-8 text-foreground px-4">
Trusted by builders from
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: 0.2 }}
className="relative"
>
</div>
<div className="relative">
{/* Left fade overlay */}
<div className="absolute left-0 top-0 bottom-0 w-24 sm:w-32 md:w-40 bg-linear-to-r from-background to-transparent z-10 pointer-events-none" />

Expand Down Expand Up @@ -98,7 +86,7 @@ export function TrustedBySection() {

{/* Right fade overlay */}
<div className="absolute right-0 top-0 bottom-0 w-24 sm:w-32 md:w-40 bg-linear-to-l from-background to-transparent z-10 pointer-events-none" />
</motion.div>
</div>
</div>
</section>
);
Expand Down
22 changes: 6 additions & 16 deletions apps/marketing/src/app/components/VideoSection/VideoSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import { useState } from "react";

Expand All @@ -12,13 +11,7 @@ export function VideoSection() {
return (
<section className="relative py-12 px-8 lg:px-[30px]">
<div className="max-w-7xl mx-auto">
<motion.div
className="mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<div className="mb-12">
<div className="space-y-1">
<h2 className="text-2xl sm:text-3xl xl:text-4xl font-medium tracking-tight text-foreground">
Code 10x faster with no switching cost
Expand All @@ -29,14 +22,9 @@ export function VideoSection() {
OpenCode, Cursor, etc.
</p>
</div>
</motion.div>
</div>

<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div>
<div className="group relative w-full aspect-video rounded-xl overflow-hidden bg-muted shadow-2xl ring-1 ring-white/10">
{isPlaying ? (
<iframe
Expand All @@ -58,6 +46,8 @@ export function VideoSection() {
alt="Video thumbnail"
fill
className="object-cover"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 90vw, 1280px"
/>
<div className="absolute inset-0 bg-black/0 transition-colors duration-300 group-hover:bg-black/30" />
<div className="absolute inset-0 flex items-center justify-center">
Expand All @@ -68,7 +58,7 @@ export function VideoSection() {
</button>
)}
</div>
</motion.div>
</div>
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { motion } from "framer-motion";
import Image from "next/image";
import { useState } from "react";
import { TESTIMONIALS, type Testimonial } from "./constants";
Expand Down Expand Up @@ -91,58 +90,34 @@ export function WallOfLoveSection() {
return (
<section className="relative py-24 px-8 lg:px-[30px]">
<div className="max-w-6xl mx-auto">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<div className="text-center mb-12">
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-semibold text-foreground">
What builders say about Superset
</h2>
</motion.div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="flex flex-col gap-4">
{leftColumn.map((testimonial, index) => (
<motion.div
key={testimonial.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
{leftColumn.map((testimonial) => (
<div key={testimonial.id}>
<TestimonialCard testimonial={testimonial} />
</motion.div>
</div>
))}
</div>

<div className="flex flex-col gap-4">
{middleColumn.map((testimonial, index) => (
<motion.div
key={testimonial.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.4, delay: index * 0.1 + 0.05 }}
>
{middleColumn.map((testimonial) => (
<div key={testimonial.id}>
<TestimonialCard testimonial={testimonial} />
</motion.div>
</div>
))}
</div>

<div className="flex flex-col gap-4 md:col-span-2 lg:col-span-1">
{rightColumn.map((testimonial, index) => (
<motion.div
key={testimonial.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.4, delay: index * 0.1 + 0.1 }}
>
{rightColumn.map((testimonial) => (
<div key={testimonial.id}>
<TestimonialCard testimonial={testimonial} />
</motion.div>
</div>
))}
</div>
</div>
Expand Down
Loading