Performance follow-ups
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-5
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user