fix(server): keep replacement patterns in resume text literal in page metadata

A name or summary containing $&, $' or $$ was expanded by String.replace into chunks of index.html, garbling the public resume's head. The inserts now use function replacers.
This commit is contained in:
Amruth Pillai
2026-09-29 18:13:07 +02:00
parent 73a3dfc423
commit 861feb22eb
2 changed files with 37 additions and 7 deletions
+15
View File
@@ -174,6 +174,21 @@ describe("web app fallback classification", () => {
expect(html).toContain('content="Ends with " and & ampersand"');
});
it("keeps replacement patterns in resume text literal", async () => {
vi.mocked(fs.readFile).mockResolvedValue(shell);
mocks.getPublicResumeSocialMeta.mockResolvedValue({
name: "Jane $& $' Doe",
title: "Jane Doe",
description: "Costs $$ and $` nothing",
template: "azurill",
});
const html = await (await handleWebApp(new Request("https://example.com/jane/resume"))).text();
expect(html).toContain("<title>Jane $&amp; $&#39; Doe - Reactive Resume</title>");
expect(html).toContain('<meta name="description" content="Costs $$ and $` nothing">');
});
it("serves the plain shell when the resume is not publicly shareable", async () => {
vi.mocked(fs.readFile).mockResolvedValue(shell);
+22 -7
View File
@@ -315,11 +315,17 @@ export async function handleWebApp(request: Request) {
.replace(/<title>[^<]*<\/title>/, "<title>Reactive Resume</title>")
.replace(/<meta\s+name="description"[^>]*>/, '<meta name="description" content="">');
const markup = `<link rel="canonical" href="${escapeAttribute(canonicalUrl)}" data-root-resume-shell><meta name="robots" content="noindex, follow" data-root-resume-shell>`;
return new Response(shell.replace("</head>", `${markup}</head>`), { headers });
return new Response(
shell.replace("</head>", () => `${markup}</head>`),
{ headers },
);
}
if (pathname === "/") {
return new Response(html.replace("</head>", `${createRootSeoMarkup(canonicalUrl)}</head>`), { headers });
return new Response(
html.replace("</head>", () => `${createRootSeoMarkup(canonicalUrl)}</head>`),
{ headers },
);
}
if (pathname === "/ats-checker") {
@@ -328,19 +334,28 @@ export async function handleWebApp(request: Request) {
.replace(/<title>[^<]*<\/title>/, `<title>${ATS_CHECKER_TITLE}</title>`)
.replace(/<meta\s+name="description"[^>]*>/, `<meta name="description" content="${ATS_CHECKER_DESCRIPTION}">`);
return new Response(withTitle.replace("</head>", `${createAtsCheckerSeoMarkup(origin)}</head>`), { headers });
return new Response(
withTitle.replace("</head>", () => `${createAtsCheckerSeoMarkup(origin)}</head>`),
{ headers },
);
}
if (isPublicResumePath(pathname)) {
const resumeSeo = await createPublicResumeSeoMarkup(pathname, new URL(env.APP_URL).origin);
if (resumeSeo) {
// The shell's generic title/description are replaced so shares and previews show the resume,
// not the marketing copy baked into index.html.
// not the marketing copy baked into index.html. Function replacers keep `$&`, `$'` etc. in user text literal.
const withTitle = html
.replace(/<title>[^<]*<\/title>/, `<title>${resumeSeo.pageTitle}</title>`)
.replace(/<meta\s+name="description"[^>]*>/, `<meta name="description" content="${resumeSeo.description}">`);
.replace(/<title>[^<]*<\/title>/, () => `<title>${resumeSeo.pageTitle}</title>`)
.replace(
/<meta\s+name="description"[^>]*>/,
() => `<meta name="description" content="${resumeSeo.description}">`,
);
return new Response(withTitle.replace("</head>", `${resumeSeo.markup}</head>`), { headers });
return new Response(
withTitle.replace("</head>", () => `${resumeSeo.markup}</head>`),
{ headers },
);
}
}