Next.js 16 (#351)

* Upgrade to next.js 16

* Allow static generation on preview

* Add note for disabled ref rule

* Report Next.js version in App Insights

* Link Next.js version
This commit is contained in:
Sam Becker 2025-11-02 11:23:52 -06:00 committed by GitHub
parent 3f1a36354d
commit c8ea51cdd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 954 additions and 848 deletions

View File

@ -18,6 +18,7 @@ const eslintConfig = defineConfig([
'@stylistic': stylistic,
},
rules: {
// Disable rule during Next.js 16 migration
'react-hooks/refs': 'off',
'@next/next/no-img-element': 'off',
'@typescript-eslint/no-explicit-any': 'off',

View File

@ -8,12 +8,12 @@
"test": "jest --watch --transformIgnorePatterns 'node_modules/(?!my-library-dir)/'",
"analyze": "ANALYZE=true next build"
},
"packageManager": "pnpm@10.19.0",
"packageManager": "pnpm@10.20.0",
"dependencies": {
"@ai-sdk/openai": "^2.0.54",
"@ai-sdk/rsc": "^1.0.81",
"@aws-sdk/client-s3": "3.917.0",
"@aws-sdk/s3-request-presigner": "3.917.0",
"@ai-sdk/openai": "^2.0.59",
"@ai-sdk/rsc": "^1.0.86",
"@aws-sdk/client-s3": "3.922.0",
"@aws-sdk/s3-request-presigner": "3.922.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-tooltip": "^1.2.8",
@ -24,8 +24,8 @@
"@vercel/analytics": "^1.5.0",
"@vercel/blob": "^2.0.0",
"@vercel/speed-insights": "^1.2.0",
"ai": "^5.0.81",
"camelcase-keys": "^10.0.0",
"ai": "^5.0.86",
"camelcase-keys": "^10.0.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"culori": "^4.0.2",
@ -37,8 +37,8 @@
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.23.24",
"nanoid": "^5.1.6",
"next": "15.5.5",
"next-auth": "5.0.0-beta.29",
"next": "16.0.1",
"next-auth": "5.0.0-beta.30",
"next-themes": "^0.4.6",
"ol": "^10.6.1",
"pg": "^8.16.3",
@ -58,8 +58,8 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@next/bundle-analyzer": "15.5.5",
"@next/eslint-plugin-next": "15.5.5",
"@next/bundle-analyzer": "16.0.1",
"@next/eslint-plugin-next": "16.0.1",
"@stylistic/eslint-plugin": "^5.5.0",
"@tailwindcss/postcss": "^4.1.16",
"@testing-library/dom": "^10.4.1",
@ -67,14 +67,14 @@
"@testing-library/react": "^16.3.0",
"@types/culori": "^4.0.1",
"@types/jest": "^30.0.0",
"@types/node": "^24.9.1",
"@types/pg": "^8.15.5",
"@types/node": "^24.9.2",
"@types/pg": "^8.15.6",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/sanitize-html": "^2.16.0",
"cross-fetch": "^4.1.0",
"eslint": "9.38.0",
"eslint-config-next": "15.5.5",
"eslint": "9.39.0",
"eslint-config-next": "16.0.1",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"postcss": "8.5.6",

1649
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ import {
PREFIX_TAG,
} from './src/app/path';
export default function middleware(req: NextRequest, res:NextResponse) {
export function proxy(req: NextRequest, res:NextResponse) {
const pathname = req.nextUrl.pathname;
if (pathname === PATH_ADMIN) {

View File

@ -11,6 +11,7 @@ import {
import AdminAppInsightsClient from './AdminAppInsightsClient';
import { getAllInsights, getGitHubMetaForCurrentApp } from '.';
import { USED_DEPRECATED_ENV_VARS } from '@/app/config';
import { dependencies } from '../../../package.json';
export default async function AdminAppInsights() {
const [
@ -42,6 +43,7 @@ export default async function AdminAppInsights() {
return (
<AdminAppInsightsClient
codeMeta={codeMeta}
nextVersion={dependencies.next}
insights={getAllInsights({
codeMeta,
photosCount,

View File

@ -48,6 +48,8 @@ import IconPhoto from '@/components/icons/IconPhoto';
import { HiOutlineDocumentText } from 'react-icons/hi';
import { ReactNode } from 'react';
import MaskedScroll from '@/components/MaskedScroll';
import IconNext from '@/components/icons/IconNext';
import Link from 'next/link';
const DEBUG_COMMIT_SHA = '4cd29ed';
const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes';
@ -113,6 +115,7 @@ const renderWarningIconSmall =
export default function AdminAppInsightsClient({
codeMeta,
nextVersion,
insights,
usedDeprecatedEnvVars,
photoStats: {
@ -129,6 +132,7 @@ export default function AdminAppInsightsClient({
},
}: {
codeMeta?: Awaited<ReturnType<typeof getGitHubMetaForCurrentApp>>
nextVersion: string
insights: ReturnType<typeof getAllInsights>
usedDeprecatedEnvVars: typeof USED_DEPRECATED_ENV_VARS
photoStats: PhotoStats
@ -276,6 +280,16 @@ export default function AdminAppInsightsClient({
</span>
</a>}
/>
<ScoreCardRow
icon={<IconNext className="self-start translate-y-px" />}
content={<Link
// eslint-disable-next-line max-len
href={`https://github.com/vercel/next.js/releases/tag/v${nextVersion}`}
target="blank"
>
Next.js {nextVersion}
</Link>}
/>
</ScoreCard>
</>}
<ScoreCard title="Template recommendations">

View File

@ -2,7 +2,6 @@ import { CategoryKey } from '../category';
import {
CATEGORY_VISIBILITY,
IS_BUILDING,
IS_PRODUCTION,
STATICALLY_OPTIMIZED_PHOTO_CATEGORIES,
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
@ -21,25 +20,24 @@ const logStaticGenerationDetails = (count: number, content: string) => {
}
};
export const staticallyGeneratePhotosIfConfigured = (type: StaticOutput) =>
IS_PRODUCTION && (
(type === 'page' && STATICALLY_OPTIMIZED_PHOTOS) ||
(type === 'image' && STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES)
)
? async () => {
const photoIds = await getPublicPhotoIds({
limit: GENERATE_STATIC_PARAMS_LIMIT,
})
.catch(e => {
console.error(`Error fetching static photo data: ${e}`);
return [];
});
if (IS_BUILDING) {
logStaticGenerationDetails(photoIds.length, `photo ${type}`);
}
return photoIds.map(photoId => ({ photoId }));
export const staticallyGeneratePhotosIfConfigured = (type: StaticOutput) => (
(type === 'page' && STATICALLY_OPTIMIZED_PHOTOS) ||
(type === 'image' && STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES)
)
? async () => {
const photoIds = await getPublicPhotoIds({
limit: GENERATE_STATIC_PARAMS_LIMIT,
})
.catch(e => {
console.error(`Error fetching static photo data: ${e}`);
return [];
});
if (IS_BUILDING) {
logStaticGenerationDetails(photoIds.length, `photo ${type}`);
}
: undefined;
return photoIds.map(photoId => ({ photoId }));
}
: undefined;
export const staticallyGenerateCategoryIfConfigured = <T, K>(
key: CategoryKey,
@ -47,8 +45,7 @@ export const staticallyGenerateCategoryIfConfigured = <T, K>(
getData: () => Promise<T[]>,
formatData: (data: T[]) => K[],
): (() => Promise<K[]>) | undefined =>
CATEGORY_VISIBILITY.includes(key) &&
IS_PRODUCTION && (
CATEGORY_VISIBILITY.includes(key) && (
(type === 'page' && STATICALLY_OPTIMIZED_PHOTO_CATEGORIES) ||
(type === 'image' && STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES)
)

View File

@ -0,0 +1,43 @@
/* eslint-disable max-len */
import clsx from 'clsx/lite';
export default function IconNext({
className,
}: {
className?: string
}) {
return (
<span className={clsx(
'text-main dark:text-black',
'border border-transparent dark:border-white/40 rounded-full',
className,
)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" width="1em" height="1em">
<mask height="180" id=":r8:mask0_408_134" maskUnits="userSpaceOnUse" width="180" x="0" y="0" style={{ maskType: 'alpha' }}>
<circle cx="90" cy="90" fill="black" r="90"></circle>
</mask>
<g mask="url(#:r8:mask0_408_134)">
<circle cx="90" cy="90" data-circle="true" fill="currentColor" r="90"></circle>
<path
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
fill="url(#:r8:paint0_linear_408_134)">
</path>
<rect fill="url(#:r8:paint1_linear_408_134)" height="72" width="12" x="115" y="54"></rect>
</g>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" id=":r8:paint0_linear_408_134" x1="109"
x2="144.5" y1="116.5" y2="160.5">
<stop stopColor="white"></stop>
<stop offset="1" stopColor="white" stopOpacity="0"></stop>
</linearGradient>
<linearGradient gradientUnits="userSpaceOnUse" id=":r8:paint1_linear_408_134" x1="121"
x2="120.799" y1="54" y2="106.875">
<stop stopColor="white"></stop>
<stop offset="1" stopColor="white" stopOpacity="0"></stop>
</linearGradient>
</defs>
</svg>
</span>
);
}

View File

@ -102,31 +102,31 @@ const getPhotosCacheKeys = (options: PhotoQueryOptions = {}) => {
};
export const revalidatePhotosKey = () =>
revalidateTag(KEY_PHOTOS);
revalidateTag(KEY_PHOTOS, 'max');
export const revalidateAlbumsKey = () =>
revalidateTag(KEY_ALBUMS);
revalidateTag(KEY_ALBUMS, 'max');
export const revalidateTagsKey = () =>
revalidateTag(KEY_TAGS);
revalidateTag(KEY_TAGS, 'max');
export const revalidateRecipesKey = () =>
revalidateTag(KEY_RECIPES);
revalidateTag(KEY_RECIPES, 'max');
export const revalidateCamerasKey = () =>
revalidateTag(KEY_CAMERAS);
revalidateTag(KEY_CAMERAS, 'max');
export const revalidateLensesKey = () =>
revalidateTag(KEY_LENSES);
revalidateTag(KEY_LENSES, 'max');
export const revalidateFilmsKey = () =>
revalidateTag(KEY_FILMS);
revalidateTag(KEY_FILMS, 'max');
export const revalidateFocalLengthsKey = () =>
revalidateTag(KEY_FOCAL_LENGTHS);
revalidateTag(KEY_FOCAL_LENGTHS, 'max');
export const revalidateYearsKey = () =>
revalidateTag(KEY_YEARS);
revalidateTag(KEY_YEARS, 'max');
export const revalidateAllKeys = () => {
revalidatePhotosKey();
@ -151,7 +151,7 @@ export const revalidateAllKeysAndPaths = () => {
export const revalidatePhoto = (photoId: string) => {
// Tags
revalidateTag(photoId);
revalidateTag(photoId, 'max');
revalidateYearsKey();
revalidateCamerasKey();
revalidateLensesKey();

View File

@ -15,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{