Skip to content
Open
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
29 changes: 19 additions & 10 deletions src/prerender/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export async function prerender(nitro: Nitro) {

// Start prerendering
const generatedRoutes = new Set();
// Crawled links, keyed by route (relative to `baseURL`), with their original path
const crawledLinkPaths = new Map<string, string>();
const failedRoutes = new Set<PrerenderRoute>();
const skippedRoutes = new Set();
const displayedLengthWarns = new Set();
Expand Down Expand Up @@ -181,8 +183,12 @@ export async function prerender(nitro: Nitro) {
const generateRoute = async (route: string) => {
const start = Date.now();

// Crawled links are fetched, resolved and written by their original path
const crawledPath = crawledLinkPaths.get(route);

// Ensure route is decoded to start with
route = decodeURI(route);
const routePath = crawledPath ? decodeURI(crawledPath) : route;

// Check if we should render route
if (!canPrerender(route)) {
Expand All @@ -195,7 +201,7 @@ export async function prerender(nitro: Nitro) {
const _route: PrerenderRoute = { route };

// Fetch the route
const encodedRoute = encodeURI(route);
const encodedRoute = encodeURI(routePath);

const req = toRequest(withBase(encodedRoute, nitro.options.baseURL), {
headers: [["x-nitro-prerender", encodedRoute]],
Expand Down Expand Up @@ -246,14 +252,14 @@ export async function prerender(nitro: Nitro) {
// Guess route type and populate fileName
const contentType = res.headers.get("content-type") || "";
const isImplicitHTML =
!route.endsWith(".html") &&
!routePath.endsWith(".html") &&
contentType.includes("html") &&
!JsonSigRx.test(dataBuff!.subarray(0, 32).toString("utf8"));
const routeWithIndex = route.endsWith("/") ? route + "index" : route;
const routeWithIndex = routePath.endsWith("/") ? routePath + "index" : routePath;
const htmlPath =
route.endsWith("/") || nitro.options.prerender.autoSubfolderIndex
? joinURL(route, "index.html")
: route + ".html";
routePath.endsWith("/") || nitro.options.prerender.autoSubfolderIndex
? joinURL(routePath, "index.html")
: routePath + ".html";
_route.fileName = withoutBase(
isImplicitHTML ? htmlPath : routeWithIndex,
nitro.options.baseURL
Expand Down Expand Up @@ -292,16 +298,19 @@ export async function prerender(nitro: Nitro) {
}

// Crawl route links
if (!_route.error && (isImplicitHTML || route.endsWith(".html"))) {
if (!_route.error && (isImplicitHTML || routePath.endsWith(".html"))) {
const extractedLinks = await extractLinks(
dataBuff!.toString("utf8"),
route,
routePath,
res,
nitro.options.prerender.crawlLinks ?? false
);
for (const _link of extractedLinks) {
if (canPrerender(_link)) {
routes.add(_link);
// Crawled links include `baseURL`, while routes (and `ignore` patterns) are relative to it
const link = withoutBase(_link, nitro.options.baseURL);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (canPrerender(link) && !routes.has(link)) {
crawledLinkPaths.set(link, _link);
routes.add(link);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions test/vite/prerender-baseurl-fixture/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "nitro";

export default defineConfig({
preset: "static",
baseURL: "/base/",
prerender: { routes: ["/"], crawlLinks: true, ignore: ["/admin"] },
});
12 changes: 12 additions & 0 deletions test/vite/prerender-baseurl-fixture/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
fetch(req: Request) {
const { pathname } = new URL(req.url);
// Links rendered by an app with `baseURL` include the base
const links = ["/base/", "/base/about", "/base/admin", "/base/base/about"]
.map((href) => `<a href="${href}">${href}</a>`)
.join("");
return new Response(`<h1>${pathname}</h1>${links}`, {
headers: { "content-type": "text/html" },
});
},
};
6 changes: 6 additions & 0 deletions test/vite/prerender-baseurl-fixture/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
plugins: [nitro()],
});
49 changes: 49 additions & 0 deletions test/vite/prerender-baseurl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { readFile, rm, mkdir } from "node:fs/promises";
import { beforeAll, describe, expect, it } from "vitest";
import { createNitro, build, prepare } from "nitro/builder";

const fixtureDir = fileURLToPath(new URL("./prerender-baseurl-fixture", import.meta.url));
const outDir = join(fixtureDir, ".tmp");

// https://github.com/nitrojs/nitro/issues/4441
describe("prerender with baseURL", () => {
const routes: string[] = [];

beforeAll(async () => {
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });
const nitro = await createNitro({
rootDir: fixtureDir,
output: { dir: outDir },
builder: "vite",
});
nitro.hooks.hook("prerender:route", (route) => {
routes.push(route.route);
});
try {
await prepare(nitro);
await build(nitro);
} finally {
await nitro.close();
}
}, 30_000);

it("renders crawled links once, without the baseURL prefix", () => {
expect(routes.sort()).toEqual(["/", "/about", "/base/about"]);
});

it("fetches and writes crawled links by their original path", async () => {
const read = (file: string) => readFile(join(outDir, "public", file), "utf8");
expect(await read("index.html")).toContain("<h1>/base</h1>");
expect(await read("about/index.html")).toContain("<h1>/base/about</h1>");
// `/base/base/about` is the `/base/about` route, not a duplicate of `/about`
expect(await read("base/about/index.html")).toContain("<h1>/base/base/about</h1>");
});

it("applies `prerender.ignore` to crawled links", () => {
expect(routes).not.toContain("/admin");
expect(routes).not.toContain("/base/admin");
});
});
Loading