diff --git a/packages/vinext/src/server/app-ssr-error-meta.ts b/packages/vinext/src/server/app-ssr-error-meta.ts
index 8b32a04b0..779793ae1 100644
--- a/packages/vinext/src/server/app-ssr-error-meta.ts
+++ b/packages/vinext/src/server/app-ssr-error-meta.ts
@@ -44,9 +44,14 @@ function renderSsrErrorMetaTag(error: unknown, options: SsrErrorMetaRenderOption
const httpError = parseNextHttpErrorDigest(digest);
if (httpError) {
- let html = '';
+ // Output format matches Next.js's `make-get-server-inserted-html.tsx`,
+ // which serializes these meta tags via React's HTML renderer. React's
+ // void-element output uses no space before `/>`, and Next.js tests assert
+ // on that exact substring (e.g. `''`).
+ // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/make-get-server-inserted-html.tsx
+ let html = '';
if ((options.nodeEnv ?? process.env.NODE_ENV) === "development") {
- html += '';
+ html += '';
}
return html;
}
@@ -61,7 +66,7 @@ function renderSsrErrorMetaTag(error: unknown, options: SsrErrorMetaRenderOption
delay +
";url=" +
escapeHtmlAttr(location) +
- '" />'
+ '"/>'
);
}
diff --git a/tests/app-ssr-error-meta.test.ts b/tests/app-ssr-error-meta.test.ts
index fd71aa4a2..7dd1e11fd 100644
--- a/tests/app-ssr-error-meta.test.ts
+++ b/tests/app-ssr-error-meta.test.ts
@@ -11,11 +11,11 @@ function digestError(digest: string): Error & { digest: string } {
describe("App SSR error meta tags", () => {
it("renders noindex meta tags for streamed notFound and HTTP access fallback errors", () => {
expect(renderSsrErrorMetaTags([digestError("NEXT_NOT_FOUND")])).toBe(
- '',
+ '',
);
expect(renderSsrErrorMetaTags([digestError("NEXT_HTTP_ERROR_FALLBACK;403")])).toBe(
- '',
+ '',
);
});
@@ -23,17 +23,17 @@ describe("App SSR error meta tags", () => {
expect(
renderSsrErrorMetaTags([digestError("NEXT_NOT_FOUND")], { nodeEnv: "development" }),
).toBe(
- '' + '',
+ '' + '',
);
});
it("renders refresh meta tags for streamed temporary and permanent redirects", () => {
expect(renderSsrErrorMetaTags([digestError("NEXT_REDIRECT;replace;/target;307")])).toBe(
- '',
+ '',
);
expect(renderSsrErrorMetaTags([digestError("NEXT_REDIRECT;replace;/target;308")])).toBe(
- '',
+ '',
);
});
@@ -43,7 +43,7 @@ describe("App SSR error meta tags", () => {
basePath: "/docs",
}),
).toBe(
- '',
+ '',
);
expect(
@@ -51,7 +51,7 @@ describe("App SSR error meta tags", () => {
basePath: "/docs",
}),
).toBe(
- '',
+ '',
);
expect(
@@ -59,14 +59,14 @@ describe("App SSR error meta tags", () => {
basePath: "/docs",
}),
).toBe(
- '',
+ '',
);
expect(
renderSsrErrorMetaTags([digestError("NEXT_REDIRECT;replace;/docs%23top;307")], {
basePath: "/docs",
}),
- ).toBe('');
+ ).toBe('');
});
it("escapes redirect meta URLs before inserting them into HTML", () => {
@@ -75,7 +75,7 @@ describe("App SSR error meta tags", () => {
digestError("NEXT_REDIRECT;replace;/target%3Fnext%3D%26%22%3Cscript%3E;307"),
]),
).toBe(
- '',
+ '',
);
});
@@ -83,12 +83,12 @@ describe("App SSR error meta tags", () => {
const renderer = createSsrErrorMetaRenderer({ nodeEnv: "production" });
renderer.capture(digestError("NEXT_NOT_FOUND"));
- expect(renderer.flush()).toBe('');
+ expect(renderer.flush()).toBe('');
expect(renderer.flush()).toBe("");
renderer.capture(digestError("NEXT_REDIRECT;replace;/target;307"));
expect(renderer.flush()).toBe(
- '',
+ '',
);
expect(renderer.flush()).toBe("");
});
diff --git a/tests/nextjs-compat/navigation.test.ts b/tests/nextjs-compat/navigation.test.ts
index 69f2ceb41..00d5c78f9 100644
--- a/tests/nextjs-compat/navigation.test.ts
+++ b/tests/nextjs-compat/navigation.test.ts
@@ -83,6 +83,32 @@ describe("Next.js compat: navigation", () => {
expect(html).toMatch(NOINDEX_META_TAG_RE);
});
+ // ── Streaming meta tags for not-found / redirect ─────────────
+ // Ported from Next.js: test/e2e/app-dir/navigation/navigation.test.ts
+ // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/navigation/navigation.test.ts#L732-L772
+ //
+ // When notFound() or redirect() is called from inside a Suspense
+ // boundary, the error surfaces AFTER the shell has already started
+ // streaming. The response can no longer return a 4xx/3xx status, so
+ // Next.js communicates the not-found / redirect intent via inline
+ // tags injected into the HTML stream. The exact substring is
+ // asserted in Next.js tests, so vinext must use the same format
+ // (React's void-element serialization — no space before `/>`).
+ //
+ // Source: packages/next/src/server/app-render/make-get-server-inserted-html.tsx
+
+ it("notFound() in Suspense streams noindex robots meta tag (exact substring)", async () => {
+ const { html } = await fetchHtml(baseUrl, "/suspense-notfound-test");
+ expect(html).toContain('');
+ });
+
+ it("redirect() in Suspense streams refresh meta tag (exact substring)", async () => {
+ const { html } = await fetchHtml(baseUrl, "/suspense-redirect-test");
+ expect(html).toContain(
+ '',
+ );
+ });
+
// ── Browser-only tests (documented, not ported) ──────────────
//
// The following tests ALL require Playwright and are N/A for HTTP-level testing:
@@ -140,7 +166,4 @@ describe("Next.js compat: navigation", () => {
//
// N/A: Metadata await promise during navigation
// Tests async metadata loading during client nav
- //
- // N/A: Redirect refresh meta tag
- // Tests HTML meta refresh tag — would need streaming-specific fixture
});
diff --git a/tests/rsc-streaming.test.ts b/tests/rsc-streaming.test.ts
index d70ad5946..343183f76 100644
--- a/tests/rsc-streaming.test.ts
+++ b/tests/rsc-streaming.test.ts
@@ -451,7 +451,7 @@ describe("Tick-buffered RSC streaming (behavioral)", () => {
const shellPos = output.indexOf("shell");
const redirectMetaPos = output.indexOf(
- '',
+ '',
);
const boundaryPos = output.indexOf("resolved boundary");