Building Local-First Web Apps: Parsing HTML and PDFs to Markdown in the Browser

작성자

카테고리:

← 피드로
DEV Community · moamen abou elsaoud · 2026-08-27 개발(SW)

moamen abou elsaoud

Local-first and privacy-focused web utilities are having a massive comeback. With browser engines becoming faster and WebAssembly/Web Workers maturing, there is rarely a reason to push sensitive user documents to an external backend for simple conversions.

While building MD-Convert (a zero-upload document to Markdown converter), I explored how to parse real-world documents into clean Markdown entirely on the client side.

Here is a breakdown of the core architecture and libraries that make purely in-browser document processing possible.

1. Converting Web Articles with Readability + Turndown

Converting messy web markup into clean Markdown involves two distinct steps:

  1. Content Extraction: Stripping ads, navbars, sidebars, and trackers.
  2. HTML-to-Markdown Transformation: Translating semantic DOM nodes into markdown tokens.

Mozilla’s @mozilla/readability paired with turndown is an incredible combination for this:

import { Readability } from '@mozilla/readability';
import TurndownService from 'turndown';

function htmlToCleanMarkdown(rawHtmlDocument, sourceUrl) {
  // 1. Extract pure article content
  const reader = new Readability(rawHtmlDocument);
  const article = reader.parse();

  if (!article || !article.content) {
    throw new Error('Unable to extract main content');
  }

  // 2. Initialize Turndown
  const turndownService = new TurndownService({
    headingStyle: 'atx',
    codeBlockStyle: 'fenced'
  });

  // Ensure image URLs remain absolute
  turndownService.addRule('absoluteImages', {
    filter: 'img',
    replacement: (content, node) => {
      const src = node.getAttribute('src');
      const alt = node.getAttribute('alt') || '';
      if (!src) return '';
      try {
        const absoluteUrl = new URL(src, sourceUrl).href;
        return `![${alt}](${absoluteUrl})\n\n`;
      } catch {
        return `![${alt}](${src})\n\n`;
      }
    }
  });

  return turndownService.turndown(article.content);
}

Enter fullscreen mode Exit fullscreen mode

  1. Offloading Heavy PDF Parsing to Web Workers Parsing large PDFs using pdf.js on the main thread is a recipe for UI freezes and dropped frames. The solution is running the extraction pipeline inside a dedicated Web Worker. Here is a simplified pattern for extracting selectable text layers asynchronously:
// worker.js
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';

self.onmessage = async (e) => {
  const { arrayBuffer } = e.data;

  try {
    const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
    const pdf = await loadingTask.promise;
    let fullText = '';

    for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
      const page = await pdf.getPage(pageNum);
      const textContent = await page.getTextContent();

      const pageText = textContent.items
        .map((item) => item.str)
        .join(' ');

      fullText += `## Page ${pageNum}\n\n${pageText}\n\n`;
    }

    self.postMessage({ status: 'success', markdown: fullText });
  } catch (error) {
    self.postMessage({ status: 'error', message: error.message });
  }
};

Enter fullscreen mode Exit fullscreen mode

The Big Advantages of Client-Side Processing
Zero Server Costs: The application can run entirely on static hosting (like Cloudflare Pages or GitHub Pages).
Absolute Privacy: User data, API keys, or confidential spreadsheets never cross the network.
Instant Latency: Conversions happen in-memory without queue waiting times.
Thoughts & Edge Cases?
The main challenge with 100% in-browser parsing remains edge cases—like complex multi-column PDF layouts or non-OCR scanned documents.
Have you built any client-side/local-first tools recently? What libraries do you prefer for client-side parsing? Let’s discuss in the comments! 👇

원문에서 계속 ↗