Skip to content

Commit 18d85c3

Browse files
authored
Merge branch 'canary' into unifyworkunit
2 parents 9eb0128 + bba7190 commit 18d85c3

File tree

12 files changed

+195
-0
lines changed

12 files changed

+195
-0
lines changed

crates/next-core/src/next_app/metadata/route.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ async fn static_route_source(
148148

149149
let original_file_content_b64 = get_base64_file_content(path).await?;
150150

151+
let is_twitter = stem == "twitter-image";
152+
let is_open_graph = stem == "opengraph-image";
153+
// Twitter image file size limit is 5MB.
154+
// General Open Graph image file size limit is 8MB.
155+
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
156+
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
157+
let file_size_limit = if is_twitter { 5 } else { 8 };
158+
let img_name = if is_twitter { "Twitter" } else { "Open Graph" };
159+
151160
let code = formatdoc! {
152161
r#"
153162
import {{ NextResponse }} from 'next/server'
@@ -156,6 +165,16 @@ async fn static_route_source(
156165
const cacheControl = {cache_control}
157166
const buffer = Buffer.from({original_file_content_b64}, 'base64')
158167
168+
if ({is_twitter} || {is_open_graph}) {{
169+
const fileSizeInMB = buffer.byteLength / 1024 / 1024
170+
if (fileSizeInMB > {file_size_limit}) {{
171+
throw new Error('File size for {img_name} image "{path}" exceeds {file_size_limit}MB. ' +
172+
`(Current: ${{fileSizeInMB.toFixed(2)}}MB)\n` +
173+
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
174+
)
175+
}}
176+
}}
177+
159178
export function GET() {{
160179
return new NextResponse(buffer, {{
161180
headers: {{
@@ -170,6 +189,11 @@ async fn static_route_source(
170189
content_type = StringifyJs(&content_type),
171190
cache_control = StringifyJs(cache_control),
172191
original_file_content_b64 = StringifyJs(&original_file_content_b64),
192+
is_twitter = is_twitter,
193+
is_open_graph = is_open_graph,
194+
file_size_limit = file_size_limit,
195+
img_name = img_name,
196+
path = path.to_string().await?,
173197
};
174198

175199
let file = File::from(code);

docs/02-app/02-api-reference/02-file-conventions/01-metadata/opengraph-image.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Next.js will evaluate the file and automatically add the appropriate tags to you
2525
| [`opengraph-image.alt`](#opengraph-imagealttxt) | `.txt` |
2626
| [`twitter-image.alt`](#twitter-imagealttxt) | `.txt` |
2727

28+
> **Good to know**:
29+
>
30+
> The `twitter-image` file size must not exceed [5MB](https://developer.x.com/en/docs/x-for-websites/cards/overview/summary), and the `opengraph-image` file size must not exceed [8MB](https://developers.facebook.com/docs/sharing/webmasters/images). If the image file size exceeds these limits, the build will fail.
31+
2832
### `opengraph-image`
2933

3034
Add an `opengraph-image.(jpg|jpeg|png|gif)` image file to any route segment.

packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ async function getStaticAssetRouteCode(
8282
: process.env.NODE_ENV !== 'production'
8383
? cacheHeader.none
8484
: cacheHeader.longCache
85+
86+
const isTwitter = fileBaseName === 'twitter-image'
87+
const isOpenGraph = fileBaseName === 'opengraph-image'
88+
// Twitter image file size limit is 5MB.
89+
// General Open Graph image file size limit is 8MB.
90+
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
91+
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
92+
const fileSizeLimit = isTwitter ? 5 : 8
93+
const imgName = isTwitter ? 'Twitter' : 'Open Graph'
94+
8595
const code = `\
8696
/* static asset route */
8797
import { NextResponse } from 'next/server'
@@ -92,6 +102,16 @@ const buffer = Buffer.from(${JSON.stringify(
92102
)}, 'base64'
93103
)
94104
105+
if (${isTwitter || isOpenGraph}) {
106+
const fileSizeInMB = buffer.byteLength / 1024 / 1024
107+
if (fileSizeInMB > ${fileSizeLimit}) {
108+
throw new Error('File size for ${imgName} image "${resourcePath}" exceeds ${fileSizeLimit}MB. ' +
109+
\`(Current: \${fileSizeInMB.toFixed(2)}MB)\n\` +
110+
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
111+
)
112+
}
113+
}
114+
95115
export function GET() {
96116
return new NextResponse(buffer, {
97117
headers: {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import zlib from 'zlib'
2+
3+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
4+
5+
function createChunk(type, data) {
6+
const length = Buffer.alloc(4)
7+
length.writeUInt32BE(data.length, 0)
8+
const crc = Buffer.alloc(4)
9+
const crcValue = calculateCRC(Buffer.concat([Buffer.from(type), data])) >>> 0 // Ensure unsigned 32-bit integer
10+
crc.writeUInt32BE(crcValue, 0)
11+
return Buffer.concat([length, Buffer.from(type), data, crc])
12+
}
13+
14+
function calculateCRC(data) {
15+
let crc = 0xffffffff
16+
for (const b of data) {
17+
crc ^= b
18+
for (let i = 0; i < 8; i++) {
19+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
20+
}
21+
}
22+
return crc ^ 0xffffffff
23+
}
24+
25+
export function generatePNG(targetSizeMB) {
26+
const targetSizeBytes = targetSizeMB * 1024 * 1024
27+
28+
let width = 2048,
29+
height = 1024
30+
let pngFile: Buffer
31+
32+
do {
33+
const ihdrData = Buffer.alloc(13)
34+
ihdrData.writeUInt32BE(width, 0)
35+
ihdrData.writeUInt32BE(height, 4)
36+
ihdrData.writeUInt8(8, 8) // bitDepth
37+
ihdrData.writeUInt8(6, 9) // colorType
38+
ihdrData.writeUInt8(0, 10) // compressionMethod
39+
ihdrData.writeUInt8(0, 11) // filterMethod
40+
ihdrData.writeUInt8(0, 12) // interlaceMethod
41+
42+
const ihdrChunk = createChunk('IHDR', ihdrData)
43+
44+
const rowSize = width * 4 + 1
45+
const imageData = Buffer.alloc(rowSize * height)
46+
47+
for (let y = 0; y < height; y++) {
48+
imageData[y * rowSize] = 0
49+
for (let x = 0; x < width; x++) {
50+
const idx = y * rowSize + 1 + x * 4
51+
imageData[idx] = (Math.random() * 256) | 0
52+
imageData[idx + 1] = (Math.random() * 256) | 0
53+
imageData[idx + 2] = (Math.random() * 256) | 0
54+
imageData[idx + 3] = 255
55+
}
56+
}
57+
58+
const compressedImageData = zlib.deflateSync(imageData)
59+
const idatChunk = createChunk('IDAT', compressedImageData)
60+
const iendChunk = createChunk('IEND', Buffer.alloc(0))
61+
62+
pngFile = Buffer.concat([PNG_SIGNATURE, ihdrChunk, idatChunk, iendChunk])
63+
64+
if (pngFile.length < targetSizeBytes) {
65+
width *= 2
66+
height *= 2
67+
}
68+
} while (pngFile.length < targetSizeBytes)
69+
70+
return pngFile
71+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { generatePNG } from '../generate-image'
3+
4+
describe('app-dir - metadata-img-too-large opengraph-image', () => {
5+
const { next } = nextTestSetup({
6+
files: __dirname,
7+
skipStart: true,
8+
})
9+
10+
const pngFile = generatePNG(8)
11+
12+
it('should throw when opengraph-image file size exceeds 8MB', async () => {
13+
await next.patchFile('app/opengraph-image.png', pngFile as any)
14+
15+
await next.build()
16+
const { cliOutput } = next
17+
expect(cliOutput).toMatch(
18+
/Error: File size for Open Graph image ".*\/app\/opengraph-image\.png" exceeds 8MB/
19+
)
20+
})
21+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}

0 commit comments

Comments
 (0)