linkedom server-side HTML parsing and rewriting: working versions (tested 2026-10-15)
Parse and transform HTML in Node with linkedom: query links and Open Graph meta tags, absolutize relative URLs, add lazy loading to images and serialize the document back.
passed2 passed · 0 failed2026-10-15 17:11 UTC- Status
- passed
- Last passed
- 2026-10-15 17:11 UTC
- Tests
- 2 / 2
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
linkedom@0.18.13@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38775053802
The code
Exactly the files that passed. Your agent can pull them with get_recipe("linkedom-html-parsing") (install), or see them on GitHub.
package.json
{
"name": "linkedom-html-parsing",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"linkedom": "0.18.13"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/page.ts
import { parseHTML } from "linkedom";
export type PageInfo = { title: string; description: string | null; og: Record<string, string>; links: string[] };
export function extractInfo(html: string, baseUrl: string): PageInfo {
const { document } = parseHTML(html);
const og: Record<string, string> = {};
for (const m of document.querySelectorAll('meta[property^="og:"]')) {
og[m.getAttribute("property")!.slice(3)] = m.getAttribute("content") ?? "";
}
const links = [...document.querySelectorAll("a[href]")]
.map((a) => new URL(a.getAttribute("href")!, baseUrl).href)
.filter((href, i, all) => href.startsWith("http") && all.indexOf(href) === i);
return {
title: document.querySelector("title")?.textContent?.trim() ?? "",
description: document.querySelector('meta[name="description"]')?.getAttribute("content") ?? null,
og,
links,
};
}
/** Absolutize links and images, lazy-load images, and drop inline scripts. */
export function rewrite(html: string, baseUrl: string): string {
const { document } = parseHTML(html);
for (const a of document.querySelectorAll("a[href]")) a.setAttribute("href", new URL(a.getAttribute("href")!, baseUrl).href);
for (const img of document.querySelectorAll("img[src]")) {
img.setAttribute("src", new URL(img.getAttribute("src")!, baseUrl).href);
img.setAttribute("loading", "lazy");
}
for (const s of document.querySelectorAll("script:not([src])")) s.remove();
return document.toString();
}
test/page.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { extractInfo, rewrite } from "../src/page.ts";
const html = `<!doctype html>
<html><head>
<title> Kenai Parts </title>
<meta name="description" content="Parts and tools">
<meta property="og:title" content="Kenai Parts">
<meta property="og:image" content="https://cdn.example.com/og.png">
</head><body>
<a href="/catalog">Catalog</a>
<a href="about">About</a>
<a href="https://other.example.org/x">Other</a>
<a href="/catalog">Dup</a>
<a href="mailto:[email protected]">Mail</a>
<img src="/img/bolt.png" alt="bolt">
<script>track()</script>
<script src="/app.js"></script>
</body></html>`;
test("extracts title, description, Open Graph and unique absolute links", () => {
assert.deepEqual(extractInfo(html, "https://example.com/shop/"), {
title: "Kenai Parts",
description: "Parts and tools",
og: { title: "Kenai Parts", image: "https://cdn.example.com/og.png" },
links: ["https://example.com/catalog", "https://example.com/shop/about", "https://other.example.org/x"],
});
});
test("rewrites URLs, lazy-loads images and removes inline scripts", () => {
const out = rewrite(html, "https://example.com/shop/");
assert.match(out, /<a href="https:\/\/example.com\/catalog">Catalog<\/a>/);
// linkedom does not keep attribute order, so check each attribute on its own.
const img = out.match(/<img [^>]+>/)![0];
assert.match(img, /src="https:\/\/example.com\/img\/bolt.png"/);
assert.match(img, /alt="bolt"/);
assert.match(img, /loading="lazy"/);
assert.doesNotMatch(out, /track\(\)/);
assert.match(out, /<script src="\/app.js"><\/script>/);
assert.match(out, /^<!DOCTYPE html>/i);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.