Claude Design to Google Slides — Deep Dive

You know that feeling when you have a brilliant deck design in Claude Artifacts — beautiful, pixel-perfect, exactly what your client wanted — and then you have to rebuild it manually in Google Slides? Yeah, I felt that pain. So I built a pipeline that takes Claude-generated HTML/CSS designs and transforms them into editable Google Slides presentations. But the path from prototype to production was paved with three massive gotchas: the watermark gate, the concurrency limiter, and Google OAuth's special brand of suffering. Here's the unfiltered story.

The Problem: Design-to-Slides Hell

I'm a solo developer building in public. For a recent client project, I needed to generate 50+ branded slides from structured content. Claude Artifacts made the design iteration fast — I could tweak colors, layout, and typography in minutes. But the final delivery had to be Google Slides (because that's what the marketing team uses).

Copy-pasting each slide? Forget it. That's 10 minutes per slide, 8 hours of monotonous work. I needed automation. So I wrote a Node.js script that parses Claude's HTML output, extracts styled elements, and uses the Google Slides API to create slides programmatically. Simple, right? Ha.

The Solution: What I Built

I created a two-stage pipeline. Stage one: Claude generates a deck as a single HTML file with inline CSS. Each <section> is a slide. Stage two: my Node.js script parses the DOM, extracts text, images, shapes, and their styles (font size, color, alignment, background), then maps them to Google Slides API requests[].

Here's a snippet of the core parsing logic:

const { JSDOM } = require('jsdom');
const { google } = require('googleapis');

async function parseSlidesFromHTML(htmlContent) {
  const dom = new JSDOM(htmlContent);
  const sections = dom.window.document.querySelectorAll('section.slide');
  
  const slides = [];
  sections.forEach((section, index) => {
    const elements = [];
    section.querySelectorAll('h1, h2, p, img').forEach(el => {
      const style = el.getAttribute('style');
      elements.push({
        type: el.tagName.toLowerCase(),
        text: el.textContent,
        style: parseInlineCSS(style),
        src: el.tagName === 'IMG' ? el.getAttribute('src') : null
      });
    });
    slides.push({ index, elements });
  });
  return slides;
}

Then I batch these into Slides API requests. But that's when the first monster showed up.

The Watermark Gate

My first test run: 10 slides, simple text, no images. It worked! But every slide had a faint "Generated by Nerd Studio" watermark at the bottom. Wait, I didn't add that. Turns out, the Google Slides API silently adds a watermark to presentations created by unverified apps. It's not in the docs — I discovered it by staring at the JSON response for an hour.

The fix: you must verify your OAuth consent screen with Google (including submitting for verification if you use sensitive scopes like https://www.googleapis.com/auth/presentations). Even then, the watermark only disappears when your app is in "production" status, not "testing." And if you're a solo dev, the verification process is a Kafkaesque nightmare of video recordings, privacy policy URLs, and a 3-week wait. I eventually created a separate "internal" Google Workspace project just to bypass this for my own use.

The Concurrency Limiter

Next trap: Google Slides API has a per-project quota of 60 requests per 100 seconds. But here's the kicker — each slide creation requires multiple API calls (one to create the slide, one per element). For a 50-slide deck with 5 elements each, that's 250 requests. You'll hit the limit in 4 seconds and get a 429: Rate Limit Exceeded error.

My solution: a simple token-bucket throttler with exponential backoff:

class RateLimiter {
  constructor(tokensPerInterval, intervalMs) {
    this.tokens = tokensPerInterval;
    this.intervalMs = intervalMs;
    this.lastRefill = Date.now();
  }

  async waitForToken() {
    while (this.tokens <= 0) {
      const now = Date.now();
      const elapsed = now - this.lastRefill;
      if (elapsed >= this.intervalMs) {
        this.tokens = tokensPerInterval;
        this.lastRefill = now;
      } else {
        await new Promise(r => setTimeout(r, 100));
      }
    }
    this.tokens--;
  }
}

With this, I queue all requests and release them at 50 requests per 100 seconds (leaving headroom). It turned a 5-second job into a 30-second job, but it never fails now.

Google OAuth Gotchas

Third circle of hell: OAuth tokens. I used a service account initially — works fine for my own Google Workspace. But when I shared the script with a friend, their slides were created in my drive, not theirs. Service accounts are tied to a single Google Workspace domain.

The fix: switch to OAuth 2.0 with user consent. But then you need a refresh token, which expires after 7 days if the app is unverified. And the refresh token flow requires a redirect URI — which means I had to deploy a tiny Express server just to handle the callback. For a CLI tool. Ridiculous.

Here's the minimal OAuth setup I ended up with:

const { google } = require('googleapis');
const readline = require('readline');

const oauth2Client = new google.auth.OAuth2(
  CLIENT_ID,
  CLIENT_SECRET,
  'http://localhost:3000/oauth2callback'
);

async function getAccessToken() {
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: ['https://www.googleapis.com/auth/presentations']
  });
  console.log('Authorize this app by visiting:', authUrl);
  
  // Start a small server to catch the redirect
  const code = await new Promise((resolve) => {
    const app = require('express')();
    app.get('/oauth2callback', (req, res) => {
      res.send('Authorization successful! You can close this tab.');
      resolve(req.query.code);
    });
    app.listen(3000);
  });
  
  const { tokens } = await oauth2Client.getToken(code);
  oauth2Client.setCredentials(tokens);
  return oauth2Client;
}

Pro tip: store the refresh token in a local file so you don't re-authenticate every run. But if the token expires, you're back to square one.

Key Takeaways

1. Google's API docs are aspirational, not accurate. The watermark behavior, rate limits, and OAuth nuances are learned by debugging, not reading. Budget time for this.

2. Throttle everything. Even if you're under quota, Google's APIs have burst limits that aren't documented. A rate limiter is non-negotiable.

3. Service accounts vs. OAuth 2.0 is a critical decision. If you're building a tool for others, bite the bullet and implement OAuth with a local server. If it's just for you, service account + domain-wide delegation is simpler.

4. The watermark is a feature, not a bug. Google uses it to push developers toward verification. If you're serious about shipping, start the verification process on day one. It takes weeks.

Closing Thought

Building this pipeline felt like assembling IKEA furniture with a blindfold on — every step revealed a new hidden screw. But once it worked, watching Claude's designs materialize as polished Google Slides was genuinely magical. The takeaway? APIs are never as simple as they advertise. But for a solo dev building in public, the grit of solving these gotchas is what turns a prototype into a tool people actually use. Now if you'll excuse me, I have a verification video to record for Google.