Skip to content

Commit

Permalink
Merge branch 'canary' into unifyworkunit
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage authored Oct 7, 2024
2 parents 9eb0128 + bba7190 commit 18d85c3
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 0 deletions.
24 changes: 24 additions & 0 deletions crates/next-core/src/next_app/metadata/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ async fn static_route_source(

let original_file_content_b64 = get_base64_file_content(path).await?;

let is_twitter = stem == "twitter-image";
let is_open_graph = stem == "opengraph-image";
// Twitter image file size limit is 5MB.
// General Open Graph image file size limit is 8MB.
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
let file_size_limit = if is_twitter { 5 } else { 8 };
let img_name = if is_twitter { "Twitter" } else { "Open Graph" };

let code = formatdoc! {
r#"
import {{ NextResponse }} from 'next/server'
Expand All @@ -156,6 +165,16 @@ async fn static_route_source(
const cacheControl = {cache_control}
const buffer = Buffer.from({original_file_content_b64}, 'base64')
if ({is_twitter} || {is_open_graph}) {{
const fileSizeInMB = buffer.byteLength / 1024 / 1024
if (fileSizeInMB > {file_size_limit}) {{
throw new Error('File size for {img_name} image "{path}" exceeds {file_size_limit}MB. ' +
`(Current: ${{fileSizeInMB.toFixed(2)}}MB)\n` +
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
)
}}
}}
export function GET() {{
return new NextResponse(buffer, {{
headers: {{
Expand All @@ -170,6 +189,11 @@ async fn static_route_source(
content_type = StringifyJs(&content_type),
cache_control = StringifyJs(cache_control),
original_file_content_b64 = StringifyJs(&original_file_content_b64),
is_twitter = is_twitter,
is_open_graph = is_open_graph,
file_size_limit = file_size_limit,
img_name = img_name,
path = path.to_string().await?,
};

let file = File::from(code);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Next.js will evaluate the file and automatically add the appropriate tags to you
| [`opengraph-image.alt`](#opengraph-imagealttxt) | `.txt` |
| [`twitter-image.alt`](#twitter-imagealttxt) | `.txt` |

> **Good to know**:
>
> 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.
### `opengraph-image`

Add an `opengraph-image.(jpg|jpeg|png|gif)` image file to any route segment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ async function getStaticAssetRouteCode(
: process.env.NODE_ENV !== 'production'
? cacheHeader.none
: cacheHeader.longCache

const isTwitter = fileBaseName === 'twitter-image'
const isOpenGraph = fileBaseName === 'opengraph-image'
// Twitter image file size limit is 5MB.
// General Open Graph image file size limit is 8MB.
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
const fileSizeLimit = isTwitter ? 5 : 8
const imgName = isTwitter ? 'Twitter' : 'Open Graph'

const code = `\
/* static asset route */
import { NextResponse } from 'next/server'
Expand All @@ -92,6 +102,16 @@ const buffer = Buffer.from(${JSON.stringify(
)}, 'base64'
)
if (${isTwitter || isOpenGraph}) {
const fileSizeInMB = buffer.byteLength / 1024 / 1024
if (fileSizeInMB > ${fileSizeLimit}) {
throw new Error('File size for ${imgName} image "${resourcePath}" exceeds ${fileSizeLimit}MB. ' +
\`(Current: \${fileSizeInMB.toFixed(2)}MB)\n\` +
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
)
}
}
export function GET() {
return new NextResponse(buffer, {
headers: {
Expand Down
71 changes: 71 additions & 0 deletions test/production/app-dir/metadata-img-too-large/generate-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import zlib from 'zlib'

const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])

function createChunk(type, data) {
const length = Buffer.alloc(4)
length.writeUInt32BE(data.length, 0)
const crc = Buffer.alloc(4)
const crcValue = calculateCRC(Buffer.concat([Buffer.from(type), data])) >>> 0 // Ensure unsigned 32-bit integer
crc.writeUInt32BE(crcValue, 0)
return Buffer.concat([length, Buffer.from(type), data, crc])
}

function calculateCRC(data) {
let crc = 0xffffffff
for (const b of data) {
crc ^= b
for (let i = 0; i < 8; i++) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0)
}
}
return crc ^ 0xffffffff
}

export function generatePNG(targetSizeMB) {
const targetSizeBytes = targetSizeMB * 1024 * 1024

let width = 2048,
height = 1024
let pngFile: Buffer

do {
const ihdrData = Buffer.alloc(13)
ihdrData.writeUInt32BE(width, 0)
ihdrData.writeUInt32BE(height, 4)
ihdrData.writeUInt8(8, 8) // bitDepth
ihdrData.writeUInt8(6, 9) // colorType
ihdrData.writeUInt8(0, 10) // compressionMethod
ihdrData.writeUInt8(0, 11) // filterMethod
ihdrData.writeUInt8(0, 12) // interlaceMethod

const ihdrChunk = createChunk('IHDR', ihdrData)

const rowSize = width * 4 + 1
const imageData = Buffer.alloc(rowSize * height)

for (let y = 0; y < height; y++) {
imageData[y * rowSize] = 0
for (let x = 0; x < width; x++) {
const idx = y * rowSize + 1 + x * 4
imageData[idx] = (Math.random() * 256) | 0
imageData[idx + 1] = (Math.random() * 256) | 0
imageData[idx + 2] = (Math.random() * 256) | 0
imageData[idx + 3] = 255
}
}

const compressedImageData = zlib.deflateSync(imageData)
const idatChunk = createChunk('IDAT', compressedImageData)
const iendChunk = createChunk('IEND', Buffer.alloc(0))

pngFile = Buffer.concat([PNG_SIGNATURE, ihdrChunk, idatChunk, iendChunk])

if (pngFile.length < targetSizeBytes) {
width *= 2
height *= 2
}
} while (pngFile.length < targetSizeBytes)

return pngFile
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { nextTestSetup } from 'e2e-utils'
import { generatePNG } from '../generate-image'

describe('app-dir - metadata-img-too-large opengraph-image', () => {
const { next } = nextTestSetup({
files: __dirname,
skipStart: true,
})

const pngFile = generatePNG(8)

it('should throw when opengraph-image file size exceeds 8MB', async () => {
await next.patchFile('app/opengraph-image.png', pngFile as any)

await next.build()
const { cliOutput } = next
expect(cliOutput).toMatch(
/Error: File size for Open Graph image ".*\/app\/opengraph-image\.png" exceeds 8MB/
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { nextTestSetup } from 'e2e-utils'
import { generatePNG } from '../generate-image'

describe('metadata-img-too-large twitter-image', () => {
const { next } = nextTestSetup({
files: __dirname,
skipStart: true,
})

const pngFile = generatePNG(6)

it('should throw when twitter-image file size exceeds 5MB', async () => {
await next.patchFile('app/twitter-image.png', pngFile as any)

await next.build()
const { cliOutput } = next
expect(cliOutput).toMatch(
/Error: File size for Twitter image ".*\/app\/twitter-image\.png" exceeds 5MB/
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig

0 comments on commit 18d85c3

Please sign in to comment.