From 861feb22ebbae63063388b32f0200695d9aabe3c Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 29 Sep 2026 18:13:07 +0200 Subject: [PATCH] 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. --- apps/server/src/static/web.test.ts | 15 +++++++++++++++ apps/server/src/static/web.ts | 29 ++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/apps/server/src/static/web.test.ts b/apps/server/src/static/web.test.ts index 419b735d8..02013cae7 100644 --- a/apps/server/src/static/web.test.ts +++ b/apps/server/src/static/web.test.ts @@ -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("Jane $& $' Doe - Reactive Resume"); + expect(html).toContain(''); + }); + it("serves the plain shell when the resume is not publicly shareable", async () => { vi.mocked(fs.readFile).mockResolvedValue(shell); diff --git a/apps/server/src/static/web.ts b/apps/server/src/static/web.ts index b50676732..bb337f134 100644 --- a/apps/server/src/static/web.ts +++ b/apps/server/src/static/web.ts @@ -315,11 +315,17 @@ export async function handleWebApp(request: Request) { .replace(/[^<]*<\/title>/, "<title>Reactive Resume") .replace(/]*>/, ''); const markup = ``; - return new Response(shell.replace("", `${markup}`), { headers }); + return new Response( + shell.replace("", () => `${markup}`), + { headers }, + ); } if (pathname === "/") { - return new Response(html.replace("", `${createRootSeoMarkup(canonicalUrl)}`), { headers }); + return new Response( + html.replace("", () => `${createRootSeoMarkup(canonicalUrl)}`), + { headers }, + ); } if (pathname === "/ats-checker") { @@ -328,19 +334,28 @@ export async function handleWebApp(request: Request) { .replace(/[^<]*<\/title>/, `<title>${ATS_CHECKER_TITLE}`) .replace(/]*>/, ``); - return new Response(withTitle.replace("", `${createAtsCheckerSeoMarkup(origin)}`), { headers }); + return new Response( + withTitle.replace("", () => `${createAtsCheckerSeoMarkup(origin)}`), + { 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>${resumeSeo.pageTitle}`) - .replace(/]*>/, ``); + .replace(/[^<]*<\/title>/, () => `<title>${resumeSeo.pageTitle}`) + .replace( + /]*>/, + () => ``, + ); - return new Response(withTitle.replace("", `${resumeSeo.markup}`), { headers }); + return new Response( + withTitle.replace("", () => `${resumeSeo.markup}`), + { headers }, + ); } }