<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Projects on Michael Brunger</title><link>https://michaelbrunger.com/projects/</link><description>South African chartered accountant in England, learning in public: finance, Python, AI and the odd side project.</description><generator>Hugo</generator><language>en-GB</language><managingEditor>michaelbrunger@gmail.com (Michael Brunger)</managingEditor><atom:link href="https://michaelbrunger.com/projects/index.xml" rel="self" type="application/rss+xml"/><item><title>X Article to PDF Converter</title><link>https://michaelbrunger.com/tools/x-article-to-pdf/</link><pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate><author>michaelbrunger@gmail.com (Michael Brunger)</author><guid>https://michaelbrunger.com/tools/x-article-to-pdf/</guid><description>&lt;p>Paste a public X (Twitter) article URL below to convert it into a clean PDF. Navigation, sidebars, and other clutter are stripped out so you get just the article content.&lt;/p>


&lt;div class="article-converter">
 &lt;form id="converter-form" onsubmit="return false;">
 &lt;p>
 &lt;label for="article-url">X article URL&lt;/label>
 &lt;input
 type="url"
 id="article-url"
 name="url"
 placeholder="https://x.com/username/status/123456..."
 required
 />
 &lt;/p>
 &lt;p>
 &lt;button type="submit" id="convert-btn">Convert to PDF&lt;/button>
 &lt;/p>
 &lt;/form>

 &lt;div id="converter-status" class="converter-status" style="display: none;">&lt;/div>
&lt;/div>

&lt;script>
 document.getElementById('converter-form').addEventListener('submit', async function (e) {
 e.preventDefault();

 var urlInput = document.getElementById('article-url');
 var btn = document.getElementById('convert-btn');
 var status = document.getElementById('converter-status');
 var url = urlInput.value.trim();

 
 var pattern = /^https:\/\/(x\.com|twitter\.com)\/[A-Za-z0-9_]+\/(status|articles)\/[0-9]+/;
 if (!pattern.test(url)) {
 status.style.display = 'block';
 status.className = 'converter-status error';
 status.textContent = 'Please enter a valid X URL (e.g. https://x.com/username/status/123...)';
 return;
 }

 
 btn.disabled = true;
 btn.innerHTML = '&lt;span class="spinner">&lt;/span> Converting\u2026';
 status.style.display = 'block';
 status.className = 'converter-status loading';
 status.textContent = 'Loading article and generating PDF\u2026 This may take up to 30 seconds.';

 var API_BASE = 'https://x-article-api.vercel.app';

 try {
 var response = await fetch(API_BASE + '/api/convert-article', {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify({ url: url }),
 });

 if (!response.ok) {
 var data = await response.json();
 throw new Error(data.error || 'Conversion failed');
 }

 
 var blob = await response.blob();
 var downloadUrl = URL.createObjectURL(blob);
 var a = document.createElement('a');
 a.href = downloadUrl;

 
 var disposition = response.headers.get('Content-Disposition') || '';
 var match = disposition.match(/filename="(.+?)"/);
 a.download = match ? match[1] : 'x-article.pdf';

 document.body.appendChild(a);
 a.click();
 document.body.removeChild(a);
 URL.revokeObjectURL(downloadUrl);

 status.className = 'converter-status success';
 status.textContent = 'PDF downloaded successfully!';
 } catch (err) {
 status.className = 'converter-status error';
 status.textContent = err.message || 'An unexpected error occurred. Please try again.';
 } finally {
 btn.disabled = false;
 btn.textContent = 'Convert to PDF';
 }
 });
&lt;/script>

&lt;h2 id="how-it-works">How it works&lt;/h2>&lt;ol>
&lt;li>Paste a public X article URL (format: &lt;code>https://x.com/username/status/123456...&lt;/code>)&lt;/li>
&lt;li>Click &lt;strong>Convert to PDF&lt;/strong>&lt;/li>
&lt;li>The article is rendered and converted to a clean A4 PDF&lt;/li>
&lt;li>Your PDF downloads automatically&lt;/li>
&lt;/ol>
&lt;h2 id="limitations">Limitations&lt;/h2>&lt;ul>
&lt;li>Only works with &lt;strong>public articles&lt;/strong> — content behind login walls cannot be converted&lt;/li>
&lt;li>The URL must be an X long-form article, not a regular tweet or thread&lt;/li>
&lt;li>Conversion may take up to 30 seconds depending on article length&lt;/li>
&lt;li>Limited to one conversion every 10 seconds&lt;/li>
&lt;/ul></description></item><item><title>Daily Brief</title><link>https://michaelbrunger.com/projects/daily-brief/</link><pubDate>Thu, 29 May 2025 00:00:00 +0000</pubDate><author>michaelbrunger@gmail.com (Michael Brunger)</author><guid>https://michaelbrunger.com/projects/daily-brief/</guid><description/></item></channel></rss>