Notifio watches a rental search page and tells you when something new appears on it. The thing it tells you is the listing's own title, which means there is exactly one string in the whole product that nobody on our side wrote, and we put it in an email.

That string takes a long route. It comes off a page in a browser running on the user's machine, gets written to a file in their app data directory, is posted as JSON to an API route that authenticates with their licence, is interpolated into an HTML document on a server, handed to Resend, and delivered to the user's own inbox. Six hops, four of them places where it is rendered.

Three of those four escape it for free. The interesting one does not.

Four destinations, one of which is hand-built HTML

The on-disk history. A ring buffer of the last 200 finds, written with JSON.stringify, which has no HTML semantics and cannot be confused into any:

/** * How many finds to keep. A busy hunt produces a few hundred a week; this is * enough to cover "what did I miss overnight" without the file growing forever. */ const MAX_FINDS = 200; 

The app window. React, rendering the title as a text child, so escaping is a property of JSX rather than a thing anybody had to remember:

<p className="truncate text-[14px] font-medium text-[#fafafa]"> {find.title || find.url} </p> 

The desktop notification. Electron's Notification takes title and body as plain strings and hands them to the OS notification centre, which has no markup to speak of:

const body = finds.length === 1 ? first.title || first.url : finds .slice(0, 3) .map((f) => f.title || f.url) .join("\n"); 

The email. Which is a template literal of hand-written table HTML, because that is what email is, and is therefore the one place where a stranger's string meets string concatenation:

<a href="${escHtml(l.url)}" style="color:#fafafa;font-weight:600;text-decoration:none;font-size:14px;line-height:1.4;">${escHtml(l.title)}</a><br> <span style="font-size:12px;color:#525252;word-break:break-all;">${escHtml(l.url)}</span> 

Three of the four are safe because of the tool, and the fourth is safe because of nine lines of code.

The nine lines

function escHtml(str: string): string { return String(str) .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;"); } 

Four characters. The ampersand first, which is the one ordering rule that matters: do it last and < becomes &lt; and then &amp;lt;, and your reader sees the markup you were trying to hide.

The notable absence is the apostrophe. ' is not escaped, which is fine here and is fine for a reason that has nothing to do with this function: every attribute in every one of these templates is double-quoted. A title containing 't throw a 500 mid-request. if (!alerts.every((a) => a && typeof a.siteName === "string" && Array.isArray(a.newListings))) { return NextResponse.json( { error: "Each alert requires siteName and newListings[]." }, { status: 400 } ); }