Converter: strip Substack app-install / subscribe / CTA clutter

Remove email-truncation and 'read in the app' promos, subscribe forms, and CTA
buttons by their Substack data-component-name (InstallSubstackAppToDOM,
SubscribeWidgetToDOM, ButtonCreateButton) plus the matching class families
(install-substack-app, subscription-widget, preamble, digest-post-embed,
fake-input/button, cta-caption). Real images (Image2ToDOM) and prose are kept.
Structural match only, never body text. Test added.
This commit is contained in:
Charles Kelsoe 2026-06-14 16:27:53 -04:00
parent 7654740d6a
commit b06dd6911d
2 changed files with 42 additions and 0 deletions

View file

@ -138,6 +138,23 @@ describe("convertHtmlToMarkdown: widget stripping and degradation", () => {
const md = convertHtmlToMarkdown("<p>You can subscribe at the library for free.</p>");
expect(md).toContain("You can subscribe at the library for free.");
});
it("strips Substack app-install and subscribe embeds by data-component-name, keeping images and prose", () => {
const html =
'<div data-component-name="InstallSubstackAppToDOM" class="install-substack-app-embed">' +
'<p class="preamble">If you are reading this in email, the text may cut off. Get the app.</p>' +
'<a class="install-substack-app-embed-btn button primary" href="https://substack.com/app">Get the app</a></div>' +
"<p>The real article body that must be kept.</p>" +
'<div class="captioned-image-container"><img src="https://substackcdn.com/image/x.png" alt="A figure" /></div>' +
'<div data-component-name="SubscribeWidgetToDOM" class="subscription-widget show-subscribe">' +
'<a href="https://pub.substack.com/subscribe">Become a supporter</a></div>';
const md = convertHtmlToMarkdown(html);
expect(md).toContain("The real article body that must be kept.");
expect(md).toContain("https://substackcdn.com/image/x.png");
expect(md).not.toContain("Get the app");
expect(md).not.toContain("reading this in email");
expect(md).not.toContain("Become a supporter");
});
});
describe("convertHtmlToMarkdown: footnotes", () => {

View file

@ -99,6 +99,27 @@ const WIDGET_STRIP_KEYS: readonly string[] = [
"poll-embed",
"comments-button",
"footer-buttons",
// Substack email-truncation / "read in the app" promo and CTA form blocks.
"install-substack-app",
"subscription-widget",
"preamble",
"digest-post-embed",
"fake-input",
"fake-button",
"email-input",
"cta-caption",
];
/**
* `data-component-name` values Substack stamps on its embedded widgets. This is
* the most reliable marker for the subscribe form, the "read in the app" promo,
* and CTA buttons. `Image2ToDOM` (real captioned images) is deliberately NOT
* listed so images are preserved. Matched exactly (not substring).
*/
const WIDGET_STRIP_COMPONENTS: readonly string[] = [
"SubscribeWidgetToDOM",
"InstallSubstackAppToDOM",
"ButtonCreateButton",
];
/**
@ -153,6 +174,10 @@ function classMatchesAny(node: ClassedElement, keys: readonly string[]): boolean
/** Known subscribe/share/button widget: removed outright. */
function isStripWidget(node: ClassedElement): boolean {
const component = node.getAttribute("data-component-name");
if (component !== null && WIDGET_STRIP_COMPONENTS.includes(component)) {
return true;
}
return classMatchesAny(node, WIDGET_STRIP_KEYS);
}