Skip to content
Merged
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
111 changes: 88 additions & 23 deletions crates/next-core/src/next_app/metadata/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
use anyhow::{Ok, Result, bail};
use base64::{display::Base64Display, engine::general_purpose::STANDARD};
use indoc::{formatdoc, indoc};
use turbo_rcstr::rcstr;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::Vc;
use turbo_tasks_fs::{self, File, FileContent, FileSystemPath};
use turbopack::ModuleAssetContext;
use turbopack_core::{
asset::AssetContent, file_source::FileSource, source::Source, virtual_source::VirtualSource,
asset::AssetContent,
file_source::FileSource,
issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
source::Source,
virtual_source::VirtualSource,
};
use turbopack_ecmascript::utils::StringifyJs;

Expand Down Expand Up @@ -133,8 +137,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
let stem = path.file_stem();
let stem = stem.unwrap_or_default();

let content_type = get_content_type(path.clone()).await?;

let cache_control = if stem == "favicon" {
CACHE_HEADER_REVALIDATE
} else if mode.is_production() {
Expand All @@ -143,16 +145,40 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
CACHE_HEADER_NONE
};

let original_file_content_b64 = get_base64_file_content(path.clone()).await?;

let is_twitter = stem == "twitter-image";
let is_open_graph = stem == "opengraph-image";

let content_type = get_content_type(path.clone()).await?;
let original_file_content_b64;

// 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 file_size_limit_mb = if is_twitter { 5 } else { 8 };
if (is_twitter || is_open_graph)
&& let Some(content) = path.read().await?.as_content()
&& let file_size = content.content().to_bytes().len()
&& file_size > (file_size_limit_mb * 1024 * 1024)
{
StaticMetadataFileSizeIssue {
img_name: if is_twitter {
rcstr!("Twitter")
} else {
rcstr!("Open Graph")
},
path: path.clone(),
file_size_limit_mb,
file_size,
}
.resolved_cell()
.emit();

// Don't inline huge string, just insert placeholder
original_file_content_b64 = "".to_string();
} else {
original_file_content_b64 = get_base64_file_content(path.clone()).await?
}

let code = formatdoc! {
r#"
Expand All @@ -162,16 +188,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
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 @@ -186,11 +202,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
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 = StringifyJs(&path.value_to_string().await?),
};

let file = File::from(code);
Expand Down Expand Up @@ -412,3 +423,57 @@ async fn dynamic_image_route_source(path: FileSystemPath) -> Result<Vc<Box<dyn S

Ok(Vc::upcast(source))
}

#[turbo_tasks::value(shared)]
struct StaticMetadataFileSizeIssue {
img_name: RcStr,
path: FileSystemPath,
file_size: usize,
file_size_limit_mb: usize,
}

#[turbo_tasks::value_impl]
impl Issue for StaticMetadataFileSizeIssue {
fn severity(&self) -> IssueSeverity {
IssueSeverity::Error
}

#[turbo_tasks::function]
fn title(&self) -> Vc<StyledString> {
StyledString::Text(rcstr!("Static metadata file size exceeded")).cell()
}

#[turbo_tasks::function]
fn stage(&self) -> Vc<IssueStage> {
IssueStage::ProcessModule.into()
}

#[turbo_tasks::function]
fn file_path(&self) -> Vc<FileSystemPath> {
self.path.clone().cell()
}

#[turbo_tasks::function]
async fn description(&self) -> Result<Vc<OptionStyledString>> {
Ok(Vc::cell(Some(
StyledString::Text(
format!(
"File size for {} image \"{}\" exceeds {}MB. (Current: {:.1}MB)",
self.img_name,
self.path.value_to_string().await?,
self.file_size_limit_mb,
(self.file_size as f32) / 1024.0 / 1024.0
)
.into(),
)
.resolved_cell(),
)))
}

#[turbo_tasks::function]
fn documentation_link(&self) -> Vc<RcStr> {
Vc::cell(rcstr!(
"https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif"
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ describe('app-dir - metadata-img-too-large opengraph-image', () => {
await next.build()

const regex = isTurbopack
? // in Turbopack, the path is simplified as [project]/...
/Error: File size for Open Graph image "\[project\]\/app\/opengraph-image\.png" exceeds 8MB/
? // in Turbopack, the path is simplified as [project]/.... It's also thrown earlier, so the prefix is slightly different.
/File size for Open Graph image "\[project\]\/app\/opengraph-image\.png" exceeds 8MB/
: /Error: File size for Open Graph image ".*\/app\/opengraph-image\.png" exceeds 8MB/

expect(next.cliOutput).toMatch(regex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ describe('app-dir - metadata-img-too-large twitter-image', () => {
await next.build()

const regex = isTurbopack
? // in Turbopack, the path is simplified as [project]/...
/Error: File size for Twitter image "\[project\]\/app\/twitter-image\.png" exceeds 5MB/
? // in Turbopack, the path is simplified as [project]/.... It's also thrown earlier, so the prefix is slightly different.
/File size for Twitter image "\[project\]\/app\/twitter-image\.png" exceeds 5MB/
: /Error: File size for Twitter image ".*\/app\/twitter-image\.png" exceeds 5MB/

expect(next.cliOutput).toMatch(regex)
Expand Down
Loading