How I Built a Claude Design → Google Slides Converter in a Weekend

Claude Code can generate beautiful HTML designs. But getting those designs into Google Slides meant copy-pasting screenshots and manually recreating layouts. There had to be a better way.

So I built one. Paste a Claude Design URL. Get a Google Slides file delivered to your Drive — with automatic sharing, watermark gating, and Lemon Squeezy subscriptions. All in a weekend.

Here's exactly how it works, from download to delivery.

The Stack

A single Express server. No React. No database migrations. No microservices. Sometimes the best architecture is the simplest one that works.

Architecture: One Request, One Pipeline

The entire conversion is a single async pipeline. No job queues. No workers. Just one function that does everything in sequence:

User pastes URL + Gmail
  ↓
1. Fetch tar bundle from api.anthropic.com/v1/design/h/...
  ↓
2. Extract HTML from tar.gz
  ↓
3. Render in Puppeteer with watermark overlay
  ↓
4. Screenshot each slide individually (show/hide loop)
  ↓
5. Build PPTX from images using pptxgenjs
  ↓
6. Upload to user's Google Drive via their OAuth token
  ↓
7. Convert to Google Slides format
  ↓
8. All done — file is in their Drive, they own it

Every step emits an SSE event. The frontend listens and shows a real-time progress checklist. No polling. No spinners. Just a smooth stream of updates.

The Watermark Gate

This was the trickiest design decision. I needed a free tier that was useful enough to try, but gated enough to convert.

Solution: pixel-baked watermarks. Free users get every slide rendered as a flat PNG with "PREVIEW — SUBSCRIBE TO REMOVE" permanently baked into the pixels. Not a deletable DOM overlay. Not a text box. An actual part of the image.

if (hasLicense) {
  // Clean path: dom-to-pptx for editable output
  await page.addScriptTag({ url: domToPptxBundle });
  const blob = await exportToPptx(sections);
} else {
  // Watermarked path: screenshot each slide as PNG
  for (const section of sections) {
    section.style.display = ''; // show one at a time
    const png = await page.screenshot();
    screenshots.push(png);
    section.style.display = 'none';
  }
  // Build PPTX from flat images
  for (const img of screenshots) {
    slide.addImage({ data: img, x: 0, y: 0, w: '100%', h: '100%' });
  }
}

To remove the watermark, you'd need Photoshop — not a right-click. The difference between free and paid is the difference between a screenshot and an editable presentation.

Google OAuth: Users Own Their Files

Early versions uploaded everything to my Drive account, then tried to share and transfer ownership. That broke. A lot. Google's API doesn't allow ownership transfer to consumer Gmail accounts — only Workspace domains.

The fix: upload directly to the user's Drive. One Google Sign-In button. The user grants Drive file scope. We use their OAuth token for the upload. They're the native owner from the moment the file is created. No sharing gymnastics. No permission errors.

// User signs in via Google Identity Services
const tokenClient = google.accounts.oauth2.initTokenClient({
  client_id: GOOGLE_CLIENT_ID,
  scope: 'https://www.googleapis.com/auth/drive.file',
  callback: (response) => {
    googleToken = response.access_token;
    // Include token with conversion request
    fetch('/convert', { body: JSON.stringify({ designURL, googleToken }) });
  },
});

Lemon Squeezy: Zero Payment UI

I didn't build a payment form. Lemon Squeezy handles checkout, tax compliance, recurring billing, and license key delivery. Our job is just one webhook endpoint:

app.post('/webhook/ls', (req, res) => {
  res.json({ ok: true }); // ack immediately
  if (event !== 'order_created') return;
  const key = 'pptx-sk-' + crypto.randomBytes(8).toString('hex');
  db.createLicense({ key, email, tier, maxMonthly, expiresAt });
  // Lemon Squeezy emails the key to user
});

User pays → webhook fires → key stored in SQLite → emailed to user. That's the entire payment infrastructure.

Concurrency: One at a Time

Puppeteer is hungry. A single conversion can spike to 500MB RAM. On a modest VPS, two simultaneous conversions means an OOM kill.

The fix isn't a fancy queue system. It's a counter and an array:

let activeJobs = 0;
const MAX_CONCURRENT = 1;
const jobQueue = [];

if (activeJobs >= MAX_CONCURRENT) {
  jobQueue.push({ id, designURL, ... });
} else {
  activeJobs++;
  processJob(id, designURL, ...)
    .finally(() => {
      activeJobs--;
      if (jobQueue.length > 0) {
        const next = jobQueue.shift();
        activeJobs++;
        processJob(next.id, next.designURL, ...);
      }
    });
}

What I'd Do Differently

Stream the upload. Currently we build the entire PPTX in memory before uploading. For presentations with 50+ slides and heavy images, this could be a problem. A streaming approach would keep memory flat.

Add analytics. I shipped without any usage tracking. Can't optimize conversion rates if you don't know where users drop off. Even a simple counter on each SSE stage would help.

Pre-render previews. The thumbnail generation is basic. A proper multi-slide preview carousel would make the free tier more compelling.

Ship It

The entire thing — Express server, Puppeteer rendering, Google OAuth, Drive upload, watermark gate, payment webhooks, and landing page — was built and deployed in a weekend.

You don't need a microservice architecture for a SaaS that does one thing well. You need a pipeline that works, a payment provider that handles the boring stuff, and a watermark that can't be deleted.

Try it yourself at pptx.nerdstudio.online

Paste a Claude Design URL. Preview is free.

Source code: github.com/AbahKili/nerdstudio-pptx — MIT licensed, open source.