Performance follow-ups

This commit is contained in:
adilallo
2026-05-26 07:24:36 -06:00
parent 3be188a3cc
commit eded97559d
16 changed files with 432 additions and 72 deletions
+9 -4
View File
@@ -1,5 +1,8 @@
import type { ReactNode } from "react";
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
import { MessagesProvider } from "../contexts/MessagesContext";
import { AuthModalProvider } from "../contexts/AuthModalContext";
import messages from "../../messages/en/index";
// Reads the session for admin chrome (matches the HttpOnly cookie on first
// HTML response). Scoped here so `(marketing)` can render statically.
@@ -9,9 +12,11 @@ export const dynamic = "force-dynamic";
// public marketing footer. Auth/access is enforced upstream.
export default function AdminLayout({ children }: { children: ReactNode }) {
return (
<>
<ConditionalNavigation />
<main className="flex-1">{children}</main>
</>
<MessagesProvider messages={messages}>
<AuthModalProvider>
<ConditionalNavigation />
<main className="flex-1">{children}</main>
</AuthModalProvider>
</MessagesProvider>
);
}
+9 -4
View File
@@ -1,5 +1,8 @@
import type { ReactNode } from "react";
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
import { MessagesProvider } from "../contexts/MessagesContext";
import { AuthModalProvider } from "../contexts/AuthModalContext";
import messages from "../../messages/en/index";
// Reads `cr_session` via Server Components on every navigation so the header
// matches the HttpOnly cookie on the first HTML response (no "Log in" flash
@@ -12,9 +15,11 @@ export const dynamic = "force-dynamic";
// CreateFlow) is composed in nested layouts.
export default function AppLayout({ children }: { children: ReactNode }) {
return (
<>
<ConditionalNavigation />
<main className="flex-1">{children}</main>
</>
<MessagesProvider messages={messages}>
<AuthModalProvider>
<ConditionalNavigation />
<main className="flex-1">{children}</main>
</AuthModalProvider>
</MessagesProvider>
);
}
+10 -1
View File
@@ -1,10 +1,19 @@
import type { ReactNode } from "react";
import { notFound } from "next/navigation";
import { MessagesProvider } from "../contexts/MessagesContext";
import { AuthModalProvider } from "../contexts/AuthModalContext";
import messages from "../../messages/en/index";
// Development-only previews (e.g. `/components-preview`) — no public chrome.
export default function DevLayout({ children }: { children: ReactNode }) {
if (process.env.NODE_ENV === "production") {
notFound();
}
return <main className="flex-1">{children}</main>;
return (
<MessagesProvider messages={messages}>
<AuthModalProvider>
<main className="flex-1">{children}</main>
</AuthModalProvider>
</MessagesProvider>
);
}
+10 -5
View File
@@ -1,6 +1,9 @@
import dynamic from "next/dynamic";
import type { ReactNode } from "react";
import MarketingNavigation from "../components/navigation/MarketingNavigation";
import { MessagesProvider } from "../contexts/MessagesContext";
import { AuthModalProvider } from "../contexts/AuthModalContext";
import marketingMessages from "../../messages/en/marketing";
// Site footer is part of the public marketing chrome only — not rendered for
// signed-in product surfaces, admin dashboards, or dev previews. See
@@ -14,10 +17,12 @@ const Footer = dynamic(() => import("../components/navigation/Footer"), {
export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
<>
<MarketingNavigation />
<main className="flex-1">{children}</main>
<Footer />
</>
<MessagesProvider messages={marketingMessages}>
<AuthModalProvider>
<MarketingNavigation />
<main className="flex-1">{children}</main>
<Footer />
</AuthModalProvider>
</MessagesProvider>
);
}
+9 -13
View File
@@ -29,22 +29,18 @@ export default function LearnPage() {
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
<ContentLockup {...contentLockupData} />
<div className="smd:hidden sm:pt-[var(--spacing-scale-024)] sm:pb-[var(--spacing-scale-024)] sm:px-[var(--spacing-scale-020)] space-y-[var(--spacing-scale-002)] sm:space-y-[var(--spacing-scale-008)]">
{/*
* Single responsive render: ContentThumbnailTemplate variant="responsive"
* uses <picture> to swap horizontal/vertical art at smd (530px). The
* container switches from a vertical flex stack (<smd) to a grid (≥smd),
* matching the prior twin-region layout without doubling the DOM.
*/}
<div className="flex flex-col space-y-[var(--spacing-scale-002)] sm:space-y-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-024)] sm:pb-[var(--spacing-scale-024)] smd:grid smd:grid-cols-2 smd:gap-[var(--spacing-scale-008)] smd:space-y-0 smd:px-[var(--spacing-scale-020)] smd:pt-[var(--spacing-scale-024)] smd:pb-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-016)] md:px-[var(--spacing-scale-032)] xmd:grid-cols-3 xmd:gap-[var(--spacing-scale-012)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-064)] lg:pt-[var(--spacing-scale-032)] lg:pb-[var(--spacing-scale-064)] lg2:grid-cols-4 lg2:gap-x-[var(--spacing-scale-016)] lg2:gap-y-[var(--spacing-scale-024)] xl:grid-cols-5 xl:gap-x-[var(--spacing-scale-016)] xl:gap-y-[var(--spacing-scale-016)] [&>*]:min-w-0">
{allPosts.map((post) => (
<ContentThumbnailTemplate
key={`${post.slug}-horizontal`}
key={post.slug}
post={post}
variant="horizontal"
/>
))}
</div>
<div className="hidden smd:grid smd:grid-cols-2 xmd:grid-cols-3 lg:grid-cols-3 lg2:grid-cols-4 xl:grid-cols-5 smd:gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-016)] xmd:gap-[var(--spacing-scale-012)] lg:gap-[var(--spacing-scale-016)] lg2:gap-x-[var(--spacing-scale-016)] lg2:gap-y-[var(--spacing-scale-024)] xl:gap-x-[var(--spacing-scale-016)] xl:gap-y-[var(--spacing-scale-016)] smd:pt-[var(--spacing-scale-024)] smd:pb-[var(--spacing-scale-024)] smd:px-[var(--spacing-scale-020)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-032)] lg:pb-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] [&>*]:min-w-0">
{allPosts.map((post) => (
<ContentThumbnailTemplate
key={`${post.slug}-vertical`}
post={post}
variant="vertical"
variant="responsive"
/>
))}
</div>
+10 -3
View File
@@ -1,4 +1,7 @@
import type { ReactNode } from "react";
import { MessagesProvider } from "../contexts/MessagesContext";
import { AuthModalProvider } from "../contexts/AuthModalContext";
import marketingMessages from "../../messages/en/marketing";
/** Full-viewport case-study surfaces (completed rule demos) — no marketing footer. */
export default function MarketingCaseStudyLayout({
@@ -7,8 +10,12 @@ export default function MarketingCaseStudyLayout({
children: ReactNode;
}) {
return (
<main className="flex h-dvh min-h-0 flex-col overflow-hidden">
{children}
</main>
<MessagesProvider messages={marketingMessages}>
<AuthModalProvider>
<main className="flex h-dvh min-h-0 flex-col overflow-hidden">
{children}
</main>
</AuthModalProvider>
</MessagesProvider>
);
}
@@ -23,14 +23,13 @@ const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
}) => {
const variant = variantProp;
const sizing = sizingProp;
// Get article-specific background image from frontmatter
const getBackgroundImage = (
post: ContentThumbnailTemplateProps["post"],
variant: "vertical" | "horizontal",
orientation: "vertical" | "horizontal",
): string => {
if (post.frontmatter?.thumbnail) {
const imageName =
variant === "vertical"
orientation === "vertical"
? post.frontmatter.thumbnail.vertical
: post.frontmatter.thumbnail.horizontal;
@@ -47,12 +46,21 @@ const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
? slug
: contentCatalogSlugForFallback(slug);
return variant === "vertical"
return orientation === "vertical"
? contentBlogVerticalPath(resolvedSlug)
: contentBlogHorizontalPath(resolvedSlug);
};
const backgroundImage = getBackgroundImage(post, variant);
// For "responsive", emit both orientations so the <picture> source can
// swap at smd without a second card in the DOM.
const backgroundImage =
variant === "responsive"
? getBackgroundImage(post, "horizontal")
: getBackgroundImage(post, variant);
const backgroundImageSmd =
variant === "responsive"
? getBackgroundImage(post, "vertical")
: undefined;
return (
<ContentThumbnailTemplateView
@@ -61,6 +69,7 @@ const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
variant={variant}
sizing={sizing}
backgroundImage={backgroundImage}
backgroundImageSmd={backgroundImageSmd}
/>
);
},
@@ -1,6 +1,9 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal";
export type ContentThumbnailTemplateVariantValue =
| "vertical"
| "horizontal"
| "responsive";
export type ContentThumbnailTemplateSizingValue = "fluid" | "fixed";
@@ -8,7 +11,8 @@ export interface ContentThumbnailTemplateProps {
post: BlogPost;
className?: string;
/**
* Content thumbnail variant.
* vertical | horizontal — single layout. responsive — horizontal at <smd,
* vertical at ≥smd (Learn grid); single card, viewport-swapped via <picture>.
*/
variant?: ContentThumbnailTemplateVariantValue;
/**
@@ -21,7 +25,9 @@ export interface ContentThumbnailTemplateProps {
export interface ContentThumbnailTemplateViewProps {
post: BlogPost;
className: string;
variant: "vertical" | "horizontal";
variant: ContentThumbnailTemplateVariantValue;
sizing: ContentThumbnailTemplateSizingValue;
backgroundImage: string;
/** Wide-viewport image source for variant="responsive" (≥smd). */
backgroundImageSmd?: string;
}
@@ -9,7 +9,41 @@ function ContentThumbnailTemplateView({
variant,
sizing,
backgroundImage,
backgroundImageSmd,
}: ContentThumbnailTemplateViewProps) {
if (variant === "responsive") {
// Single card; <picture> swaps the orientation-specific image at smd
// (530px), aspect-ratio and content positioning switch via Tailwind.
return (
<Link
href={`/blog/${post.slug}`}
className={`group block w-full transition-transform duration-200 hover:scale-[1.02] ${className}`}
>
<div className="relative aspect-[320/225.5] w-full overflow-hidden smd:aspect-[260/390]">
<div className="absolute inset-0 z-0">
<picture>
{backgroundImageSmd ? (
<source
media="(min-width: 530px)"
srcSet={backgroundImageSmd}
/>
) : null}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={backgroundImage}
alt=""
className="pointer-events-none size-full object-cover"
/>
</picture>
</div>
<div className="absolute left-[4.375%] top-[6.099%] z-20 w-[71.875%] smd:left-[6.923%] smd:top-[4.615%] smd:w-[76.923%]">
<ContentContainer post={post} size="xs" />
</div>
</div>
</Link>
);
}
if (variant === "vertical") {
if (sizing === "fixed") {
return (
@@ -5,11 +5,20 @@
*/
import { memo } from "react";
import Image from "next/image";
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentLockup from "../../type/ContentLockup";
import HeroDecor from "./HeroDecor";
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
/**
* Intrinsic dimensions of `public/assets/marketing/hero-image.png` (2560×1600,
* 16:10). Passed to `next/image` to reserve aspect ratio + drive responsive
* srcset generation. Actual rendered size is governed by `sizes`.
*/
const HERO_IMAGE_WIDTH = 2560;
const HERO_IMAGE_HEIGHT = 1600;
interface HeroBannerProps {
title?: string;
subtitle?: string;
@@ -50,13 +59,14 @@ const HeroBanner = memo<HeroBannerProps>(
{/* Hero Image Container */}
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic path from getAssetPath */}
<img
<Image
src={getAssetPath(ASSETS.HERO_IMAGE)}
alt={imageAlt}
width={HERO_IMAGE_WIDTH}
height={HERO_IMAGE_HEIGHT}
priority
sizes="(min-width: 768px) 50vw, 100vw"
className="w-full h-auto"
loading="eager"
fetchPriority="high"
/>
</div>
</div>
+6 -7
View File
@@ -1,8 +1,6 @@
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import { AuthModalProvider } from "./contexts/AuthModalContext";
import { MessagesProvider } from "./contexts/MessagesContext";
import messages from "../messages/en/index";
import { ASSETS, getAssetPath } from "../lib/assetUtils";
import "./globals.css";
@@ -11,6 +9,11 @@ import "./globals.css";
// (the only groups that read the session via `ConditionalNavigation`). Marketing
// renders a client-side `MarketingNavigation` so its HTML can be statically
// optimized — TTFB drops to CDN speed for guests.
//
// MessagesProvider + AuthModalProvider are mounted per route group (Phase 4b):
// `(marketing)` gets a trimmed slice without `create.*` (~41 KB gzipped saved
// per static page); `(app)`/`(admin)`/`(dev)` get the full tree. See
// `messages/en/marketing.ts` and `docs/perf/next16-eval.md`.
const inter = Inter({
subsets: ["latin"],
@@ -142,11 +145,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<body
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable}`}
>
<MessagesProvider messages={messages}>
<AuthModalProvider>
<div className="min-h-screen flex flex-col">{children}</div>
</AuthModalProvider>
</MessagesProvider>
<div className="min-h-screen flex flex-col">{children}</div>
</body>
</html>
);