5
5
use anyhow:: { Ok , Result , bail} ;
6
6
use base64:: { display:: Base64Display , engine:: general_purpose:: STANDARD } ;
7
7
use indoc:: { formatdoc, indoc} ;
8
- use turbo_rcstr:: rcstr;
8
+ use turbo_rcstr:: { RcStr , rcstr} ;
9
9
use turbo_tasks:: Vc ;
10
10
use turbo_tasks_fs:: { self , File , FileContent , FileSystemPath } ;
11
11
use turbopack:: ModuleAssetContext ;
12
12
use turbopack_core:: {
13
- asset:: AssetContent , file_source:: FileSource , source:: Source , virtual_source:: VirtualSource ,
13
+ asset:: AssetContent ,
14
+ file_source:: FileSource ,
15
+ issue:: { Issue , IssueExt , IssueSeverity , IssueStage , OptionStyledString , StyledString } ,
16
+ source:: Source ,
17
+ virtual_source:: VirtualSource ,
14
18
} ;
15
19
use turbopack_ecmascript:: utils:: StringifyJs ;
16
20
@@ -133,8 +137,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
133
137
let stem = path. file_stem ( ) ;
134
138
let stem = stem. unwrap_or_default ( ) ;
135
139
136
- let content_type = get_content_type ( path. clone ( ) ) . await ?;
137
-
138
140
let cache_control = if stem == "favicon" {
139
141
CACHE_HEADER_REVALIDATE
140
142
} else if mode. is_production ( ) {
@@ -143,16 +145,40 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
143
145
CACHE_HEADER_NONE
144
146
} ;
145
147
146
- let original_file_content_b64 = get_base64_file_content ( path. clone ( ) ) . await ?;
147
-
148
148
let is_twitter = stem == "twitter-image" ;
149
149
let is_open_graph = stem == "opengraph-image" ;
150
+
151
+ let content_type = get_content_type ( path. clone ( ) ) . await ?;
152
+ let original_file_content_b64;
153
+
150
154
// Twitter image file size limit is 5MB.
151
155
// General Open Graph image file size limit is 8MB.
152
156
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
153
157
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
154
- let file_size_limit = if is_twitter { 5 } else { 8 } ;
155
- let img_name = if is_twitter { "Twitter" } else { "Open Graph" } ;
158
+ let file_size_limit_mb = if is_twitter { 5 } else { 8 } ;
159
+ if ( is_twitter || is_open_graph)
160
+ && let Some ( content) = path. read ( ) . await ?. as_content ( )
161
+ && let file_size = content. content ( ) . to_bytes ( ) . len ( )
162
+ && file_size > ( file_size_limit_mb * 1024 * 1024 )
163
+ {
164
+ StaticMetadataFileSizeIssue {
165
+ img_name : if is_twitter {
166
+ rcstr ! ( "Twitter" )
167
+ } else {
168
+ rcstr ! ( "Open Graph" )
169
+ } ,
170
+ path : path. clone ( ) ,
171
+ file_size_limit_mb,
172
+ file_size,
173
+ }
174
+ . resolved_cell ( )
175
+ . emit ( ) ;
176
+
177
+ // Don't inline huge string, just insert placeholder
178
+ original_file_content_b64 = "" . to_string ( ) ;
179
+ } else {
180
+ original_file_content_b64 = get_base64_file_content ( path. clone ( ) ) . await ?
181
+ }
156
182
157
183
let code = formatdoc ! {
158
184
r#"
@@ -162,16 +188,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
162
188
const cacheControl = {cache_control}
163
189
const buffer = Buffer.from({original_file_content_b64}, 'base64')
164
190
165
- if ({is_twitter} || {is_open_graph}) {{
166
- const fileSizeInMB = buffer.byteLength / 1024 / 1024
167
- if (fileSizeInMB > {file_size_limit}) {{
168
- throw new Error('File size for {img_name} image {path} exceeds {file_size_limit}MB. ' +
169
- `(Current: ${{fileSizeInMB.toFixed(2)}}MB)\n` +
170
- 'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
171
- )
172
- }}
173
- }}
174
-
175
191
export function GET() {{
176
192
return new NextResponse(buffer, {{
177
193
headers: {{
@@ -186,11 +202,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
186
202
content_type = StringifyJs ( & content_type) ,
187
203
cache_control = StringifyJs ( cache_control) ,
188
204
original_file_content_b64 = StringifyJs ( & original_file_content_b64) ,
189
- is_twitter = is_twitter,
190
- is_open_graph = is_open_graph,
191
- file_size_limit = file_size_limit,
192
- img_name = img_name,
193
- path = StringifyJs ( & path. value_to_string( ) . await ?) ,
194
205
} ;
195
206
196
207
let file = File :: from ( code) ;
@@ -412,3 +423,57 @@ async fn dynamic_image_route_source(path: FileSystemPath) -> Result<Vc<Box<dyn S
412
423
413
424
Ok ( Vc :: upcast ( source) )
414
425
}
426
+
427
+ #[ turbo_tasks:: value( shared) ]
428
+ struct StaticMetadataFileSizeIssue {
429
+ img_name : RcStr ,
430
+ path : FileSystemPath ,
431
+ file_size : usize ,
432
+ file_size_limit_mb : usize ,
433
+ }
434
+
435
+ #[ turbo_tasks:: value_impl]
436
+ impl Issue for StaticMetadataFileSizeIssue {
437
+ fn severity ( & self ) -> IssueSeverity {
438
+ IssueSeverity :: Error
439
+ }
440
+
441
+ #[ turbo_tasks:: function]
442
+ fn title ( & self ) -> Vc < StyledString > {
443
+ StyledString :: Text ( rcstr ! ( "Static metadata file size exceeded" ) ) . cell ( )
444
+ }
445
+
446
+ #[ turbo_tasks:: function]
447
+ fn stage ( & self ) -> Vc < IssueStage > {
448
+ IssueStage :: ProcessModule . into ( )
449
+ }
450
+
451
+ #[ turbo_tasks:: function]
452
+ fn file_path ( & self ) -> Vc < FileSystemPath > {
453
+ self . path . clone ( ) . cell ( )
454
+ }
455
+
456
+ #[ turbo_tasks:: function]
457
+ async fn description ( & self ) -> Result < Vc < OptionStyledString > > {
458
+ Ok ( Vc :: cell ( Some (
459
+ StyledString :: Text (
460
+ format ! (
461
+ "File size for {} image \" {}\" exceeds {}MB. (Current: {:.1}MB)" ,
462
+ self . img_name,
463
+ self . path. value_to_string( ) . await ?,
464
+ self . file_size_limit_mb,
465
+ ( self . file_size as f32 ) / 1024.0 / 1024.0
466
+ )
467
+ . into ( ) ,
468
+ )
469
+ . resolved_cell ( ) ,
470
+ ) ) )
471
+ }
472
+
473
+ #[ turbo_tasks:: function]
474
+ fn documentation_link ( & self ) -> Vc < RcStr > {
475
+ Vc :: cell ( rcstr ! (
476
+ "https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif"
477
+ ) )
478
+ }
479
+ }
0 commit comments