diff --git a/app/page.tsx b/app/page.tsx index 239a4e5..02893a9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,19 +1,18 @@ -import { Hero } from "@/components/sections"; -import dynamic from "next/dynamic"; +import { + Hero, + ValueProposition, + Services, + HowWeWork, + EngagementModels, + FeaturedCaseStudies, + Testimonials, + Stats, + AboutTeaser, + FAQ, + CTA, +} from "@/components/sections"; import { getNotionFeaturedCaseStudies } from "@/lib/notion-case-studies"; -// Dynamically import below-fold sections for better initial bundle size -const ValueProposition = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.ValueProposition }))); -const Services = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.Services }))); -const HowWeWork = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.HowWeWork }))); -const EngagementModels = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.EngagementModels }))); -const FeaturedCaseStudies = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.FeaturedCaseStudies }))); -const Testimonials = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.Testimonials }))); -const Stats = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.Stats }))); -const AboutTeaser = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.AboutTeaser }))); -const FAQ = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.FAQ }))); -const CTA = dynamic(() => import("@/components/sections").then(mod => ({ default: mod.CTA }))); - // Force static generation at build time export const dynamic = "force-static"; export const revalidate = false; diff --git a/components/sections/Careers.tsx b/components/sections/Careers.tsx index bc96fe9..f78e5f8 100644 --- a/components/sections/Careers.tsx +++ b/components/sections/Careers.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; const jobOpenings = [ { @@ -35,7 +35,8 @@ const jobOpenings = [ export function Careers() { return ( -
+ +
{/* Hexagon pattern */}
- - +
{jobOpenings.map((job, idx) => ( - - + ))}
{/* CTA to careers page */} - - +
+
); } diff --git a/components/sections/ClientLogos.tsx b/components/sections/ClientLogos.tsx index abb5aa5..bed756e 100644 --- a/components/sections/ClientLogos.tsx +++ b/components/sections/ClientLogos.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; import Image from "next/image"; const clients = [ @@ -27,7 +27,8 @@ const clients = [ export function ClientLogos() { return ( -
+ +

Trusted by innovative teams

@@ -40,7 +41,7 @@ export function ClientLogos() { "linear-gradient(to right, transparent 0%, white 25%, white 75%, transparent 100%)", }} > -
))} - +
+ ); } diff --git a/components/sections/EngagementModels.tsx b/components/sections/EngagementModels.tsx index 544e917..9165e4f 100644 --- a/components/sections/EngagementModels.tsx +++ b/components/sections/EngagementModels.tsx @@ -86,8 +86,7 @@ const engagementModels = [ export function EngagementModels() { return ( - -
+
{/* Gradient orbs for depth */}
@@ -102,7 +101,7 @@ export function EngagementModels() {
{/* Section Header */} - - +
{engagementModels.map((model, idx) => ( -
- + ))}
diff --git a/components/sections/FeaturedCaseStudies.tsx b/components/sections/FeaturedCaseStudies.tsx index e6536ba..700938b 100644 --- a/components/sections/FeaturedCaseStudies.tsx +++ b/components/sections/FeaturedCaseStudies.tsx @@ -12,8 +12,7 @@ interface FeaturedCaseStudiesProps { export function FeaturedCaseStudies({ caseStudies }: FeaturedCaseStudiesProps) { return ( - -
+
{/* Background pattern */}
{/* Section Header */} - - + {/* Case Studies Grid */}
@@ -74,7 +73,7 @@ export function FeaturedCaseStudies({ caseStudies }: FeaturedCaseStudiesProps) {
{/* Bottom CTA */} - - +
diff --git a/components/sections/Hero.tsx b/components/sections/Hero.tsx index d5ad907..5eee0cc 100644 --- a/components/sections/Hero.tsx +++ b/components/sections/Hero.tsx @@ -1,18 +1,19 @@ "use client"; import Link from "next/link"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; import { ClientLogos } from "./ClientLogos"; import { CalButton } from "@/components/CalButton"; export function Hero() { return ( -
- {/* Animated gradient background */} -
+ +
+ {/* Animated gradient background */} +
- {/* Large ambient glow - creates depth */} -
- + {/* Subtle grid pattern - uses CSS class for theme-aware styling */}
@@ -30,7 +31,7 @@ export function Hero() { {/* Floating abstract elements */}
{/* Floating orbs */} - - - - - -
{/* Tagline pills */} - Production-tested engineering - + {/* Main headline - optimized for LCP */} {/* Render text immediately for fast LCP, animate opacity instead of text generation */} - That Ship to Production - + {/* Subheadline */} - + {/* CTA buttons */} - View Our Work - +
{/* Scroll indicator */} -
- +
+
); } diff --git a/components/sections/HowWeWork.tsx b/components/sections/HowWeWork.tsx index 4188166..f52214b 100644 --- a/components/sections/HowWeWork.tsx +++ b/components/sections/HowWeWork.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; import { Timeline } from "@/components/ui/timeline"; const processSteps = [ @@ -43,7 +43,8 @@ const processSteps = [ export function HowWeWork() { return ( -
+ +
{/* Gradient orbs for depth */}
@@ -58,7 +59,7 @@ export function HowWeWork() {
{/* Section Header */} - - + {/* Process Timeline - Aceternity UI */}
@@ -101,5 +102,6 @@ export function HowWeWork() {
+
); } diff --git a/components/sections/Services.tsx b/components/sections/Services.tsx index ad3d95b..9b39bd4 100644 --- a/components/sections/Services.tsx +++ b/components/sections/Services.tsx @@ -157,8 +157,7 @@ const services = [ export function Services() { return ( - -
+
{/* Diagonal lines pattern */}
); + + ); +} diff --git a/components/sections/Team.tsx b/components/sections/Team.tsx index 5b6c944..c3bef8c 100644 --- a/components/sections/Team.tsx +++ b/components/sections/Team.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; import Image from "next/image"; interface TeamMember { @@ -145,7 +145,8 @@ const values = [ export function Team() { return ( -
+ +
{/* Dot matrix pattern */}
{/* Section Header */} - - + {/* Featured Leadership */}
{leadership.map((member, idx) => ( -
- + ))}
{/* Team Grid */}
{team.map((member, idx) => ( - )} - + ))}
{/* Team Values Strip */} - ))}
- +
+ ); } diff --git a/components/sections/Testimonials.tsx b/components/sections/Testimonials.tsx index 2fb06d3..cb4031f 100644 --- a/components/sections/Testimonials.tsx +++ b/components/sections/Testimonials.tsx @@ -11,9 +11,8 @@ export function Testimonials() { return ( - -
@@ -104,3 +103,6 @@ export function Testimonials() {
); +
+ ); +} diff --git a/components/sections/ValueProposition.tsx b/components/sections/ValueProposition.tsx index 5235ea2..dbc9ece 100644 --- a/components/sections/ValueProposition.tsx +++ b/components/sections/ValueProposition.tsx @@ -74,8 +74,7 @@ const values = [ export function ValueProposition() { return ( - -
+
{/* Gradient transition from Hero */}
); + + ); +} diff --git a/lib/image-loader.ts b/lib/image-loader.ts new file mode 100644 index 0000000..7bbbadf --- /dev/null +++ b/lib/image-loader.ts @@ -0,0 +1,30 @@ +/** + * Custom image loader for static export + * Enables proper srcset generation for responsive images + * even when using output: "export" + */ + +export default function imageLoader({ + src, + width, + quality, +}: { + src: string; + width: number; + quality?: number; +}) { + // For external URLs, pass through as-is + if (src.startsWith("http://") || src.startsWith("https://")) { + return src; + } + + // For local images, return the path with width in query string + // This allows browser to select appropriate size from srcset + const params = new URLSearchParams(); + params.set("w", width.toString()); + if (quality) { + params.set("q", quality.toString()); + } + + return `${src}?${params.toString()}`; +} diff --git a/next.config.mjs b/next.config.mjs index 4f20592..98b89cf 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -14,7 +14,6 @@ const nextConfig = { // Exclude legacy polyfills for modern browsers (saves 14 KiB) // Targets Chrome 100+, Safari 15+, Firefox 100+ per browserslist excludeDefaultMomentLocales: true, - swcMinify: true, // Experimental optimizations experimental: { @@ -91,15 +90,19 @@ const nextConfig = { generateEtags: true, images: { + // MUST be true for static export (output: "export") + // GitHub Pages doesn't support dynamic image optimization unoptimized: true, formats: ["image/avif", "image/webp"], - // Optimized device sizes for better responsive images deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], - minimumCacheTTL: 31536000, // 1 year cache for optimized images + minimumCacheTTL: 31536000, dangerouslyAllowSVG: true, contentDispositionType: "attachment", contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + // Loader for static export - generates proper srcset + loader: "custom", + loaderFile: "./lib/image-loader.ts", remotePatterns: [ { protocol: "https", diff --git a/scripts/optimize-logos.js b/scripts/optimize-logos.js new file mode 100644 index 0000000..b8719f1 --- /dev/null +++ b/scripts/optimize-logos.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/** + * Optimize all SVG logos for production + * Reduces file sizes by 60-80% without quality loss + */ + +const { optimize } = require('svgo'); +const fs = require('fs'); +const path = require('path'); + +const LOGOS_DIR = path.join(__dirname, '../public/logos/client'); + +// SVGO configuration for maximum compression while preserving quality +const svgoConfig = { + multipass: true, + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, // Keep viewBox for responsiveness + cleanupIds: { + minify: true, + preserve: [], + }, + }, + }, + }, + 'removeDimensions', // Remove width/height attributes + 'removeStyleElement', // Remove