<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ken-fs]]></title><description><![CDATA[ken-fs]]></description><link>https://ken-fs.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>ken-fs</title><link>https://ken-fs.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 02 Sep 2026 15:56:22 GMT</lastBuildDate><atom:link href="https://ken-fs.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Convert Word to Clean HTML (Without the Word Cruft)]]></title><description><![CDATA[Paste a Word document or a Google Doc into a CMS, an email builder, or a plain HTML page and you get a horror show: hundreds of lines of mso- styles, <span>s wrapping every word, class names like c17,]]></description><link>https://ken-fs.hashnode.dev/how-to-convert-word-to-clean-html-without-the-word-cruft</link><guid isPermaLink="true">https://ken-fs.hashnode.dev/how-to-convert-word-to-clean-html-without-the-word-cruft</guid><category><![CDATA[HTML]]></category><category><![CDATA[WordPress]]></category><category><![CDATA[webdev]]></category><category><![CDATA[email]]></category><dc:creator><![CDATA[Ken Lee]]></dc:creator><pubDate>Mon, 31 Aug 2026 08:17:46 GMT</pubDate><content:encoded><![CDATA[<p>Paste a Word document or a Google Doc into a CMS, an email builder, or a plain HTML page and you get a horror show: hundreds of lines of <code>mso-</code> styles, <code>&lt;span&gt;</code>s wrapping every word, class names like <code>c17</code>, and font tags nobody asked for. It <em>looks</em> right until you open the source, or until it renders differently in an email client. Here's how to turn a document into <strong>clean, semantic HTML</strong> instead — the kind you'd write by hand.</p>
<h2>Why "Save as HTML" from Word is the wrong tool</h2>
<p>Word's own <em>Save as Web Page</em> was built to round-trip back into Word, not to publish. So it embeds everything it might need to reopen the file: Office-specific style blocks, conditional comments, VML fallbacks. Your <code>&lt;h1&gt;</code> becomes a styled <code>&lt;p&gt;</code> with a hardcoded font. Drop that into a modern site and it fights your stylesheet; drop it into an email and half the clients mangle it.</p>
<p>What you actually want is the <em>structure</em> — headings as headings, lists as lists, links as links — and none of the styling, because your site or email template already owns the look.</p>
<h2>The clean-output approach</h2>
<p>A good converter throws away everything Office-specific and keeps only semantic tags. A heading becomes an <code>&lt;h2&gt;</code>, a bulleted list becomes a real <code>&lt;ul&gt;</code>, bold becomes <code>&lt;strong&gt;</code>, and the <code>mso-</code> soup simply doesn't make it into the output. That's the job of a <a href="https://docs2html.com/docx-to-html/">Word to clean HTML converter</a>: same words, same structure, markup you can actually read and style.</p>
<p>If you're working from a live Google Doc rather than a <code>.docx</code>, the same idea applies — copy the content and run it through a <a href="https://docs2html.com/google-docs-to-html/">Google Docs to HTML converter</a>, and the <code>c1</code>/<code>c17</code> class soup Docs loves to emit gets stripped down to plain tags.</p>
<h2>Tables and images, the two things people get burned by</h2>
<ul>
<li><p><strong>Tables:</strong> a real converter emits a proper <code>&lt;table&gt;</code> with <code>&lt;th&gt;</code> and <code>&lt;td&gt;</code>, not a grid of positioned divs. Merged cells are the one genuine limitation — HTML supports them, but they don't always survive a document's messy internal representation, so check anything with a complex header.</p>
</li>
<li><p><strong>Images:</strong> decide up front whether you want them inlined as base64 (one self-contained file) or pulled out as separate assets. Base64 is convenient for a single email; separate files are better for a page you'll maintain.</p>
</li>
</ul>
<h2>Don't upload the document to do it</h2>
<p>Most "doc to HTML" sites upload your file to a server to convert it. For a blog draft, whatever. For a client proposal, an internal memo, or an unpublished press release, that's a copy of a confidential document sitting on someone else's machine — to save a couple of minutes.</p>
<p>The conversion doesn't need a server. A browser can parse the document and emit HTML locally, with nothing leaving the tab. <a href="https://docs2html.com/">docs2html</a> works that way — open the network panel and you'll see it stays silent, and it keeps working with your Wi-Fi off. If your target format is Markdown instead of HTML (for a README or a wiki), its sibling <a href="https://docstomd.com/">docs to Markdown tool</a> does the same thing locally.</p>
<h2>The short version</h2>
<ol>
<li><p>Never publish Word's "Save as HTML" output — it's built to reopen in Word, not to ship.</p>
</li>
<li><p>Use a converter that keeps semantic tags and drops <code>mso-</code> styles and class soup.</p>
</li>
<li><p>Check tables with merged cells and decide how images should travel.</p>
</li>
<li><p>Prefer a local, no-upload tool for anything confidential.</p>
</li>
</ol>
<p>Clean HTML pastes into any CMS, survives email clients, and matches your own stylesheet instead of overriding it. It's the difference between content you can maintain and a block you're afraid to touch.</p>
]]></content:encoded></item><item><title><![CDATA[How to Convert Google Docs to Markdown (Without Uploading Your Document)]]></title><description><![CDATA[If you write in Google Docs but your content ends up in a README, a static-site post, a wiki, or a Git repo, you eventually hit the same wall: you need Markdown, and copy-pasting a Google Doc gives yo]]></description><link>https://ken-fs.hashnode.dev/how-to-convert-google-docs-to-markdown-without-uploading-your-document</link><guid isPermaLink="true">https://ken-fs.hashnode.dev/how-to-convert-google-docs-to-markdown-without-uploading-your-document</guid><category><![CDATA[markdown]]></category><category><![CDATA[Google Docs]]></category><category><![CDATA[writing]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Ken Lee]]></dc:creator><pubDate>Mon, 31 Aug 2026 08:11:06 GMT</pubDate><content:encoded><![CDATA[<p>If you write in Google Docs but your content ends up in a README, a static-site post, a wiki, or a Git repo, you eventually hit the same wall: you need <strong>Markdown</strong>, and copy-pasting a Google Doc gives you a mess of inline styles instead of clean <code>#</code> headings and <code>-</code> bullets. Here's how to get real Markdown out of a Google Doc — and why you don't need to upload the document to anyone to do it.</p>
<h2>Option 1: The built-in export (okay, but lossy)</h2>
<p>Google Docs has <em>File → Download → Markdown (.md)</em> now. It works for simple documents, but it stumbles on the things you actually care about: nested lists flatten, tables come out inconsistent, and anything you pasted in with odd formatting carries that baggage into the output. For a quick note it's fine. For anything structured, you'll be cleaning up by hand.</p>
<h2>Option 2: Copy, then paste as Markdown (fastest)</h2>
<p>The trick most people miss: your clipboard already carries the document's structure as rich HTML. If you select the content in the doc, copy it, and paste it into a converter that reads that rich clipboard, the headings, bold, links, and lists survive — because it's converting the <em>structure</em>, not a screenshot of the text.</p>
<p>That's the workflow I use now: select all in the Google Doc, copy, and <a href="https://docstomd.com/guides/google-docs-to-markdown-paste/">paste it straight into the converter as Markdown</a>. Headings become <code>##</code>, bold stays <code>**bold**</code>, links keep their targets, and lists keep their nesting. It takes about three seconds and there's nothing to clean up.</p>
<h2>Option 3: Convert the file (for .docx exports)</h2>
<p>If what you have is a downloaded <code>.docx</code> instead of a live doc, drop it into a <a href="https://docstomd.com/google-docs-to-markdown/">Google Docs to Markdown converter</a> and it'll do the same job on the file. Same clean output, no manual fixups.</p>
<h2>Why "without uploading" matters here</h2>
<p>Most online converters work by uploading your file to a server, converting it there, and sending the result back. For a throwaway note that's no big deal. For a contract draft, an internal spec, an unpublished article, or anything with a client's name in it, you've just handed a copy to a third party you know nothing about — to save yourself thirty seconds of formatting.</p>
<p>It doesn't have to work that way. Modern browsers can do the whole conversion locally: the document is read, parsed, and turned into Markdown in the browser tab, and nothing is ever sent anywhere. That's the approach <a href="https://docstomd.com/">docstomd</a> takes — you can watch the network tab, it stays quiet. It even works offline once the page has loaded.</p>
<h2>A few things Markdown can't hold</h2>
<p>Set expectations before you convert, so you're not surprised:</p>
<ul>
<li><p><strong>Fonts and colors</strong> don't survive — Markdown is plain text, and that's the point. Styling is the renderer's job.</p>
</li>
<li><p><strong>Merged table cells</strong> have no equivalent; Markdown tables are strictly rectangular.</p>
</li>
<li><p><strong>Comments and suggestions</strong> aren't content, so they're dropped.</p>
</li>
</ul>
<p>Everything structural — headings, lists, links, bold, italics, code, blockquotes, plain tables — comes across cleanly.</p>
<h2>If you need HTML instead</h2>
<p>Sometimes the target isn't Markdown but clean HTML — for an email, a CMS, or a page. Same idea, different output: a <a href="https://docs2html.com/">local Google-Docs-to-HTML converter</a> gives you semantic <code>&lt;h2&gt;</code> and <code>&lt;p&gt;</code> tags without the <code>mso-</code> style soup Word and Docs usually leave behind.</p>
<h2>The short version</h2>
<ol>
<li><p>Select the content in your Google Doc and copy it.</p>
</li>
<li><p>Paste it into a converter that reads the rich clipboard — structure survives.</p>
</li>
<li><p>Prefer one that converts locally, so the document never leaves your machine.</p>
</li>
<li><p>Don't expect fonts, colors, or merged cells — Markdown keeps structure, not styling.</p>
</li>
</ol>
<p>Once it's Markdown, it drops straight into Git, a static site, or a wiki and stays diff-able forever. That's the whole reason to get out of the word processor in the first place.</p>
]]></content:encoded></item><item><title><![CDATA[Building image & video conversion that never leaves the browser]]></title><description><![CDATA[How VisualRefiner converts HEIC, compresses images, and transcodes video with no server, no upload, and no account — using the Canvas API, WebCodecs, and a little WebAssembly.
Most "free online conver]]></description><link>https://ken-fs.hashnode.dev/building-image-video-conversion-that-never-leaves-the-browser</link><guid isPermaLink="true">https://ken-fs.hashnode.dev/building-image-video-conversion-that-never-leaves-the-browser</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[WebAssembly]]></category><category><![CDATA[privacy]]></category><dc:creator><![CDATA[Ken Lee]]></dc:creator><pubDate>Wed, 19 Aug 2026 02:59:44 GMT</pubDate><content:encoded><![CDATA[<p><em>How VisualRefiner converts HEIC, compresses images, and transcodes video with no server, no upload, and no account — using the Canvas API, WebCodecs, and a little WebAssembly.</em></p>
<p>Most "free online converters" work like this: you pick a file, it's uploaded to a server, converted there, and sent back. That's fine for a meme. It's less fine for a folder of personal photos, a passport scan, or client footage under NDA.</p>
<p>I built <a href="https://visualrefiner.com">VisualRefiner</a> to do the same jobs without the upload. Every conversion runs in the browser tab on your own machine. There is no backend to send files to — the whole site is a static export — so "your files never leave your device" isn't a privacy promise, it's just how the architecture works. Here's the interesting part of how it's put together.</p>
<h2>The core: <code>&lt;canvas&gt;</code> is a format converter</h2>
<p>The humble 2D canvas is already an image transcoder. Decode any image the browser understands into an <code>ImageBitmap</code>, draw it, and re-encode with <code>toBlob()</code> — the MIME type you pass decides the output format:</p>
<pre><code class="language-js">async function convert(file, type = "image/jpeg", quality = 0.82) {
  const bitmap = await createImageBitmap(file);
  const canvas = document.createElement("canvas");
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext("2d", { alpha: true }).drawImage(bitmap, 0, 0);
  bitmap.close();

  return new Promise((resolve, reject) =&gt;
    canvas.toBlob(
      (blob) =&gt; (blob ? resolve(blob) : reject(new Error("encode failed"))),
      type,           // "image/jpeg" | "image/png" | "image/webp"
      quality,        // ignored for PNG (lossless)
    ),
  );
}
</code></pre>
<p>That single function is PNG↔JPG↔WebP conversion and JPG/WebP compression (the <code>quality</code> argument) all at once. No dependency, no upload, works offline. The result is a <code>Blob</code>; wrap it in <code>URL.createObjectURL()</code> for a preview or a download link.</p>
<p>Two details that bite you if you skip them:</p>
<ul>
<li><p><strong>Revoke your object URLs.</strong> Each <code>createObjectURL</code> pins the blob in memory until you <code>URL.revokeObjectURL()</code> it. In a tool people use repeatedly, forgetting this is a slow leak. Revoke the previous URL every time you produce a new one.</p>
</li>
<li><p><strong>PNG ignores</strong> <code>quality</code><strong>.</strong> A "quality slider" on a PNG export does nothing — PNG is lossless. Hide the control or convert to WebP if the user wants a smaller file with a quality knob.</p>
</li>
</ul>
<h2>HEIC: the format the canvas can't read</h2>
<p>Then there's HEIC — the format iPhones save by default. Most browsers <em>won't</em> decode it in a <code>&lt;canvas&gt;</code> pipeline, so <code>createImageBitmap()</code> throws. The fix is to decode it ourselves first, with <a href="https://github.com/strukturag/libheif">libheif</a> compiled to WebAssembly (via <a href="https://github.com/alexcorvi/heic2any">heic2any</a>):</p>
<pre><code class="language-js">async function normalize(file, quality) {
  const isHeic = /heic|heif/i.test(file.type) || /\.hei[cf]$/i.test(file.name);
  if (!isHeic) return file;

  // Only pulled in when someone actually hands us a HEIC file.
  const { default: heic2any } = await import("heic2any");
  const out = await heic2any({ blob: file, toType: "image/jpeg", quality });
  return Array.isArray(out) ? out[0] : out;
}
</code></pre>
<p>Now HEIC flows into the same canvas function as everything else. The WASM decoder is the heaviest thing on the site (~2 MB), which brings up the next point.</p>
<h2>Load the heavy parts lazily</h2>
<p>If you bundle libheif, a video engine, and a GIF encoder into your main chunk, your homepage pays for tools the visitor may never touch. The trick is a dynamic <code>import()</code> at the moment of use — note the <code>await import(...)</code> inside both functions above and below. The bundler splits each into its own chunk, and the network tab confirms the 2 MB HEIC decoder only downloads when someone converts a HEIC file. The <a href="https://visualrefiner.com">homepage</a> ships a small bundle; the weight arrives on demand.</p>
<p>High-quality <strong>resizing</strong> is the same story. Canvas <code>drawImage</code> downscaling aliases badly (jagged edges, moiré). <a href="https://github.com/nodeca/pica">pica</a> does proper Lanczos-style resampling, so it's loaded only on the resize path:</p>
<pre><code class="language-js">if (needsResize) {
  const { default: pica } = await import("pica");
  await pica().resize(sourceCanvas, targetCanvas);
}
</code></pre>
<h2>Video, without FFmpeg-in-WASM</h2>
<p>Video is where people reach for <code>ffmpeg.wasm</code>, but it's a big download and slow. Modern browsers expose <a href="https://developer.mozilla.org/docs/Web/API/WebCodecs_API">WebCodecs</a> — hardware-accelerated encode/decode built into the browser. VisualRefiner drives it through <a href="https://github.com/Vanilagy/mediabunny">mediabunny</a> to transcode clips to MP4 or WebM, extract frames as PNG, or build a GIF (with <a href="https://github.com/mattdesl/gifenc">gifenc</a>) — all in the tab.</p>
<p>The honest caveat: WebCodecs support and the available codecs <strong>depend on the browser</strong>. So conversion isn't guaranteed for every exotic input the way the canvas image path is. The tool checks and tells you when a track can't be decoded, rather than pretending. That trade — depend on the platform, degrade honestly — beats shipping a 25 MB WASM FFmpeg to every visitor.</p>
<h2>Why static export makes the privacy claim trivially true</h2>
<p>The site is a Next.js app exported to static files (<code>output: "export"</code>) and served as plain assets from a CDN. There is no API route, no server that receives a file, nothing to log. You don't have to trust a privacy policy — open DevTools → Network, convert something, and watch: <strong>zero upload requests</strong>. The bytes stay in the tab.</p>
<p>That also means the "server cost" of a converter that could handle thousands of files is… a static host. All the compute is the user's.</p>
<h2>Takeaways</h2>
<ul>
<li><p><code>&lt;canvas&gt;</code> + <code>toBlob()</code> is a complete image converter/compressor with zero deps.</p>
</li>
<li><p>For formats the browser can't decode (HEIC), a WASM decoder bridges the gap — load it lazily so everyone else doesn't pay for it.</p>
</li>
<li><p>Prefer WebCodecs over WASM FFmpeg for video when you can accept browser-dependent codec support.</p>
</li>
<li><p>Dynamic <code>import()</code> per feature keeps the initial bundle small.</p>
</li>
<li><p>A static export turns "we don't upload your files" from a promise into an architectural fact.</p>
</li>
</ul>
<p>If you want to see it in action, the tools are at <a href="https://visualrefiner.com">visualrefiner.com</a> — <a href="https://visualrefiner.com/heic-to-jpg">HEIC → JPG</a>, <a href="https://visualrefiner.com/image-compressor">image compressor</a>, <a href="https://visualrefiner.com/video-to-gif">video to GIF</a> — and the code is on <a href="https://github.com/ken-fs/VisualRefiner">GitHub</a>.</p>
]]></content:encoded></item></channel></rss>