Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
15 changes: 9 additions & 6 deletions packages/vinext/src/build/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ export async function prerenderPages({
// Skip internal pages (_app, _document, _error, etc.)
const routeName = path.basename(route.filePath, path.extname(route.filePath));
if (routeName.startsWith("_")) continue;
// `/404` is rendered by the dedicated 404 block below. Production serves
// it with a 404 status, so the generic static-page loop must not treat
// that non-2xx response as a prerender failure.
if (route.pattern === "/404") continue;

// Cross-reference with file-system route scan.
const fsRoute = routes.find(
Expand Down Expand Up @@ -765,14 +769,13 @@ export async function prerenderPages({
results.push(...pageResults);

// ── Render 404 page ───────────────────────────────────────────────────
const has404 =
findFileWithExtensions(path.join(pagesDir, "404"), fileMatcher) ||
findFileWithExtensions(path.join(pagesDir, "_error"), fileMatcher);
if (has404) {
const hasCustom404 = findFileWithExtensions(path.join(pagesDir, "404"), fileMatcher);
const hasErrorPage = findFileWithExtensions(path.join(pagesDir, "_error"), fileMatcher);
if (hasCustom404 || hasErrorPage) {
try {
const notFoundRes = await renderPage(NOT_FOUND_SENTINEL_PATH);
const notFoundRes = await renderPage(hasCustom404 ? "/404" : NOT_FOUND_SENTINEL_PATH);
const contentType = notFoundRes.headers.get("content-type") ?? "";
if (contentType.includes("text/html")) {
if (notFoundRes.status === 404 && contentType.includes("text/html")) {
Comment thread
NathanDrake2406 marked this conversation as resolved.
Comment thread
NathanDrake2406 marked this conversation as resolved.
const html404 = await notFoundRes.text();
const fullPath = path.join(outDir, "404.html");
fs.writeFileSync(fullPath, html404, "utf-8");
Expand Down
25 changes: 23 additions & 2 deletions packages/vinext/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,10 +883,22 @@ export default {
// ── 9. Page routes ────────────────────────────────────────────
let response: Response | undefined;
if (typeof renderPage === "function") {
response = await renderPage(request, resolvedUrl, null, ctx);
const renderPageMatch =
typeof matchPageRoute === "function" ? matchPageRoute(resolvedPathname, request) : null;
const shouldDeferErrorPageOnMiss = typeof matchPageRoute === "function" && !renderPageMatch;
const initialRenderOptions = shouldDeferErrorPageOnMiss
? { renderErrorPageOnMiss: false }
: undefined;
response = await renderPage(request, resolvedUrl, null, ctx, undefined, initialRenderOptions);

// ── 10. Fallback rewrites (if SSR returned 404) ─────────────
if (response && response.status === 404 && configRewrites.fallback?.length) {
let matchedFallbackRewrite = false;
if (
response &&
response.status === 404 &&
shouldDeferErrorPageOnMiss &&
configRewrites.fallback?.length
) {
const fallbackRewrite = matchRewrite(
Comment thread
NathanDrake2406 marked this conversation as resolved.
matchResolvedPathname(resolvedPathname),
configRewrites.fallback,
Expand All @@ -897,6 +909,7 @@ export default {
if (isExternalUrl(fallbackRewrite)) {
return proxyExternalRequest(request, fallbackRewrite);
}
matchedFallbackRewrite = true;
response = await renderPage(
request,
mergeRewriteQuery(resolvedUrl, fallbackRewrite),
Expand All @@ -905,6 +918,14 @@ export default {
);
}
}
Comment thread
NathanDrake2406 marked this conversation as resolved.
if (
response &&
response.status === 404 &&
shouldDeferErrorPageOnMiss &&
!matchedFallbackRewrite
) {
response = await renderPage(request, resolvedUrl, null, ctx);
}
}

if (!response) {
Expand Down
112 changes: 91 additions & 21 deletions packages/vinext/src/entries/pages-server-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ export async function generateServerEntry(
` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`,
);

// Check for _app and _document
// Check for _app, _document, and _error.
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
const errorFilePath = findFileWithExts(pagesDir, "_error", fileMatcher);
// Embed the resolved _app path (or null) so the runtime can look it up
// in the SSR manifest and include any CSS/JS chunks `_app` brings in
// (e.g. global stylesheets imported by `_app.tsx`) alongside the page's
Expand All @@ -87,6 +88,13 @@ export async function generateServerEntry(
? `import { default as DocumentComponent } from ${JSON.stringify(normalizePathSeparators(docFilePath))};`
: `const DocumentComponent = null;`;

const errorAssetPathJson =
errorFilePath !== null ? JSON.stringify(normalizePathSeparators(errorFilePath)) : "null";
const errorImportCode =
errorFilePath !== null
? `import * as ErrorPageModule from ${JSON.stringify(normalizePathSeparators(errorFilePath))};`
: `const ErrorPageModule = null;`;

// Serialize i18n config for embedding in the server entry
const i18nConfigJson = nextConfig?.i18n
? JSON.stringify({
Expand Down Expand Up @@ -335,11 +343,22 @@ ${apiImports.join("\n")}

${appImportCode}
${docImportCode}
${errorImportCode}

export const pageRoutes = [
${pageRouteEntries.join(",\n")}
];
const _pageRouteTrie = _buildRouteTrie(pageRoutes);
const _errorPageRoute = ErrorPageModule
? {
pattern: "/_error",
patternParts: ["_error"],
isDynamic: false,
params: [],
module: ErrorPageModule,
filePath: ${errorAssetPathJson},
}
: null;

const apiRoutes = [
${apiRouteEntries.join(",\n")}
Expand Down Expand Up @@ -399,11 +418,29 @@ function patternToNextFormat(pattern) {
.replace(/:([^\\/]+?)(?=\\/|$)/g, "[$1]");
}

function collectAssetTags(manifest, moduleIds, scriptNonce) {
function resolveSsrManifest(manifest) {
// Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
const m = (manifest && Object.keys(manifest).length > 0)
return (manifest && Object.keys(manifest).length > 0)
? manifest
: (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
}

function getManifestFilesForModule(manifest, moduleId) {
if (!manifest || !moduleId) return null;

var files = manifest[moduleId];
if (files) return files;

for (var key in manifest) {
if (moduleId.endsWith("/" + key) || moduleId === key) {
return manifest[key];
}
}
return null;
}

function collectAssetTags(manifest, moduleIds, scriptNonce) {
const m = resolveSsrManifest(manifest);
const tags = [];
const seen = new Set();
const nonceAttr = __createNonceAttribute(scriptNonce);
Expand Down Expand Up @@ -436,18 +473,7 @@ function collectAssetTags(manifest, moduleIds, scriptNonce) {
// Collect assets for the requested page modules
for (var mi = 0; mi < moduleIds.length; mi++) {
var id = moduleIds[mi];
var files = m[id];
if (!files) {
// Absolute path didn't match — try matching by suffix.
// Manifest keys are relative (e.g. "pages/about.tsx") while
// moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
for (var mk in m) {
if (id.endsWith("/" + mk) || id === mk) {
files = m[mk];
break;
}
}
}
var files = getManifestFilesForModule(m, id);
if (files) {
for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
}
Expand Down Expand Up @@ -508,6 +534,18 @@ function collectAssetTags(manifest, moduleIds, scriptNonce) {
return tags.join("\\n ");
}

function resolveClientModuleUrl(manifest, moduleId) {
const files = getManifestFilesForModule(resolveSsrManifest(manifest), moduleId);
Comment thread
NathanDrake2406 marked this conversation as resolved.
if (!files) return undefined;
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!file || !file.endsWith(".js")) continue;
if (file.charAt(0) !== "/") file = "/" + file;
return file;
}
return undefined;
Comment thread
NathanDrake2406 marked this conversation as resolved.
}

export async function renderPage(request, url, manifest, ctx, middlewareHeaders, options) {
if (ctx) return _runWithExecutionContext(ctx, () => _renderPage(request, url, manifest, middlewareHeaders, options));
return _renderPage(request, url, manifest, middlewareHeaders, options);
Expand All @@ -530,6 +568,9 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
}
}
}
const statusCode = options && typeof options.statusCode === "number" ? options.statusCode : undefined;
const asPath = options && typeof options.asPath === "string" ? options.asPath : undefined;
const renderErrorPageOnMiss = !(options && options.renderErrorPageOnMiss === false);
const localeInfo = i18nConfig
? resolvePagesI18nRequest(
url,
Expand All @@ -551,13 +592,31 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
return new Response(null, { status: 307, headers: { Location: localeInfo.redirectUrl } });
}

const match = matchRoute(routeUrl, pageRoutes);
let match = matchRoute(routeUrl, pageRoutes);
let renderStatusCodeOverride = statusCode;
let renderAsPath = asPath;
if (!match) {
if (isDataReq) {
return __buildNextDataNotFoundResponse();
}
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
{ status: 404, headers: { "Content-Type": "text/html" } });
if (!renderErrorPageOnMiss) {
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
{ status: 404, headers: { "Content-Type": "text/html" } });
}
const notFoundMatch = matchRoute("/404", pageRoutes);
// matchRoute may match a catch-all (e.g. [...slug]) — only use the explicit pages/404 route.
if (notFoundMatch && notFoundMatch.route.pattern === "/404") {
Comment thread
NathanDrake2406 marked this conversation as resolved.
Comment thread
NathanDrake2406 marked this conversation as resolved.
match = notFoundMatch;
renderStatusCodeOverride = 404;
renderAsPath = routeUrl;
} else if (_errorPageRoute) {
match = { route: _errorPageRoute, params: {} };
renderStatusCodeOverride = 404;
renderAsPath = routeUrl;
} else {
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
{ status: 404, headers: { "Content-Type": "text/html" } });
}
}

const { route, params } = match;
Expand All @@ -568,12 +627,13 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
ensureFetchPatch();
try {
const routePattern = patternToNextFormat(route.pattern);
const renderStatusCode = renderStatusCodeOverride ?? (routePattern === "/404" ? 404 : undefined);
const query = mergeRouteParamsIntoQuery(parseQuery(routeUrl), params);
if (typeof setSSRContext === "function") {
setSSRContext({
pathname: routePattern,
query,
asPath: routeUrl,
asPath: renderAsPath || routeUrl,
locale: locale,
locales: i18nConfig ? i18nConfig.locales : undefined,
defaultLocale: currentDefaultLocale,
Expand Down Expand Up @@ -616,7 +676,7 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
setSSRContext({
pathname: routePattern,
query,
asPath: routeUrl,
asPath: renderAsPath || routeUrl,
locale: locale,
locales: i18nConfig ? i18nConfig.locales : undefined,
defaultLocale: currentDefaultLocale,
Expand Down Expand Up @@ -681,6 +741,9 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
return pageDataResult.response;
}
let pageProps = pageDataResult.pageProps;
if (routePattern === "/_error" && typeof renderStatusCode === "number") {
Comment thread
NathanDrake2406 marked this conversation as resolved.
pageProps = { ...pageProps, statusCode: renderStatusCode };
}
var gsspRes = pageDataResult.gsspRes;
let isrRevalidateSeconds = pageDataResult.isrRevalidateSeconds;
const isFallbackRender = pageDataResult.isFallback === true;
Expand All @@ -693,7 +756,7 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
setSSRContext({
pathname: routePattern,
query,
asPath: routeUrl,
asPath: renderAsPath || routeUrl,
locale: locale,
locales: i18nConfig ? i18nConfig.locales : undefined,
defaultLocale: currentDefaultLocale,
Expand Down Expand Up @@ -732,6 +795,8 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
if (route.filePath) pageModuleIds.push(route.filePath);
if (_appAssetPath) pageModuleIds.push(_appAssetPath);
const assetTags = collectAssetTags(manifest, pageModuleIds, scriptNonce);
const pageModuleUrl = resolveClientModuleUrl(manifest, route.filePath);
const appModuleUrl = resolveClientModuleUrl(manifest, _appAssetPath);

return __renderPagesPageResponse({
assetTags,
Expand Down Expand Up @@ -795,6 +860,11 @@ async function _renderPage(request, url, manifest, middlewareHeaders, options) {
routeUrl,
safeJsonStringify,
scriptNonce,
statusCode: renderStatusCode,
vinext: {
pageModuleUrl,
appModuleUrl,
},
});
} catch (e) {
console.error("[vinext] SSR error:", e);
Expand Down
15 changes: 11 additions & 4 deletions packages/vinext/src/server/pages-page-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ type RenderPagesPageResponseOptions = {
routeUrl: string;
safeJsonStringify: (value: unknown) => string;
scriptNonce?: string;
statusCode?: number;
vinext?: VinextNextData["__vinext"];
};

function buildPagesFontHeadHtml(
Expand Down Expand Up @@ -265,9 +267,13 @@ function schedulePagesIsrCacheWrite(options: {
getRequestExecutionContext()?.waitUntil(cacheWritePromise);
}

function applyGsspHeaders(headers: Headers, gsspRes: PagesGsspResponse | null): number {
function applyGsspHeaders(
headers: Headers,
gsspRes: PagesGsspResponse | null,
statusCode?: number,
): number {
if (!gsspRes) {
return 200;
return statusCode ?? 200;
}

const gsspHeaders = gsspRes.getHeaders();
Expand All @@ -289,7 +295,7 @@ function applyGsspHeaders(headers: Headers, gsspRes: PagesGsspResponse | null):
}
}
headers.set("Content-Type", "text/html");
return gsspRes.statusCode;
return statusCode ?? gsspRes.statusCode;
Comment thread
NathanDrake2406 marked this conversation as resolved.
}

export async function renderPagesPageResponse(
Expand Down Expand Up @@ -318,6 +324,7 @@ export async function renderPagesPageResponse(
routePattern: options.routePattern,
safeJsonStringify: options.safeJsonStringify,
scriptNonce: options.scriptNonce,
vinext: options.vinext,
});
const bodyMarker = "<!--VINEXT_STREAM_BODY-->";
// Render the page FIRST so that <Head> and other SSR state collectors
Expand Down Expand Up @@ -374,7 +381,7 @@ export async function renderPagesPageResponse(
);

const responseHeaders = new Headers({ "Content-Type": "text/html" });
const finalStatus = applyGsspHeaders(responseHeaders, options.gsspRes);
const finalStatus = applyGsspHeaders(responseHeaders, options.gsspRes, options.statusCode);

if (options.scriptNonce) {
responseHeaders.set("Cache-Control", "no-store, must-revalidate");
Expand Down
Loading
Loading