ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.