Skip to content
Open
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
902 changes: 902 additions & 0 deletions docs/nextjs-16-data-loading-and-caching.md

Large diffs are not rendered by default.

893 changes: 893 additions & 0 deletions docs/nextjs-16-suspense-architecture.md

Large diffs are not rendered by default.

703 changes: 703 additions & 0 deletions docs/nextjs-16-suspense-enhancements.md

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions docs/pr-review-changes-nextjs-16.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# PR Review Changes - Next.js 16 Migration

This document summarizes the changes made based on PR review feedback for the Next.js 16 migration.

---

## 1. Missing `loading.tsx` Files

**Reviewer:** @fonodi
**Issue:** Documentation stated that `loading.tsx` files should be added for multiple routes (blog, blog/[slug], products, products/[slug], [slug]), but only `blog/[slug]/loading.tsx` was implemented.

### Changes Made

Created the missing `loading.tsx` files with appropriate skeleton UIs:

| File | Description |
|------|-------------|
| `app/[locale]/(marketing)/blog/loading.tsx` | Blog list skeleton (6-card grid) |
| `app/[locale]/(marketing)/blog/[slug]/loading.tsx` | Blog post skeleton (title, meta, image, content) |
| `app/[locale]/(marketing)/products/loading.tsx` | Products list skeleton (8-card grid) |
| `app/[locale]/(marketing)/products/[slug]/loading.tsx` | Product detail skeleton (image + details grid) |
| `app/[locale]/(marketing)/[slug]/loading.tsx` | Generic page skeleton (title + content lines) |

Each skeleton matches the layout of its corresponding page for a smooth loading experience.

---

## 2. Redundant Suspense Boundaries

**Reviewer:** @fonodi
**Issue:** The documentation showed wrapping `{children}` in `<Suspense>` within layouts, but `loading.tsx` files already create Suspense boundaries automatically. This is redundant.

### Explanation

Next.js automatically wraps page content in a `<Suspense>` boundary when a `loading.tsx` file exists in that route segment. The `loading.tsx` component becomes the fallback.

**When you need explicit `<Suspense>`:**
- Async components in layouts that are NOT pages (e.g., `NavbarWrapper`, `FooterWrapper`)

**When you do NOT need explicit `<Suspense>`:**
- Around `{children}` in layouts, if routes have `loading.tsx` files

### Changes Made

Updated `docs/nextjs-16-suspense-architecture.md`:

