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
23 changes: 20 additions & 3 deletions packages/cli/src/lib/url-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,26 @@ function getMatchingDynamicSsrRoute(compilation, route) {

// get params for dynamic routes from URLPattern based segment extraction
function getParamsFromSegment(compilation, segment, route) {
return new URLPattern({ pathname: `${compilation.config.basePath}${segment.pathname}` }).exec(
`https://example.com${route}`,
)?.pathname?.groups;
const groups = new URLPattern({
pathname: `${compilation.config.basePath}${segment.pathname}`,
}).exec(`https://example.com${route}`)?.pathname?.groups;

if (!groups) {
return groups;
}

// URLPattern groups are percent-encoded, so decode them (mirroring graph.js) so params
// round-trip losslessly to getBody / getStaticParams for non-ASCII / space slugs
// https://github.com/ProjectEvergreen/greenwood/issues/1713
return Object.fromEntries(
Object.entries(groups).map(([key, value]) => {
try {
return [key, value === undefined ? value : decodeURIComponent(value)];
} catch {
return [key, value];
}
}),
);
}

// get the full route for a static path
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Use Case
* Run Greenwood build command for a dynamic route whose getStaticPaths returns a
* non-ASCII slug, and whose getBody echoes the slug param back into the page.
*
* User Result
* Should generate a static page whose params are decoded (café), not percent-encoded (caf%C3%A9).
*
* User Command
* greenwood build
*
* User Config
* N / A
*
* User Workspace
* src/
* pages/
* [slug].js
*/
import { expect } from "chai";
import fs from "node:fs/promises";
import { JSDOM } from "jsdom";
import path from "node:path";
import { getOutputTeardownFiles } from "../../../../../test/utils.js";
import { Runner } from "gallinago";
import { fileURLToPath } from "node:url";

// https://github.com/ProjectEvergreen/greenwood/issues/1713
describe("Build Greenwood With: ", function () {
const LABEL = "Dynamic Route with getStaticPaths returning a non-ASCII (percent-encodable) slug";
const cliPath = path.join(process.cwd(), "packages/cli/src/bin.js");
const outputPath = fileURLToPath(new URL(".", import.meta.url));
let runner;

before(function () {
this.context = {
publicDir: path.join(outputPath, "public"),
};
runner = new Runner();
});

describe(LABEL, function () {
before(async function () {
await runner.setup(outputPath);
await runner.runCommand(cliPath, "build");
});

describe("A static page generated for a non-ASCII slug", function () {
let dom;
let html;

before(async function () {
html = await fs.readFile(new URL("./public/café/index.html", import.meta.url), "utf-8");
dom = new JSDOM(html);
});

it("should generate the page at the decoded slug directory", function () {
expect(html.length).to.be.greaterThan(0);
});

it("should render the decoded slug param in the page body, not the percent-encoded value", function () {
const headings = dom.window.document.querySelectorAll("body > h2");

expect(headings.length).to.equal(1);
expect(headings[0].textContent).to.equal("café");
});

it("should not leak the percent-encoded slug value into the output", function () {
expect(html).to.not.include("caf%C3%A9");
});
});
});

after(async function () {
await runner.teardown(getOutputTeardownFiles(outputPath));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export async function getStaticPaths() {
return [{ params: { slug: "café" } }];
}

export async function getBody(compilation, page, request, params) {
return `<h2>${params.slug}</h2>`;
}
Loading