1. **Architecture diagram** - Shows `{children}` without Suspense wrapper
2. **Key Principles** - Added principle about not duplicating boundaries
3. **Locale layout example** - Removed `<Suspense>` around `{children}` and `PageLoading` component
4. **Migration checklist** - Changed to "Do NOT wrap `{children}` in Suspense"
5. **Common Mistakes** - Added new section (#5) warning against redundant Suspense

### Before

```tsx
// Locale layout - REDUNDANT
<Suspense fallback={<NavbarSkeleton />}>
<NavbarWrapper locale={locale} />
</Suspense>

<Suspense fallback={<PageLoading />}>
{children} {/* Unnecessary! */}
</Suspense>

<Suspense fallback={<FooterSkeleton />}>
<FooterWrapper locale={locale} />
</Suspense>
```

### After

```tsx
// Locale layout - CORRECT
<Suspense fallback={<NavbarSkeleton />}>
<NavbarWrapper locale={locale} />
</Suspense>

{children} {/* loading.tsx handles this */}

<Suspense fallback={<FooterSkeleton />}>
<FooterWrapper locale={locale} />
</Suspense>
```

---

## 3. Unsafe Spread Pattern

**Reviewer:** @fonodi
**Issue:** Using `...(condition && {...})` spreads `false` when the condition is falsy. While JavaScript handles this as a no-op, it's not semantically correct and could cause issues.

### Explanation

```javascript
// When NODE_ENV !== 'development':
...(process.env.NODE_ENV === 'development' && { unoptimized: true })
// Becomes:
...(false) // Spreads false - works but semantically incorrect

// Better approach:
...(process.env.NODE_ENV === 'development' ? { unoptimized: true } : {})
// Becomes:
...({}) // Spreads empty object - semantically correct
```

### Changes Made

**File:** `next/next.config.mjs`

```diff
- ...(process.env.NODE_ENV === 'development' && {
- unoptimized: true,
- }),
+ ...(process.env.NODE_ENV === 'development' ? { unoptimized: true } : {}),
```

**File:** `next/components/dynamic-zone/hero.tsx`

```diff
- {...(cta.variant && { variant: cta.variant })}
+ {...(cta.variant ? { variant: cta.variant } : {})}
```

---

## Summary

| Issue | Files Changed |
|-------|---------------|
| Missing loading.tsx files | 5 files created |
| Redundant Suspense documentation | `docs/nextjs-16-suspense-architecture.md` |
| Unsafe spread pattern | `next/next.config.mjs`, `next/components/dynamic-zone/hero.tsx` |
16 changes: 16 additions & 0 deletions next/app/[locale]/(marketing)/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default function Loading() {
return (
<div className="container mx-auto px-4 py-16">
<div className="h-12 bg-gray-700 rounded w-1/2 mb-8 animate-pulse" />
<div className="space-y-4">
{[...Array(8)].map((_, i) => (
<div
key={i}
className="h-4 bg-gray-700 rounded animate-pulse"
style={{ width: `${75 + (i % 3) * 8}%` }}
/>
))}
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions next/app/[locale]/(marketing)/blog/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default function Loading() {
return (
<article className="container mx-auto px-4 py-16 max-w-4xl">
{/* Title skeleton */}
<div className="h-12 bg-gray-700 rounded w-3/4 mb-4 animate-pulse" />

{/* Meta info skeleton */}
<div className="flex gap-4 mb-8">
<div className="h-4 bg-gray-700 rounded w-24 animate-pulse" />
<div className="h-4 bg-gray-700 rounded w-32 animate-pulse" />
</div>

{/* Featured image skeleton */}
<div className="aspect-video bg-gray-700 rounded-lg mb-8 animate-pulse" />

{/* Content skeleton */}
<div className="space-y-4">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="h-4 bg-gray-700 rounded animate-pulse"
style={{ width: `${70 + (i % 4) * 8}%` }}
/>
))}
</div>
</article>
);
}
17 changes: 17 additions & 0 deletions next/app/[locale]/(marketing)/blog/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function Loading() {
return (
<div className="container mx-auto px-4 py-16">
<div className="h-10 bg-gray-700 rounded w-48 mb-8 animate-pulse" />

<div className="grid md:grid-cols-3 gap-8">
{[...Array(6)].map((_, i) => (
<div key={i} className="space-y-4">
<div className="aspect-video bg-gray-700 rounded-lg animate-pulse" />
<div className="h-6 bg-gray-700 rounded w-3/4 animate-pulse" />
<div className="h-4 bg-gray-700 rounded w-1/2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
22 changes: 22 additions & 0 deletions next/app/[locale]/(marketing)/products/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function Loading() {
return (
<div className="container mx-auto px-4 py-16">
<div className="grid md:grid-cols-2 gap-12">
{/* Image skeleton */}
<div className="aspect-square bg-gray-700 rounded-lg animate-pulse" />

{/* Details skeleton */}
<div className="space-y-6">
<div className="h-10 bg-gray-700 rounded w-3/4 animate-pulse" />
<div className="h-6 bg-gray-700 rounded w-1/4 animate-pulse" />
<div className="space-y-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-4 bg-gray-700 rounded animate-pulse" />
))}
</div>
<div className="h-12 bg-gray-700 rounded w-40 animate-pulse" />
</div>
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions next/app/[locale]/(marketing)/products/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function Loading() {
return (
<div className="container mx-auto px-4 py-16">
<div className="h-10 bg-gray-700 rounded w-48 mb-8 animate-pulse" />

<div className="grid md:grid-cols-4 gap-6">
{[...Array(8)].map((_, i) => (
<div key={i} className="space-y-3">
<div className="aspect-square bg-gray-700 rounded-lg animate-pulse" />
<div className="h-5 bg-gray-700 rounded w-3/4 animate-pulse" />
<div className="h-4 bg-gray-700 rounded w-1/3 animate-pulse" />
</div>
))}
</div>
</div>
);
}
19 changes: 15 additions & 4 deletions next/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Viewport } from 'next';
import { Suspense } from 'react';

import { Locale, i18n } from '@/i18n.config';
import { i18n } from '@/i18n.config';

import './globals.css';

Expand All @@ -18,16 +19,26 @@ export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }));
}

function RootLoading() {
return (
<div className="flex min-h-screen items-center justify-center bg-charcoal">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-cyan-500 border-t-transparent" />
</div>
);
}

export default function RootLayout({
children,
}: {
}: Readonly<{
children: React.ReactNode;
}) {
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body suppressHydrationWarning>
<Preview />
<SlugProvider>{children}</SlugProvider>
<SlugProvider>
<Suspense fallback={<RootLoading />}>{children}</Suspense>
</SlugProvider>
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion next/components/dynamic-zone/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const Hero = ({
key={cta?.id}
as={Link}
href={`/${locale}${cta.URL}`}
{...(cta.variant && { variant: cta.variant })}
{...(cta.variant ? { variant: cta.variant } : {})}
>
{cta.text}
</Button>
Expand Down
Loading